Skip to content

Commit 503055c

Browse files
Correct configuration state priority merging
To ensure loaded data from configuration files override default application values and CLI flags always take precedence over all other loaded states the overall loading logic has been refactored. It is achieved by implementing a command initialization function (called before each command) that merges the configuration states in the correct order. Also the loading logic has been moved from `pkg/config/constants.go` into the `pkg/config/config.go` file to better separate the purpose of both files. Epic GH-33 Resolves GH-68
1 parent 008edbc commit 503055c

File tree

4 files changed

+105
-51
lines changed

4 files changed

+105
-51
lines changed

cmd/snowsaw/snowsaw.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ var (
3030
// debug indicates if the `debug` flag has been set to enable configure the logging for the debug scope.
3131
debug bool
3232
// explicitConfigFilePath stores the path to the application configuration file when the `config` flag is specified.
33+
// It takes precedence over the default application configuration paths.
3334
explicitConfigFilePath string
35+
// snowblockBaseDirs stores the comma-separated paths to the snowblock directories when the `basedirs` flag is
36+
// specified.
37+
// It takes precedence over the path of the application configuration file(s).
38+
snowblockBaseDirs []string
3439
)
3540

3641
// rootCmd is the root command of the application.
@@ -48,7 +53,7 @@ var rootCmd = &cobra.Command{
4853
// Run is the main application function that adds all child commands to the root command and sets flags appropriately.
4954
// This is called by `main.main()` and only needs to be run once for the root command.
5055
func Run() {
51-
// Disable verbose errors to provide custom formatted CLI output via application-wide printer.
56+
// Disable cobra's verbose errors and replace with custom formatted CLI logger from the `pkg/prt` package.
5257
rootCmd.SilenceErrors = true
5358

5459
// Run the application with the given commands, flags and arguments and exit on any (downstream) error.
@@ -60,35 +65,39 @@ func Run() {
6065

6166
func init() {
6267
// Specify the functions to be run before each command gets executed.
63-
cobra.OnInitialize(initDebugScope, initConfig, initPrinter)
68+
cobra.OnInitialize(initDebugScope, initConfig, initPrinter, mergeConfigValues)
6469

6570
// Define global application flags.
66-
rootCmd.PersistentFlags().StringVar(&explicitConfigFilePath, "config", "", "set the configuration file")
71+
rootCmd.PersistentFlags().StringVar(&explicitConfigFilePath, "config", "", "path to the configuration file")
6772
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug information output")
73+
rootCmd.PersistentFlags().StringSliceVarP(
74+
&snowblockBaseDirs, "basedirs", "b", config.AppConfig.Snowblocks.Paths,
75+
"comma-separated paths to snowblock base directories")
6876

6977
// Set the app version information for the automatically generated `version` flag.
7078
rootCmd.Version = color.CyanString(config.Version)
7179
rootCmd.SetVersionTemplate(`{{printf "%s\n" .Version}}`)
7280

7381
// Create and register all subcommands.
7482
rootCmd.AddCommand(info.NewInfoCmd())
83+
rootCmd.AddCommand(bootstrap.NewBootstrapCmd())
7584
}
7685

7786
// initConfig searches and loads either the default application configuration file paths or the explicit file at the
7887
// given path specified through the global `config` flag.
7988
func initConfig() {
8089
if explicitConfigFilePath != "" {
81-
if err := builder.Load(file.NewFile(explicitConfigFilePath)).Into(&config.AppConfig); err != nil {
82-
prt.Errorf("while loading custom application configuration file:\n%v", err)
90+
if err := builder.Load(file.NewFile(explicitConfigFilePath)).Into(&config.AppConfig, false); err != nil {
91+
prt.Errorf("Failed to load application configuration file: %v", err)
8392
os.Exit(1)
8493
}
8594
} else {
8695
b := builder.Load(config.AppConfigPaths...)
8796
if len(b.Files) == 0 {
88-
prt.Debugf("No configuration files found, using default application configuration.")
97+
prt.Debugf("No configuration files found, using application defaults.")
8998
}
90-
if err := b.Into(&config.AppConfig); err != nil {
91-
prt.Errorf("while loading application configuration files:\n%v", err)
99+
if err := b.Into(&config.AppConfig, true); err != nil {
100+
prt.Errorf("Failed to load application configuration file(s): %v", err)
92101
os.Exit(1)
93102
}
94103
}
@@ -103,13 +112,36 @@ func initDebugScope() {
103112

104113
// setPrinterVerbosityLevel configures the global CLI printer like the verbosity level.
105114
func initPrinter() {
106-
lvl, err := prt.ParseVerbosityLevel(strings.ToUpper(config.AppConfig.LogLevel))
107-
if err != nil {
108-
prt.Debugf("Error while parsing log level from configuration: %v", err)
109-
prt.Debugf("Using default INFO level as fallback")
110-
prt.SetVerbosityLevel(prt.InfoVerbosity)
111-
} else {
112-
prt.Debugf("Using configured logger level: %s", strings.ToUpper(config.AppConfig.LogLevel))
113-
prt.SetVerbosityLevel(lvl)
115+
// The `debug` flag always takes precedence and overrides the application configuration value.
116+
if !debug {
117+
lvl, err := prt.ParseVerbosityLevel(strings.ToUpper(config.AppConfig.LogLevel))
118+
if err != nil {
119+
prt.Debugf("Error while parsing log level from configuration: %v", err)
120+
prt.Debugf("Using default INFO level as fallback")
121+
prt.SetVerbosityLevel(prt.InfoVerbosity)
122+
} else {
123+
prt.Debugf("Using configured logger level: %s", strings.ToUpper(config.AppConfig.LogLevel))
124+
prt.SetVerbosityLevel(lvl)
125+
}
126+
}
127+
}
128+
129+
// mergeConfigValues merges the specified (global) flags and merges the corresponding values with the default
130+
// application configuration values.
131+
// Since flags have the highest precedence their value will override application defaults as well as value from
132+
// loaded application configuration files.
133+
func mergeConfigValues() {
134+
// Use configured or individual snowblock base directories if specified, otherwise fall back to the default directory.
135+
if len(config.AppConfig.Snowblocks.BaseDirs) == 0 {
136+
config.AppConfig.Snowblocks.BaseDirs = append(
137+
config.AppConfig.Snowblocks.BaseDirs, config.DefaultSnowblocksBaseDirectoryName)
138+
}
139+
if len(snowblockBaseDirs) > 0 {
140+
config.AppConfig.Snowblocks.BaseDirs = snowblockBaseDirs
141+
}
142+
143+
// If the logging level has not been specified fall back to the default level.
144+
if config.AppConfig.LogLevel == "" {
145+
config.AppConfig.LogLevel = config.DefaultLoggingLevel
114146
}
115147
}

magefile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func Test() {
208208
mg.SerialDeps(unitTests)
209209
}
210210

211-
// Test runs all unit tests with enabled race detection.
211+
// TestCover runs all unit tests with with coverage reports and enabled race detection.
212212
func TestCover() {
213213
mg.SerialDeps(Clean)
214214
// Ensure the required directory structure exists, `go test` doesn't create it automatically.

pkg/config/config.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,52 @@
1313
// It provides a file abstraction to de/encode, load and validate YAML and JSON data using the builder design pattern.
1414
package config
1515

16+
import (
17+
"fmt"
18+
"os"
19+
"path/filepath"
20+
21+
"github.com/mitchellh/go-homedir"
22+
23+
"github.com/arcticicestudio/snowsaw/pkg/config/encoder"
24+
"github.com/arcticicestudio/snowsaw/pkg/config/source/file"
25+
)
26+
1627
// Config represents the application-wide configurations.
1728
type Config struct {
18-
// LogLevel is the verbosity level of the application-wide logging behavior.
19-
LogLevel string `json:"logLevel"`
29+
// LogLevel is the application-wide logging verbosity level.
30+
LogLevel string `json:"logLevel" yaml:"logLevel"`
31+
32+
// Snowblocks are general snowblocks configurations.
33+
Snowblocks Snowblocks `json:"snowblocks,flow" yaml:"snowblocks,flow"`
34+
}
35+
36+
// Snowblocks represents the general snowblocks configurations.
37+
type Snowblocks struct {
38+
// BaseDirs are the paths of the snowblock base directories.
39+
BaseDirs []string `json:"baseDirs,flow" yaml:"baseDirs,flow"`
40+
// Paths are the paths of the snowblocks directories.
41+
Paths []string `json:"paths,flow" yaml:"paths,flow"`
42+
}
43+
44+
func init() {
45+
AppConfigPaths = genConfigPaths()
46+
}
47+
48+
func genConfigPaths() []*file.File {
49+
var files []*file.File
50+
51+
// Include the user-level dotfile configuration from user's home directory.
52+
home, err := homedir.Dir()
53+
if err == nil {
54+
files = append(files, file.NewFile(filepath.Join(home, fmt.Sprintf(".%s.%s", ProjectName, encoder.ExtensionsYaml))))
55+
}
56+
57+
// A file placed in the current working directory takes precedence over the user-level configuration.
58+
pwd, err := os.Getwd()
59+
if err == nil {
60+
files = append(files, file.NewFile(filepath.Join(pwd, fmt.Sprintf("%s.%s", ProjectName, encoder.ExtensionsYaml))))
61+
}
62+
63+
return files
2064
}

pkg/config/constants.go

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@
1212
package config
1313

1414
import (
15-
"fmt"
16-
"os"
17-
"path/filepath"
18-
19-
"github.com/mitchellh/go-homedir"
20-
21-
"github.com/arcticicestudio/snowsaw/pkg/config/encoder"
15+
"github.com/arcticicestudio/snowsaw/pkg/api/snowblock"
2216
"github.com/arcticicestudio/snowsaw/pkg/config/source/file"
17+
"github.com/arcticicestudio/snowsaw/pkg/snowblock/task"
18+
"github.com/arcticicestudio/snowsaw/pkg/snowblock/task/link"
2319
)
2420

2521
const (
22+
// DefaultLoggingLevel is the default application-wide level of logging the verbosity.
23+
DefaultLoggingLevel = "info"
24+
25+
// DefaultSnowblocksBaseDirectoryName is the default name of the snowblocks base directory.
26+
DefaultSnowblocksBaseDirectoryName = "snowblocks"
27+
2628
// PackageName is the name of this Go module
2729
PackageName = "github.com/arcticicestudio/" + ProjectName
30+
2831
// ProjectName is the name of the project.
2932
ProjectName = "snowsaw"
3033
)
@@ -42,28 +45,3 @@ var (
4245
// Version is the application version.
4346
Version = "0.0.0"
4447
)
45-
46-
func init() {
47-
AppConfig = Config{
48-
LogLevel: "info",
49-
}
50-
AppConfigPaths = genConfigPaths()
51-
}
52-
53-
func genConfigPaths() []*file.File {
54-
var files []*file.File
55-
56-
// Include the user-level dotfile configuration from user's home directory.
57-
home, err := homedir.Dir()
58-
if err == nil {
59-
files = append(files, file.NewFile(filepath.Join(home, fmt.Sprintf(".%s.%s", ProjectName, encoder.ExtensionsYaml))))
60-
}
61-
62-
// A file placed in the current working directory takes precedence over the user-level configuration.
63-
pwd, err := os.Getwd()
64-
if err == nil {
65-
files = append(files, file.NewFile(filepath.Join(pwd, fmt.Sprintf("%s.%s", ProjectName, encoder.ExtensionsYaml))))
66-
}
67-
68-
return files
69-
}

0 commit comments

Comments
 (0)