1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

(feat) support for kustomize in gitopsUpdateDeployment step (#3524)

* (feat) support for kustomize in gitopsUpdateDeployment step

Signed-off-by: Michael Sprauer <Michael.Sprauer@sap.com>

* add missing documentation

* add another detail in the documentation

Signed-off-by: Michael Sprauer <Michael.Sprauer@sap.com>

* generate again the update doc

Signed-off-by: Michael Sprauer <Michael.Sprauer@sap.com>

Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
This commit is contained in:
Michael 2022-02-14 07:45:54 +01:00 committed by GitHub
parent f08ff92171
commit 1ea965ae69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 230 additions and 22 deletions

View File

@ -20,6 +20,7 @@ import (
const toolKubectl = "kubectl"
const toolHelm = "helm"
const toolKustomize = "kustomize"
type iGitopsUpdateDeploymentGitUtils interface {
CommitSingleFile(filePath, commitMessage, author string) (plumbing.Hash, error)
@ -38,6 +39,7 @@ type gitopsUpdateDeploymentExecRunner interface {
RunExecutable(executable string, params ...string) error
Stdout(out io.Writer)
Stderr(err io.Writer)
SetDir(dir string)
}
type gitopsUpdateDeploymentGitUtils struct {
@ -121,6 +123,11 @@ func runGitopsUpdateDeployment(config *gitopsUpdateDeploymentOptions, command gi
if err != nil {
return errors.Wrap(err, "failed to apply helm command")
}
} else if config.Tool == toolKustomize {
outputBytes, err = runKustomizeCommand(command, config, filePath)
if err != nil {
return errors.Wrap(err, "failed to apply kustomize command")
}
} else {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.New("tool " + config.Tool + " is not supported")
@ -154,6 +161,12 @@ func checkRequiredFieldsForDeployTool(config *gitopsUpdateDeploymentOptions) err
return errors.Wrap(err, "missing required fields for kubectl")
}
logNotRequiredButFilledFieldForKubectl(config)
} else if config.Tool == toolKustomize {
err := checkRequiredFieldsForKustomize(config)
if err != nil {
return errors.Wrap(err, "missing required fields for kustomize")
}
logNotRequiredButFilledFieldForKustomize(config)
}
return nil
@ -174,6 +187,21 @@ func checkRequiredFieldsForHelm(config *gitopsUpdateDeploymentOptions) error {
return nil
}
func checkRequiredFieldsForKustomize(config *gitopsUpdateDeploymentOptions) error {
var missingParameters []string
if config.FilePath == "" {
missingParameters = append(missingParameters, "filePath")
}
if config.DeploymentName == "" {
missingParameters = append(missingParameters, "deploymentName")
}
if len(missingParameters) > 0 {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Errorf("the following parameters are necessary for kustomize: %v", missingParameters)
}
return nil
}
func checkRequiredFieldsForKubectl(config *gitopsUpdateDeploymentOptions) error {
var missingParameters []string
if config.ContainerName == "" {
@ -203,6 +231,14 @@ func logNotRequiredButFilledFieldForKubectl(config *gitopsUpdateDeploymentOption
log.Entry().Info("deploymentName is not used for kubectl and can be removed")
}
}
func logNotRequiredButFilledFieldForKustomize(config *gitopsUpdateDeploymentOptions) {
if config.ChartPath != "" {
log.Entry().Info("chartPath is not used for kubectl and can be removed")
}
if len(config.HelmValues) > 0 {
log.Entry().Info("helmValues is not used for kubectl and can be removed")
}
}
func cloneRepositoryAndChangeBranch(config *gitopsUpdateDeploymentOptions, gitUtils iGitopsUpdateDeploymentGitUtils, temporaryFolder string) error {
err := gitUtils.PlainClone(config.Username, config.Password, config.ServerURL, temporaryFolder)
@ -292,6 +328,27 @@ func runHelmCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdat
return helmOutput.Bytes(), nil
}
func runKustomizeCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdateDeploymentOptions, filePath string) ([]byte, error) {
var kustomizeOutput = bytes.Buffer{}
runner.Stdout(&kustomizeOutput)
kustomizeParams := []string{
"edit",
"set",
"image",
config.DeploymentName + "=" + config.ContainerImageNameTag,
}
runner.SetDir(filepath.Dir(filePath))
err := runner.RunExecutable(toolKustomize, kustomizeParams...)
if err != nil {
return nil, errors.Wrap(err, "failed to execute kustomize command")
}
return kustomizeOutput.Bytes(), nil
}
// buildRegistryPlusImageAndTagSeparately combines the registry together with the image name. Handles the tag separately.
// Tag is defined by everything on the right hand side of the colon sign. This looks weird for sha container versions but works for helm.
func buildRegistryPlusImageAndTagSeparately(config *gitopsUpdateDeploymentOptions) (string, string, error) {

View File

@ -28,7 +28,7 @@ type gitopsUpdateDeploymentOptions struct {
ChartPath string `json:"chartPath,omitempty"`
HelmValues []string `json:"helmValues,omitempty"`
DeploymentName string `json:"deploymentName,omitempty"`
Tool string `json:"tool,omitempty" validate:"possible-values=kubectl helm"`
Tool string `json:"tool,omitempty" validate:"possible-values=kubectl helm kustomize"`
}
// GitopsUpdateDeploymentCommand Updates Kubernetes Deployment Manifest in an Infrastructure Git Repository
@ -49,9 +49,10 @@ func GitopsUpdateDeploymentCommand() *cobra.Command {
It can for example be used for GitOps scenarios where the update of the manifests triggers an update of the corresponding deployment in Kubernetes.
As of today, it supports the update of deployment yaml files via kubectl patch and update a whole helm template.
For kubectl the container inside the yaml must be described within the following hierarchy: ` + "`" + `{"spec":{"template":{"spec":{"containers":[{...}]}}}}` + "`" + `
For helm the whole template is generated into a file and uploaded into the repository.`,
As of today, it supports the update of deployment yaml files via kubectl patch, update a whole helm template and kustomize.
For *kubectl* the container inside the yaml must be described within the following hierarchy: ` + "`" + `{"spec":{"template":{"spec":{"containers":[{...}]}}}}` + "`" + `
For *helm* the whole template is generated into a file and uploaded into the repository.
For *kustomize* the ` + "`" + `images` + "`" + ` section will be update with the current image.`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
@ -133,13 +134,13 @@ func addGitopsUpdateDeploymentFlags(cmd *cobra.Command, stepConfig *gitopsUpdate
cmd.Flags().StringVar(&stepConfig.ServerURL, "serverUrl", `https://github.com`, "GitHub server url to the repository.")
cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User name for git authentication")
cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password/token for git authentication.")
cmd.Flags().StringVar(&stepConfig.FilePath, "filePath", os.Getenv("PIPER_filePath"), "Relative path in the git repository to the deployment descriptor file that shall be updated")
cmd.Flags().StringVar(&stepConfig.FilePath, "filePath", os.Getenv("PIPER_filePath"), "Relative path in the git repository to the deployment descriptor file that shall be updated. For different tools this has different semantics:\n\n * `kubectl` - path to the `deployment.yaml` that should be patched.\n * `helm` - not used. Please use `chartPath` instead.\n * `kustomize` - path to the `kustomization.yaml`.\n")
cmd.Flags().StringVar(&stepConfig.ContainerName, "containerName", os.Getenv("PIPER_containerName"), "The name of the container to update")
cmd.Flags().StringVar(&stepConfig.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "http(s) url of the Container registry where the image is located")
cmd.Flags().StringVar(&stepConfig.ContainerImageNameTag, "containerImageNameTag", os.Getenv("PIPER_containerImageNameTag"), "Container image name with version tag to annotate in the deployment configuration.")
cmd.Flags().StringVar(&stepConfig.ChartPath, "chartPath", os.Getenv("PIPER_chartPath"), "Defines the chart path for deployments using helm.")
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.DeploymentName, "deploymentName", os.Getenv("PIPER_deploymentName"), "Defines the name of the deployment.")
cmd.Flags().StringVar(&stepConfig.DeploymentName, "deploymentName", os.Getenv("PIPER_deploymentName"), "Defines the name of the deployment. In case of `kustomize` this is the name or alias of the image in the `kustomization.yaml`")
cmd.Flags().StringVar(&stepConfig.Tool, "tool", `kubectl`, "Defines the tool which should be used to update the deployment description.")
cmd.MarkFlagRequired("branchName")
@ -313,6 +314,7 @@ func gitopsUpdateDeploymentMetadata() config.StepData {
Containers: []config.Container{
{Image: "dtzar/helm-kubectl:3.3.4", WorkingDir: "/config", Options: []config.Option{{Name: "-u", Value: "0"}}, Conditions: []config.Condition{{ConditionRef: "strings-equal", Params: []config.Param{{Name: "tool", Value: "helm"}}}}},
{Image: "dtzar/helm-kubectl:2.17.0", WorkingDir: "/config", Options: []config.Option{{Name: "-u", Value: "0"}}, Conditions: []config.Condition{{ConditionRef: "strings-equal", Params: []config.Param{{Name: "tool", Value: "kubectl"}}}}},
{Image: "k8s.gcr.io/kustomize/kustomize:v3.8.7", WorkingDir: "/config", Options: []config.Option{{Name: "-u", Value: "0"}}, Conditions: []config.Condition{{ConditionRef: "strings-equal", Params: []config.Param{{Name: "tool", Value: "kustomize"}}}}},
},
},
}

View File

@ -96,6 +96,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -117,6 +118,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -138,6 +140,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -158,6 +161,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -178,6 +182,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -323,6 +328,7 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -346,6 +352,7 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -369,6 +376,7 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -391,6 +399,7 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
runnerMock.expectedYaml = expectedYaml
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
@ -525,11 +534,74 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
})
}
func TestRunGitopsUpdateDeploymentWithKustomize(t *testing.T) {
var validConfiguration = &gitopsUpdateDeploymentOptions{
BranchName: "main",
CommitMessage: "This is the commit message",
ServerURL: "https://github.com",
Username: "admin3",
Password: "validAccessToken",
FilePath: "kustomization.yaml",
ContainerRegistryURL: "https://myregistry.com",
ContainerImageNameTag: "registry/containers/myFancyContainer:1337",
Tool: "kustomize",
DeploymentName: "myFancyDeployment",
}
t.Parallel()
t.Run("successful run", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{}
runnerMock.expectedYaml = expectedKustomize
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, fsMock)
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedKustomize, gitUtilsMock.savedFile)
assert.Equal(t, "This is the commit message", gitUtilsMock.commitMessage)
assert.Equal(t, "kustomize", runnerMock.executable)
assert.Equal(t, "edit", runnerMock.params[0])
assert.Equal(t, "set", runnerMock.params[1])
assert.Equal(t, "image", runnerMock.params[2])
assert.Equal(t, "myFancyDeployment=registry/containers/myFancyContainer:1337", runnerMock.params[3])
})
t.Run("error on kustomize execution", func(t *testing.T) {
t.Parallel()
runner := &gitOpsExecRunnerMock{failOnRunExecutable: true}
err := runGitopsUpdateDeployment(validConfiguration, runner, &gitUtilsMock{}, &filesMock{})
assert.EqualError(t, err, "failed to apply kustomize command: failed to execute kustomize command: error happened")
})
t.Run("missing FilePath", func(t *testing.T) {
t.Parallel()
var configuration = *validConfiguration
configuration.FilePath = ""
err := runGitopsUpdateDeployment(&configuration, &gitOpsExecRunnerMock{}, &gitUtilsMock{}, &filesMock{})
assert.EqualError(t, err, "missing required fields for kustomize: the following parameters are necessary for kustomize: [filePath]")
})
t.Run("missing DeploymentName", func(t *testing.T) {
t.Parallel()
var configuration = *validConfiguration
configuration.DeploymentName = ""
err := runGitopsUpdateDeployment(&configuration, &gitOpsExecRunnerMock{}, &gitUtilsMock{}, &filesMock{})
assert.EqualError(t, err, "missing required fields for kustomize: the following parameters are necessary for kustomize: [deploymentName]")
})
}
type gitOpsExecRunnerMock struct {
out io.Writer
params []string
executable string
failOnRunExecutable bool
dir string
expectedYaml string
}
func (e *gitOpsExecRunnerMock) Stdout(out io.Writer) {
@ -540,13 +612,17 @@ func (gitOpsExecRunnerMock) Stderr(io.Writer) {
panic("implement me")
}
func (e *gitOpsExecRunnerMock) SetDir(d string) {
e.dir = d
}
func (e *gitOpsExecRunnerMock) RunExecutable(executable string, params ...string) error {
if e.failOnRunExecutable {
return errors.New("error happened")
}
e.executable = executable
e.params = params
_, err := e.out.Write([]byte(expectedYaml))
_, err := e.out.Write([]byte(e.expectedYaml))
return err
}
@ -602,19 +678,19 @@ func (v *gitUtilsMock) ChangeBranch(branchName string) error {
return nil
}
func (v *gitUtilsMock) CommitSingleFile(_ string, commitMessage string, _ string) (plumbing.Hash, error) {
func (v *gitUtilsMock) CommitSingleFile(newFile string, commitMessage string, _ string) (plumbing.Hash, error) {
if v.failOnCommit {
return [20]byte{}, errors.New("error on commit")
}
v.commitMessage = commitMessage
matches, _ := piperutils.Files{}.Glob(v.temporaryDirectory + "/dir1/dir2/depl.yaml")
if len(matches) < 1 {
return [20]byte{}, errors.New("could not find file")
filepath := filepath.Join(v.temporaryDirectory, newFile)
fileContent, err := piperutils.Files{}.FileRead(filepath)
if err != nil {
return [20]byte{}, errors.New("could not find file " + filepath)
}
fileRead, _ := piperutils.Files{}.FileRead(matches[0])
v.savedFile = string(fileRead)
v.savedFile = string(fileContent)
return [20]byte{123}, nil
}
@ -630,17 +706,73 @@ func (v *gitUtilsMock) PlainClone(_, _, _, directory string) error {
return errors.New("error on clone")
}
v.temporaryDirectory = directory
filePath := filepath.Join(directory, "dir1/dir2/depl.yaml")
err := piperutils.Files{}.MkdirAll(filepath.Join(directory, "dir1/dir2"), 0755)
if err != nil {
return err
}
err = piperutils.Files{}.FileWrite(filePath, []byte(existingYaml), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "dir1/dir2/depl.yaml"), []byte(existingYaml), 0755)
if err != nil {
return err
}
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "kustomization.yaml"), []byte(existingKustomize), 0755)
if err != nil {
return err
}
return nil
}
var existingYaml = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myFancyApp\n labels:\n tier: application\nspec:\n replicas: 4\n selector:\n matchLabels:\n run: myContainer\n template:\n metadata:\n labels:\n run: myContainer\n spec:\n containers:\n - image: myregistry.com/myFancyContainer:1336\n name: myContainer"
var expectedYaml = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myFancyApp\n labels:\n tier: application\nspec:\n replicas: 4\n selector:\n matchLabels:\n run: myContainer\n template:\n metadata:\n labels:\n run: myContainer\n spec:\n containers:\n - image: myregistry.com/myFancyContainer:1337\n name: myContainer"
var existingYaml = `apiVersion: apps/v1
kind: Deployment
metadata:
name: myFancyApp
labels:
tier: application
spec:
replicas: 4
selector:
matchLabels:
run: myContainer
template:
metadata:
labels:
run: myContainer
spec:
containers:
- image: myregistry.com/myFancyContainer:1336
name: myContainer`
var expectedYaml = `apiVersion: apps/v1
kind: Deployment
metadata:
name: myFancyApp
labels:
tier: application
spec:
replicas: 4
selector:
matchLabels:
run: myContainer
template:
metadata:
labels:
run: myContainer
spec:
containers:
- image: myregistry.com/myFancyContainer:1337
name: myContainer`
var existingKustomize = `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: myFancyDeployment
newTag: "0"
`
var expectedKustomize = `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: myFancyDeployment
newName: registry/containers/myFancyContainer
newTag: "1337"
`

View File

@ -6,9 +6,10 @@ metadata:
It can for example be used for GitOps scenarios where the update of the manifests triggers an update of the corresponding deployment in Kubernetes.
As of today, it supports the update of deployment yaml files via kubectl patch and update a whole helm template.
For kubectl the container inside the yaml must be described within the following hierarchy: `{"spec":{"template":{"spec":{"containers":[{...}]}}}}`
For helm the whole template is generated into a file and uploaded into the repository.
As of today, it supports the update of deployment yaml files via kubectl patch, update a whole helm template and kustomize.
For *kubectl* the container inside the yaml must be described within the following hierarchy: `{"spec":{"template":{"spec":{"containers":[{...}]}}}}`
For *helm* the whole template is generated into a file and uploaded into the repository.
For *kustomize* the `images` section will be update with the current image.
spec:
@ -77,7 +78,12 @@ spec:
type: secret
param: password
- name: filePath
description: Relative path in the git repository to the deployment descriptor file that shall be updated
description: |
Relative path in the git repository to the deployment descriptor file that shall be updated. For different tools this has different semantics:
* `kubectl` - path to the `deployment.yaml` that should be patched.
* `helm` - not used. Please use `chartPath` instead.
* `kustomize` - path to the `kustomization.yaml`.
scope:
- PARAMETERS
- STAGES
@ -140,7 +146,7 @@ spec:
aliases:
- name: helmDeploymentName
type: string
description: Defines the name of the deployment.
description: Defines the name of the deployment. In case of `kustomize` this is the name or alias of the image in the `kustomization.yaml`
scope:
- PARAMETERS
- STAGES
@ -158,6 +164,7 @@ spec:
possibleValues:
- kubectl
- helm
- kustomize
containers:
- image: dtzar/helm-kubectl:3.3.4
workingDir: /config
@ -179,3 +186,13 @@ spec:
params:
- name: tool
value: kubectl
- image: k8s.gcr.io/kustomize/kustomize:v3.8.7
workingDir: /config
options:
- name: -u
value: "0"
conditions:
- conditionRef: strings-equal
params:
- name: tool
value: kustomize