From a67b4ce558f162b5d764a3123914a2b02ec9139b Mon Sep 17 00:00:00 2001 From: Pavel Busko Date: Mon, 28 Feb 2022 10:43:55 +0100 Subject: [PATCH] feat(kubernetesDeploy): added valuesMapping config option (#3568) --- cmd/kubernetesDeploy.go | 55 ++++++++++++--- cmd/kubernetesDeploy_generated.go | 70 ++++++++++-------- cmd/kubernetesDeploy_test.go | 90 ++++++++++++++++++++++++ resources/metadata/kubernetesDeploy.yaml | 16 +++++ 4 files changed, 192 insertions(+), 39 deletions(-) diff --git a/cmd/kubernetesDeploy.go b/cmd/kubernetesDeploy.go index c0129a435..783e37183 100644 --- a/cmd/kubernetesDeploy.go +++ b/cmd/kubernetesDeploy.go @@ -16,6 +16,7 @@ import ( "github.com/SAP/jenkins-library/pkg/kubernetes" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/pkg/errors" ) func kubernetesDeploy(config kubernetesDeployOptions, telemetryData *telemetry.CustomData) { @@ -52,7 +53,9 @@ func runHelmDeploy(config kubernetesDeployOptions, utils kubernetes.DeployUtils, log.Entry().WithError(err).Fatalf("Container registry url '%v' incorrect", config.ContainerRegistryURL) } - helmValues := helmValues{} + helmValues := helmValues{ + mapping: config.ValuesMapping, + } if len(config.ImageNames) > 0 { if len(config.ImageNames) != len(config.ImageNameTags) { @@ -74,7 +77,7 @@ func runHelmDeploy(config kubernetesDeployOptions, utils kubernetes.DeployUtils, } } } else { - //support either image or containerImageName and containerImageTag + // support either image or containerImageName and containerImageTag containerImageName := "" containerImageTag := "" if len(config.Image) > 0 { @@ -177,6 +180,11 @@ func runHelmDeploy(config kubernetesDeployOptions, utils kubernetes.DeployUtils, upgradeParams = append(upgradeParams, "--values", v) } + err = helmValues.mapValues() + if err != nil { + return errors.Wrap(err, "failed to map values using 'valuesMapping' configuration") + } + upgradeParams = append( upgradeParams, "--install", @@ -304,7 +312,7 @@ func runKubectlDeploy(config kubernetesDeployOptions, utils kubernetes.DeployUti log.Entry().WithError(err).Fatalf("Error when reading appTemplate '%v'", config.AppTemplate) } - //support either image or containerImageName and containerImageTag + // support either image or containerImageName and containerImageTag fullImage := "" if len(config.Image) > 0 { @@ -339,8 +347,11 @@ func runKubectlDeploy(config kubernetesDeployOptions, utils kubernetes.DeployUti return nil } -type helmValues []struct { - key, value string +type helmValues struct { + mapping map[string]interface{} + values []struct { + key, value string + } } func joinKey(parts ...string) string { @@ -352,8 +363,8 @@ func joinKey(parts ...string) string { return strings.Join(escapedParts, ".") } -func (values *helmValues) add(key, value string) { - *values = append(*values, struct { +func (hv *helmValues) add(key, value string) { + hv.values = append(hv.values, struct { key string value string }{ @@ -362,9 +373,35 @@ func (values *helmValues) add(key, value string) { }) } -func (values helmValues) marshal() string { +func (hv helmValues) get(key string) string { + for _, item := range hv.values { + if item.key == key { + return item.value + } + } + + return "" +} + +func (hv *helmValues) mapValues() error { + for dst, src := range hv.mapping { + srcString, ok := src.(string) + if !ok { + return fmt.Errorf("invalid path '%#v' is used for valuesMapping, only strings are supported", src) + } + if val := hv.get(srcString); val != "" { + hv.add(dst, val) + } else { + log.Entry().Warnf("can not map '%s: %s', %s is not set", dst, src, src) + } + } + + return nil +} + +func (hv helmValues) marshal() string { builder := strings.Builder{} - for idx, item := range values { + for idx, item := range hv.values { if idx > 0 { builder.WriteString(",") } diff --git a/cmd/kubernetesDeploy_generated.go b/cmd/kubernetesDeploy_generated.go index faaf8e6c6..e3651743f 100644 --- a/cmd/kubernetesDeploy_generated.go +++ b/cmd/kubernetesDeploy_generated.go @@ -16,36 +16,37 @@ import ( ) type kubernetesDeployOptions struct { - AdditionalParameters []string `json:"additionalParameters,omitempty"` - APIServer string `json:"apiServer,omitempty"` - AppTemplate string `json:"appTemplate,omitempty"` - ChartPath string `json:"chartPath,omitempty"` - ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"` - ContainerImageName string `json:"containerImageName,omitempty"` - ContainerImageTag string `json:"containerImageTag,omitempty"` - ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"` - ContainerRegistryUser string `json:"containerRegistryUser,omitempty"` - ContainerRegistrySecret string `json:"containerRegistrySecret,omitempty"` - CreateDockerRegistrySecret bool `json:"createDockerRegistrySecret,omitempty"` - DeploymentName string `json:"deploymentName,omitempty"` - DeployTool string `json:"deployTool,omitempty" validate:"possible-values=kubectl helm helm3"` - ForceUpdates bool `json:"forceUpdates,omitempty"` - HelmDeployWaitSeconds int `json:"helmDeployWaitSeconds,omitempty"` - HelmValues []string `json:"helmValues,omitempty"` - Image string `json:"image,omitempty"` - ImageNames []string `json:"imageNames,omitempty"` - ImageNameTags []string `json:"imageNameTags,omitempty"` - IngressHosts []string `json:"ingressHosts,omitempty"` - KeepFailedDeployments bool `json:"keepFailedDeployments,omitempty"` - RunHelmTests bool `json:"runHelmTests,omitempty"` - ShowTestLogs bool `json:"showTestLogs,omitempty"` - KubeConfig string `json:"kubeConfig,omitempty"` - KubeContext string `json:"kubeContext,omitempty"` - KubeToken string `json:"kubeToken,omitempty"` - Namespace string `json:"namespace,omitempty"` - TillerNamespace string `json:"tillerNamespace,omitempty"` - DockerConfigJSON string `json:"dockerConfigJSON,omitempty"` - DeployCommand string `json:"deployCommand,omitempty" validate:"possible-values=apply replace"` + AdditionalParameters []string `json:"additionalParameters,omitempty"` + APIServer string `json:"apiServer,omitempty"` + AppTemplate string `json:"appTemplate,omitempty"` + ChartPath string `json:"chartPath,omitempty"` + ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"` + ContainerImageName string `json:"containerImageName,omitempty"` + ContainerImageTag string `json:"containerImageTag,omitempty"` + ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"` + ContainerRegistryUser string `json:"containerRegistryUser,omitempty"` + ContainerRegistrySecret string `json:"containerRegistrySecret,omitempty"` + CreateDockerRegistrySecret bool `json:"createDockerRegistrySecret,omitempty"` + DeploymentName string `json:"deploymentName,omitempty"` + DeployTool string `json:"deployTool,omitempty" validate:"possible-values=kubectl helm helm3"` + ForceUpdates bool `json:"forceUpdates,omitempty"` + HelmDeployWaitSeconds int `json:"helmDeployWaitSeconds,omitempty"` + HelmValues []string `json:"helmValues,omitempty"` + ValuesMapping map[string]interface{} `json:"valuesMapping,omitempty"` + Image string `json:"image,omitempty"` + ImageNames []string `json:"imageNames,omitempty"` + ImageNameTags []string `json:"imageNameTags,omitempty"` + IngressHosts []string `json:"ingressHosts,omitempty"` + KeepFailedDeployments bool `json:"keepFailedDeployments,omitempty"` + RunHelmTests bool `json:"runHelmTests,omitempty"` + ShowTestLogs bool `json:"showTestLogs,omitempty"` + KubeConfig string `json:"kubeConfig,omitempty"` + KubeContext string `json:"kubeContext,omitempty"` + KubeToken string `json:"kubeToken,omitempty"` + Namespace string `json:"namespace,omitempty"` + TillerNamespace string `json:"tillerNamespace,omitempty"` + DockerConfigJSON string `json:"dockerConfigJSON,omitempty"` + DeployCommand string `json:"deployCommand,omitempty" validate:"possible-values=apply replace"` } // KubernetesDeployCommand Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster. @@ -175,6 +176,7 @@ func addKubernetesDeployFlags(cmd *cobra.Command, stepConfig *kubernetesDeployOp cmd.Flags().BoolVar(&stepConfig.ForceUpdates, "forceUpdates", true, "Adds `--force` flag to a helm resource update command or to a kubectl replace command") cmd.Flags().IntVar(&stepConfig.HelmDeployWaitSeconds, "helmDeployWaitSeconds", 300, "Number of seconds before helm deploy returns.") cmd.Flags().StringSliceVar(&stepConfig.HelmValues, "helmValues", []string{}, "List of helm values as YAML file reference or URL (as per helm parameter description for `-f` / `--values`)") + cmd.Flags().StringVar(&stepConfig.Image, "image", os.Getenv("PIPER_image"), "Full name of the image to be deployed.") cmd.Flags().StringSliceVar(&stepConfig.ImageNames, "imageNames", []string{}, "List of names of the images to be deployed.") cmd.Flags().StringSliceVar(&stepConfig.ImageNameTags, "imageNameTags", []string{}, "List of full names (registry and tag) of the images to be deployed.") @@ -391,6 +393,14 @@ func kubernetesDeployMetadata() config.StepData { Aliases: []config.Alias{}, Default: []string{}, }, + { + Name: "valuesMapping", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "map[string]interface{}", + Mandatory: false, + Aliases: []config.Alias{}, + }, { Name: "image", ResourceRef: []config.ResourceReference{ diff --git a/cmd/kubernetesDeploy_test.go b/cmd/kubernetesDeploy_test.go index fa3476767..5b74a6615 100644 --- a/cmd/kubernetesDeploy_test.go +++ b/cmd/kubernetesDeploy_test.go @@ -799,6 +799,96 @@ func TestRunKubernetesDeploy(t *testing.T) { assert.EqualError(t, err, "number of imageNames and imageNameTags must be equal") }) + t.Run("test helm v3 - with multiple images and valuesMapping", func(t *testing.T) { + opts := kubernetesDeployOptions{ + ContainerRegistryURL: "https://my.registry:55555", + ContainerRegistryUser: "registryUser", + ContainerRegistryPassword: "dummy", + ContainerRegistrySecret: "testSecret", + ChartPath: "path/to/chart", + DeploymentName: "deploymentName", + DeployTool: "helm3", + ForceUpdates: true, + HelmDeployWaitSeconds: 400, + HelmValues: []string{"values1.yaml", "values2.yaml"}, + ValuesMapping: map[string]interface{}{ + "subchart.image.registry": "image.myImage.repository", + "subchart.image.tag": "image.myImage.tag", + }, + ImageNames: []string{"myImage", "myImage.sub1", "myImage.sub2"}, + ImageNameTags: []string{"myImage:myTag", "myImage-sub1:myTag", "myImage-sub2:myTag"}, + AdditionalParameters: []string{"--testParam", "testValue"}, + KubeContext: "testCluster", + Namespace: "deploymentNamespace", + DockerConfigJSON: ".pipeline/docker/config.json", + } + + dockerConfigJSON := `{"kind": "Secret","data":{".dockerconfigjson": "ThisIsOurBase64EncodedSecret=="}}` + + mockUtils := newKubernetesDeployMockUtils() + mockUtils.StdoutReturn = map[string]string{ + `kubectl create secret generic testSecret --from-file=.dockerconfigjson=.pipeline/docker/config.json --type=kubernetes.io/dockerconfigjson --insecure-skip-tls-verify=true --dry-run=client --output=json`: dockerConfigJSON, + } + + var stdout bytes.Buffer + + require.NoError(t, runKubernetesDeploy(opts, &telemetry.CustomData{}, mockUtils, &stdout)) + + assert.Equal(t, "kubectl", mockUtils.Calls[0].Exec, "Wrong secret creation command") + assert.Equal(t, []string{ + "create", + "secret", + "generic", + "testSecret", + "--from-file=.dockerconfigjson=.pipeline/docker/config.json", + "--type=kubernetes.io/dockerconfigjson", + "--insecure-skip-tls-verify=true", + "--dry-run=client", + "--output=json"}, + mockUtils.Calls[0].Params, "Wrong secret creation parameters") + + assert.Equal(t, "helm", mockUtils.Calls[1].Exec, "Wrong upgrade command") + + assert.Contains(t, mockUtils.Calls[1].Params, `image.myImage.repository=my.registry:55555/myImage,image.myImage.tag=myTag,image.myImage\.sub1.repository=my.registry:55555/myImage-sub1,image.myImage\.sub1.tag=myTag,image.myImage\.sub2.repository=my.registry:55555/myImage-sub2,image.myImage\.sub2.tag=myTag,secret.name=testSecret,secret.dockerconfigjson=ThisIsOurBase64EncodedSecret==,imagePullSecrets[0].name=testSecret,subchart.image.registry=my.registry:55555/myImage,subchart.image.tag=myTag`, "Wrong upgrade parameters") + + }) + + t.Run("test helm v3 - with multiple images and incorrect valuesMapping", func(t *testing.T) { + opts := kubernetesDeployOptions{ + ContainerRegistryURL: "https://my.registry:55555", + ContainerRegistryUser: "registryUser", + ContainerRegistryPassword: "dummy", + ContainerRegistrySecret: "testSecret", + ChartPath: "path/to/chart", + DeploymentName: "deploymentName", + DeployTool: "helm3", + ForceUpdates: true, + HelmDeployWaitSeconds: 400, + HelmValues: []string{"values1.yaml", "values2.yaml"}, + ValuesMapping: map[string]interface{}{ + "subchart.image.registry": false, + }, + ImageNames: []string{"myImage", "myImage.sub1", "myImage.sub2"}, + ImageNameTags: []string{"myImage:myTag", "myImage-sub1:myTag", "myImage-sub2:myTag"}, + AdditionalParameters: []string{"--testParam", "testValue"}, + KubeContext: "testCluster", + Namespace: "deploymentNamespace", + DockerConfigJSON: ".pipeline/docker/config.json", + } + + dockerConfigJSON := `{"kind": "Secret","data":{".dockerconfigjson": "ThisIsOurBase64EncodedSecret=="}}` + + mockUtils := newKubernetesDeployMockUtils() + mockUtils.StdoutReturn = map[string]string{ + `kubectl create secret generic testSecret --from-file=.dockerconfigjson=.pipeline/docker/config.json --type=kubernetes.io/dockerconfigjson --insecure-skip-tls-verify=true --dry-run=client --output=json`: dockerConfigJSON, + } + + var stdout bytes.Buffer + + require.Error(t, runKubernetesDeploy(opts, &telemetry.CustomData{}, mockUtils, &stdout), "invalid path 'false' is used for valueMapping, only strings are supported") + + }) + t.Run("test helm3 - fails without image information", func(t *testing.T) { opts := kubernetesDeployOptions{ ContainerRegistryURL: "https://my.registry:55555", diff --git a/resources/metadata/kubernetesDeploy.yaml b/resources/metadata/kubernetesDeploy.yaml index 4434e4bd5..b1dff4a5f 100644 --- a/resources/metadata/kubernetesDeploy.yaml +++ b/resources/metadata/kubernetesDeploy.yaml @@ -223,6 +223,22 @@ spec: - PARAMETERS - STAGES - STEPS + - name: valuesMapping + type: "map[string]interface{}" + longDescription: | + Mapping of values provided by Piper onto custom paths in format `[custom-path]: [piper-value]` + + Example: + ```yaml + valuesMapping: + subchart.image.tag: image.debug.tag + subchart.image.repository: image.debug.repository + subchart.image.pullsecret: secret.dockerconfigjson + ``` + scope: + - PARAMETERS + - STAGES + - STEPS - name: image aliases: - name: deployImage