diff --git a/contrib/integrations/kubernetes/kubernetes.yml b/contrib/integrations/kubernetes/kubernetes.yml index 81714a660a..b31c52181a 100644 --- a/contrib/integrations/kubernetes/kubernetes.yml +++ b/contrib/integrations/kubernetes/kubernetes.yml @@ -15,11 +15,11 @@ deployment_default_config: timeout: type: string value: 180 - description: timeout in seconds + description: "timeout in seconds for v2 or duration for v3 (ex: 3m)" namespace: type: string value: default - description: Kubernetes namespace in which you want to deploy your components (OPTIONAL) + description: "Kubernetes namespace in which you want to deploy your components (OPTIONAL)" deployment_files: type: string description: Glob to yaml filepaths @@ -29,3 +29,6 @@ deployment_default_config: helm_values: type: string description: specify helm values in a YAML file or a URL to configure/override your helm chart + helm_version: + type: string + description: "specify helm version to use (default: 2.12.2)" diff --git a/contrib/integrations/kubernetes/plugin-kubernetes/main.go b/contrib/integrations/kubernetes/plugin-kubernetes/main.go index eed6256a57..cbb3754ee3 100644 --- a/contrib/integrations/kubernetes/plugin-kubernetes/main.go +++ b/contrib/integrations/kubernetes/plugin-kubernetes/main.go @@ -14,9 +14,11 @@ import ( "os/exec" "path" "path/filepath" + "strconv" "strings" "time" + "github.com/blang/semver" "github.com/golang/protobuf/ptypes/empty" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -277,24 +279,26 @@ func executeK8s(q *integrationplugin.DeployQuery) error { } func executeHelm(q *integrationplugin.DeployQuery) error { - releaseName := q.GetOptions()["cds.integration.release_name"] - namespace := q.GetOptions()["cds.integration.namespace"] - helmChart := q.GetOptions()["cds.integration.helm_chart"] - helmValues := q.GetOptions()["cds.integration.helm_values"] - timeoutStr := q.GetOptions()["cds.integration.timeout"] project := q.GetOptions()["cds.project"] workflow := q.GetOptions()["cds.workflow"] - application := q.GetOptions()["cds.application"] - if namespace == "" { - namespace = "default" + helmVersion := q.GetOptions()["cds.integration.helm_version"] + + if helmVersion == "" { + helmVersion = "2.12.2" } - if releaseName != "" { - application = releaseName + + version, err := semver.Parse(helmVersion) + if err != nil { + return fmt.Errorf("Invalid Helm Version %s - err: %v", helmVersion, err) + } + supportedVersion := ">=2.0.0 <4.0.0" + expectedRange, err := semver.ParseRange(supportedVersion) + if err != nil { + return fmt.Errorf("Fail to parse semver range : %v", err) } - helmFound := false - if _, err := exec.LookPath("helm"); err == nil { - helmFound = true + if !expectedRange(version) { + return fmt.Errorf("Unsupported helm version, should be : %s", supportedVersion) } cwd, err := os.Getwd() @@ -303,12 +307,38 @@ func executeHelm(q *integrationplugin.DeployQuery) error { } binaryName := "helm" + kubeCfg := "KUBECONFIG=" + path.Join(cwd, ".kube/config") + + helmFound := false + if _, err := exec.LookPath("helm"); err == nil { + helmFound = true + } + + if helmFound { + out, err := exec.Command(binaryName, "version", "--client", "--short").Output() + if err != nil { + return fmt.Errorf("Cannot check helm version : %v", err) + } + installedHelm := strings.TrimPrefix(string(out), "Client: ") + installedVersion, err := semver.ParseTolerant(installedHelm) + if err != nil { + return fmt.Errorf("Invalid installed Helm Version %s - err: %v", installedHelm, err) + } + + if !version.Equals(installedVersion) { + fmt.Println("Helm in path is not at correct version, need installation") + fmt.Printf("Path version : %s\n", installedVersion.String()) + fmt.Printf("Target version : %s\n", version.String()) + helmFound = false + } + } + if !helmFound { - fmt.Println("Download helm in progress...") + fmt.Printf("Download helm %s in progress...\n", version.String()) netClient := &http.Client{ Timeout: time.Second * 600, } - response, err := netClient.Get("https://storage.googleapis.com/kubernetes-helm/helm-v2.12.2-" + sdk.GOOS + "-" + sdk.GOARCH + ".tar.gz") + response, err := netClient.Get(fmt.Sprintf("https://get.helm.sh/helm-v%s-%s-%s.tar.gz", version.String(), sdk.GOOS, sdk.GOARCH)) if err != nil { return fmt.Errorf("Cannot download helm : %v", err) } @@ -334,6 +364,36 @@ func executeHelm(q *integrationplugin.DeployQuery) error { binaryName = path.Join(".", binaryName, sdk.GOOS+"-"+sdk.GOARCH, "helm") } + switch version.Major { + case 2: + return executeHelmV2(binaryName, kubeCfg, q) + case 3: + return executeHelmV3(binaryName, kubeCfg, q) + } + + return fmt.Errorf("Unsupported helm version") +} + +func executeHelmV2(binaryName, kubeCfg string, q *integrationplugin.DeployQuery) error { + releaseName := q.GetOptions()["cds.integration.release_name"] + namespace := q.GetOptions()["cds.integration.namespace"] + helmChart := q.GetOptions()["cds.integration.helm_chart"] + helmValues := q.GetOptions()["cds.integration.helm_values"] + timeoutStr := q.GetOptions()["cds.integration.timeout"] + application := q.GetOptions()["cds.application"] + + if namespace == "" { + namespace = "default" + } + if releaseName != "" { + application = releaseName + } + + if d, err := time.ParseDuration(timeoutStr); err == nil { + timeoutStr = strconv.Itoa(int(d.Seconds())) + fmt.Println("timeout is a duration, converting timeout in seconds to " + timeoutStr) + } + cmdInit := exec.Command(binaryName, "init", "--client-only") cmdInit.Env = os.Environ() cmdInit.Stderr = os.Stderr @@ -341,7 +401,6 @@ func executeHelm(q *integrationplugin.DeployQuery) error { if err := cmdInit.Run(); err != nil { return fmt.Errorf("Cannot execute helm init : %v", err) } - kubeCfg := "KUBECONFIG=" + path.Join(cwd, ".kube/config") if _, err := os.Stat(helmChart); err == nil { fmt.Println("Helm dependency update") @@ -402,6 +461,93 @@ func executeHelm(q *integrationplugin.DeployQuery) error { return nil } +func executeHelmV3(binaryName, kubeCfg string, q *integrationplugin.DeployQuery) error { + releaseName := q.GetOptions()["cds.integration.release_name"] + namespace := q.GetOptions()["cds.integration.namespace"] + helmChart := q.GetOptions()["cds.integration.helm_chart"] + helmValues := q.GetOptions()["cds.integration.helm_values"] + timeoutStr := q.GetOptions()["cds.integration.timeout"] + application := q.GetOptions()["cds.application"] + + if namespace == "" { + namespace = "default" + } + if releaseName != "" { + application = releaseName + } + if _, err := time.ParseDuration(timeoutStr); err != nil { + timeoutStr = timeoutStr + "s" + fmt.Println("timeout is not a duration, setting timeout to " + timeoutStr) + } + + cmdRepoAdd := exec.Command(binaryName, "repo", "add", "stable", "https://kubernetes-charts.storage.googleapis.com/") + cmdRepoAdd.Env = os.Environ() + cmdRepoAdd.Stderr = os.Stderr + cmdRepoAdd.Stdout = os.Stdout + if err := cmdRepoAdd.Run(); err != nil { + return fmt.Errorf("Cannot execute helm repo add stable : %v", err) + } + + if _, err := os.Stat(helmChart); err == nil { + fmt.Println("Helm dependency update") + cmdDependency := exec.Command(binaryName, "dependency", "update", helmChart) + cmdDependency.Env = os.Environ() + cmdDependency.Env = append(cmdDependency.Env, kubeCfg) + cmdDependency.Stderr = os.Stderr + cmdDependency.Stdout = os.Stdout + if errCmd := cmdDependency.Run(); errCmd != nil { + return fmt.Errorf("Cannot execute helm dependency update : %v", errCmd) + } + } + + cmdGet := exec.Command(binaryName, "get", "all", "--namespace="+namespace, application) + cmdGet.Env = os.Environ() + cmdGet.Env = append(cmdGet.Env, kubeCfg) + errCmd := cmdGet.Run() + + var args []string + if errCmd != nil { // Install + fmt.Printf("Install helm release '%s' with chart '%s'...\n", application, helmChart) + args = []string{"install", "--debug", "--timeout=" + timeoutStr, "--wait=true", "--namespace=" + namespace} + if helmValues != "" { + args = append(args, "-f", helmValues) + } + + helmChartArgs := strings.Split(helmChart, " ") + if len(helmChartArgs) > 1 { + args = append(args, "--repo="+helmChartArgs[0], helmChartArgs[1]) + args = append(args, application, helmChartArgs[1]) + } else { + args = append(args, application, helmChart) + } + } else { + fmt.Printf("Update helm release '%s' with chart '%s'...\n", application, helmChart) + args = []string{"upgrade", "--timeout=" + timeoutStr, "--wait=true", "--namespace=" + namespace} + if helmValues != "" { + args = append(args, "-f", helmValues) + } + + helmChartArgs := strings.Split(helmChart, " ") + if len(helmChartArgs) > 1 { + args = append(args, "--repo="+helmChartArgs[0], application, helmChartArgs[1]) + } else { + args = append(args, application, helmChart) + } + } + + fmt.Printf("Execute: helm %s\n", strings.Join(args, " ")) + cmd := exec.Command(binaryName, args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, kubeCfg) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { + return fmt.Errorf("Cannot execute helm install/update : %v", err) + } + + return nil +} + func writeHelmBinary(pathname string, gzipStream io.Reader) error { uncompressedStream, err := gzip.NewReader(gzipStream) if err != nil { diff --git a/docs/content/docs/concepts/files/application-syntax.md b/docs/content/docs/concepts/files/application-syntax.md index 11e0bc937c..9c0311e50c 100644 --- a/docs/content/docs/concepts/files/application-syntax.md +++ b/docs/content/docs/concepts/files/application-syntax.md @@ -48,6 +48,8 @@ deployments: value: deploy/helm/ helm_values: type: deploy/helm/values.yaml + helm_version: + type: 2.12.2 ``` ## Variables @@ -148,6 +150,8 @@ deployments: value: deploy/helm/ helm_values: type: deploy/helm/values-cluster-A.yaml + helm_version: + type: 2.12.2 my-kubernetes-cluster-B: namespace: @@ -156,6 +160,8 @@ deployments: value: deploy/helm/ helm_values: type: deploy/helm/values-cluster-B.yaml + helm_version: + type: 2.12.2 ``` The list of the availabe deployment platform is available from the Web UI on the `project / integration` section, or with the command `cdsctl project integration list` diff --git a/docs/content/docs/integrations/kubernetes/kubernetes_deployment.md b/docs/content/docs/integrations/kubernetes/kubernetes_deployment.md index 89a3c63951..f870f832a8 100644 --- a/docs/content/docs/integrations/kubernetes/kubernetes_deployment.md +++ b/docs/content/docs/integrations/kubernetes/kubernetes_deployment.md @@ -29,7 +29,7 @@ deployment_default_config: timeout: type: string value: 180 - description: timeout in seconds + description: timeout in seconds for v2 or duration for v3 (ex: 3m) namespace: type: string value: default @@ -43,6 +43,9 @@ deployment_default_config: helm_values: type: string description: specify helm values in a YAML file or a URL to configure/override your helm chart + helm_version: + type: string + description: specify helm version to use (default: v2.12.2) ``` Import the integration with : @@ -126,6 +129,10 @@ model: type: string description: specify helm values in a YAML file or a URL to configure/override your helm chart + helm_version: + value: "" + type: string + description: specify helm version to use (default: v2.12.2) namespace: value: default type: string @@ -185,6 +192,10 @@ deployment_default_config: value: "" type: string description: specify helm values in a YAML file or a URL to configure/override your helm chart + helm_version: + value: "" + type: string + description: specify helm version to use (default: v2.12.2) namespace: value: default type: string