Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions cli/azd/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"maps"
"os"
"path/filepath"
"runtime"
"slices"
Expand Down Expand Up @@ -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, output.NoneFormat},
DefaultFormat: output.NoneFormat,
})

return group
}

Expand Down Expand Up @@ -510,3 +525,200 @@ func getCmdListAlphaHelpFooter(*cobra.Command) string {
),
})
}

func getCmdConfigOptionsHelpFooter(*cobra.Command) string {
return generateCmdHelpSamplesBlock(map[string]string{
"List all available configuration settings": output.WithHighLightFormat(
"azd config options",
),
"List all available configuration settings in JSON format": output.WithHighLightFormat(
"azd config options -o json",
),
"List all available configuration settings in table format": output.WithHighLightFormat(
"azd config options -o table",
),
})
}

// 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
}

if a.formatter.Kind() == output.TableFormat {
// 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 = "<object>"
case []any:
currentValue = "<array>"
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
}

// Default format: human-readable list (similar to list-alpha)
var optionsOutput []string
for _, option := range options {
var parts []string
parts = append(parts, fmt.Sprintf("Key: %s", option.Key))
parts = append(parts, fmt.Sprintf("Description: %s", option.Description))

// Get current value from config if available
if !strings.HasPrefix(option.Key, config.EnvOnlyPrefix) {
if val, ok := currentConfig.Get(option.Key); ok {
var currentValue string
switch v := val.(type) {
case string:
currentValue = v
case map[string]any:
currentValue = "<object>"
case []any:
currentValue = "<array>"
default:
currentValue = fmt.Sprintf("%v", v)
}
parts = append(parts, fmt.Sprintf("Current Value: %s", currentValue))
}
}

if len(option.AllowedValues) > 0 {
parts = append(parts, fmt.Sprintf("Allowed Values: %s", strings.Join(option.AllowedValues, ", ")))
}

if option.EnvVar != "" {
parts = append(parts, fmt.Sprintf("Environment Variable: %s", option.EnvVar))
}

if option.Example != "" {
parts = append(parts, fmt.Sprintf("Example: %s", option.Example))
}

optionsOutput = append(optionsOutput, strings.Join(parts, "\n"))
}

a.console.Message(ctx, strings.Join(optionsOutput, "\n\n"))

// No UX output
return nil, nil
}
139 changes: 139 additions & 0 deletions cli/azd/cmd/config_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"bytes"
"context"
"encoding/json"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/require"
)

func TestConfigOptionsAction_JSON(t *testing.T) {
buf := &bytes.Buffer{}
mockContext := mocks.NewMockContext(context.Background())
console := mockContext.Console

// Create a temporary config file
tempDir := t.TempDir()
configPath := tempDir + "/config.json"
manager := config.NewManager()
fileConfigManager := config.NewFileConfigManager(manager)
userConfigManager := config.NewUserConfigManager(fileConfigManager)

// Save an empty config
err := fileConfigManager.Save(config.NewEmptyConfig(), configPath)
require.NoError(t, err)

action := newConfigOptionsAction(
console,
&output.JsonFormatter{},
buf,
userConfigManager,
[]string{},
)

_, err = action.Run(*mockContext.Context)
require.NoError(t, err)

var options []config.ConfigOption
err = json.Unmarshal(buf.Bytes(), &options)
require.NoError(t, err)
require.NotEmpty(t, options)

// Verify expected options are present
foundDefaults := false
foundAlpha := false
for _, opt := range options {
if opt.Key == "defaults.subscription" {
foundDefaults = true
require.Equal(t, "string", opt.Type)
}
if opt.Key == "alpha.all" {
foundAlpha = true
require.Contains(t, opt.AllowedValues, "on")
require.Contains(t, opt.AllowedValues, "off")
}
}
require.True(t, foundDefaults, "defaults.subscription should be present")
require.True(t, foundAlpha, "alpha.all should be present")
}

func TestConfigOptionsAction_Table(t *testing.T) {
buf := &bytes.Buffer{}
mockContext := mocks.NewMockContext(context.Background())
console := mockContext.Console

tempDir := t.TempDir()
configPath := tempDir + "/config.json"
manager := config.NewManager()
fileConfigManager := config.NewFileConfigManager(manager)
userConfigManager := config.NewUserConfigManager(fileConfigManager)

err := fileConfigManager.Save(config.NewEmptyConfig(), configPath)
require.NoError(t, err)

action := newConfigOptionsAction(
console,
&output.TableFormatter{},
buf,
userConfigManager,
[]string{},
)

_, err = action.Run(*mockContext.Context)
require.NoError(t, err)

outputStr := buf.String()
require.Contains(t, outputStr, "Key")
require.Contains(t, outputStr, "Description")
require.Contains(t, outputStr, "defaults.subscription")
require.Contains(t, outputStr, "alpha.all")
}

func TestConfigOptionsAction_DefaultFormat(t *testing.T) {
buf := &bytes.Buffer{}
mockContext := mocks.NewMockContext(context.Background())
console := mockContext.Console

tempDir := t.TempDir()
configPath := tempDir + "/config.json"
manager := config.NewManager()
fileConfigManager := config.NewFileConfigManager(manager)
userConfigManager := config.NewUserConfigManager(fileConfigManager)

err := fileConfigManager.Save(config.NewEmptyConfig(), configPath)
require.NoError(t, err)

action := newConfigOptionsAction(
console,
&output.NoneFormatter{},
buf,
userConfigManager,
[]string{},
)

_, err = action.Run(*mockContext.Context)
require.NoError(t, err)

// For NoneFormatter, output goes to console.Message(), check the console's output
output := console.Output()
require.NotEmpty(t, output)
outputStr := output[0] // Should be a single message
require.Contains(t, outputStr, "Key: defaults.subscription")
require.Contains(t, outputStr, "Description:")
require.Contains(t, outputStr, "Key: alpha.all")
require.Contains(t, outputStr, "Allowed Values:")
}

func TestConfigOptionsAction_WithCurrentValues(t *testing.T) {
t.Skip("UserConfigManager loads from global config path, making this test complex to set up properly")
// This test would require mocking the global config directory or setting AZD_CONFIG_DIR
// The functionality is better tested through end-to-end tests or manual testing
}
8 changes: 8 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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.',
Expand Down
Loading
Loading