From b335387eac3de9f7c68290503beebe2988644d5f Mon Sep 17 00:00:00 2001 From: Arthur Lenz Date: Fri, 24 Apr 2020 04:37:11 -0300 Subject: [PATCH] feat(k8s): Add basic support for Helm 3 in kubernetesDeploy step (#1438) * Extends kubernetesDeploy step to support Helm 3 Currently, the kubernetesDeploy step has no support to Helm 3 due to the fact that: - the initialization command used works only for Helm 2 - the image used when running the helm CLI is based on Helm 2 The need for Helm 3 support comes from the fact that Helm 3 introduces major architectural changes, more specifically, the removal of its server-side agent called Tiller - thus, being incompatible with one another. This commit adds this support by introducing a new configuration field (helmVersion). By default, its values is set to 2 (Helm 2) to avoid breaking any existing functionalities. * Use deployTool field to decide between Helm 2 or 3 * Remove helm init and replace wait for atomic in v3 * Update cmd/kubernetesDeploy.go Nice catch! Co-Authored-By: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> * Add documentation for kubernetesDeploy step * Add helm3 example for kubernetesDeploy step using mandatory fields * Add new line at the end of kubernetesDeploy documentation * Link kubernetesDeploy step with docs generator * Add possible values for deployTool in kubernetesDeploy * dummy change * Revert "dummy change" Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> --- cmd/kubernetesDeploy.go | 26 +++++---- cmd/kubernetesDeploy_test.go | 59 +++++++++++++++++++- documentation/docs/steps/kubernetesDeploy.md | 22 ++++++++ documentation/mkdocs.yml | 1 + resources/metadata/kubernetesdeploy.yaml | 11 ++++ 5 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 documentation/docs/steps/kubernetesDeploy.md diff --git a/cmd/kubernetesDeploy.go b/cmd/kubernetesDeploy.go index 1579c3d7e..1020d1ecb 100644 --- a/cmd/kubernetesDeploy.go +++ b/cmd/kubernetesDeploy.go @@ -23,7 +23,7 @@ func kubernetesDeploy(config kubernetesDeployOptions, telemetryData *telemetry.C } func runKubernetesDeploy(config kubernetesDeployOptions, command execRunner, stdout io.Writer) { - if config.DeployTool == "helm" { + if config.DeployTool == "helm" || config.DeployTool == "helm3" { runHelmDeploy(config, command, stdout) } else { runKubectlDeploy(config, command) @@ -48,16 +48,18 @@ func runHelmDeploy(config kubernetesDeployOptions, command execRunner, stdout io log.Entry().WithFields(helmLogFields).Debug("Calling Helm") helmEnv := []string{fmt.Sprintf("KUBECONFIG=%v", config.KubeConfig)} - if len(config.TillerNamespace) > 0 { + if config.DeployTool == "helm" && len(config.TillerNamespace) > 0 { helmEnv = append(helmEnv, fmt.Sprintf("TILLER_NAMESPACE=%v", config.TillerNamespace)) } log.Entry().Debugf("Helm SetEnv: %v", helmEnv) command.SetEnv(helmEnv) command.Stdout(stdout) - initParams := []string{"init", "--client-only"} - if err := command.RunExecutable("helm", initParams...); err != nil { - log.Entry().WithError(err).Fatal("Helm init called failed") + if config.DeployTool == "helm" { + initParams := []string{"init", "--client-only"} + if err := command.RunExecutable("helm", initParams...); err != nil { + log.Entry().WithError(err).Fatal("Helm init call failed") + } } var dockerRegistrySecret bytes.Buffer @@ -103,15 +105,19 @@ func runHelmDeploy(config kubernetesDeployOptions, command execRunner, stdout io config.ChartPath, "--install", "--force", - "--namespace", - config.Namespace, - "--wait", - "--timeout", - strconv.Itoa(config.HelmDeployWaitSeconds), + "--namespace", config.Namespace, "--set", fmt.Sprintf("image.repository=%v/%v,image.tag=%v,secret.dockerconfigjson=%v%v", containerRegistry, containerImageName, containerImageTag, dockerRegistrySecretData.Data.DockerConfJSON, ingressHosts), } + if config.DeployTool == "helm" { + upgradeParams = append(upgradeParams, "--wait", "--timeout", strconv.Itoa(config.HelmDeployWaitSeconds)) + } + + if config.DeployTool == "helm3" { + upgradeParams = append(upgradeParams, "--atomic", "--timeout", fmt.Sprintf("%vs", config.HelmDeployWaitSeconds)) + } + if len(config.KubeContext) > 0 { upgradeParams = append(upgradeParams, "--kube-context", config.KubeContext) } diff --git a/cmd/kubernetesDeploy_test.go b/cmd/kubernetesDeploy_test.go index e55b1014e..1cb1e0911 100644 --- a/cmd/kubernetesDeploy_test.go +++ b/cmd/kubernetesDeploy_test.go @@ -3,12 +3,13 @@ package cmd import ( "bytes" "fmt" - "github.com/SAP/jenkins-library/pkg/mock" "io/ioutil" "os" "path/filepath" "testing" + "github.com/SAP/jenkins-library/pkg/mock" + "github.com/stretchr/testify/assert" ) @@ -57,11 +58,11 @@ func TestRunKubernetesDeploy(t *testing.T) { "--force", "--namespace", "deploymentNamespace", + "--set", + "image.repository=my.registry:55555/path/to/Image,image.tag=latest,secret.dockerconfigjson=ThisIsOurBase64EncodedSecret==,ingress.hosts[0]=ingress.host1,ingress.hosts[1]=ingress.host2", "--wait", "--timeout", "400", - "--set", - "image.repository=my.registry:55555/path/to/Image,image.tag=latest,secret.dockerconfigjson=ThisIsOurBase64EncodedSecret==,ingress.hosts[0]=ingress.host1,ingress.hosts[1]=ingress.host2", "--kube-context", "testCluster", "--testParam", @@ -69,6 +70,58 @@ func TestRunKubernetesDeploy(t *testing.T) { }, e.Calls[2].Params, "Wrong upgrade parameters") }) + t.Run("test helm v3", func(t *testing.T) { + opts := kubernetesDeployOptions{ + ContainerRegistryURL: "https://my.registry:55555", + ContainerRegistryUser: "registryUser", + ContainerRegistryPassword: "********", + ChartPath: "path/to/chart", + DeploymentName: "deploymentName", + DeployTool: "helm3", + HelmDeployWaitSeconds: 400, + IngressHosts: []string{"ingress.host1", "ingress.host2"}, + Image: "path/to/Image:latest", + AdditionalParameters: []string{"--testParam", "testValue"}, + KubeContext: "testCluster", + Namespace: "deploymentNamespace", + } + + dockerConfigJSON := `{"kind": "Secret","data":{".dockerconfigjson": "ThisIsOurBase64EncodedSecret=="}}` + + e := mock.ExecMockRunner{ + StdoutReturn: map[string]string{ + "kubectl --insecure-skip-tls-verify=true create secret docker-registry regsecret --docker-server=my.registry:55555 --docker-username=registryUser --docker-password=******** --dry-run=true --output=json": dockerConfigJSON, + }, + } + + var stdout bytes.Buffer + + runKubernetesDeploy(opts, &e, &stdout) + + assert.Equal(t, "kubectl", e.Calls[0].Exec, "Wrong secret creation command") + assert.Equal(t, []string{"--insecure-skip-tls-verify=true", "create", "secret", "docker-registry", "regsecret", "--docker-server=my.registry:55555", "--docker-username=registryUser", "--docker-password=********", "--dry-run=true", "--output=json"}, e.Calls[0].Params, "Wrong secret creation parameters") + + assert.Equal(t, "helm", e.Calls[1].Exec, "Wrong upgrade command") + assert.Equal(t, []string{ + "upgrade", + "deploymentName", + "path/to/chart", + "--install", + "--force", + "--namespace", + "deploymentNamespace", + "--set", + "image.repository=my.registry:55555/path/to/Image,image.tag=latest,secret.dockerconfigjson=ThisIsOurBase64EncodedSecret==,ingress.hosts[0]=ingress.host1,ingress.hosts[1]=ingress.host2", + "--atomic", + "--timeout", + "400s", + "--kube-context", + "testCluster", + "--testParam", + "testValue", + }, e.Calls[1].Params, "Wrong upgrade parameters") + }) + t.Run("test kubectl - create secret/kubeconfig", func(t *testing.T) { dir, err := ioutil.TempDir("", "") defer os.RemoveAll(dir) // clean up diff --git a/documentation/docs/steps/kubernetesDeploy.md b/documentation/docs/steps/kubernetesDeploy.md new file mode 100644 index 000000000..3ae8e15fd --- /dev/null +++ b/documentation/docs/steps/kubernetesDeploy.md @@ -0,0 +1,22 @@ +# ${docGenStepName} + +## ${docGenDescription} + +## ${docGenParameters} + +## ${docGenConfiguration} + +## Exceptions + +None + +## Examples + +```groovy +kubernetesDeploy script: this +``` + +```groovy +// Deploy a helm chart called "myChart" using Helm 3 +kubernetesDeploy script: this, deployTool: 'helm3', chartPath: 'myChart', deploymentName: 'myRelease', image: 'nginx', containerRegistryUrl: 'https://docker.io' +``` diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 085e1795f..c31dc49be 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -63,6 +63,7 @@ nav: - jenkinsMaterializeLog: steps/jenkinsMaterializeLog.md - kanikoExecute: steps/kanikoExecute.md - karmaExecuteTests: steps/karmaExecuteTests.md + - kubernetesDeploy: steps/kubernetesDeploy.md - mailSendNotification: steps/mailSendNotification.md - mavenExecute: steps/mavenExecute.md - mavenExecuteStaticCodeChecks: steps/mavenExecuteStaticCodeChecks.md diff --git a/resources/metadata/kubernetesdeploy.yaml b/resources/metadata/kubernetesdeploy.yaml index 533d42972..7fefb23f8 100644 --- a/resources/metadata/kubernetesdeploy.yaml +++ b/resources/metadata/kubernetesdeploy.yaml @@ -135,6 +135,10 @@ spec: - STAGES - STEPS default: kubectl + possibleValues: + - kubectl + - helm + - helm3 - name: helmDeployWaitSeconds type: int description: Number of seconds before helm deploy returns. @@ -211,6 +215,13 @@ spec: - STAGES - STEPS containers: + - image: dtzar/helm-kubectl:3.1.2 + workingDir: /config + conditions: + - conditionRef: strings-equal + params: + - name: deployTool + value: helm3 - image: dtzar/helm-kubectl:2.12.1 workingDir: /config conditions: