Skip to content

Commit 5560b37

Browse files
authored
feat(config): enable permissive config file parsing (#472)
1 parent 26bc986 commit 5560b37

16 files changed

+261
-50
lines changed

internal/app/azldev/app.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,17 @@ type PostInitCallbackFunc func(app *App, env *Env) error
4040
// An instance of the azldev CLI application. This is typically used as a singleton.
4141
type App struct {
4242
// Global options for the CLI.
43-
explicitProjectDir string
44-
verbose bool
45-
quiet bool
46-
acceptAllPrompts bool
47-
dryRun bool
48-
networkRetries int
49-
reportFormat ReportFormat
50-
disableDefaultConfig bool
51-
configFiles []string
52-
colorMode ColorMode
43+
explicitProjectDir string
44+
verbose bool
45+
quiet bool
46+
acceptAllPrompts bool
47+
dryRun bool
48+
networkRetries int
49+
reportFormat ReportFormat
50+
disableDefaultConfig bool
51+
permissiveConfigParsing bool
52+
configFiles []string
53+
colorMode ColorMode
5354

5455
// Root command for the CLI.
5556
cmd cobra.Command
@@ -127,6 +128,7 @@ func NewApp(fsFactory opctx.FileSystemFactory, osEnvFactory opctx.OSEnvFactory)
127128
env.SetAcceptAllPrompts(app.acceptAllPrompts)
128129
env.SetColorMode(app.colorMode)
129130
env.SetNetworkRetries(app.networkRetries)
131+
env.SetPermissiveConfigParsing(app.permissiveConfigParsing)
130132

131133
return nil
132134
},
@@ -165,6 +167,8 @@ func NewApp(fsFactory opctx.FileSystemFactory, osEnvFactory opctx.OSEnvFactory)
165167
"output format {csv, json, markdown, table}")
166168
app.cmd.PersistentFlags().Var(&app.colorMode, "color",
167169
"output colorization mode {always, auto, never}")
170+
app.cmd.PersistentFlags().BoolVar(&app.permissiveConfigParsing, "permissive-config",
171+
false, "do not fail on unknown fields in TOML config files")
168172

169173
return app
170174
}
@@ -443,6 +447,8 @@ func (a *App) handParseConfigFlags(args []string) {
443447
}
444448
case "--no-default-config":
445449
a.disableDefaultConfig = true
450+
case "--permissive-config":
451+
a.permissiveConfigParsing = true
446452
case "--config-file":
447453
index++
448454
if index < len(args) {
@@ -498,6 +504,7 @@ func (a *App) findAndLoadConfig(dryRunnable opctx.DryRunnable, tempDirPath strin
498504
a.disableDefaultConfig,
499505
tempDirPath,
500506
extraConfigFiles,
507+
a.permissiveConfigParsing,
501508
)
502509
if err != nil {
503510
return projectDir, config, fmt.Errorf("failed to load project configuration:\n%w", err)

internal/app/azldev/app_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,53 @@ func TestApp_QuietLongOption(t *testing.T) {
143143
app.Execute([]string{"--quiet"})
144144
assert.True(t, callbackCalled)
145145
}
146+
147+
func TestApp_PermissiveConfigOption(t *testing.T) {
148+
app := createTestApp(t)
149+
150+
ran := false
151+
cmd := &cobra.Command{
152+
Use: "test-cmd",
153+
RunE: func(cmd *cobra.Command, args []string) error {
154+
env, err := azldev.GetEnvFromCommand(cmd)
155+
require.NoError(t, err)
156+
157+
assert.True(t, env.PermissiveConfigParsing())
158+
159+
ran = true
160+
161+
return nil
162+
},
163+
}
164+
165+
app.AddTopLevelCommand(cmd)
166+
167+
result := app.Execute([]string{"--permissive-config", "test-cmd"})
168+
assert.Zero(t, result)
169+
assert.True(t, ran)
170+
}
171+
172+
func TestApp_PermissiveConfigOption_DefaultFalse(t *testing.T) {
173+
app := createTestApp(t)
174+
175+
ran := false
176+
cmd := &cobra.Command{
177+
Use: "test-cmd",
178+
RunE: func(cmd *cobra.Command, args []string) error {
179+
env, err := azldev.GetEnvFromCommand(cmd)
180+
require.NoError(t, err)
181+
182+
assert.False(t, env.PermissiveConfigParsing())
183+
184+
ran = true
185+
186+
return nil
187+
},
188+
}
189+
190+
app.AddTopLevelCommand(cmd)
191+
192+
result := app.Execute([]string{"test-cmd"})
193+
assert.Zero(t, result)
194+
assert.True(t, ran)
195+
}

internal/app/azldev/env.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,15 @@ type Env struct {
5858
classicToolkitDir string
5959

6060
// Tool behavior/preferences.
61-
defaultReportFormat ReportFormat
62-
colorMode ColorMode
63-
reportFile io.Writer
64-
verbose bool
65-
quiet bool
66-
promptsAllowed bool
67-
acceptAllPrompts bool
68-
networkRetries int
61+
defaultReportFormat ReportFormat
62+
colorMode ColorMode
63+
reportFile io.Writer
64+
verbose bool
65+
quiet bool
66+
promptsAllowed bool
67+
acceptAllPrompts bool
68+
networkRetries int
69+
permissiveConfigParsing bool
6970

7071
// Injected dependencies.
7172
cmdFactory opctx.CmdFactory
@@ -124,12 +125,13 @@ func NewEnv(ctx context.Context, options EnvOptions) *Env {
124125
osEnvFactory: options.Interfaces.OSEnvFactory,
125126

126127
// Tool behavior/preferences
127-
defaultReportFormat: ReportFormatTable,
128-
colorMode: ColorModeAuto,
129-
reportFile: os.Stdout,
130-
verbose: false,
131-
quiet: false,
132-
promptsAllowed: isatty.IsTerminal(os.Stdin.Fd()),
128+
defaultReportFormat: ReportFormatTable,
129+
colorMode: ColorModeAuto,
130+
reportFile: os.Stdout,
131+
verbose: false,
132+
quiet: false,
133+
promptsAllowed: isatty.IsTerminal(os.Stdin.Fd()),
134+
permissiveConfigParsing: false,
133135

134136
// Start time.
135137
constructionTime: time.Now(),
@@ -187,6 +189,18 @@ func (env *Env) SetNetworkRetries(retries int) {
187189
env.networkRetries = retries
188190
}
189191

192+
// PermissiveConfigParsing returns whether permissive parsing of configuration files
193+
// is enabled, where unknown fields are ignored instead of causing an error.
194+
func (env *Env) PermissiveConfigParsing() bool {
195+
return env.permissiveConfigParsing
196+
}
197+
198+
// SetPermissiveConfigParsing enables or disables permissive parsing of
199+
// configuration files, where unknown fields are ignored instead of causing an error.
200+
func (env *Env) SetPermissiveConfigParsing(permissive bool) {
201+
env.permissiveConfigParsing = permissive
202+
}
203+
190204
// SetEventListener registers the event listener to be used in this environment.
191205
func (env *Env) SetEventListener(eventListener opctx.EventListener) {
192206
env.eventListener = eventListener

internal/app/azldev/env_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func TestNewEnv(t *testing.T) {
5454
assert.Equal(t, env.ReportFile(), os.Stdout)
5555
assert.False(t, env.Quiet())
5656
assert.False(t, env.Verbose())
57+
assert.False(t, env.PermissiveConfigParsing())
5758

5859
// Confirm that our parameters were appropriately wrapped.
5960
assert.Equal(t, testProjectRoot, env.ProjectDir())
@@ -94,6 +95,21 @@ func TestSetNetworkRetries(t *testing.T) {
9495
}
9596
}
9697

98+
func TestSetPermissiveConfigParsing(t *testing.T) {
99+
testEnv := testutils.NewTestEnv(t)
100+
101+
// Default should be false.
102+
assert.False(t, testEnv.Env.PermissiveConfigParsing())
103+
104+
// Setting to true should stick.
105+
testEnv.Env.SetPermissiveConfigParsing(true)
106+
assert.True(t, testEnv.Env.PermissiveConfigParsing())
107+
108+
// Setting back to false should work.
109+
testEnv.Env.SetPermissiveConfigParsing(false)
110+
assert.False(t, testEnv.Env.PermissiveConfigParsing())
111+
}
112+
97113
func TestEnvConstructionTime(t *testing.T) {
98114
testEnv := testutils.NewTestEnv(t)
99115

internal/projectconfig/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func LoadProjectConfig(
2323
disableDefaultConfig bool,
2424
tempDirPath string,
2525
extraConfigFilePaths []string,
26+
permissiveConfigParsing bool,
2627
) (projectDir string, config *ProjectConfig, err error) {
2728
// Look for project root and azldev.toml file.
2829
projectDir, projectFilePath, err := FindProjectRootAndConfigFile(fs, referenceDir)
@@ -59,7 +60,7 @@ func LoadProjectConfig(
5960
//
6061
// NOTE: We don't wrap the error returned back here (if one is returned) because we already have
6162
// a decent one coming from this function.
62-
config, err = loadAndResolveProjectConfig(fs, configFilePaths...)
63+
config, err = loadAndResolveProjectConfig(fs, permissiveConfigParsing, configFilePaths...)
6364
if err != nil {
6465
return "", nil, err
6566
}

internal/projectconfig/loader.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ var (
3030
// Loads and resolves the project configuration files located at the given path. Referenced include files
3131
// are recursively loaded and appropriately merged. If multiple file paths are provided, they are each
3232
// fully loaded and merged in specified order, with later files overriding earlier ones.
33-
func loadAndResolveProjectConfig(fs opctx.FS, configFilePaths ...string) (*ProjectConfig, error) {
33+
func loadAndResolveProjectConfig(
34+
fs opctx.FS, permissiveConfigParsing bool, configFilePaths ...string,
35+
) (*ProjectConfig, error) {
3436
resolvedCfg := &ProjectConfig{
3537
ComponentGroups: make(map[string]ComponentGroupConfig),
3638
Components: make(map[string]ComponentConfig),
@@ -40,7 +42,7 @@ func loadAndResolveProjectConfig(fs opctx.FS, configFilePaths ...string) (*Proje
4042

4143
for _, configFilePath := range configFilePaths {
4244
// Load the project config file and all transitive includes.
43-
err := loadAndMergeConfigWithIncludes(resolvedCfg, fs, configFilePath)
45+
err := loadAndMergeConfigWithIncludes(resolvedCfg, fs, configFilePath, permissiveConfigParsing)
4446
if err != nil {
4547
return nil, err
4648
}
@@ -55,9 +57,12 @@ func loadAndResolveProjectConfig(fs opctx.FS, configFilePaths ...string) (*Proje
5557
return resolvedCfg, nil
5658
}
5759

58-
func loadAndMergeConfigWithIncludes(configToUpdate *ProjectConfig, fs opctx.FS, filePath string) error {
60+
func loadAndMergeConfigWithIncludes(
61+
configToUpdate *ProjectConfig, fs opctx.FS, filePath string,
62+
permissiveConfigParsing bool,
63+
) error {
5964
// Load the project config file and all transitive includes.
60-
loadedCfgs, err := loadProjectConfigWithIncludes(fs, filePath)
65+
loadedCfgs, err := loadProjectConfigWithIncludes(fs, filePath, permissiveConfigParsing)
6166
if err != nil {
6267
return err
6368
}
@@ -154,9 +159,11 @@ func mergeConfigFile(resolvedCfg *ProjectConfig, loadedCfg *ConfigFile) error {
154159
return nil
155160
}
156161

157-
func loadProjectConfigWithIncludes(fs opctx.FS, filePath string) ([]*ConfigFile, error) {
162+
func loadProjectConfigWithIncludes(
163+
fs opctx.FS, filePath string, permissiveConfigParsing bool,
164+
) ([]*ConfigFile, error) {
158165
// Load the immediate config file.
159-
cfg, err := loadProjectConfigFile(fs, filePath)
166+
cfg, err := loadProjectConfigFile(fs, filePath, permissiveConfigParsing)
160167
if err != nil {
161168
return nil, err
162169
}
@@ -188,7 +195,9 @@ func loadProjectConfigWithIncludes(fs opctx.FS, filePath string) ([]*ConfigFile,
188195
for _, includePath := range matches {
189196
absIncludePath := makeAbsolute(cfg.dir, includePath)
190197

191-
includeCfgs, err := loadProjectConfigWithIncludes(fs, absIncludePath)
198+
includeCfgs, err := loadProjectConfigWithIncludes(
199+
fs, absIncludePath, permissiveConfigParsing,
200+
)
192201
if err != nil {
193202
return nil, err
194203
}
@@ -200,7 +209,9 @@ func loadProjectConfigWithIncludes(fs opctx.FS, filePath string) ([]*ConfigFile,
200209
return allCfgs, nil
201210
}
202211

203-
func loadProjectConfigFile(fs opctx.FS, filePath string) (*ConfigFile, error) {
212+
func loadProjectConfigFile(
213+
fs opctx.FS, filePath string, permissiveConfigParsing bool,
214+
) (*ConfigFile, error) {
204215
slog.Debug("Loading project config", "filePath", filePath)
205216

206217
absFilePath, err := filepath.Abs(filePath)
@@ -214,7 +225,10 @@ func loadProjectConfigFile(fs opctx.FS, filePath string) (*ConfigFile, error) {
214225
}
215226

216227
decoder := toml.NewDecoder(projectFile)
217-
decoder.DisallowUnknownFields()
228+
229+
if !permissiveConfigParsing {
230+
decoder.DisallowUnknownFields()
231+
}
218232

219233
cfg := &ConfigFile{}
220234

0 commit comments

Comments
 (0)