diff --git a/README.md b/README.md index c664e73..a812b72 100644 --- a/README.md +++ b/README.md @@ -76,17 +76,24 @@ Note: if an alias is set for a sub-chart, then this is this alias that should be ### Flags: ``` - --debug enable verbose output - --dry-run simulate a spray - --force force resource update through delete/recreate if needed - -h, --help help for helm - -n, --namespace string namespace to spray the chart into. (default "default") - --reset-values when upgrading, reset the values to the ones built into the chart - --reuse-values when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored. - --set string set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -t, --target strings specify the subchart to target (can specify multiple). If --target is not specified, all subcharts are targeted - -f, --values strings specify values in a YAML file or a URL (can specify multiple) - --version string specify the exact chart version to install. If this is not specified, the latest version is installed + --debug enable helm debug output (also include spray verbose output) + --dry-run simulate a spray + --force force resource update through delete/recreate if needed + -h, --help help for helm + -n, --namespace string namespace to spray the chart into. (default "default") + --prefix-releases string prefix the releases by the given string, resulting into releases names formats: + "-" + Allowed characters are a-z A-Z 0-9 and - + --prefix-releases-with-namespace prefix the releases by the name of the namespace, resulting into releases names formats: + "-" + --reset-values when upgrading, reset the values to the ones built into the chart + --reuse-values when upgrading, reuse the last release's values and merge in any overrides from the command line via '--set' and '-f'. + If '--reset-values' is specified, this is ignored. + --set string set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + -t, --target strings specify the subchart to target (can specify multiple). If '--target' is not specified, all subcharts are targeted + -f, --values strings specify values in a YAML file or a URL (can specify multiple) + --verbose enable spray verbose output + --version string specify the exact chart version to install. If this is not specified, the latest version is installed ``` ## Install diff --git a/main.go b/main.go index 87ff82f..781686e 100644 --- a/main.go +++ b/main.go @@ -16,9 +16,11 @@ import ( "errors" "fmt" "os" + "bufio" "strconv" "strings" "time" + "text/tabwriter" "github.com/gemalto/helm-spray/pkg/helm" "github.com/gemalto/helm-spray/pkg/kubectl" @@ -28,26 +30,30 @@ import ( ) type sprayCmd struct { - chartName string - chartVersion string - targets []string - namespace string - resetValues bool - reuseValues bool - valueFiles []string - valuesSet string - force bool - dryRun bool - debug bool + chartName string + chartVersion string + targets []string + namespace string + prefixReleases string + prefixReleasesWithNamespace bool + resetValues bool + reuseValues bool + valueFiles []string + valuesSet string + force bool + dryRun bool + verbose bool + debug bool } // Dependency ... type Dependency struct { - Name string - Alias string - UsedName string - Targeted bool - Weight int + Name string + Alias string + UsedName string + Targeted bool + Weight int + CorrespondingReleaseName string } var ( @@ -116,24 +122,34 @@ func newSprayCmd(args []string) *cobra.Command { fmt.Println("You cannot use --version together with chart directory") os.Exit(1) } + + log(1, "fetching chart \"%s\" version %s...", p.chartName, p.chartVersion) helm.Fetch(p.chartName, p.chartVersion) } + if p.prefixReleasesWithNamespace == true && p.prefixReleases != "" { + fmt.Println("You cannot use both --prefix-releases and --prefix-releases-with-namespace together") + os.Exit(1) + } + return p.spray() }, } f := cmd.Flags() f.StringSliceVarP(&p.valueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") - f.StringVarP(&p.namespace, "namespace", "n", "default", "namespace to spray the chart into.") + f.StringVarP(&p.namespace, "namespace", "n", "default", "namespace to spray the chart into") f.StringVarP(&p.chartVersion, "version", "", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") - f.StringSliceVarP(&p.targets, "target", "t", []string{}, "specify the subchart to target (can specify multiple). If --target is not specified, all subcharts are targeted") + f.StringSliceVarP(&p.targets, "target", "t", []string{}, "specify the subchart to target (can specify multiple). If '--target' is not specified, all subcharts are targeted") + f.StringVarP(&p.prefixReleases, "prefix-releases", "", "", "prefix the releases by the given string, resulting into releases names formats:\n \"-\"\nAllowed characters are a-z A-Z 0-9 and -") + f.BoolVar(&p.prefixReleasesWithNamespace, "prefix-releases-with-namespace", false, "prefix the releases by the name of the namespace, resulting into releases names formats:\n \"-\"") f.BoolVar(&p.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") - 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. If '--reset-values' is specified, this is ignored.") + 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.BoolVar(&p.dryRun, "dry-run", false, "simulate a spray") - f.BoolVar(&p.debug, "debug", false, "enable verbose output") + 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)") f.Parse(args) // When called through helm, debug mode is transmitted through the HELM_DEBUG envvar @@ -142,6 +158,9 @@ func newSprayCmd(args []string) *cobra.Command { p.debug = true } } + if p.debug { + p.verbose = true + } return cmd @@ -167,8 +186,8 @@ func (p *sprayCmd) spray() error { panic(fmt.Errorf("%s", err)) } + // Build the list of all rependencies, and their key attributes dependencies := make([]Dependency, len(reqs.Dependencies)) - for i, req := range reqs.Dependencies { // Dependency name and alias dependencies[i].Name = req.Name @@ -203,27 +222,74 @@ func (p *sprayCmd) spray() error { } dependencies[i].Weight = w } + + // Compute the corresponding release name + if p.prefixReleasesWithNamespace == true { + dependencies[i].CorrespondingReleaseName = p.namespace + "-" + dependencies[i].UsedName + } else if p.prefixReleases != "" { + dependencies[i].CorrespondingReleaseName = p.prefixReleases + "-" + dependencies[i].UsedName + } else { + dependencies[i].CorrespondingReleaseName = dependencies[i].UsedName + } } - // For debug... - if p.debug { + + // Starting the processing... + if p.prefixReleasesWithNamespace == true { + log(1, "deploying solution chart \"%s\" in namespace \"%s\", with releases prefix \"%s-\"", p.chartName, p.namespace, p.namespace) + } else if p.prefixReleases != "" { + log(1, "deploying solution chart \"%s\" in namespace \"%s\", with releases prefix \"%s-\"", p.chartName, p.namespace, p.prefixReleases) + } else { + log(1, "deploying solution chart \"%s\" in namespace \"%s\"", p.chartName, p.namespace) + } + + helmReleases := helm.List(p.namespace) + + 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") + for _, dependency := range dependencies { + currentRevision := "None" + currentStatus := "Not deployed" + if release, ok := helmReleases[dependency.CorrespondingReleaseName]; ok { + currentRevision = strconv.Itoa(release.Revision) + currentStatus = release.Status + } + if dependency.Alias == "" { - fmt.Printf("[spray] subchart: \"%s\" | targeted: %t | weight: %d\n", dependency.Name, dependency.Targeted, dependency.Weight) + 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.Printf("[spray] subchart: \"%s\" (is alias of: \"%s\") | targeted: %t | weight: %d\n", dependency.Alias, dependency.Name, dependency.Targeted, dependency.Weight) + 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() } + // Loop on the increasing weight for i := 0; i <= getMaxWeight(dependencies); i++ { shouldWait := false + firstInWeight := true + helmstatusses := make([]helm.HelmStatus, 0) - // Upgrade the current (targeted) Deployments, following the increasing weight + // Upgrade the targeted Deployments corresponding the the current weight for _, dependency := range dependencies { if dependency.Targeted { if dependency.Weight == i { - fmt.Println("[spray] upgrading release: \"" + dependency.UsedName + "\"...") + if firstInWeight { + log(1, "processing sub-charts of weight %d", dependency.Weight) + firstInWeight = false + } + + if release, ok := helmReleases[dependency.CorrespondingReleaseName]; ok { + log(2, "upgrading release \"%s\": going from revision %d (status %s) to %d...", dependency.CorrespondingReleaseName, release.Revision, release.Status, release.Revision+1) + + } else { + log(2, "upgrading release \"%s\": deploying first revision...", dependency.CorrespondingReleaseName) + } + shouldWait = true // Add the ".enabled" flags to ensure that only the current chart is to be executed @@ -238,29 +304,73 @@ func (p *sprayCmd) spray() error { valuesSet = valuesSet + p.valuesSet // Upgrade the Deployment - helm.UpgradeWithValues(p.namespace, dependency.UsedName, dependency.UsedName, 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.dryRun, p.debug) + helmstatusses = append(helmstatusses, helmstatus) + + log(3, "release: \"%s\" upgraded", dependency.CorrespondingReleaseName) + if p.verbose { + log(3, "helm status: %s", helmstatus.Status) + } - if !p.dryRun { - status := helm.GetHelmStatus(dependency.UsedName) - if status != "DEPLOYED" { - os.Exit(1) + if p.verbose { + log(3, "helm resources:") + var scanner = bufio.NewScanner(strings.NewReader(helmstatus.Resources)) + for scanner.Scan() { + if len (scanner.Text()) > 0 { + log(4, scanner.Text()) + } } } - fmt.Println("[spray] release: \"" + dependency.UsedName + "\" upgraded") + if helmstatus.Status == "" { + log(2, "Warning: no status returned by helm.") + } else if helmstatus.Status != "DEPLOYED" { + log(2, "Error: status returned by helm differs from \"DEPLOYED\". Cannot continue spray processing.") + os.Exit(1) + } } } } - // Wait availability of the Deployment just upgraded + // Wait availability of the just upgraded Releases if shouldWait { - fmt.Println("[spray] waiting for Liveness and Readiness...") + log(2, "waiting for Liveness and Readiness...") if !p.dryRun { - for _, dependency := range dependencies { - if dependency.Weight == i && dependency.Targeted == true { + for _, status := range helmstatusses { + // Wait for completion of the Deployments update + for _, dep := range status.Deployments { + for { + if p.verbose { + log(3, "waiting for Deployment \"%s\"", dep) + } + if kubectl.IsDeploymentUpToDate(dep, p.namespace) { + break + } + time.Sleep(5 * time.Second) + } + } + + // Wait for completion of the StatefulSets update + for _, ss := range status.StatefulSets { + for { + if p.verbose { + log(3, "waiting for StatefulSet \"%s\"", ss) + } + if kubectl.IsStatefulSetUpToDate(ss, p.namespace) { + break + } + time.Sleep(5 * time.Second) + } + } + + // Wait for completion of the Jobs + for _, job := range status.Jobs { for { - if kubectl.IsDeploymentUpToDate(dependency.UsedName, p.namespace) { + if p.verbose { + log(3, "waiting for Job \"%s\"", job) + } + if kubectl.IsJobCompleted(job, p.namespace) { break } time.Sleep(5 * time.Second) @@ -271,7 +381,7 @@ func (p *sprayCmd) spray() error { } } - fmt.Println("[spray] upgrade completed.") + log(1, "upgrade of solution chart \"%s\" completed", p.chartName) return nil } @@ -289,6 +399,23 @@ func getMaxWeight(v []Dependency) (m int) { return m } +// Log spray messages +func log(level int, str string, params ...interface{}) { + var logStr = "[spray] " + + if level == 2 { + logStr = logStr + " > " + } else if level == 3 { + logStr = logStr + " o " + } else if level == 4 { + logStr = logStr + " - " + } else if level >= 5 { + logStr = logStr + " . " + } + + fmt.Println(logStr + fmt.Sprintf(str, params...)) +} + func main() { cmd := newSprayCmd(os.Args[1:]) if err := cmd.Execute(); err != nil { diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 85c4867..0289cb3 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -14,20 +14,42 @@ package helm import ( "bytes" + "strings" + "bufio" "fmt" "os" "os/exec" "regexp" + "encoding/json" ) -type helmStatus struct { - status string + +// Types returned by some of the functions +type HelmStatus struct { + Namespace string + Status string + Resources string + Deployments []string + StatefulSets []string + Jobs []string +} + +type HelmRelease struct { + Name string + Revision int + Updated string + Status string + Chart string + AppVersion string + Namespace string } + +// Printing error or outputs func printError(err error) { if err != nil { os.Stderr.WriteString(fmt.Sprintf("==> Error: %s\n", err.Error())) - os.Exit(-1) + os.Exit(1) } } @@ -37,14 +59,104 @@ func printOutput(outs []byte) { } } -func parseOutput(outs []byte, helmstatus *helmStatus) { +// Utility functions to parse strings +func getStringAfter(value string, a string) string { + // Get substring after a string. + pos := strings.LastIndex(value, a) + if pos == -1 { + return "" + } + adjustedPos := pos + len(a) + if adjustedPos >= len(value) { + return "" + } + return value[adjustedPos:len(value)] +} + +func getStringBetween(value string, a string, b string) string { + // Get substring between two strings. + posFirst := strings.Index(value, a) + if posFirst == -1 { + return "" + } + + posFirstAdjusted := posFirst + len(a) + posLast := strings.Index(value[posFirstAdjusted:], b) + if posLast == -1 { + return "" + } + posLastAdjusted := posFirstAdjusted + posLast + return value[posFirstAdjusted:posLastAdjusted] +} + +// Parse the "helm status"-like output to extract releant information +func parseStatusOutput(outs []byte, helmstatus *HelmStatus) { + var out_str = string(outs) + + // Extract the namespace + var namespace = regexp.MustCompile(`NAMESPACE: (.*)`) + result := namespace.FindStringSubmatch(out_str) + if len(result) > 0 { + helmstatus.Namespace = string(result[1]) + } + + // Extract the status var status = regexp.MustCompile(`STATUS: (.*)`) - result := status.FindStringSubmatch(string(outs)) + result = status.FindStringSubmatch(out_str) if len(result) > 0 { - helmstatus.status = string(result[1]) + helmstatus.Status = string(result[1]) + } + + // Extract the resources + helmstatus.Resources = getStringAfter (out_str, "RESOURCES:") + + // ... and get the Deployments from the resources + var res = getStringBetween (helmstatus.Resources + "==>", "==> v1beta1/Deployment", "==>") + var res_as_slice = make([]string, 0) + var scanner = bufio.NewScanner(strings.NewReader(res)) + for scanner.Scan() { + if len (scanner.Text()) > 0 { + name := strings.Fields(scanner.Text())[0] + res_as_slice = append (res_as_slice, name) + } + } + if len(res_as_slice) > 0 { + helmstatus.Deployments = res_as_slice[1:] + } + + // ... and get the StatefulSets from the resources + res = getStringBetween (helmstatus.Resources + "==>", "==> v1beta1/StatefulSet", "==>") + res_as_slice = make([]string, 0) + scanner = bufio.NewScanner(strings.NewReader(res)) + for scanner.Scan() { + if len (scanner.Text()) > 0 { + name := strings.Fields(scanner.Text())[0] + res_as_slice = append (res_as_slice, name) + } + } + if len(res_as_slice) > 0 { + helmstatus.StatefulSets = res_as_slice[1:] + } + + // ... and get the Jobs from the resources + res = getStringBetween (helmstatus.Resources + "==>", "==> v1/Job", "==>") + res_as_slice = make([]string, 0) + scanner = bufio.NewScanner(strings.NewReader(res)) + for scanner.Scan() { + if len (scanner.Text()) > 0 { + name := strings.Fields(scanner.Text())[0] + res_as_slice = append (res_as_slice, name) + } + } + if len(res_as_slice) > 0 { + helmstatus.Jobs = res_as_slice[1:] } } + +// Helm functions calls +// -------------------- + // Version ... func Version() { fmt.Print("helm version: ") @@ -52,6 +164,7 @@ func Version() { cmdOutput := &bytes.Buffer{} cmd.Stdout = cmdOutput if err := cmd.Run(); err != nil { + printError(err) os.Exit(1) } output := cmdOutput.Bytes() @@ -59,23 +172,51 @@ func Version() { } // List ... -func List(namespace string) { - cmd := exec.Command("helm", "list", "--namespace", namespace, "-c") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - os.Exit(1) +type helmReleasesList struct { + Next string + Releases []HelmRelease +} + +func List(namespace string) map[string]HelmRelease { + helmlist := make(map[string]HelmRelease, 0) + next := "~FIRST" + + // Loop on the chunks returned by the "helm list" command + for next != "" { + if next == "~FIRST" { + next = "" + } + + // Get the list of Releases of the chunk + cmd := exec.Command("helm", "list", "--namespace", namespace, "-c", "--output", "json", "-o", next) + cmdOutput := &bytes.Buffer{} + cmd.Stdout = cmdOutput + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + printError(err) + os.Exit(1) + } + + // Transform the received json into structs + output := cmdOutput.Bytes() + var releases helmReleasesList + json.Unmarshal([]byte(output), &releases) + + // Add the Releases into a map + for _, r := range releases.Releases { + helmlist[r.Name] = r + } + + // Loop on next chunk + next = releases.Next } + + return helmlist } // ListAll ... -func ListAll() { - cmd := exec.Command("helm", "list") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - os.Exit(1) - } +func ListAll() map[string]HelmRelease { + return List ("") } // Delete chart @@ -90,105 +231,82 @@ func Delete(chart string, dryRun bool) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { + printError(err) os.Exit(1) } } // UpgradeWithValues ... -func UpgradeWithValues(namespace string, release string, chartName string, chartPath string, resetValues bool, reuseValues bool, valueFiles []string, valuesSet string, force bool, dryRun bool, debug bool) { - var myargs []string = []string{"upgrade", "--install", release, chartPath, "--namespace", namespace, "--set", valuesSet} +func UpgradeWithValues(namespace string, releaseName string, chartPath string, resetValues bool, reuseValues bool, valueFiles []string, valuesSet string, force bool, dryRun bool, debug bool) HelmStatus { + // Prepare parameters... + var myargs []string = []string{"upgrade", "--install", releaseName, chartPath, "--namespace", namespace, "--set", valuesSet} for _, v := range valueFiles { myargs = append(myargs, "-f") myargs = append(myargs, v) } - if resetValues { myargs = append(myargs, "--reset-values") } - if reuseValues { myargs = append(myargs, "--reuse-values") } - if force { myargs = append(myargs, "--force") } - if dryRun { myargs = append(myargs, "--dry-run") } - if debug { myargs = append(myargs, "--debug") - fmt.Printf("[spray] running helm command for \"%s\": %v\n", release, myargs) + fmt.Printf("[spray] running helm command for \"%s\": %v\n", releaseName, myargs) } + // Run the upgrade command cmd := exec.Command("helm", myargs...) - if debug { - cmd.Stdout = os.Stdout - } else { - cmdOutput := &bytes.Buffer{} - cmd.Stdout = cmdOutput - } - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - os.Exit(1) - } - -} -// Upgrade ... -func Upgrade(namespace string, chart string, chartPath string, valuesSet string, dryRun bool, debug bool) { - - var myargs []string - if dryRun { - myargs = []string{"upgrade", "--install", "--namespace", namespace, "--set", chart + ".enabled=true," + valuesSet, chart, chartPath, "--dry-run"} - } else { - myargs = []string{"upgrade", "--install", "--namespace", namespace, "--set", chart + ".enabled=true," + valuesSet, chart, chartPath} - } - - if debug { - myargs = append(myargs, "--debug") - fmt.Printf("[spray] running command: %v\n", myargs) - } + cmdOutput := &bytes.Buffer{} + cmd.Stderr = os.Stderr + cmd.Stdout = cmdOutput + err := cmd.Run() + output := cmdOutput.Bytes() - cmd := exec.Command("helm", myargs...) if debug { - cmd.Stdout = os.Stdout - } else { - cmdOutput := &bytes.Buffer{} - cmd.Stdout = cmdOutput + fmt.Printf(string(output)) } - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { + if err != nil { + printError(err) os.Exit(1) } + + // Parse the ending helm status. + helmstatus := HelmStatus{} + parseStatusOutput(output, &helmstatus) + return helmstatus } -// GetHelmStatus ... -func GetHelmStatus(chart string) string { +// Status ... +func Status(chart string) HelmStatus { cmd := exec.Command("helm", "status", chart) cmdOutput := &bytes.Buffer{} cmd.Stdout = cmdOutput if err := cmd.Run(); err != nil { + printError(err) os.Exit(1) } output := cmdOutput.Bytes() - helmstatus := helmStatus{} - parseOutput(output, &helmstatus) - return helmstatus.status + helmstatus := HelmStatus{} + parseStatusOutput(output, &helmstatus) + return helmstatus } // Fetch ... func Fetch(chart string, version string) { - fmt.Println("[spray] Fetching chart " + chart + " version " + version + " ...") cmd := exec.Command("helm", "fetch", chart, "--version", version) cmdOutput := &bytes.Buffer{} cmd.Stdout = cmdOutput cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { + printError(err) os.Exit(1) } } diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index d68101c..23814ce 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -59,15 +59,27 @@ func GetDeployment(namespace string) { } } +// GetJob ... +func GetJob(namespace string) { + cmd := exec.Command("kubectl", "get", "jobs", "--namespace", namespace) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + os.Exit(1) + } +} + + // IsDeploymentUpToDate ... func IsDeploymentUpToDate(deployment string, namespace string) bool { - desired := getDesired("deployment", namespace, deployment) - updated := getUpdated("deployment", namespace, deployment) - ready := getReady("deployment", namespace, deployment) + desired := getDesired("deployment", deployment, namespace) + current := getCurrent("deployment", deployment, namespace) + updated := getUpdated("deployment", deployment, namespace) + ready := getReady("deployment", deployment, namespace) if desired != ready { return false } else { - if desired == updated { + if (desired == updated) && (desired == current) { return true } else { return false @@ -77,8 +89,8 @@ func IsDeploymentUpToDate(deployment string, namespace string) bool { // IsStatefulSetUpToDate ... func IsStatefulSetUpToDate(deployment string, namespace string) bool { - desired := getDesired("statefulset", namespace, deployment) - ready := getReady("statefulset", namespace, deployment) + desired := getDesired("statefulset", deployment, namespace) + ready := getReady("statefulset", deployment, namespace) if desired == ready { return true } else { @@ -86,29 +98,44 @@ func IsStatefulSetUpToDate(deployment string, namespace string) bool { } } -func getDesired(k8stype string, namespace string, deployment string) string { - desired, err := exec.Command("kubectl", "--namespace", namespace, "get", k8stype, deployment, "-o=jsonpath='{.spec.replicas}'").Output() - if err != nil { - // Cannot make the difference between an error when calling kubectl and no corresponding resource found. Return "0" in any case. - return "0" +// IsJobCompleted ... +func IsJobCompleted(job string, namespace string) bool { + succeeded := getSucceeded("job", job, namespace) + if succeeded == "'1'" { + return true + } else { + return false } - return string(desired) } -func getReady(k8stype string, namespace string, deployment string) string { - ready, err := exec.Command("kubectl", "--namespace", namespace, "get", k8stype, deployment, "-o=jsonpath='{.status.readyReplicas}'").Output() + +// Utility functions to get informations extracted from a 'kubectl get' command result +func getObjectDescriptionItem(k8sObjectType string, objectName string, namespace string, itemJsonPath string) string { + item, err := exec.Command("kubectl", "--namespace", namespace, "get", k8sObjectType, objectName, "-o=jsonpath='{" + itemJsonPath + "}'").Output() if err != nil { - // Cannot make the difference between an error when calling kubectl and no corresponding resource found. Return "0" in any case. - return "0" + // Cannot make the difference between an error when calling kubectl and no corresponding resource found. Return "" in any case. + return "" } - return string(ready) + return string(item) } -func getUpdated(k8stype string, namespace string, deployment string) string { - updated, err := exec.Command("kubectl", "--namespace", namespace, "get", k8stype, deployment, "-o=jsonpath='{.status.updatedReplicas}'").Output() - if err != nil { - // Cannot make the difference between an error when calling kubectl and no corresponding resource found. Return "0" in any case. - return "0" - } - return string(updated) +func getDesired(k8sObjectType string, objectName string, namespace string) string { + return getObjectDescriptionItem(k8sObjectType, objectName, namespace, ".spec.replicas") +} + +func getCurrent(k8sObjectType string, objectName string, namespace string) string { + return getObjectDescriptionItem(k8sObjectType, objectName, namespace, ".status.replicas") +} + +func getReady(k8sObjectType string, objectName string, namespace string) string { + return getObjectDescriptionItem(k8sObjectType, objectName, namespace, ".status.readyReplicas") +} + +func getUpdated(k8sObjectType string, objectName string, namespace string) string { + return getObjectDescriptionItem(k8sObjectType, objectName, namespace, ".status.updatedReplicas") } + +func getSucceeded(k8sObjectType string, objectName string, namespace string) string { + return getObjectDescriptionItem(k8sObjectType, objectName, namespace, ".status.succeeded") +} +