diff --git a/cli/azd/cmd/config.go b/cli/azd/cmd/config.go index 9f06cc19ded..e9366247504 100644 --- a/cli/azd/cmd/config.go +++ b/cli/azd/cmd/config.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "maps" + "os" "path/filepath" "runtime" "slices" @@ -169,6 +170,20 @@ $ azd config set defaults.location eastus`, ActionResolver: newConfigListAlphaAction, }) + group.Add("options", &actions.ActionDescriptorOptions{ + Command: &cobra.Command{ + Short: "List all available configuration settings.", + Long: "List all possible configuration settings that can be set with azd, " + + "including descriptions and allowed values.", + }, + HelpOptions: actions.ActionHelpOptions{ + Footer: getCmdConfigOptionsHelpFooter, + }, + ActionResolver: newConfigOptionsAction, + OutputFormats: []output.Format{output.JsonFormat, output.TableFormat}, + DefaultFormat: output.TableFormat, + }) + return group } @@ -510,3 +525,154 @@ func getCmdListAlphaHelpFooter(*cobra.Command) string { ), }) } + +func getCmdConfigOptionsHelpFooter(*cobra.Command) string { + return generateCmdHelpSamplesBlock(map[string]string{ + "List all available configuration settings in table format": output.WithHighLightFormat( + "azd config options", + ), + "List all available configuration settings in JSON format": output.WithHighLightFormat( + "azd config options -o json", + ), + "View a setting and set it": output.WithHighLightFormat( + "azd config options") + "\n " + output.WithHighLightFormat( + "azd config set ", + ), + }) +} + +// azd config options + +type configOptionsAction struct { + console input.Console + formatter output.Formatter + writer io.Writer + configManager config.UserConfigManager + args []string +} + +func newConfigOptionsAction( + console input.Console, + formatter output.Formatter, + writer io.Writer, + configManager config.UserConfigManager, + args []string) actions.Action { + return &configOptionsAction{ + console: console, + formatter: formatter, + writer: writer, + configManager: configManager, + args: args, + } +} + +func (a *configOptionsAction) Run(ctx context.Context) (*actions.ActionResult, error) { + options := config.GetAllConfigOptions() + + // Load current config to show current values + currentConfig, err := a.configManager.Load() + if err != nil { + // Only ignore "file not found" errors; other errors should be logged + if !os.IsNotExist(err) { + // Log the error but continue with empty config to ensure the command still works + fmt.Fprintf(a.console.Handles().Stderr, "Warning: failed to load config: %v\n", err) + } + currentConfig = config.NewEmptyConfig() + } + + if a.formatter.Kind() == output.JsonFormat { + err := a.formatter.Format(options, a.writer, nil) + if err != nil { + return nil, fmt.Errorf("failed formatting config options: %w", err) + } + return nil, nil + } + + // Table format + type tableRow struct { + Key string + Description string + Type string + CurrentValue string + AllowedValues string + EnvVar string + Example string + } + + var rows []tableRow + for _, option := range options { + allowedValues := "" + if len(option.AllowedValues) > 0 { + allowedValues = strings.Join(option.AllowedValues, ", ") + } + + // Get current value from config + currentValue := "" + // Skip environment-only variables (those with keys starting with EnvOnlyPrefix) + if !strings.HasPrefix(option.Key, config.EnvOnlyPrefix) { + if val, ok := currentConfig.Get(option.Key); ok { + // Convert value to string representation + switch v := val.(type) { + case string: + currentValue = v + case map[string]any: + currentValue = "" + case []any: + currentValue = "" + default: + currentValue = fmt.Sprintf("%v", v) + } + } + } + + rows = append(rows, tableRow{ + Key: option.Key, + Description: option.Description, + Type: option.Type, + CurrentValue: currentValue, + AllowedValues: allowedValues, + EnvVar: option.EnvVar, + Example: option.Example, + }) + } + + columns := []output.Column{ + { + Heading: "Key", + ValueTemplate: "{{.Key}}", + }, + { + Heading: "Description", + ValueTemplate: "{{.Description}}", + }, + { + Heading: "Type", + ValueTemplate: "{{.Type}}", + }, + { + Heading: "Current Value", + ValueTemplate: "{{.CurrentValue}}", + }, + { + Heading: "Allowed Values", + ValueTemplate: "{{.AllowedValues}}", + }, + { + Heading: "Environment Variable", + ValueTemplate: "{{.EnvVar}}", + }, + { + Heading: "Example", + ValueTemplate: "{{.Example}}", + }, + } + + err = a.formatter.Format(rows, a.writer, output.TableFormatterOptions{ + Columns: columns, + }) + if err != nil { + return nil, fmt.Errorf("failed formatting config options: %w", err) + } + + return nil, nil +} diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index 11f41f1eb82..bd1184173f9 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -324,6 +324,10 @@ const completionSpec: Fig.Spec = { name: ['list-alpha'], description: 'Display the list of available features in alpha stage.', }, + { + name: ['options'], + description: 'List all available configuration settings.', + }, { name: ['reset'], description: 'Resets configuration to default.', @@ -1584,6 +1588,10 @@ const completionSpec: Fig.Spec = { name: ['list-alpha'], description: 'Display the list of available features in alpha stage.', }, + { + name: ['options'], + description: 'List all available configuration settings.', + }, { name: ['reset'], description: 'Resets configuration to default.', diff --git a/cli/azd/cmd/testdata/TestUsage-azd-config-options.snap b/cli/azd/cmd/testdata/TestUsage-azd-config-options.snap new file mode 100644 index 00000000000..79bebb97bdd --- /dev/null +++ b/cli/azd/cmd/testdata/TestUsage-azd-config-options.snap @@ -0,0 +1,25 @@ + +List all available configuration settings. + +Usage + azd config options [flags] + +Global Flags + -C, --cwd string : Sets the current working directory. + --debug : Enables debugging and diagnostics logging. + --docs : Opens the documentation for azd config options in your web browser. + -h, --help : Gets help for options. + --no-prompt : Accepts the default value instead of prompting, or it fails if there is no default. + +Examples + List all available configuration settings in JSON format + azd config options -o json + + List all available configuration settings in table format + azd config options + + View a setting and set it + azd config options + azd config set + + diff --git a/cli/azd/cmd/testdata/TestUsage-azd-config.snap b/cli/azd/cmd/testdata/TestUsage-azd-config.snap index 671d9e0476d..a8540bd7c0a 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-config.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-config.snap @@ -11,6 +11,7 @@ Usage Available Commands get : Gets a configuration. list-alpha : Display the list of available features in alpha stage. + options : List all available configuration settings. reset : Resets configuration to default. set : Sets a configuration. show : Show all the configuration values. diff --git a/cli/azd/pkg/config/config_options.go b/cli/azd/pkg/config/config_options.go new file mode 100644 index 00000000000..f6401ec683e --- /dev/null +++ b/cli/azd/pkg/config/config_options.go @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package config + +import ( + "log" + + "github.com/azure/azure-dev/cli/azd/resources" + "github.com/braydonk/yaml" +) + +// EnvOnlyPrefix is the prefix used to mark environment-only configuration options +const EnvOnlyPrefix = "(env) " + +// ConfigOption defines a configuration setting that can be set in azd config +type ConfigOption struct { + Key string `yaml:"key"` + Description string `yaml:"description"` + Type string `yaml:"type"` + AllowedValues []string `yaml:"allowedValues,omitempty"` + Example string `yaml:"example,omitempty"` + EnvVar string `yaml:"envVar,omitempty"` +} + +var allConfigOptions []ConfigOption + +func init() { + err := yaml.Unmarshal(resources.ConfigOptions, &allConfigOptions) + if err != nil { + log.Panicf("Can't unmarshal config options! %v", err) + } +} + +// GetAllConfigOptions returns all available configuration options +func GetAllConfigOptions() []ConfigOption { + return allConfigOptions +} diff --git a/cli/azd/pkg/config/config_options_test.go b/cli/azd/pkg/config/config_options_test.go new file mode 100644 index 00000000000..eb09650c582 --- /dev/null +++ b/cli/azd/pkg/config/config_options_test.go @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetAllConfigOptions(t *testing.T) { + options := GetAllConfigOptions() + + // Should have at least some options + require.NotEmpty(t, options) + require.Greater(t, len(options), 0) + + // Check that we have the expected default options + foundDefaultsSubscription := false + foundDefaultsLocation := false + foundAlphaAll := false + + for _, option := range options { + require.NotEmpty(t, option.Key, "Config option key should not be empty") + require.NotEmpty(t, option.Description, "Config option description should not be empty") + require.NotEmpty(t, option.Type, "Config option type should not be empty") + + switch option.Key { + case "defaults.subscription": + foundDefaultsSubscription = true + require.Equal(t, "string", option.Type) + require.Contains(t, option.Description, "subscription") + case "defaults.location": + foundDefaultsLocation = true + require.Equal(t, "string", option.Type) + require.Contains(t, option.Description, "location") + case "alpha.all": + foundAlphaAll = true + require.Equal(t, "string", option.Type) + require.Contains(t, option.AllowedValues, "on") + require.Contains(t, option.AllowedValues, "off") + require.Equal(t, "AZD_ALPHA_ENABLE_ALL", option.EnvVar) + } + } + + // Verify expected options are present + require.True(t, foundDefaultsSubscription, "defaults.subscription option should be present") + require.True(t, foundDefaultsLocation, "defaults.location option should be present") + require.True(t, foundAlphaAll, "alpha.all option should be present") +} + +func TestConfigOptionStructure(t *testing.T) { + options := GetAllConfigOptions() + + for _, option := range options { + // All options should have required fields + require.NotEmpty(t, option.Key) + require.NotEmpty(t, option.Description) + require.NotEmpty(t, option.Type) + + // If AllowedValues is set, it should not be empty + if len(option.AllowedValues) > 0 { + for _, val := range option.AllowedValues { + require.NotEmpty(t, val, "Allowed value should not be empty") + } + } + } +} diff --git a/cli/azd/resources/config_options.yaml b/cli/azd/resources/config_options.yaml new file mode 100644 index 00000000000..0bf7b3a2749 --- /dev/null +++ b/cli/azd/resources/config_options.yaml @@ -0,0 +1,64 @@ +- key: defaults.subscription + description: "Default Azure subscription ID to use for operations." + type: string + example: "00000000-0000-0000-0000-000000000000" +- key: defaults.location + description: "Default Azure location/region to use for deployments." + type: string + example: "eastus" +- key: alpha.all + description: "Enable or disable all alpha features at once." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_ALL" +- key: alpha.aks.helm + description: "Enable Helm support for AKS deployments." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_AKS_HELM" +- key: alpha.aks.kustomize + description: "Enable Kustomize support for AKS deployments." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_AKS_KUSTOMIZE" +- key: alpha.aca.persistDomains + description: "Do not change custom domains when deploying Azure Container Apps." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_ACA_PERSISTDOMAINS" +- key: alpha.azd.operations + description: "Extends provisioning providers with azd operations." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_AZD_OPERATIONS" +- key: alpha.aca.persistIngressSessionAffinity + description: "Do not change Ingress Session Affinity when deploying Azure Container Apps." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_ACA_PERSISTINGRESSSESSIONAFFINITY" +- key: alpha.deployment.stacks + description: "Enables Azure deployment stacks for ARM/Bicep based deployments." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_DEPLOYMENT_STACKS" +- key: alpha.llm + description: "Enables the use of LLMs in the CLI." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_LLM" +- key: alpha.language.custom + description: "Enables support for services to use custom language." + type: string + allowedValues: ["on", "off"] + envVar: "AZD_ALPHA_ENABLE_LANGUAGE_CUSTOM" +- key: template.sources + description: "Custom template sources for azd template list and azd init." + type: object + example: "template.sources..type" +- key: pipeline.config.applicationServiceManagementReference + description: "Application Service Management Reference for Azure pipeline configuration." + type: string +- key: (env) AZD_CONFIG_DIR + description: "Override the default configuration directory location." + type: envvar + example: "/path/to/config" diff --git a/cli/azd/resources/resources.go b/cli/azd/resources/resources.go index dd28cbe3739..c3b7ef201f4 100644 --- a/cli/azd/resources/resources.go +++ b/cli/azd/resources/resources.go @@ -13,6 +13,9 @@ var TemplatesJson []byte //go:embed alpha_features.yaml var AlphaFeatures []byte +//go:embed config_options.yaml +var ConfigOptions []byte + //go:embed minimal/main.bicep var MinimalBicep []byte