-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(workflow): base cli for verifying and writing configs (#50)
- Loading branch information
Conor Mc Govern
authored
Jul 5, 2023
1 parent
7652ee1
commit 392db30
Showing
15 changed files
with
1,300 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
venom*.log | ||
test_results | ||
*.out | ||
*.out | ||
|
||
cmd/manager/apid.yml | ||
cmd/manager/config_create_test | ||
cmd/manager/config_test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package cmd | ||
|
||
import ( | ||
"path/filepath" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/ugcompsoc/apid/cmd/manager/utils" | ||
) | ||
|
||
// configCmd represents the config command | ||
var configCmd *cobra.Command | ||
|
||
func NewConfigCmd() *cobra.Command { | ||
configCmd = &cobra.Command{ | ||
Use: "config", | ||
Short: "Displays a config given a directory", | ||
Long: `Prints out the config in the default directory if no directory is specified. | ||
If a directory is specified it will look for a compatible file in there and | ||
will print it out instead.`, | ||
Run: VerifyConfig, | ||
} | ||
|
||
configCmd.AddCommand(NewCreateConfigCmd()) | ||
configCmd.PersistentFlags().BoolP("print", "p", false, "Print config") | ||
configCmd.PersistentFlags().BoolP("secrets", "s", false, "Print secrets") | ||
|
||
return configCmd | ||
} | ||
|
||
func VerifyConfig(cmd *cobra.Command, args []string) { | ||
filename, _ := cmd.Flags().GetString("filename") | ||
err := utils.VerifyFilename(filename) | ||
if err != nil { | ||
cmd.Printf("An error occured while verifying the filename: %s\n", err) | ||
return | ||
} | ||
|
||
directory, _ := cmd.Flags().GetString("directory") | ||
absoluteFilePath := filepath.Join(directory, filename) | ||
c, err := utils.ExtractFile(absoluteFilePath) | ||
if err != nil { | ||
cmd.Printf("An error occured while extracting the file: %s\n", err) | ||
return | ||
} | ||
|
||
issues, err := c.Verify() | ||
if err != nil { | ||
cmd.Printf("An error occured while verifying the config: %s\n", err) | ||
return | ||
} | ||
if len(issues) != 0 { | ||
cmd.Printf("Error(s) were found while parsing %s, please address them:\n", absoluteFilePath) | ||
for _, err := range issues { | ||
cmd.Printf(" - %s\n", err) | ||
} | ||
return | ||
} | ||
|
||
cmd.Print("OK\n") | ||
|
||
print, _ := cmd.Flags().GetBool("print") | ||
printSecrets, _ := cmd.Flags().GetBool("secrets") | ||
if print { | ||
yamlStr, err := utils.PrintConfig(c, printSecrets) | ||
if err != nil { | ||
cmd.Printf("An error occured while attempting to print the config: %s\n", err) | ||
return | ||
} | ||
cmd.Printf("\n%s", yamlStr) | ||
} | ||
} | ||
|
||
func init() { | ||
configCmd = NewConfigCmd() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package cmd | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"time" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/ugcompsoc/apid/cmd/manager/utils" | ||
"github.com/ugcompsoc/apid/internal/config" | ||
"gopkg.in/yaml.v2" | ||
) | ||
|
||
// createConfigCmd represents the config command | ||
var createConfigCmd *cobra.Command | ||
|
||
func NewCreateConfigCmd() *cobra.Command { | ||
createConfigCmd = &cobra.Command{ | ||
Use: "create", | ||
Short: "Create a config for the APId", | ||
Long: `Given a set of APId configuration flags and variables, a config file will | ||
be generated in the folder specified.`, | ||
Run: CreateConfig, | ||
} | ||
|
||
createConfigCmd.Flags().String("log_level", "debug", "Log level; Available values: trace, disabled, panic, fatal, error, warn, info, or debug") | ||
createConfigCmd.Flags().String("timeouts_startup", "30s", "Startup Timeout") | ||
createConfigCmd.Flags().String("timeouts_shutdown", "30s", "Shutdown Timeout") | ||
createConfigCmd.Flags().String("http_listen_address", ":8080", "HTTP Listen Address; In the form of 'IP/DOMAIN:PORT'") | ||
createConfigCmd.Flags().StringSlice("http_cors_allowed_orgins", []string{"*"}, "HTTP CORS Allowed Origins; In the form '[ORIGIN,ORIGIN]'") | ||
createConfigCmd.Flags().String("database_host", "mongodb://ugcompsoc_apid_local_db", "Database Host") | ||
createConfigCmd.Flags().String("database_name", "apid", "Database Name") | ||
createConfigCmd.Flags().String("database_username", "", "Database Username") | ||
createConfigCmd.Flags().String("database_password", "", "Database Password") | ||
createConfigCmd.MarkFlagRequired("database_username") | ||
createConfigCmd.MarkFlagRequired("database_password") | ||
|
||
return createConfigCmd | ||
} | ||
|
||
func CreateConfig(cmd *cobra.Command, args []string) { | ||
c := &config.Config{} | ||
issues := []string{} | ||
var err error | ||
|
||
c.LogLevel, _ = createConfigCmd.Flags().GetString("log_level") | ||
startupTimeout, _ := createConfigCmd.Flags().GetString("timeouts_startup") | ||
c.Timeouts.Startup, err = time.ParseDuration(startupTimeout) | ||
if err != nil { | ||
issues = append(issues, "Could not parse startup timeout. Use the format '[NUMBER]s'") | ||
} | ||
shutdownTimeout, _ := createConfigCmd.Flags().GetString("timeouts_shutdown") | ||
c.Timeouts.Shutdown, err = time.ParseDuration(shutdownTimeout) | ||
if err != nil { | ||
issues = append(issues, "Could not parse shutdown timeout. Use the format '[NUMBER]s'") | ||
} | ||
c.HTTP.ListenAddress, _ = createConfigCmd.Flags().GetString("http_listen_address") | ||
c.HTTP.CORS.AllowedOrigins, _ = createConfigCmd.Flags().GetStringSlice("http_cors_allowed_orgins") | ||
c.Database.Host, _ = createConfigCmd.Flags().GetString("database_host") | ||
c.Database.Name, _ = createConfigCmd.Flags().GetString("database_name") | ||
c.Database.Username, _ = createConfigCmd.Flags().GetString("database_username") | ||
c.Database.Password, _ = createConfigCmd.Flags().GetString("database_password") | ||
if c.Database.Username == "" { | ||
issues = append(issues, "Database username has no default value and is required") | ||
} | ||
if c.Database.Password == "" { | ||
issues = append(issues, "Database password has no default value and is required") | ||
} | ||
|
||
if len(issues) != 0 { | ||
cmd.Print("Error(s) were found while generating the config, please address them:\n") | ||
for _, issue := range issues { | ||
cmd.Printf(" - %s\n", issue) | ||
} | ||
return | ||
} | ||
|
||
filename, _ := cmd.Flags().GetString("filename") | ||
err = utils.VerifyFilename(filename) | ||
if err != nil { | ||
cmd.Printf("An error occured while verifying the filename: %s\n", err) | ||
return | ||
} | ||
directory, _ := cmd.Flags().GetString("directory") | ||
absoluteFilePath := filepath.Join(directory, filename) | ||
|
||
issues, err = c.Verify() | ||
if err != nil { | ||
cmd.Printf("An error occured while verifying the config: %s\n", err) | ||
return | ||
} | ||
if len(issues) != 0 { | ||
cmd.Printf("Error(s) were found while parsing %s, please address them:\n", absoluteFilePath) | ||
for _, err := range issues { | ||
cmd.Printf(" - %s\n", err) | ||
} | ||
return | ||
} | ||
|
||
cYaml, err := yaml.Marshal(c) | ||
if err != nil { | ||
cmd.Printf("Could not marshall the config struct: %s\n", err) | ||
return | ||
} | ||
err = os.WriteFile(absoluteFilePath, cYaml, 0644) | ||
if err != nil { | ||
cmd.Printf("Could not write file to %s: %s\n", absoluteFilePath, err) | ||
return | ||
} | ||
|
||
cmd.Print("OK\n") | ||
|
||
print, _ := cmd.Flags().GetBool("print") | ||
printSecrets, _ := cmd.Flags().GetBool("secrets") | ||
if print { | ||
yamlStr, err := utils.PrintConfig(c, printSecrets) | ||
if err != nil { | ||
cmd.Printf("An error occured while attempting to print the config: %s\n", err) | ||
return | ||
} | ||
cmd.Printf("\n%s", yamlStr) | ||
} | ||
} | ||
|
||
func init() { | ||
createConfigCmd = NewCreateConfigCmd() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package cmd | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestCreateConfig(t *testing.T) { | ||
helpMessage := `Usage: | ||
manager config create [flags] | ||
Flags: | ||
--database_host string Database Host (default "mongodb://ugcompsoc_apid_local_db") | ||
--database_name string Database Name (default "apid") | ||
--database_password string Database Password | ||
--database_username string Database Username | ||
-h, --help help for create | ||
--http_cors_allowed_orgins strings HTTP CORS Allowed Origins; In the form '[ORIGIN,ORIGIN]' (default [*]) | ||
--http_listen_address string HTTP Listen Address; In the form of 'IP/DOMAIN:PORT' (default ":8080") | ||
--log_level string Log level; Available values: trace, disabled, panic, fatal, error, warn, info, or debug (default "debug") | ||
--timeouts_shutdown string Shutdown Timeout (default "30s") | ||
--timeouts_startup string Startup Timeout (default "30s") | ||
Global Flags: | ||
-d, --directory string Directory for config (default ".") | ||
-f, --filename string filename for config (default "apid.yml") | ||
-p, --print Print config | ||
-s, --secrets Print secrets` | ||
|
||
directory := "config_create_test" | ||
if _, err := os.Stat(directory); err != nil { | ||
err := os.Mkdir(directory, os.ModePerm) | ||
assert.NoError(t, err, "could not create test directory") | ||
} | ||
absoluteFilePath := filepath.Join(directory, "apid.yml") | ||
|
||
argsWithDirectory := []string{"config", "create", "-d=" + directory} | ||
argsWithDatabaseSet := append(argsWithDirectory, []string{"--database_password=test_password", "--database_username=test_username"}...) | ||
|
||
errorsWereFoundGenerating := "Error(s) were found while generating the config, please address them:\n" | ||
errorsWereFoundVerifying := "Error(s) were found while parsing " + absoluteFilePath + ", please address them:\n" | ||
|
||
runs := []struct { | ||
name string | ||
args []string | ||
out string | ||
err error | ||
}{ | ||
{ | ||
name: "no default set for database username and password", | ||
args: []string{"config", "create"}, | ||
out: "Error: required flag(s) \"database_password\", \"database_username\" not set\n" + helpMessage, | ||
err: errors.New("Error: required flag(s) \"database_password\", \"database_username\" not set"), | ||
}, | ||
{ | ||
name: "no default set for database username", | ||
args: []string{"config", "create", "--database_password='test'"}, | ||
out: "Error: required flag(s) \"database_username\" not set\n" + helpMessage, | ||
err: errors.New("Error: required flag(s) \"database_username\" not set"), | ||
}, | ||
{ | ||
name: "no default set for database password", | ||
args: []string{"config", "create", "--database_username='testpassword'"}, | ||
out: "Error: required flag(s) \"database_password\" not set\n" + helpMessage, | ||
err: errors.New("Error: required flag(s) \"database_password\" not set"), | ||
}, | ||
{ | ||
name: "no errors and no issues - happy path", | ||
args: argsWithDatabaseSet, | ||
out: "OK", | ||
}, | ||
{ | ||
name: "will not parse startup timeout", | ||
args: append(argsWithDatabaseSet, "--timeouts_startup", "30"), | ||
out: errorsWereFoundGenerating + " - Could not parse startup timeout. Use the format '[NUMBER]s'", | ||
}, | ||
{ | ||
name: "will not parse shutdown timeout", | ||
args: append(argsWithDatabaseSet, "--timeouts_shutdown", "30"), | ||
out: errorsWereFoundGenerating + " - Could not parse shutdown timeout. Use the format '[NUMBER]s'", | ||
}, | ||
{ | ||
name: "database username has no default value", | ||
args: append(argsWithDatabaseSet, "--database_username", ""), | ||
out: errorsWereFoundGenerating + " - Database username has no default value and is required", | ||
}, | ||
{ | ||
name: "database password has no default value", | ||
args: append(argsWithDatabaseSet, "--database_password", ""), | ||
out: errorsWereFoundGenerating + " - Database password has no default value and is required", | ||
}, | ||
{ | ||
name: "log level is not set, from verify function", | ||
args: append(argsWithDatabaseSet, "--log_level", ""), | ||
out: errorsWereFoundVerifying + " - An invalid log level was specified", | ||
}, | ||
{ | ||
name: "will fail to write file because there is no directory", | ||
args: []string{"config", "create", "--directory=dir_not_in_existance", "--database_password=test_password", "--database_username=test_username"}, | ||
out: "Could not write file to dir_not_in_existance/apid.yml: open dir_not_in_existance/apid.yml: no such file or directory", | ||
}, | ||
{ | ||
name: "an invalid filename will cause an issue", | ||
args: append(argsWithDatabaseSet, "--filename=apid"), | ||
out: "An error occured while verifying the filename: The filename is not in the form [NAME].yml", | ||
}, | ||
} | ||
|
||
for _, run := range runs { | ||
t.Run(run.name, func(t *testing.T) { | ||
out, err := execute(t, NewRootCmd(), run.args...) | ||
if run.err == nil { | ||
assert.NoError(t, err, "expected no error running manager") | ||
} else { | ||
assert.Error(t, err, "expected error running manager") | ||
} | ||
assert.Equal(t, run.out, out, "unexpected manager output") | ||
if _, err := os.Stat(directory + "/apid.yml"); err == nil { | ||
os.Remove(directory + "/apid.yml") | ||
} | ||
}) | ||
} | ||
|
||
t.Run("prints multiple issues", func(t *testing.T) { | ||
out, err := execute(t, NewRootCmd(), append(argsWithDatabaseSet, []string{"--database_username=t", "--database_name=t"}...)...) | ||
assert.NoError(t, err, "expected no error running manager") | ||
assert.Equal(t, errorsWereFoundVerifying+` - Mongo database name is not long enough | ||
- Mongo database username is not long enough`, out, "print to screen did not match expected error(s)") | ||
}) | ||
|
||
t.Run("prints secrets from config", func(t *testing.T) { | ||
out, err := execute(t, NewRootCmd(), append(argsWithDatabaseSet, []string{"--print", "--secrets"}...)...) | ||
assert.NoError(t, err, "expected no error running manager") | ||
assert.Contains(t, out, "username: test_username", "expected secrets to be shown in config") | ||
assert.Contains(t, out, "password: test_password", "expected secrets to be shown in config") | ||
}) | ||
|
||
t.Run("prints config to screen", func(t *testing.T) { | ||
out, err := execute(t, NewRootCmd(), append(argsWithDatabaseSet, []string{"--print"}...)...) | ||
assert.NoError(t, err, "expected no error running manager") | ||
assert.Equal(t, `OK | ||
log_level: debug | ||
timeouts: | ||
startup: 30s | ||
shutdown: 30s | ||
http: | ||
listen_address: :8080 | ||
cors: | ||
allowed_origins: | ||
- '*' | ||
database: | ||
host: mongodb://ugcompsoc_apid_local_db | ||
name: apid | ||
username: '********' | ||
password: '********'`, out, "print to screen did not match expected config") | ||
}) | ||
|
||
err := os.RemoveAll(directory) | ||
assert.NoError(t, err, "could not delete testing directory") | ||
} |
Oops, something went wrong.