Skip to content

Commit

Permalink
feat(workflow): base cli for verifying and writing configs (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
Conor Mc Govern authored Jul 5, 2023
1 parent 7652ee1 commit 392db30
Show file tree
Hide file tree
Showing 15 changed files with 1,300 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if [ -s go.mod ]; then
exit 1
fi

go test $(go list ./... | grep -v cmd | grep -v internal/services/database_test_utils) -coverprofile cover.out
go test $(go list ./... | grep -vw "apid/cmd$" | grep -v internal/services/database_test_utils) -coverprofile cover.out
TOTAL_COVERAGE=$(go tool cover -func cover.out | grep total | grep -Eo '[0-9]+\.[0-9]+')
if (( $(echo "$TOTAL_COVERAGE $COVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then
echo "Code coverage is above $COVERAGE_THRESHOLD"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
env:
COVERAGE_THRESHOLD: 50
run: |
go test $(go list ./... | grep -v cmd | grep -v internal/services/database_test_utils) -coverprofile cover.out
go test $(go list ./... | grep -vw "apid/cmd$" | grep -v internal/services/database_test_utils) -coverprofile cover.out
TOTAL_COVERAGE=$(go tool cover -func cover.out | grep total | grep -Eo '[0-9]+\.[0-9]+')
if (( $(echo "$TOTAL_COVERAGE $COVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then
echo "Code coverage is above $COVERAGE_THRESHOLD"
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
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
75 changes: 75 additions & 0 deletions cmd/manager/cmd/config.go
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()
}
127 changes: 127 additions & 0 deletions cmd/manager/cmd/config_create.go
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()
}
165 changes: 165 additions & 0 deletions cmd/manager/cmd/config_create_test.go
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")
}
Loading

0 comments on commit 392db30

Please sign in to comment.