diff --git a/examples/minimal-example.toml b/examples/minimal-example.toml index 245f2036..bec47cff 100644 --- a/examples/minimal-example.toml +++ b/examples/minimal-example.toml @@ -3,8 +3,8 @@ ## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md [helmRepos] - jenkins = https://charts.jenkins.io - center = https://repo.chartcenter.io + jenkins = "https://charts.jenkins.io" + center = "https://repo.chartcenter.io" [namespaces] [namespaces.staging] diff --git a/examples/minimal-example.yaml b/examples/minimal-example.yaml index 9866a92d..fae065c7 100644 --- a/examples/minimal-example.yaml +++ b/examples/minimal-example.yaml @@ -2,8 +2,8 @@ ## It will use your current kube context and will deploy Tiller without RBAC service account. ## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md helmRepos: - jenkins = https://charts.jenkins.io - center = https://repo.chartcenter.io + jenkins: https://charts.jenkins.io + center: https://repo.chartcenter.io namespaces: staging: diff --git a/internal/app/cli.go b/internal/app/cli.go index b58a12c6..947b926f 100644 --- a/internal/app/cli.go +++ b/internal/app/cli.go @@ -164,7 +164,7 @@ func (c *cli) parse() { os.Setenv("KUBECONFIG", c.kubeconfig) } - if !ToolExists("kubectl") { + if !ToolExists(kubectlBin) { log.Fatal("kubectl is not installed/configured correctly. Aborting!") } @@ -191,13 +191,13 @@ func (c *cli) parse() { } // readState gets the desired state from files -func (c *cli) readState(s *state) { +func (c *cli) readState(s *state) error { // read the env file if len(c.envFiles) == 0 { if _, err := os.Stat(".env"); err == nil { err = godotenv.Load() if err != nil { - log.Fatal("Error loading .env file") + return fmt.Errorf("error loading .env file: %w", err) } } } @@ -205,7 +205,7 @@ func (c *cli) readState(s *state) { for _, e := range c.envFiles { err := godotenv.Load(e) if err != nil { - log.Fatal("Error loading " + e + " env file") + return fmt.Errorf("error loading %s env file: %w", e, err) } } @@ -221,49 +221,38 @@ func (c *cli) readState(s *state) { if result { log.Info(msg) } else { - log.Fatal(msg) + return fmt.Errorf(msg) } // Merge Apps that already existed in the state for appName, app := range fileState.Apps { if _, ok := s.Apps[appName]; ok { if err := mergo.Merge(s.Apps[appName], app, mergo.WithAppendSlice, mergo.WithOverride); err != nil { - log.Fatal("Failed to merge " + appName + " from desired state file" + f) + return fmt.Errorf("failed to merge %s from desired state file %s: %w", appName, f, err) } } } // Merge the remaining Apps if err := mergo.Merge(&s.Apps, &fileState.Apps); err != nil { - log.Fatal("Failed to merge desired state file" + f) + return fmt.Errorf("failed to merge desired state file %s: %w", f, err) } // All the apps are already merged, make fileState.Apps empty to avoid conflicts in the final merge fileState.Apps = make(map[string]*release) if err := mergo.Merge(s, &fileState, mergo.WithAppendSlice, mergo.WithOverride); err != nil { - log.Fatal("Failed to merge desired state file" + f) + return fmt.Errorf("failed to merge desired state file %s: %w", f, err) } } - s.setDefaults() - s.initializeNamespaces() + s.init() // Set defaults s.disableUntargetedApps(c.group, c.target) - if len(c.target) > 0 && len(s.TargetMap) == 0 { - log.Info("No apps defined with -target flag were found, exiting") - os.Exit(0) - } - - if len(c.group) > 0 && len(s.TargetMap) == 0 { - log.Info("No apps defined with -group flag were found, exiting") - os.Exit(0) - } - if !c.skipValidation { // validate the desired state content if len(c.files) > 0 { log.Info("Validating desired state definition") if err := s.validate(); err != nil { // syntax validation - log.Fatal(err.Error()) + return err } } } else { @@ -273,6 +262,7 @@ func (c *cli) readState(s *state) { if c.debug { s.print() } + return nil } // getDryRunFlags returns dry-run flag diff --git a/internal/app/cli_test.go b/internal/app/cli_test.go index 04b381cd..79680b1f 100644 --- a/internal/app/cli_test.go +++ b/internal/app/cli_test.go @@ -7,39 +7,115 @@ var _ = func() bool { return true }() -func Test_toolExists(t *testing.T) { - type args struct { - tool string +func Test_readState(t *testing.T) { + type result struct { + numApps int + numNSs int + numEnabledApps int + numEnabledNSs int } tests := []struct { - name string - args args - want bool + name string + flags cli + want result }{ { - name: "test case 1 -- checking helm exists.", - args: args{ - tool: helmBin, - }, - want: true, - }, { - name: "test case 2 -- checking kubectl exists.", - args: args{ - tool: "kubectl", - }, - want: true, - }, { - name: "test case 3 -- checking nonExistingTool exists.", - args: args{ - tool: "nonExistingTool", - }, - want: false, + name: "yaml minimal example; no validation", + flags: cli{ + files: stringArray([]string{"../../examples/minimal-example.yaml"}), + skipValidation: true, + }, + want: result{ + numApps: 2, + numNSs: 1, + numEnabledApps: 2, + numEnabledNSs: 1, + }, + }, + { + name: "toml minimal example; no validation", + flags: cli{ + files: stringArray([]string{"../../examples/minimal-example.toml"}), + skipValidation: true, + }, + want: result{ + numApps: 2, + numNSs: 1, + numEnabledApps: 2, + numEnabledNSs: 1, + }, + }, + { + name: "yaml minimal example; no validation with bad target", + flags: cli{ + target: stringArray([]string{"foo"}), + files: stringArray([]string{"../../examples/minimal-example.yaml"}), + skipValidation: true, + }, + want: result{ + numApps: 2, + numNSs: 1, + numEnabledApps: 0, + numEnabledNSs: 0, + }, + }, + { + name: "yaml minimal example; no validation; target jenkins", + flags: cli{ + target: stringArray([]string{"jenkins"}), + files: stringArray([]string{"../../examples/minimal-example.yaml"}), + skipValidation: true, + }, + want: result{ + numApps: 2, + numNSs: 1, + numEnabledApps: 1, + numEnabledNSs: 1, + }, + }, + { + name: "yaml and toml minimal examples merged; no validation", + flags: cli{ + files: stringArray([]string{"../../examples/minimal-example.yaml", "../../examples/minimal-example.toml"}), + skipValidation: true, + }, + want: result{ + numApps: 2, + numNSs: 1, + numEnabledApps: 2, + numEnabledNSs: 1, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := ToolExists(tt.args.tool); got != tt.want { - t.Errorf("toolExists() = %v, want %v", got, tt.want) + s := state{} + if err := tt.flags.readState(&s); err != nil { + t.Errorf("readState() = Unexpected error: %v", err) + } + if len(s.Apps) != tt.want.numApps { + t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numApps, len(s.Apps)) + } + if len(s.Namespaces) != tt.want.numNSs { + t.Errorf("readState() = NS count mismatch: want: %d, got: %d", tt.want.numNSs, len(s.Namespaces)) + } + + var enabledApps, enabledNSs int + for _, a := range s.Apps { + if !a.disabled { + enabledApps++ + } + } + if enabledApps != tt.want.numEnabledApps { + t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numEnabledApps, enabledApps) + } + for _, n := range s.Namespaces { + if !n.disabled { + enabledNSs++ + } + } + if enabledNSs != tt.want.numEnabledNSs { + t.Errorf("readState() = app count mismatch: want: %d, got: %d", tt.want.numEnabledNSs, enabledNSs) } }) } diff --git a/internal/app/command_test.go b/internal/app/command_test.go index a0ddfcd1..e3ca5d76 100644 --- a/internal/app/command_test.go +++ b/internal/app/command_test.go @@ -5,6 +5,44 @@ import ( "testing" ) +func Test_toolExists(t *testing.T) { + type args struct { + tool string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "test case 1 -- checking helm exists.", + args: args{ + tool: helmBin, + }, + want: true, + }, { + name: "test case 2 -- checking kubectl exists.", + args: args{ + tool: kubectlBin, + }, + want: true, + }, { + name: "test case 3 -- checking nonExistingTool exists.", + args: args{ + tool: "nonExistingTool", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToolExists(tt.args.tool); got != tt.want { + t.Errorf("toolExists() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_command_exec(t *testing.T) { type fields struct { Cmd string diff --git a/internal/app/kube_helpers.go b/internal/app/kube_helpers.go index 9c1a825c..bfcc2c97 100644 --- a/internal/app/kube_helpers.go +++ b/internal/app/kube_helpers.go @@ -38,7 +38,7 @@ func addNamespaces(s *state) { // kubectl prepares a kubectl command to be executed func kubectl(args []string, desc string) Command { return Command{ - Cmd: "kubectl", + Cmd: kubectlBin, Args: args, Description: desc, } diff --git a/internal/app/main.go b/internal/app/main.go index bc2a77f9..fc09f83c 100644 --- a/internal/app/main.go +++ b/internal/app/main.go @@ -7,6 +7,7 @@ import ( const ( helmBin = "helm" + kubectlBin = "kubectl" appVersion = "v3.6.1" tempFilesDir = ".helmsman-tmp" defaultContextName = "default" @@ -35,7 +36,20 @@ func Main() { defer s.cleanup() } - flags.readState(&s) + if err := flags.readState(&s); err != nil { + log.Fatal(err.Error()) + } + + if len(flags.target) > 0 && len(s.TargetMap) == 0 { + log.Info("No apps defined with -target flag were found, exiting") + os.Exit(0) + } + + if len(flags.group) > 0 && len(s.TargetMap) == 0 { + log.Info("No apps defined with -group flag were found, exiting") + os.Exit(0) + } + log.SlackWebhook = s.Settings.SlackWebhook settings = &s.Settings diff --git a/internal/app/namespace.go b/internal/app/namespace.go index 27fb5e9e..5086f25f 100644 --- a/internal/app/namespace.go +++ b/internal/app/namespace.go @@ -52,9 +52,11 @@ func (n *namespace) Disable() { // print prints the namespace func (n *namespace) print() { - fmt.Println("") - fmt.Println("\tprotected : ", n.Protected) - fmt.Println("\tlabels : ") + fmt.Println("\tprotected: ", n.Protected) + fmt.Println("\tdisabled: ", n.disabled) + fmt.Println("\tlabels:") printMap(n.Labels, 2) - fmt.Println("------------------- ") + fmt.Println("\tannotations:") + printMap(n.Annotations, 2) + fmt.Println("-------------------") } diff --git a/internal/app/state.go b/internal/app/state.go index 9d587f11..aaf40ff4 100644 --- a/internal/app/state.go +++ b/internal/app/state.go @@ -44,6 +44,11 @@ type state struct { ChartInfo map[string]map[string]*chartInfo } +func (s *state) init() { + s.setDefaults() + s.initializeNamespaces() +} + func (s *state) setDefaults() { if s.Settings.StorageBackend != "" { os.Setenv("HELM_DRIVER", s.Settings.StorageBackend) diff --git a/internal/app/utils.go b/internal/app/utils.go index 92ebf8a3..697b7287 100644 --- a/internal/app/utils.go +++ b/internal/app/utils.go @@ -31,8 +31,9 @@ func printMap(m map[string]string, indent int) { // printObjectMap prints to the console any map of string keys and object values. func printNamespacesMap(m map[string]*namespace) { - for key, value := range m { - fmt.Println(key, " : protected = ", value) + for name, ns := range m { + fmt.Println(name, ":") + ns.print() } }