Skip to content

Commit 49820ff

Browse files
committed
feat: allowing use of parameters over config
1 parent 84c2801 commit 49820ff

File tree

3 files changed

+142
-26
lines changed

3 files changed

+142
-26
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ voyage deploy -config /path/to/config.json
4040
As an alternative to providing all arguments on the command line, you can use a JSON configuration file by specifying the `-config` flag.
4141

4242
> [!NOTE]
43-
> When the `-config` flag is used, all other command-line arguments for the deploy command are ignored.
43+
> When the `-config` flag is used, values from the configuration file will be overridden by any command-line arguments that are provided.
4444
4545
**Example `config.json`:**
4646

command/parser.go

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,57 @@ import (
88
"strings"
99
)
1010

11+
// Constants for default values and configuration
12+
const (
13+
defaultLogLevel = "info"
14+
)
15+
16+
// PrintUsageFunc represents a function that prints command usage information
1117
type PrintUsageFunc func()
1218

19+
// missingParamsError represents an error when required parameters are missing
1320
type missingParamsError struct {
1421
params []string
1522
}
1623

24+
// Error implements the error interface for missingParamsError
1725
func (e *missingParamsError) Error() string {
26+
if len(e.params) == 1 {
27+
return fmt.Sprintf("missing required parameter: %s", e.params[0])
28+
}
1829
return fmt.Sprintf("missing required parameters: %s", strings.Join(e.params, ", "))
1930
}
2031

32+
// deployCommandParametersParser parses command line arguments and configuration file
33+
// to create DeployCommandParameters for the deploy command.
34+
//
35+
// It supports both command-line flags and JSON configuration files, with command-line
36+
// flags taking precedence over configuration file values.
37+
//
38+
// Returns the parsed parameters, a usage function, and any error encountered.
2139
func deployCommandParametersParser(args []string) (DeployCommandParameters, PrintUsageFunc, error) {
40+
fs := setupDeployFlags()
41+
42+
if err := fs.Parse(args); err != nil {
43+
return DeployCommandParameters{}, fs.Usage, err
44+
}
45+
46+
params, err := loadConfigFromFile(fs)
47+
if err != nil {
48+
return DeployCommandParameters{}, fs.Usage, err
49+
}
50+
51+
params = overrideWithFlags(fs, params)
52+
53+
if err := validateParameters(params); err != nil {
54+
return DeployCommandParameters{}, fs.Usage, err
55+
}
56+
57+
return params, fs.Usage, nil
58+
}
59+
60+
// setupDeployFlags creates and configures the flag set for deploy command
61+
func setupDeployFlags() *flag.FlagSet {
2262
fs := flag.NewFlagSet("deploy", flag.ContinueOnError)
2363
fs.Usage = func() {
2464
fmt.Fprintf(fs.Output(), "Usage of %s:\n", fs.Name())
@@ -29,40 +69,75 @@ func deployCommandParametersParser(args []string) (DeployCommandParameters, Prin
2969
fmt.Fprintf(fs.Output(), " voyage deploy -config my-config.json\n")
3070
}
3171

32-
configPath := fs.String("config", "", "path to a JSON configuration file")
33-
repo := fs.String("r", "", "repository name")
34-
var composePaths stringSlice
35-
fs.Var(&composePaths, "c", "path to docker-compose.yml (can be specified multiple times)")
36-
branch := fs.String("b", "", "branch name")
37-
outPath := fs.String("o", "", "out path")
38-
force := fs.Bool("f", false, "force deployment even if no changes detected")
39-
logLevel := fs.String("l", "info", "log level (debug, info, error, fatal)")
72+
fs.String("config", "", "path to a JSON configuration file")
73+
fs.String("r", "", "repository name")
74+
fs.Var(&stringSlice{}, "c", "path to docker-compose.yml (can be specified multiple times)")
75+
fs.String("b", "", "branch name")
76+
fs.String("o", "", "out path")
77+
fs.Bool("f", false, "force deployment even if no changes detected")
78+
fs.String("l", defaultLogLevel, "log level (debug, info, error, fatal)")
4079

41-
if err := fs.Parse(args); err != nil {
42-
return DeployCommandParameters{}, fs.Usage, err
43-
}
80+
return fs
81+
}
4482

83+
// loadConfigFromFile loads parameters from configuration file if provided
84+
func loadConfigFromFile(fs *flag.FlagSet) (DeployCommandParameters, error) {
4585
params := DeployCommandParameters{}
86+
configPath := fs.Lookup("config").Value.String()
4687

47-
if *configPath != "" {
48-
file, err := os.ReadFile(*configPath)
88+
if configPath != "" {
89+
file, err := os.ReadFile(configPath)
4990
if err != nil {
50-
return DeployCommandParameters{}, fs.Usage, fmt.Errorf("error reading config file %s: %w", *configPath, err)
91+
return params, fmt.Errorf("error reading config file %s: %w", configPath, err)
5192
}
5293
if err := json.Unmarshal(file, &params); err != nil {
53-
return DeployCommandParameters{}, fs.Usage, fmt.Errorf("error parsing config file %s: %w", *configPath, err)
94+
return params, fmt.Errorf("error parsing config file %s: %w", configPath, err)
95+
}
96+
}
97+
98+
return params, nil
99+
}
100+
101+
// overrideWithFlags applies command-line flag values to override config file values
102+
func overrideWithFlags(fs *flag.FlagSet, params DeployCommandParameters) DeployCommandParameters {
103+
// Override with command-line flags if they were provided
104+
if repo := fs.Lookup("r").Value.String(); repo != "" {
105+
params.Repo = repo
106+
}
107+
108+
if composePathsFlag := fs.Lookup("c"); composePathsFlag != nil {
109+
if composePaths, ok := composePathsFlag.Value.(*stringSlice); ok && len(*composePaths) > 0 {
110+
params.RemoteComposePaths = []string(*composePaths)
54111
}
55-
} else {
56-
params.Repo = *repo
57-
params.RemoteComposePaths = composePaths
58-
params.Branch = *branch
59-
params.OutPath = *outPath
60-
params.Force = *force
61-
params.LogLevel = *logLevel
62112
}
63113

64-
// Validate the final parameters
114+
if branch := fs.Lookup("b").Value.String(); branch != "" {
115+
params.Branch = branch
116+
}
117+
118+
if outPath := fs.Lookup("o").Value.String(); outPath != "" {
119+
params.OutPath = outPath
120+
}
121+
122+
// Handle boolean flag - check if it was explicitly set
123+
if forceFlag := fs.Lookup("f"); forceFlag.Value.String() == "true" {
124+
params.Force = true
125+
}
126+
127+
// Handle log level - always override if different from default
128+
if logLevel := fs.Lookup("l").Value.String(); logLevel != defaultLogLevel {
129+
params.LogLevel = logLevel
130+
} else if params.LogLevel == "" {
131+
params.LogLevel = defaultLogLevel
132+
}
133+
134+
return params
135+
}
136+
137+
// validateParameters validates that all required parameters are present
138+
func validateParameters(params DeployCommandParameters) error {
65139
var missingParams []string
140+
66141
if params.Repo == "" {
67142
missingParams = append(missingParams, "-r (repository)")
68143
}
@@ -77,8 +152,8 @@ func deployCommandParametersParser(args []string) (DeployCommandParameters, Prin
77152
}
78153

79154
if len(missingParams) > 0 {
80-
return DeployCommandParameters{}, fs.Usage, &missingParamsError{params: missingParams}
155+
return &missingParamsError{params: missingParams}
81156
}
82157

83-
return params, fs.Usage, nil
158+
return nil
84159
}

command/parser_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,47 @@ func TestDeployCommandParametersParser(t *testing.T) {
7979
}
8080
})
8181

82+
t.Run(("Merges config file and flags, with flags taking precedence"), func(t *testing.T) {
83+
tempDir := t.TempDir()
84+
configPath := filepath.Join(tempDir, "config.json")
85+
configContent := `{
86+
"repo": "my-repo",
87+
"branch": "main",
88+
"outPath": "/tmp/voyage",
89+
"remoteComposePaths": ["docker-compose.yml"],
90+
"force": false,
91+
"logLevel": "info"
92+
}`
93+
94+
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
95+
t.Fatal(err)
96+
}
97+
98+
args := []string{
99+
"-config", configPath,
100+
"-b", "feature-branch", // Override branch
101+
"-f", // Override force to true
102+
}
103+
104+
params, _, err := deployCommandParametersParser(args)
105+
if err != nil {
106+
t.Fatalf("Expected no error, but got %v", err)
107+
}
108+
109+
if params.Repo != "my-repo" {
110+
t.Errorf("Expected repo to be 'my-repo', got '%s'", params.Repo)
111+
}
112+
if params.Branch != "feature-branch" {
113+
t.Errorf("Expected branch to be 'feature-branch', got '%s'", params.Branch)
114+
}
115+
if !params.Force {
116+
t.Errorf("Expected force to be true, got false")
117+
}
118+
if params.LogLevel != "info" {
119+
t.Errorf("Expected logLevel to be 'info', got '%s'", params.LogLevel)
120+
}
121+
})
122+
82123
t.Run("Returns error for non-existent config file", func(t *testing.T) {
83124
args := []string{"-config", "/path/to/non-existent-config.json"}
84125
_, _, err := deployCommandParametersParser(args)

0 commit comments

Comments
 (0)