diff --git a/.goreleaser.yml b/.goreleaser.yml index 83cce6e1..6619ee39 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -8,5 +8,6 @@ builds: goos: - darwin - linux + - windows goarch: - amd64 \ No newline at end of file diff --git a/command.go b/command.go index 5b00ede3..c5fc3664 100644 --- a/command.go +++ b/command.go @@ -33,14 +33,22 @@ func (c command) printFullCommand() { // exec executes the executable command and returns the exit code and execution result func (c command) exec(debug bool, verbose bool) (int, string) { + // Only use non-empty string args + args := []string{} + for _, str := range c.Args { + if str != "" { + args = append(args, str) + } + } + if debug { log.Println("INFO: " + c.Description) } if verbose { - log.Println("VERBOSE: " + strings.Join(c.Args[1:], " ")) + log.Println("VERBOSE: " + c.Cmd + " " + strings.Join(args, " ")) } - cmd := exec.Command(c.Cmd, c.Args...) + cmd := exec.Command(c.Cmd, args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -50,7 +58,8 @@ func (c command) exec(debug bool, verbose bool) (int, string) { cmd.Env = append(os.Environ(), "HELM_TILLER_SILENT=true") if err := cmd.Start(); err != nil { - logError("ERROR: cmd.Start: " + err.Error()) + log.Println("ERROR: cmd.Start: " + err.Error()) + return 1, err.Error() } if err := cmd.Wait(); err != nil { diff --git a/decision_maker.go b/decision_maker.go index f7a64634..eb62b7ce 100644 --- a/decision_maker.go +++ b/decision_maker.go @@ -3,6 +3,7 @@ package main import ( "fmt" "regexp" + "runtime" "strconv" "strings" ) @@ -107,25 +108,28 @@ func decide(r *release, s *state) { // operate without a tiller it will return `helm tiller run NAMESPACE -- helm` // where NAMESPACE is the namespace that the release is configured to use. // If not configured to run without a tiller will just return `helm`. -func helmCommand(namespace string) string { +func helmCommand(namespace string) []string { if settings.Tillerless { - return "helm tiller run " + namespace + " -- helm" + if runtime.GOOS == "windows" { + logError("ERROR: Tillerless Helm plugin is not supported on Windows") + } + return []string{"tiller", "run", namespace, "--", "helm"} } - return "helm" + return nil } // helmCommandFromConfig calls helmCommand returning the correct way to invoke // helm. -func helmCommandFromConfig(r *release) string { +func helmCommandFromConfig(r *release) []string { return helmCommand(getDesiredTillerNamespace(r)) } // testRelease creates a Helm command to test a particular release. func testRelease(r *release) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " test " + r.Name + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r)}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"test", r.Name}, getDesiredTillerNamespaceFlag(r), getTLSFlags(r)), Description: "running tests for release [ " + r.Name + " ]", } outcome.addCommand(cmd, r.Priority, r) @@ -136,8 +140,8 @@ func testRelease(r *release) { // installRelease creates a Helm command to install a particular release in a particular namespace using a particular Tiller. func installRelease(r *release) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " install " + r.Chart + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + " --version " + strconv.Quote(r.Version) + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getHelmFlags(r)}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"install", r.Chart, "-n", r.Name, "--namespace", r.Namespace}, getValuesFiles(r), []string{"--version", r.Version}, getSetValues(r), getSetStringValues(r), getWait(r), getDesiredTillerNamespaceFlag(r), getTLSFlags(r), getHelmFlags(r)), Description: "installing release [ " + r.Name + " ] in namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(cmd, r.Priority, r) @@ -157,8 +161,8 @@ func rollbackRelease(r *release, rs releaseState) { if r.Namespace == rs.Namespace { cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " rollback " + r.Name + " " + getReleaseRevision(rs) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"rollback", r.Name, getReleaseRevision(rs)}, getWait(r), getDesiredTillerNamespaceFlag(r), getTLSFlags(r), getTimeout(r), getNoHooks(r), getDryRunFlags()), Description: "rolling back release [ " + r.Name + " ] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(cmd, r.Priority, r) @@ -193,8 +197,8 @@ func deleteRelease(r *release, rs releaseState) { } cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " delete " + p + " " + r.Name + getCurrentTillerNamespaceFlag(rs) + getTLSFlags(r) + getDryRunFlags()}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"delete", p, r.Name}, getCurrentTillerNamespaceFlag(rs), getTLSFlags(r), getDryRunFlags()), Description: "deleting release [ " + r.Name + " ] from namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(cmd, priority, r) @@ -257,21 +261,21 @@ func diffRelease(r *release) string { exitCode := 0 msg := "" colorFlag := "" - diffContextFlag := "" + diffContextFlag := []string{} suppressDiffSecretsFlag := "" if noColors { - colorFlag = "--no-color " + colorFlag = "--no-color" } if diffContext != -1 { - diffContextFlag = "--context " + strconv.Itoa(diffContext) + " " + diffContextFlag = []string{"--context", strconv.Itoa(diffContext)} } if suppressDiffSecrets { - suppressDiffSecretsFlag = "--suppress-secrets " + suppressDiffSecretsFlag = "--suppress-secrets" } cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " diff " + colorFlag + diffContextFlag + suppressDiffSecretsFlag + "upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + strconv.Quote(r.Version) + " " + getSetValues(r) + getSetStringValues(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r)}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"diff", colorFlag}, diffContextFlag, []string{suppressDiffSecretsFlag, "upgrade", r.Name, r.Chart}, getValuesFiles(r), []string{"--version", r.Version}, getSetValues(r), getSetStringValues(r), getDesiredTillerNamespaceFlag(r), getTLSFlags(r)), Description: "diffing release [ " + r.Name + " ] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } @@ -290,11 +294,11 @@ func diffRelease(r *release) string { func upgradeRelease(r *release) { var force string if forceUpgrades { - force = " --force " + force = "--force" } cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " upgrade " + r.Name + " " + r.Chart + getValuesFiles(r) + " --version " + strconv.Quote(r.Version) + force + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getHelmFlags(r)}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"upgrade", r.Name, r.Chart}, getValuesFiles(r), []string{"--version", r.Version, force}, getSetValues(r), getSetStringValues(r), getWait(r), getDesiredTillerNamespaceFlag(r), getTLSFlags(r), getHelmFlags(r)), Description: "upgrading release [ " + r.Name + " ] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } @@ -306,15 +310,15 @@ func upgradeRelease(r *release) { func reInstallRelease(r *release, rs releaseState) { delCmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " delete --purge " + r.Name + getCurrentTillerNamespaceFlag(rs) + getTLSFlags(r) + getDryRunFlags()}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"delete", "--purge", r.Name}, getCurrentTillerNamespaceFlag(rs), getTLSFlags(r), getDryRunFlags()), Description: "deleting release [ " + r.Name + " ] from namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(delCmd, r.Priority, r) installCmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommandFromConfig(r) + " install " + r.Chart + " --version " + r.Version + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getHelmFlags(r)}, + Cmd: "helm", + Args: concat(helmCommandFromConfig(r), []string{"install", r.Chart, "--version", r.Version, "-n", r.Name, "--namespace", r.Namespace}, getValuesFiles(r), getSetValues(r), getSetStringValues(r), getWait(r), getDesiredTillerNamespaceFlag(r), getTLSFlags(r), getHelmFlags(r)), Description: "installing release [ " + r.Name + " ] in namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(installCmd, r.Priority, r) @@ -343,23 +347,23 @@ func extractChartName(releaseChart string) string { var chartNameExtractor = regexp.MustCompile(`[\\/]([^\\/]+)$`) // getNoHooks returns the no-hooks flag for install/upgrade commands -func getNoHooks(r *release) string { +func getNoHooks(r *release) []string { if r.NoHooks { - return " --no-hooks " + return []string{"--no-hooks"} } - return "" + return []string{} } // getTimeout returns the timeout flag for install/upgrade commands -func getTimeout(r *release) string { +func getTimeout(r *release) []string { if r.Timeout != 0 { - return " --timeout " + strconv.Itoa(r.Timeout) + return []string{"--timeout", strconv.Itoa(r.Timeout)} } - return "" + return []string{} } // getValuesFiles return partial install/upgrade release command to substitute the -f flag in Helm. -func getValuesFiles(r *release) string { +func getValuesFiles(r *release) []string { var fileList []string if r.ValuesFile != "" { @@ -393,36 +397,37 @@ func getValuesFiles(r *release) string { fileList = append(fileList, r.SecretsFiles...) } - if len(fileList) > 0 { - return " -f " + strings.Join(fileList, " -f ") + fileListArgs := []string{} + for _, file := range fileList { + fileListArgs = append(fileListArgs, "-f", file) } - return "" + return fileListArgs } // getSetValues returns --set params to be used with helm install/upgrade commands -func getSetValues(r *release) string { - result := "" +func getSetValues(r *release) []string { + result := []string{} for k, v := range r.Set { - result = result + " --set " + k + "=\"" + strings.Replace(v, ",", "\\,", -1) + "\"" + result = append(result, "--set", k+"="+strings.Replace(v, ",", "\\,", -1)+"") } return result } // getSetStringValues returns --set-string params to be used with helm install/upgrade commands -func getSetStringValues(r *release) string { - result := "" +func getSetStringValues(r *release) []string { + result := []string{} for k, v := range r.SetString { - result = result + " --set-string " + k + "=\"" + strings.Replace(v, ",", "\\,", -1) + "\"" + result = append(result, "--set-string", k+"="+strings.Replace(v, ",", "\\,", -1)+"") } return result } // getWait returns a partial helm command containing the helm wait flag (--wait) if the wait flag for the release was set to true // Otherwise, retruns an empty string -func getWait(r *release) string { - result := "" +func getWait(r *release) []string { + result := []string{} if r.Wait { - result = " --wait" + result = append(result, "--wait") } return result } @@ -464,8 +469,8 @@ func isProtected(r *release, rs releaseState) bool { } // getDesiredTillerNamespaceFlag returns a tiller-namespace flag with which a release is desired to be maintained -func getDesiredTillerNamespaceFlag(r *release) string { - return " --tiller-namespace " + getDesiredTillerNamespace(r) +func getDesiredTillerNamespaceFlag(r *release) []string { + return []string{"--tiller-namespace", getDesiredTillerNamespace(r)} } // getDesiredTillerNamespace returns the Tiller namespace with which a release should be managed @@ -480,35 +485,35 @@ func getDesiredTillerNamespace(r *release) string { } // getCurrentTillerNamespaceFlag returns the tiller-namespace with which a release is currently maintained -func getCurrentTillerNamespaceFlag(rs releaseState) string { +func getCurrentTillerNamespaceFlag(rs releaseState) []string { if rs.TillerNamespace != "" { - return " --tiller-namespace " + rs.TillerNamespace + return []string{"--tiller-namespace", rs.TillerNamespace} } - return "" + return []string{} } // getTLSFlags returns TLS flags with which a release is maintained // If the release where the namespace is to be deployed has Tiller deployed, the TLS flags will use certs/keys for that namespace (if any) // otherwise, it will be the certs/keys for the kube-system namespace. -func getTLSFlags(r *release) string { - tls := "" +func getTLSFlags(r *release) []string { + tls := []string{} ns := s.Namespaces[r.TillerNamespace] if r.TillerNamespace != "" { if tillerTLSEnabled(ns) { - tls = " --tls --tls-ca-cert " + r.TillerNamespace + "-ca.cert --tls-cert " + r.TillerNamespace + "-client.cert --tls-key " + r.TillerNamespace + "-client.key " + tls = append(tls, "--tls", "--tls-ca-cert", r.TillerNamespace+"-ca.cert", "--tls-cert", r.TillerNamespace+"-client.cert", "--tls-key", r.TillerNamespace+"-client.key") } } else if s.Namespaces[r.Namespace].InstallTiller { ns := s.Namespaces[r.Namespace] if tillerTLSEnabled(ns) { - tls = " --tls --tls-ca-cert " + r.Namespace + "-ca.cert --tls-cert " + r.Namespace + "-client.cert --tls-key " + r.Namespace + "-client.key " + tls = append(tls, "--tls", "--tls-ca-cert", r.Namespace+"-ca.cert", "--tls-cert", r.Namespace+"-client.cert", "--tls-key", r.Namespace+"-client.key") } } else { ns := s.Namespaces["kube-system"] if tillerTLSEnabled(ns) { - tls = " --tls --tls-ca-cert kube-system-ca.cert --tls-cert kube-system-client.cert --tls-key kube-system-client.key " + tls = append(tls, "--tls", "--tls-ca-cert", "kube-system-ca.cert", "--tls-cert", "kube-system-client.cert", "--tls-key", "kube-system-client.key") } } @@ -516,21 +521,21 @@ func getTLSFlags(r *release) string { } // getDryRunFlags returns dry-run flag -func getDryRunFlags() string { +func getDryRunFlags() []string { if dryRun { - return " --dry-run --debug " + return []string{"--dry-run", "--debug"} } - return "" + return []string{} } // getHelmFlags returns helm flags -func getHelmFlags(r *release) string { - var flags string +func getHelmFlags(r *release) []string { + var flags []string for _, flag := range r.HelmFlags { - flags = flags + " " + flag + flags = append(flags, flag) } - return getNoHooks(r) + getTimeout(r) + getDryRunFlags() + flags + return concat(getNoHooks(r), getTimeout(r), getDryRunFlags(), flags) } func checkChartDepUpdate(r *release) { diff --git a/decision_maker_test.go b/decision_maker_test.go index 08ce6237..05fe6b1e 100644 --- a/decision_maker_test.go +++ b/decision_maker_test.go @@ -1,6 +1,8 @@ package main import ( + "os" + "reflect" "testing" ) @@ -11,7 +13,7 @@ func Test_getValuesFiles(t *testing.T) { tests := []struct { name string args args - want string + want []string }{ { name: "test case 1", @@ -29,7 +31,7 @@ func Test_getValuesFiles(t *testing.T) { }, //s: st, }, - want: " -f test_files/values.yaml", + want: []string{"-f", "test_files/values.yaml"}, }, { name: "test case 2", @@ -47,7 +49,7 @@ func Test_getValuesFiles(t *testing.T) { }, //s: st, }, - want: " -f test_files/values.yaml", + want: []string{"-f", "test_files/values.yaml"}, }, { name: "test case 1", @@ -65,12 +67,12 @@ func Test_getValuesFiles(t *testing.T) { }, //s: st, }, - want: " -f test_files/values.yaml -f test_files/values2.yaml", + want: []string{"-f", "test_files/values.yaml", "-f", "test_files/values2.yaml"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := getValuesFiles(tt.args.r); got != tt.want { + if got := getValuesFiles(tt.args.r); !reflect.DeepEqual(got, tt.want) { t.Errorf("getValuesFiles() = %v, want %v", got, tt.want) } }) @@ -78,6 +80,8 @@ func Test_getValuesFiles(t *testing.T) { } func Test_inspectUpgradeScenario(t *testing.T){ + localChartsPath := os.TempDir() + "/helmsman-tests/local/charts" + os.MkdirAll(localChartsPath, os.ModePerm) type args struct { r *release s releaseState @@ -94,7 +98,7 @@ func Test_inspectUpgradeScenario(t *testing.T){ Name: "release1", Namespace: "namespace", Version: "1.0.0", - Chart: "/local/charts", + Chart: localChartsPath, Enabled: true, }, s: releaseState{ diff --git a/helm_helpers.go b/helm_helpers.go index 625c8185..a9f08e40 100644 --- a/helm_helpers.go +++ b/helm_helpers.go @@ -47,8 +47,8 @@ type tillerReleases struct { // getHelmClientVersion returns Helm client Version func getHelmClientVersion() string { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm version --client --short"}, + Cmd: "helm", + Args: []string{"version", "--client", "--short"}, Description: "checking Helm version ", } @@ -81,9 +81,9 @@ func getAllReleases() tillerReleases { func getTillerReleases(tillerNS string) tillerReleases { v1, _ := version.NewVersion(helmVersion) jsonConstraint, _ := version.NewConstraint(">=2.10.0-rc.1") - var outputFormat string + var outputFormat []string if jsonConstraint.Check(v1) { - outputFormat = "--output json" + outputFormat = append(outputFormat, "--output", "json") } output, err := helmList(tillerNS, outputFormat, "") @@ -118,7 +118,7 @@ func parseJSONListAndFollow(input, tillerNS string) (tillerReleases, error) { var releases tillerReleases for { - output, err := helmList(tillerNS, "--output json", releases.Next) + output, err := helmList(tillerNS, []string{"--output", "json"}, releases.Next) if err != nil { return allReleases, err } @@ -152,13 +152,14 @@ func parseTextList(input string) tillerReleases { return out } -func helmList(tillerNS, outputFormat, offset string) (string, error) { - arg := fmt.Sprintf("%s list --all --max 0 --offset \"%s\" %s --tiller-namespace %s %s", - helmCommand(tillerNS), offset, outputFormat, tillerNS, getNSTLSFlags(tillerNS), - ) +func helmList(tillerNS string, outputFormat []string, offset string) (string, error) { + args := []string{"list", "--all", "--max", "0"} + if offset != "" { + args = append(args, "--offset", offset) + } cmd := command{ - Cmd: "bash", - Args: []string{"-c", arg}, + Cmd: "helm", + Args: concat(helmCommand(tillerNS), args, outputFormat, []string{"--tiller-namespace", tillerNS}, getNSTLSFlags(tillerNS)), Description: "listing all existing releases in namespace [ " + tillerNS + " ]...", } @@ -254,12 +255,11 @@ func getReleaseChartVersion(rs releaseState) string { } // getNSTLSFlags returns TLS flags for a given namespace if it's deployed with TLS -func getNSTLSFlags(namespace string) string { - tls := "" +func getNSTLSFlags(namespace string) []string { + tls := []string{} ns := s.Namespaces[namespace] if tillerTLSEnabled(ns) { - - tls = " --tls --tls-ca-cert " + namespace + "-ca.cert --tls-cert " + namespace + "-client.cert --tls-key " + namespace + "-client.key " + tls = append(tls, "--tls", "--tls-ca-cert", namespace+"-ca.cert", "--tls-cert", namespace+"-client.cert", "--tls-key", namespace+"-client.key") } return tls } @@ -280,8 +280,8 @@ func validateReleaseCharts(apps map[string]*release) (bool, string) { if validateCurrentChart { if isLocalChart(r.Chart) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm inspect chart '" + r.Chart + "'"}, + Cmd: "helm", + Args: []string{"inspect", "chart", r.Chart}, Description: "validating if chart at " + r.Chart + " is available.", } @@ -302,8 +302,8 @@ func validateReleaseCharts(apps map[string]*release) (bool, string) { } else { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm search " + r.Chart + " --version " + strconv.Quote(r.Version) + " -l"}, + Cmd: "helm", + Args: []string{"search", r.Chart, "--version", r.Version, "-l"}, Description: "validating if chart " + r.Chart + "-" + r.Version + " is available in the defined repos.", } @@ -325,8 +325,8 @@ func getChartVersion(r *release) (string, string) { return r.Version, "" } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm search " + r.Chart + " --version " + strconv.Quote(r.Version)}, + Cmd: "helm", + Args: []string{"search", r.Chart, "--version", r.Version}, Description: "getting latest chart version " + r.Chart + "-" + r.Version + "", } @@ -352,8 +352,8 @@ func waitForTiller(namespace string) { attempt := 0 cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm list --tiller-namespace " + namespace + getNSTLSFlags(namespace)}, + Cmd: "helm", + Args: concat([]string{"list", "--tiller-namespace", namespace}, getNSTLSFlags(namespace)), Description: "checking if helm Tiller is ready in namespace [ " + namespace + " ].", } @@ -379,7 +379,7 @@ func waitForTiller(namespace string) { func addHelmRepos(repos map[string]string) (bool, string) { for repoName, repoLink := range repos { - basicAuth := "" + basicAuthArgs := []string{} // check if repo is in GCS, then perform GCS auth -- needed for private GCS helm repos // failed auth would not throw an error here, as it is possible that the repo is public and does not need authentication if strings.HasPrefix(repoLink, "gs://") { @@ -395,13 +395,13 @@ func addHelmRepos(repos map[string]string) (bool, string) { if !ok { logError("ERROR: helm repo " + repoName + " has incomplete basic auth info. Missing the password!") } - basicAuth = " --username " + u.User.Username() + " --password " + p + basicAuthArgs = append(basicAuthArgs, "--username", u.User.Username(), "--password", p) } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm repo add " + basicAuth + " " + repoName + " " + strconv.Quote(repoLink)}, + Cmd: "helm", + Args: concat([]string{"repo", "add", repoName, repoLink}, basicAuthArgs), Description: "adding repo " + repoName, } @@ -412,8 +412,8 @@ func addHelmRepos(repos map[string]string) (bool, string) { } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm repo update "}, + Cmd: "helm", + Args: []string{"repo", "update"}, Description: "updating helm repos", } @@ -430,7 +430,7 @@ func addHelmRepos(repos map[string]string) (bool, string) { // If no namespace is provided, Tiller is deployed to kube-system func deployTiller(namespace string, serviceAccount string, defaultServiceAccount string, role string, roleTemplateFile string, tillerMaxHistory int) (bool, string) { log.Println("INFO: deploying Tiller in namespace [ " + namespace + " ].") - sa := "" + sa := []string{} if serviceAccount != "" { if ok, err := validateServiceAccount(serviceAccount, namespace); !ok { if strings.Contains(err, "NotFound") || strings.Contains(err, "not found") { @@ -443,7 +443,7 @@ func deployTiller(namespace string, serviceAccount string, defaultServiceAccount return false, "ERROR: while validating/creating service account [ " + serviceAccount + " ] in namespace [" + namespace + "]: " + err } } - sa = "--service-account " + serviceAccount + sa = []string{"--service-account", serviceAccount} } else { roleName := "helmsman-tiller" defaultServiceAccountName := "helmsman" @@ -466,34 +466,34 @@ func deployTiller(namespace string, serviceAccount string, defaultServiceAccount return false, "ERROR: while validating/creating service account [ " + defaultServiceAccountName + " ] in namespace [" + namespace + "]: " + err } } - sa = " --service-account " + defaultServiceAccountName + sa = []string{"--service-account", defaultServiceAccountName} } if namespace == "" { namespace = "kube-system" } - tillerNameSpace := " --tiller-namespace " + namespace + tillerNameSpace := []string{"--tiller-namespace", namespace} - maxHistory := "" + maxHistory := []string{} if tillerMaxHistory > 0 { - maxHistory = " --history-max " + strconv.Itoa(tillerMaxHistory) + maxHistory = []string{"--history-max", strconv.Itoa(tillerMaxHistory)} } - tls := "" + tls := []string{} ns := s.Namespaces[namespace] if tillerTLSEnabled(ns) { tillerCert := namespace + "-tiller.cert" tillerKey := namespace + "-tiller.key" caCert := namespace + "-ca.cert" - tls = " --tiller-tls --tiller-tls-cert " + tillerCert + " --tiller-tls-key " + tillerKey + " --tiller-tls-verify --tls-ca-cert " + caCert + tls = []string{"--tiller-tls", "--tiller-tls-cert", tillerCert, "--tiller-tls-key", tillerKey, "--tiller-tls-verify", "--tls-ca-cert", caCert} } - storageBackend := "" + storageBackend := []string{} if s.Settings.StorageBackend == "secret" { - storageBackend = " --override 'spec.template.spec.containers[0].command'='{/tiller,--storage=secret}'" + storageBackend = []string{"--override", "'spec.template.spec.containers[0].command'='{/tiller,--storage=secret}'"} } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm init --force-upgrade " + maxHistory + sa + tillerNameSpace + tls + storageBackend}, + Cmd: "helm", + Args: concat([]string{"init", "--force-upgrade"}, maxHistory, sa, tillerNameSpace, tls, storageBackend), Description: "initializing helm on the current context and upgrading Tiller on namespace [ " + namespace + " ].", } @@ -506,8 +506,8 @@ func deployTiller(namespace string, serviceAccount string, defaultServiceAccount // initHelmClientOnly initializes the helm client only (without deploying Tiller) func initHelmClientOnly() (bool, string) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm init --client-only "}, + Cmd: "helm", + Args: []string{"init", "--client-only"}, Description: "initializing helm on the client only.", } @@ -602,15 +602,15 @@ func cleanUntrackedReleases() { // deleteUntrackedRelease creates the helm command to purge delete an untracked release func deleteUntrackedRelease(release string, tillerNamespace string) { - tls := "" + tls := []string{} ns := s.Namespaces[tillerNamespace] if tillerTLSEnabled(ns) { - tls = " --tls --tls-ca-cert " + tillerNamespace + "-ca.cert --tls-cert " + tillerNamespace + "-client.cert --tls-key " + tillerNamespace + "-client.key " + tls = []string{"--tls", "--tls-ca-cert", tillerNamespace + "-ca.cert", "--tls-cert", tillerNamespace + "-client.cert", "--tls-key", tillerNamespace + "-client.key"} } cmd := command{ - Cmd: "bash", - Args: []string{"-c", helmCommand(tillerNamespace) + " delete --purge " + release + " --tiller-namespace " + tillerNamespace + tls + getDryRunFlags()}, + Cmd: "helm", + Args: concat(helmCommand(tillerNamespace), []string{"delete", "--purge", release, "--tiller-namespace", tillerNamespace}, tls, getDryRunFlags()), Description: "deleting untracked release [ " + release + " ] from Tiller in namespace [[ " + tillerNamespace + " ]]", } @@ -620,8 +620,8 @@ func deleteUntrackedRelease(release string, tillerNamespace string) { // decrypt a helm secret file func decryptSecret(name string) bool { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm secrets dec " + name}, + Cmd: "helm", + Args: []string{"secrets", "dec", name}, Description: "Decrypting " + name, } @@ -637,8 +637,8 @@ func decryptSecret(name string) bool { // updateChartDep updates dependencies for a local chart func updateChartDep(chartPath string) (bool, string) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm dependency update " + chartPath}, + Cmd: "helm", + Args: []string{"dependency", "update", chartPath}, Description: "Updateing dependency for local chart " + chartPath, } diff --git a/helm_helpers_test.go b/helm_helpers_test.go index 57f0c243..eac9bd0c 100644 --- a/helm_helpers_test.go +++ b/helm_helpers_test.go @@ -8,11 +8,11 @@ import ( func setupTestCase(t *testing.T) func(t *testing.T) { t.Log("setup test case") - os.MkdirAll("/tmp/helmsman-tests/myapp", os.ModePerm) - os.MkdirAll("/tmp/helmsman-tests/dir-with space/myapp", os.ModePerm) + os.MkdirAll(os.TempDir()+"/helmsman-tests/myapp", os.ModePerm) + os.MkdirAll(os.TempDir()+"/helmsman-tests/dir-with space/myapp", os.ModePerm) cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm create '/tmp/helmsman-tests/dir-with space/myapp'"}, + Cmd: "helm", + Args: []string{"create", os.TempDir() + "/helmsman-tests/dir-with space/myapp"}, Description: "creating an empty local chart directory", } if exitCode, msg := cmd.exec(debug, verbose); exitCode != 0 { @@ -29,13 +29,13 @@ func Test_validateReleaseCharts(t *testing.T) { apps map[string]*release } tests := []struct { - name string + name string targetFlag []string - args args - want bool + args args + want bool }{ { - name: "test case 1: valid local path with no chart", + name: "test case 1: valid local path with no chart", targetFlag: []string{}, args: args{ apps: map[string]*release{ @@ -44,7 +44,7 @@ func Test_validateReleaseCharts(t *testing.T) { Description: "", Namespace: "", Enabled: true, - Chart: "/tmp/helmsman-tests/myapp", + Chart: os.TempDir() + "/helmsman-tests/myapp", Version: "", ValuesFile: "", ValuesFiles: []string{}, @@ -66,7 +66,7 @@ func Test_validateReleaseCharts(t *testing.T) { }, want: false, }, { - name: "test case 2: invalid local path", + name: "test case 2: invalid local path", targetFlag: []string{}, args: args{ apps: map[string]*release{ @@ -75,7 +75,7 @@ func Test_validateReleaseCharts(t *testing.T) { Description: "", Namespace: "", Enabled: true, - Chart: "/tmp/does-not-exist/myapp", + Chart: os.TempDir() + "/does-not-exist/myapp", Version: "", ValuesFile: "", ValuesFiles: []string{}, @@ -97,7 +97,7 @@ func Test_validateReleaseCharts(t *testing.T) { }, want: false, }, { - name: "test case 3: valid chart local path with whitespace", + name: "test case 3: valid chart local path with whitespace", targetFlag: []string{}, args: args{ apps: map[string]*release{ @@ -106,7 +106,7 @@ func Test_validateReleaseCharts(t *testing.T) { Description: "", Namespace: "", Enabled: true, - Chart: "/tmp/helmsman-tests/dir-with space/myapp", + Chart: os.TempDir() + "/helmsman-tests/dir-with space/myapp", Version: "0.1.0", ValuesFile: "", ValuesFiles: []string{}, @@ -128,7 +128,7 @@ func Test_validateReleaseCharts(t *testing.T) { }, want: true, }, { - name: "test case 4: valid chart from repo", + name: "test case 4: valid chart from repo", targetFlag: []string{}, args: args{ apps: map[string]*release{ @@ -138,7 +138,7 @@ func Test_validateReleaseCharts(t *testing.T) { Namespace: "", Enabled: true, Chart: "stable/prometheus", - Version: "", + Version: "*", ValuesFile: "", ValuesFiles: []string{}, SecretsFile: "", @@ -159,7 +159,7 @@ func Test_validateReleaseCharts(t *testing.T) { }, want: true, }, { - name: "test case 5: invalid local path for chart ignored with -target flag, while other app was targeted", + name: "test case 5: invalid local path for chart ignored with -target flag, while other app was targeted", targetFlag: []string{"notThisOne"}, args: args{ apps: map[string]*release{ @@ -168,7 +168,7 @@ func Test_validateReleaseCharts(t *testing.T) { Description: "", Namespace: "", Enabled: true, - Chart: "/tmp/does-not-exist/myapp", + Chart: os.TempDir() + "/does-not-exist/myapp", Version: "", ValuesFile: "", ValuesFiles: []string{}, @@ -190,7 +190,7 @@ func Test_validateReleaseCharts(t *testing.T) { }, want: true, }, { - name: "test case 6: invalid local path for chart included with -target flag", + name: "test case 6: invalid local path for chart included with -target flag", targetFlag: []string{"app"}, args: args{ apps: map[string]*release{ @@ -199,7 +199,7 @@ func Test_validateReleaseCharts(t *testing.T) { Description: "", Namespace: "", Enabled: true, - Chart: "/tmp/does-not-exist/myapp", + Chart: os.TempDir() + "/does-not-exist/myapp", Version: "", ValuesFile: "", ValuesFiles: []string{}, @@ -233,7 +233,7 @@ func Test_validateReleaseCharts(t *testing.T) { targetMap[target] = true } if got, msg := validateReleaseCharts(tt.args.apps); got != tt.want { - t.Errorf("getReleaseChartName() = %v, want %v , msg: %v", got, tt.want, msg) + t.Errorf("validateReleaseCharts() = %v, want %v , msg: %v", got, tt.want, msg) } }) } @@ -334,7 +334,7 @@ func Test_getReleaseChartVersion(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Log(tt.want) if got := getReleaseChartVersion(tt.args.r); got != tt.want { - t.Errorf("getReleaseChartName() = %v, want %v", got, tt.want) + t.Errorf("getReleaseChartVersion() = %v, want %v", got, tt.want) } }) } @@ -343,7 +343,8 @@ func Test_getReleaseChartVersion(t *testing.T) { func Test_getChartVersion(t *testing.T) { // version string = the first semver-valid string after the last hypen in the chart string. - + localChartsPath := os.TempDir() + "/helmsman-tests/local/charts" + os.MkdirAll(localChartsPath, os.ModePerm) type args struct { r *release } @@ -359,7 +360,7 @@ func Test_getChartVersion(t *testing.T) { Name: "release1", Namespace: "namespace", Version: "1.0.0", - Chart: "/local/charts", + Chart: localChartsPath, Enabled: true, }, }, diff --git a/init.go b/init.go index d5e95916..fa21236f 100644 --- a/init.go +++ b/init.go @@ -211,8 +211,8 @@ func init() { // It takes as input the tool's command to check if it is recognizable or not. e.g. helm or kubectl func toolExists(tool string) bool { cmd := command{ - Cmd: "bash", - Args: []string{"-c", tool}, + Cmd: tool, + Args: []string{}, Description: "validating that " + tool + " is installed.", } @@ -229,8 +229,8 @@ func toolExists(tool string) bool { // It takes as input the plugin's name to check if it is recognizable or not. e.g. diff func helmPluginExists(plugin string) bool { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm plugin list"}, + Cmd: "helm", + Args: []string{"plugin", "list"}, Description: "validating that " + plugin + " is installed.", } diff --git a/init_test.go b/init_test.go index c27815ce..35028b1d 100644 --- a/init_test.go +++ b/init_test.go @@ -24,9 +24,9 @@ func Test_toolExists(t *testing.T) { }, want: true, }, { - name: "test case 3 -- checking ipconfig exists.", + name: "test case 3 -- checking nonExistingTool exists.", args: args{ - tool: "ipconfig", + tool: "nonExistingTool", }, want: false, }, diff --git a/kube_helpers.go b/kube_helpers.go index d81902b8..b78c92cd 100644 --- a/kube_helpers.go +++ b/kube_helpers.go @@ -14,11 +14,11 @@ func validateServiceAccount(sa string, namespace string) (bool, string) { if namespace == "" { namespace = "default" } - ns := " -n " + namespace + ns := []string{"-n", namespace} cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl get serviceaccount " + sa + ns}, + Cmd: "kubectl", + Args: append([]string{"get", "serviceaccount", sa}, ns...), Description: "validating if serviceaccount [ " + sa + " ] exists in namespace [ " + namespace + " ].", } @@ -88,8 +88,8 @@ func overrideAppsNamespace(newNs string) { // createNamespace creates a namespace in the k8s cluster func createNamespace(ns string) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl create namespace " + ns}, + Cmd: "kubectl", + Args: []string{"create", "namespace", ns}, Description: "creating namespace " + ns, } @@ -103,8 +103,8 @@ func createNamespace(ns string) { func labelNamespace(ns string, labels map[string]string) { for k, v := range labels { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl label --overwrite namespace/" + ns + " " + k + "=" + v}, + Cmd: "kubectl", + Args: []string{"label", "--overwrite", "namespace/" + ns, k + "=" + v}, Description: "labeling namespace " + ns, } @@ -119,8 +119,8 @@ func labelNamespace(ns string, labels map[string]string) { func annotateNamespace(ns string, labels map[string]string) { for k, v := range labels { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl annotate --overwrite namespace/" + ns + " " + k + "=" + v}, + Cmd: "kubectl", + Args: []string{"annotate", "--overwrite", "namespace/" + ns, k + "=" + v}, Description: "annotating namespace " + ns, } @@ -159,8 +159,8 @@ spec: } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl apply -f temp-LimitRange.yaml -n " + ns}, + Cmd: "kubectl", + Args: []string{"apply", "-f", "temp-LimitRange.yaml", "-n", ns}, Description: "creating LimitRange in namespace [ " + ns + " ]", } @@ -230,23 +230,23 @@ func createContext() (bool, string) { } // connecting to the cluster - setCredentialsCmd := "" + setCredentialsCmdArgs := []string{} if s.Settings.BearerToken { token := readFile(tokenPath) if s.Settings.Username == "" { s.Settings.Username = "helmsman" } - setCredentialsCmd = "kubectl config set-credentials " + s.Settings.Username + " --token=" + token + setCredentialsCmdArgs = append(setCredentialsCmdArgs, "config", "set-credentials", s.Settings.Username, "--token="+token) } else { - setCredentialsCmd = "kubectl config set-credentials " + s.Settings.Username + " --username=" + s.Settings.Username + - " --password=" + s.Settings.Password + " --client-key=" + caKey + setCredentialsCmdArgs = append(setCredentialsCmdArgs, "config", "set-credentials", s.Settings.Username, "--username="+s.Settings.Username, + "--password="+s.Settings.Password, "--client-key="+caKey) if caClient != "" { - setCredentialsCmd = setCredentialsCmd + " --client-certificate=" + caClient + setCredentialsCmdArgs = append(setCredentialsCmdArgs, "--client-certificate="+caClient) } } cmd := command{ - Cmd: "bash", - Args: []string{"-c", setCredentialsCmd}, + Cmd: "kubectl", + Args: setCredentialsCmdArgs, Description: "creating kubectl context - setting credentials.", } @@ -255,9 +255,8 @@ func createContext() (bool, string) { } cmd = command{ - Cmd: "bash", - Args: []string{"-c", "kubectl config set-cluster " + s.Settings.KubeContext + " --server=" + s.Settings.ClusterURI + - " --certificate-authority=" + caCrt}, + Cmd: "kubectl", + Args: []string{"config", "set-cluster", s.Settings.KubeContext, "--server=" + s.Settings.ClusterURI, "--certificate-authority=" + caCrt}, Description: "creating kubectl context - setting cluster.", } @@ -266,9 +265,8 @@ func createContext() (bool, string) { } cmd = command{ - Cmd: "bash", - Args: []string{"-c", "kubectl config set-context " + s.Settings.KubeContext + " --cluster=" + s.Settings.KubeContext + - " --user=" + s.Settings.Username}, + Cmd: "kubectl", + Args: []string{"config", "set-context", s.Settings.KubeContext, "--cluster=" + s.Settings.KubeContext, "--user=" + s.Settings.Username}, Description: "creating kubectl context - setting context.", } @@ -291,8 +289,8 @@ func setKubeContext(context string) bool { } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl config use-context " + context}, + Cmd: "kubectl", + Args: []string{"config", "use-context", context}, Description: "setting kubectl context to [ " + context + " ]", } @@ -310,8 +308,8 @@ func setKubeContext(context string) bool { // It returns false if no context is set. func getKubeContext() bool { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl config current-context"}, + Cmd: "kubectl", + Args: []string{"config", "current-context"}, Description: "getting kubectl context", } @@ -328,8 +326,8 @@ func getKubeContext() bool { // createServiceAccount creates a service account in a given namespace and associates it with a cluster-admin role func createServiceAccount(saName string, namespace string) (bool, string) { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl create serviceaccount -n " + namespace + " " + saName}, + Cmd: "kubectl", + Args: []string{"create", "serviceaccount", "-n", namespace, saName}, Description: "creating service account [ " + saName + " ] in namespace [ " + namespace + " ]", } @@ -362,8 +360,8 @@ func createRoleBinding(role string, saName string, namespace string) (bool, stri log.Println("INFO: creating " + resource + " for service account [ " + saName + " ] in namespace [ " + namespace + " ] with role: " + role + ".") cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl create " + resource + " " + bindingName + " " + bindingOption + " --serviceaccount " + namespace + ":" + saName + " -n " + namespace}, + Cmd: "kubectl", + Args: []string{"create", resource, bindingName, bindingOption, "--serviceaccount", namespace + ":" + saName, "-n", namespace}, Description: "creating " + resource + " for service account [ " + saName + " ] in namespace [ " + namespace + " ] with role: " + role, } @@ -394,8 +392,8 @@ func createRole(namespace string, role string, roleTemplateFile string) (bool, s replaceStringInFile(resource, "temp-modified-role.yaml", map[string]string{"<>": namespace, "<>": role}) cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl apply -f temp-modified-role.yaml "}, + Cmd: "kubectl", + Args: []string{"apply", "-f", "temp-modified-role.yaml"}, Description: "creating role [" + role + "] in namespace [ " + namespace + " ]", } @@ -421,8 +419,8 @@ func labelResource(r *release) { } cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl label " + storageBackend + " -n " + getDesiredTillerNamespace(r) + " -l NAME=" + r.Name + " MANAGED-BY=HELMSMAN NAMESPACE=" + r.Namespace + " TILLER_NAMESPACE=" + getDesiredTillerNamespace(r) + " --overwrite"}, + Cmd: "kubectl", + Args: []string{"label", storageBackend, "-n", getDesiredTillerNamespace(r), "-l", "NAME=" + r.Name, "MANAGED-BY=HELMSMAN", "NAMESPACE=" + r.Namespace, "TILLER_NAMESPACE=" + getDesiredTillerNamespace(r), "--overwrite"}, Description: "applying labels to Helm state in [ " + getDesiredTillerNamespace(r) + " ] for " + r.Name, } @@ -461,8 +459,8 @@ func getHelmsmanReleases() map[string]map[string]bool { for _, ns := range namespaces { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl get " + storageBackend + " -n " + ns + " -l MANAGED-BY=HELMSMAN"}, + Cmd: "kubectl", + Args: []string{"get", storageBackend, "-n", ns, "-l", "MANAGED-BY=HELMSMAN"}, Description: "getting helm releases which are managed by Helmsman in namespace [[ " + ns + " ]].", } @@ -494,8 +492,8 @@ func getHelmsmanReleases() map[string]map[string]bool { // getKubectlClientVersion returns kubectl client version func getKubectlClientVersion() string { cmd := command{ - Cmd: "bash", - Args: []string{"-c", "kubectl version --client --short"}, + Cmd: "kubectl", + Args: []string{"version", "--client", "--short"}, Description: "checking kubectl version ", } diff --git a/release.go b/release.go index 83805afe..1a8ade0a 100644 --- a/release.go +++ b/release.go @@ -64,14 +64,15 @@ func validateRelease(appLabel string, r *release, names map[string]map[string]bo return false, "release " + r.Name + " is using namespace [ " + r.Namespace + " ] which is not defined in the Namespaces section of your desired state file." + " Release [ " + r.Name + " ] can't be installed in that Namespace until its defined." } - if r.Chart == "" || !strings.ContainsAny(r.Chart, "/") { + _, err := os.Stat(r.Chart) + if r.Chart == "" || os.IsNotExist(err) && !strings.ContainsAny(r.Chart, "/") { return false, "chart can't be empty and must be of the format: repo/chart." } if r.Version == "" { return false, "version can't be empty." } - _, err := os.Stat(r.ValuesFile) + _, err = os.Stat(r.ValuesFile) if r.ValuesFile != "" && (!isOfType(r.ValuesFile, []string{".yaml", ".yml", ".json"}) || err != nil) { return false, fmt.Sprintf("valuesFile must be a valid relative (from dsf file) file path for a yaml file, or can be left empty (provided path resolved to %q).", r.ValuesFile) } else if r.ValuesFile != "" && len(r.ValuesFiles) > 0 { diff --git a/utils.go b/utils.go index fb89d823..3a30acad 100644 --- a/utils.go +++ b/utils.go @@ -519,3 +519,12 @@ func Indent(s, prefix string) string { func isLocalChart(chart string) bool { return filepath.IsAbs(chart) } + +// concat appends all slices to a single slice +func concat(slices ...[]string) []string { + slice := []string{} + for _, item := range slices { + slice = append(slice, item...) + } + return slice +}