From 7fce142fe03be9c63764c716c0ae83e18653d384 Mon Sep 17 00:00:00 2001 From: pamiel <14542297+pamiel@users.noreply.github.com> Date: Fri, 29 Mar 2019 23:57:58 +0100 Subject: [PATCH 1/4] Add --timeout option + refactor some error message --- main.go | 59 +++++++++++++++++++++++++++++++++++++----------- pkg/helm/helm.go | 5 ++-- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index 781686e..41b4f7c 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ type sprayCmd struct { valueFiles []string valuesSet string force bool + timeout int dryRun bool verbose bool debug bool @@ -115,11 +116,11 @@ func newSprayCmd(args []string) *cobra.Command { if p.chartVersion != "" { if strings.Contains(p.chartName, "tgz") { - fmt.Println("You cannot use --version together with chart archive") + os.Stderr.WriteString("You cannot use --version together with chart archive\n") os.Exit(1) } if _, err := os.Stat(p.chartName); err == nil { - fmt.Println("You cannot use --version together with chart directory") + os.Stderr.WriteString("You cannot use --version together with chart directory\n") os.Exit(1) } @@ -128,6 +129,7 @@ func newSprayCmd(args []string) *cobra.Command { } if p.prefixReleasesWithNamespace == true && p.prefixReleases != "" { + os.Stderr.WriteString("You cannot use both --prefix-releases and --prefix-releases-with-namespace together\n") fmt.Println("You cannot use both --prefix-releases and --prefix-releases-with-namespace together") os.Exit(1) } @@ -147,6 +149,7 @@ func newSprayCmd(args []string) *cobra.Command { f.BoolVar(&p.reuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via '--set' and '-f'.\nIf '--reset-values' is specified, this is ignored") f.StringVarP(&p.valuesSet, "set", "", "", "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.BoolVar(&p.force, "force", false, "force resource update through delete/recreate if needed") + f.IntVar(&p.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)\nand for liveness and readiness (like Deployments and regular Jobs completion)") f.BoolVar(&p.dryRun, "dry-run", false, "simulate a spray") f.BoolVar(&p.verbose, "verbose", false, "enable spray verbose output") f.BoolVar(&p.debug, "debug", false, "enable helm debug output (also include spray verbose output)") @@ -247,8 +250,8 @@ func (p *sprayCmd) spray() error { if p.verbose { w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug) - fmt.Fprintln(w, "[spray] \tsubchart\tis alias of\ttargeted\tweight\t|corresponding release\trevision\tstatus\t") - fmt.Fprintln(w, "[spray] \t--------\t-----------\t--------\t------\t|---------------------\t--------\t------\t") + fmt.Fprintln(w, "[spray] \t subchart\t is alias of\t targeted\t weight\t| corresponding release\t revision\t status\t") + fmt.Fprintln(w, "[spray] \t --------\t -----------\t --------\t ------\t| ---------------------\t --------\t ------\t") for _, dependency := range dependencies { currentRevision := "None" @@ -259,10 +262,10 @@ func (p *sprayCmd) spray() error { } if dependency.Alias == "" { - fmt.Fprintln(w, fmt.Sprintf ("[spray] \t%s\t%s\t%t\t%d\t|%s\t%s\t%s\t", dependency.Name, "-", dependency.Targeted, dependency.Weight, dependency.CorrespondingReleaseName, currentRevision, currentStatus)) + fmt.Fprintln(w, fmt.Sprintf ("[spray] \t %s\t %s\t %t\t %d\t| %s\t %s\t %s\t", dependency.Name, "-", dependency.Targeted, dependency.Weight, dependency.CorrespondingReleaseName, currentRevision, currentStatus)) } else { - fmt.Fprintln(w, fmt.Sprintf ("[spray] \t%s\t%s\t%t\t%d\t|%s\t%s\t%s\t", dependency.Alias, dependency.Name, dependency.Targeted, dependency.Weight, dependency.CorrespondingReleaseName, currentRevision, currentStatus)) + fmt.Fprintln(w, fmt.Sprintf ("[spray] \t %s\t %s\t %t\t %d\t| %s\t %s\t %s\t", dependency.Alias, dependency.Name, dependency.Targeted, dependency.Weight, dependency.CorrespondingReleaseName, currentRevision, currentStatus)) } } w.Flush() @@ -304,7 +307,7 @@ w.Flush() valuesSet = valuesSet + p.valuesSet // Upgrade the Deployment - helmstatus := helm.UpgradeWithValues(p.namespace, dependency.CorrespondingReleaseName, p.chartName, p.resetValues, p.reuseValues, p.valueFiles, valuesSet, p.force, p.dryRun, p.debug) + helmstatus := helm.UpgradeWithValues(p.namespace, dependency.CorrespondingReleaseName, p.chartName, p.resetValues, p.reuseValues, p.valueFiles, valuesSet, p.force, p.timeout, p.dryRun, p.debug) helmstatusses = append(helmstatusses, helmstatus) log(3, "release: \"%s\" upgraded", dependency.CorrespondingReleaseName) @@ -338,42 +341,72 @@ w.Flush() if !p.dryRun { for _, status := range helmstatusses { + sleep_time := 5 + // Wait for completion of the Deployments update for _, dep := range status.Deployments { - for { + done := false + + for i := 0; i < p.timeout; { if p.verbose { log(3, "waiting for Deployment \"%s\"", dep) } if kubectl.IsDeploymentUpToDate(dep, p.namespace) { + done = true break } - time.Sleep(5 * time.Second) + time.Sleep(time.Duration(sleep_time) * time.Second) + i = i + sleep_time + } + + if !done { + os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n") + os.Stderr.WriteString("==> Error: exit status 1\n") + os.Exit(1) } } // Wait for completion of the StatefulSets update for _, ss := range status.StatefulSets { - for { + done := false + + for i := 0; i < p.timeout; { if p.verbose { log(3, "waiting for StatefulSet \"%s\"", ss) } if kubectl.IsStatefulSetUpToDate(ss, p.namespace) { + done = true break } - time.Sleep(5 * time.Second) + time.Sleep(time.Duration(sleep_time) * time.Second) + i = i + sleep_time + } + + if !done { + os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n") + os.Stderr.WriteString("==> Error: exit status 1\n") } } // Wait for completion of the Jobs for _, job := range status.Jobs { - for { + done := false + + for i := 0; i < p.timeout; { if p.verbose { log(3, "waiting for Job \"%s\"", job) } if kubectl.IsJobCompleted(job, p.namespace) { + done = true break } - time.Sleep(5 * time.Second) + time.Sleep(time.Duration(sleep_time) * time.Second) + i = i + sleep_time + } + + if !done { + os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n") + os.Stderr.WriteString("==> Error: exit status 1\n") } } } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 0289cb3..99e1ef3 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -15,6 +15,7 @@ package helm import ( "bytes" "strings" + "strconv" "bufio" "fmt" "os" @@ -237,9 +238,9 @@ func Delete(chart string, dryRun bool) { } // UpgradeWithValues ... -func UpgradeWithValues(namespace string, releaseName string, chartPath string, resetValues bool, reuseValues bool, valueFiles []string, valuesSet string, force bool, dryRun bool, debug bool) HelmStatus { +func UpgradeWithValues(namespace string, releaseName string, chartPath string, resetValues bool, reuseValues bool, valueFiles []string, valuesSet string, force bool, timeout int, dryRun bool, debug bool) HelmStatus { // Prepare parameters... - var myargs []string = []string{"upgrade", "--install", releaseName, chartPath, "--namespace", namespace, "--set", valuesSet} + var myargs []string = []string{"upgrade", "--install", releaseName, chartPath, "--namespace", namespace, "--set", valuesSet, "--timeout", strconv.Itoa(timeout)} for _, v := range valueFiles { myargs = append(myargs, "-f") myargs = append(myargs, v) From a92cea9ad63c8be7b245eb1078c3c53320bf3f2d Mon Sep 17 00:00:00 2001 From: pamiel <14542297+pamiel@users.noreply.github.com> Date: Mon, 1 Apr 2019 17:01:59 +0200 Subject: [PATCH 2/4] Correct management of the fetcing use cases --- main.go | 36 ++++++++++++++++++++++++++++-------- pkg/helm/helm.go | 21 ++++++++++++++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 41b4f7c..a7561b4 100644 --- a/main.go +++ b/main.go @@ -83,13 +83,13 @@ round-trip to the Tiller server. There are four different ways you can express the chart you want to install: - 1. By chart reference: helm spray stable/umbrella-chart + 1. By chart reference within a repo: helm spray stable/umbrella-chart 2. By path to a packaged chart: helm spray umbrella-chart-1.0.0-rc.1+build.32.tgz 3. By path to an unpacked chart directory: helm spray ./umbrella-chart 4. By absolute URL: helm spray https://example.com/charts/umbrella-chart-1.0.0-rc.1+build.32.tgz -It will install the latest version of that chart unless you also supply a version number with the -'--version' flag. +When specifying a chart reference or a chart URL, it installs the latest version +of that chart unless you also supply a version number with the '--version' flag. To see the list of chart repositories, use 'helm repo list'. To search for charts in a repository, use 'helm search'. @@ -111,29 +111,49 @@ func newSprayCmd(args []string) *cobra.Command { return errors.New("This command needs at least 1 argument: chart name") } - // TODO: check format for chart name (directory, url, tgz...) p.chartName = args[0] if p.chartVersion != "" { - if strings.Contains(p.chartName, "tgz") { + if strings.HasSuffix(p.chartName, "tgz") { os.Stderr.WriteString("You cannot use --version together with chart archive\n") os.Exit(1) } + if _, err := os.Stat(p.chartName); err == nil { os.Stderr.WriteString("You cannot use --version together with chart directory\n") os.Exit(1) } - log(1, "fetching chart \"%s\" version %s...", p.chartName, p.chartVersion) - helm.Fetch(p.chartName, p.chartVersion) + if (strings.HasPrefix(p.chartName, "http://") || strings.HasPrefix(p.chartName, "https://")) { + os.Stderr.WriteString("You cannot use --version together with chart URL\n") + os.Exit(1) + } } if p.prefixReleasesWithNamespace == true && p.prefixReleases != "" { os.Stderr.WriteString("You cannot use both --prefix-releases and --prefix-releases-with-namespace together\n") - fmt.Println("You cannot use both --prefix-releases and --prefix-releases-with-namespace together") os.Exit(1) } + + // If chart is specified through an url, the fetch it from the url. + if (strings.HasPrefix(p.chartName, "http://") || strings.HasPrefix(p.chartName, "https://")) { + log(1, "fetching chart from url \"%s\"...", p.chartName) + p.chartName = helm.Fetch(p.chartName, "") + + // If local file (or directory) does not exist, then fetch it from a repo. + } else if _, err := os.Stat(p.chartName); err != nil { + if p.chartVersion != "" { + log(1, "fetching chart \"%s\" version \"%s\" from repos...", p.chartName, p.chartVersion) + } else { + log(1, "fetching chart \"%s\" from repos...", p.chartName) + } + p.chartName = helm.Fetch(p.chartName, p.chartVersion) + + } else { + log(1, "processing chart from local file or directory \"%s\"...", p.chartName) + } + return p.spray() }, } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 99e1ef3..f30c433 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -15,7 +15,7 @@ package helm import ( "bytes" "strings" - "strconv" + "strconv" "bufio" "fmt" "os" @@ -91,6 +91,8 @@ func getStringBetween(value string, a string, b string) string { } // Parse the "helm status"-like output to extract releant information +// WARNING: this code has been developed and tested with version 'v2.12.2' of Helm +// it may need to be adapted to other versions of Helm. func parseStatusOutput(outs []byte, helmstatus *HelmStatus) { var out_str = string(outs) @@ -301,8 +303,15 @@ func Status(chart string) HelmStatus { } // Fetch ... -func Fetch(chart string, version string) { - cmd := exec.Command("helm", "fetch", chart, "--version", version) +func Fetch(chart string, version string) string { + var command string + if version != "" { + command = "mkdir /tmp/$$ && helm fetch " + chart + " --destination /tmp/$$ --version " + version + " && ls /tmp/$$ && mv /tmp/$$/* . && rmdir /tmp/$$" + } else { + command = "mkdir /tmp/$$ && helm fetch " + chart + " --destination /tmp/$$ && ls /tmp/$$ && mv /tmp/$$/* . && rmdir /tmp/$$" + } + + cmd := exec.Command("sh", "-c", command) cmdOutput := &bytes.Buffer{} cmd.Stdout = cmdOutput cmd.Stderr = os.Stderr @@ -310,4 +319,10 @@ func Fetch(chart string, version string) { printError(err) os.Exit(1) } + + output := cmdOutput.Bytes() + var output_str = string(output) + var result = strings.Trim (output_str, "\n") + return string(result) } + From a8c27ef78d0e2e4a779009ec8c5b0b12b411fcac Mon Sep 17 00:00:00 2001 From: pamiel <14542297+pamiel@users.noreply.github.com> Date: Thu, 4 Apr 2019 09:47:13 +0200 Subject: [PATCH 3/4] Add missing Exit --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index a7561b4..b945114 100644 --- a/main.go +++ b/main.go @@ -405,6 +405,7 @@ w.Flush() if !done { os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n") os.Stderr.WriteString("==> Error: exit status 1\n") + os.Exit(1) } } @@ -427,6 +428,7 @@ w.Flush() if !done { os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n") os.Stderr.WriteString("==> Error: exit status 1\n") + os.Exit(1) } } } From cbac2896d7da4464baded30b298c9d94d995d1d6 Mon Sep 17 00:00:00 2001 From: pamiel <14542297+pamiel@users.noreply.github.com> Date: Thu, 4 Apr 2019 10:12:40 +0200 Subject: [PATCH 4/4] Using Go's tmpDir instead of shell's mkdir --- pkg/helm/helm.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index f30c433..29364b0 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -17,6 +17,7 @@ import ( "strings" "strconv" "bufio" + "io/ioutil" "fmt" "os" "os/exec" @@ -304,12 +305,19 @@ func Status(chart string) HelmStatus { // Fetch ... func Fetch(chart string, version string) string { + tempDir, err := ioutil.TempDir("", "spray-") + if err != nil { + printError(err) + } + defer os.RemoveAll(tempDir) + var command string if version != "" { - command = "mkdir /tmp/$$ && helm fetch " + chart + " --destination /tmp/$$ --version " + version + " && ls /tmp/$$ && mv /tmp/$$/* . && rmdir /tmp/$$" + command = "helm fetch " + chart + " --destination " + tempDir + " --version " + version } else { - command = "mkdir /tmp/$$ && helm fetch " + chart + " --destination /tmp/$$ && ls /tmp/$$ && mv /tmp/$$/* . && rmdir /tmp/$$" + command = "helm fetch " + chart + " --destination " + tempDir } + command = command + " && ls " + tempDir + " && cp " + tempDir + "/* ." cmd := exec.Command("sh", "-c", command) cmdOutput := &bytes.Buffer{}