1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-18 05:18:24 +02:00

(feat) gitopsUpdateDeployment supports globbing 🌟 (#3533)

* (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>

* (feat) gitopsUpdateDeployment now supports globbing

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

* generate and fmt

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

* fix tests

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-18 08:43:34 +01:00 committed by GitHub
parent 20f5e955f9
commit 385038e652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 287 additions and 74 deletions

View File

@ -11,11 +11,14 @@ import (
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/pkg/errors"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
const toolKubectl = "kubectl"
@ -23,7 +26,7 @@ const toolHelm = "helm"
const toolKustomize = "kustomize"
type iGitopsUpdateDeploymentGitUtils interface {
CommitSingleFile(filePath, commitMessage, author string) (plumbing.Hash, error)
CommitFiles(filePaths []string, commitMessage, author string) (plumbing.Hash, error)
PushChangesToRepository(username, password string) error
PlainClone(username, password, serverURL, directory string) error
ChangeBranch(branchName string) error
@ -33,6 +36,7 @@ type gitopsUpdateDeploymentFileUtils interface {
TempDir(dir, pattern string) (name string, err error)
RemoveAll(path string) error
FileWrite(path string, content []byte, perm os.FileMode) error
Glob(pattern string) ([]string, error)
}
type gitopsUpdateDeploymentExecRunner interface {
@ -47,8 +51,24 @@ type gitopsUpdateDeploymentGitUtils struct {
repository *git.Repository
}
func (g *gitopsUpdateDeploymentGitUtils) CommitSingleFile(filePath, commitMessage, author string) (plumbing.Hash, error) {
return gitUtil.CommitSingleFile(filePath, commitMessage, author, g.worktree)
func (g *gitopsUpdateDeploymentGitUtils) CommitFiles(filePaths []string, commitMessage, author string) (plumbing.Hash, error) {
for _, path := range filePaths {
_, err := g.worktree.Add(path)
if err != nil {
return [20]byte{}, errors.Wrap(err, "failed to add file to git")
}
}
commit, err := g.worktree.Commit(commitMessage, &git.CommitOptions{
All: true,
Author: &object.Signature{Name: author, When: time.Now()},
})
if err != nil {
return [20]byte{}, errors.Wrap(err, "failed to commit file")
}
return commit, nil
}
func (g *gitopsUpdateDeploymentGitUtils) PushChangesToRepository(username, password string) error {
@ -94,6 +114,7 @@ func runGitopsUpdateDeployment(config *gitopsUpdateDeploymentOptions, command gi
}
temporaryFolder, err := fileUtils.TempDir(".", "temp-")
temporaryFolder = regexp.MustCompile(`^./`).ReplaceAllString(temporaryFolder, "")
if err != nil {
return errors.Wrap(err, "failed to create temporary directory")
}
@ -111,18 +132,42 @@ func runGitopsUpdateDeployment(config *gitopsUpdateDeploymentOptions, command gi
}
filePath := filepath.Join(temporaryFolder, config.FilePath)
if config.Tool == toolHelm {
filePath = filepath.Join(temporaryFolder, config.ChartPath)
}
allFiles, err := fileUtils.Glob(filePath)
if err != nil {
return errors.Wrap(err, "unable to expand globbing pattern")
} else if len(allFiles) == 0 {
return errors.New("no matching files found for provided globbing pattern")
}
command.SetDir("./")
var outputBytes []byte
for _, currentFile := range allFiles {
if config.Tool == toolKubectl {
outputBytes, err = executeKubectl(config, command, outputBytes, filePath)
outputBytes, err = executeKubectl(config, command, outputBytes, currentFile)
if err != nil {
return errors.Wrap(err, "error on kubectl execution")
}
} else if config.Tool == toolHelm {
outputBytes, err = runHelmCommand(command, config)
out, err := runHelmCommand(command, config, currentFile)
if err != nil {
return errors.Wrap(err, "failed to apply helm command")
}
// join all helm outputs into the same "FilePath"
outputBytes = append(outputBytes, []byte("---\n")...)
outputBytes = append(outputBytes, out...)
currentFile = filepath.Join(temporaryFolder, config.FilePath)
} else if config.Tool == toolKustomize {
_, err = runKustomizeCommand(command, config, currentFile)
if err != nil {
return errors.Wrap(err, "failed to apply kustomize command")
}
outputBytes = nil
} else if config.Tool == toolKustomize {
outputBytes, err = runKustomizeCommand(command, config, filePath)
if err != nil {
@ -133,12 +178,24 @@ func runGitopsUpdateDeployment(config *gitopsUpdateDeploymentOptions, command gi
return errors.New("tool " + config.Tool + " is not supported")
}
err = fileUtils.FileWrite(filePath, outputBytes, 0755)
if outputBytes != nil {
err = fileUtils.FileWrite(currentFile, outputBytes, 0755)
if err != nil {
return errors.Wrap(err, "failed to write file")
}
}
}
if config.Tool == toolHelm {
// helm only creates one output file.
allFiles = []string{config.FilePath}
} else {
// git expects the file path relative to its root:
for i := range allFiles {
allFiles[i] = strings.ReplaceAll(allFiles[i], temporaryFolder+"/", "")
}
}
commit, err := commitAndPushChanges(config, gitUtils)
commit, err := commitAndPushChanges(config, gitUtils, allFiles)
if err != nil {
return errors.Wrap(err, "failed to commit and push changes")
}
@ -260,6 +317,7 @@ func executeKubectl(config *gitopsUpdateDeploymentOptions, command gitopsUpdateD
}
patchString := "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"" + config.ContainerName + "\",\"image\":\"" + registryImage + "\"}]}}}}"
log.Entry().Infof("[kubectl] updating '%s'", filePath)
outputBytes, err = runKubeCtlCommand(command, patchString, filePath)
if err != nil {
return nil, errors.Wrap(err, "failed to apply kubectl command")
@ -301,9 +359,9 @@ func runKubeCtlCommand(command gitopsUpdateDeploymentExecRunner, patchString str
return kubectlOutput.Bytes(), nil
}
func runHelmCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdateDeploymentOptions) ([]byte, error) {
func runHelmCommand(command gitopsUpdateDeploymentExecRunner, config *gitopsUpdateDeploymentOptions, filePath string) ([]byte, error) {
var helmOutput = bytes.Buffer{}
runner.Stdout(&helmOutput)
command.Stdout(&helmOutput)
registryImage, imageTag, err := buildRegistryPlusImageAndTagSeparately(config)
if err != nil {
@ -312,7 +370,7 @@ func runHelmCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdat
helmParams := []string{
"template",
config.DeploymentName,
filepath.Join(".", config.ChartPath),
filePath,
"--set=image.repository=" + registryImage,
"--set=image.tag=" + imageTag,
}
@ -321,16 +379,17 @@ func runHelmCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdat
helmParams = append(helmParams, "--values", value)
}
err = runner.RunExecutable(toolHelm, helmParams...)
log.Entry().Infof("[helmn] updating '%s'", filePath)
err = command.RunExecutable(toolHelm, helmParams...)
if err != nil {
return nil, errors.Wrap(err, "failed to execute helm command")
}
return helmOutput.Bytes(), nil
}
func runKustomizeCommand(runner gitopsUpdateDeploymentExecRunner, config *gitopsUpdateDeploymentOptions, filePath string) ([]byte, error) {
func runKustomizeCommand(command gitopsUpdateDeploymentExecRunner, config *gitopsUpdateDeploymentOptions, filePath string) ([]byte, error) {
var kustomizeOutput = bytes.Buffer{}
runner.Stdout(&kustomizeOutput)
command.Stdout(&kustomizeOutput)
kustomizeParams := []string{
"edit",
@ -339,9 +398,10 @@ func runKustomizeCommand(runner gitopsUpdateDeploymentExecRunner, config *gitops
config.DeploymentName + "=" + config.ContainerImageNameTag,
}
runner.SetDir(filepath.Dir(filePath))
command.SetDir(filepath.Dir(filePath))
err := runner.RunExecutable(toolKustomize, kustomizeParams...)
log.Entry().Infof("[kustomize] updating '%s'", filePath)
err := command.RunExecutable(toolKustomize, kustomizeParams...)
if err != nil {
return nil, errors.Wrap(err, "failed to execute kustomize command")
}
@ -387,14 +447,14 @@ func buildRegistryPlusImageAndTagSeparately(config *gitopsUpdateDeploymentOption
}
func commitAndPushChanges(config *gitopsUpdateDeploymentOptions, gitUtils iGitopsUpdateDeploymentGitUtils) (plumbing.Hash, error) {
func commitAndPushChanges(config *gitopsUpdateDeploymentOptions, gitUtils iGitopsUpdateDeploymentGitUtils, filePaths []string) (plumbing.Hash, error) {
commitMessage := config.CommitMessage
if commitMessage == "" {
commitMessage = defaultCommitMessage(config)
}
commit, err := gitUtils.CommitSingleFile(config.FilePath, commitMessage, config.Username)
commit, err := gitUtils.CommitFiles(filePaths, commitMessage, config.Username)
if err != nil {
return [20]byte{}, errors.Wrap(err, "committing changes failed")
}

View File

@ -50,8 +50,9 @@ 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, 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 *helm* the whole template is generated into a single file (` + "`" + `filePath` + "`" + `) 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()
@ -134,11 +135,11 @@ 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. 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.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. Supports globbing.\n * `helm` - path where the helm chart will be generated into. Here no globbing is supported.\n * `kustomize` - path to the `kustomization.yaml`. Supports globbing.\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().StringVar(&stepConfig.ChartPath, "chartPath", os.Getenv("PIPER_chartPath"), "Defines the chart path for deployments using helm. Globbing is supported to merge multiple charts into one resource.yaml that will be commited.")
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. 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.")

View File

@ -101,7 +101,8 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "This is the commit message", gitUtilsMock.commitMessage)
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
@ -123,7 +124,8 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "Updated myregistry.com/myFancyContainer to version 1337", gitUtilsMock.commitMessage)
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
@ -145,7 +147,8 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
assert.Equal(t, "--local", runnerMock.params[1])
@ -166,7 +169,8 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
assert.Equal(t, "--local", runnerMock.params[1])
@ -187,7 +191,8 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
assert.Equal(t, "--local", runnerMock.params[1])
@ -195,6 +200,35 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
assert.Equal(t, `--patch={"spec":{"template":{"spec":{"containers":[{"name":"myContainer","image":"myregistry.com/myFancyContainer:1337"}]}}}}`, runnerMock.params[3])
assert.True(t, strings.Contains(runnerMock.params[4], filepath.Join("dir1/dir2/depl.yaml")))
})
t.Run("successful run with glob", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{}
runnerMock.expectedYaml = expectedYaml
var configuration = *validConfiguration
configuration.FilePath = "glob/kubectl/**/*.yaml"
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, fsMock)
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Len(t, gitUtilsMock.savedFiles, 2)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, expectedYaml, gitUtilsMock.savedFiles[1])
assert.Equal(t, "kubectl", runnerMock.executable)
assert.Equal(t, "patch", runnerMock.params[0])
assert.Equal(t, "--local", runnerMock.params[1])
assert.Equal(t, "--output=yaml", runnerMock.params[2])
assert.Equal(t, `--patch={"spec":{"template":{"spec":{"containers":[{"name":"myContainer","image":"myregistry.com/myFancyContainer:1337"}]}}}}`, runnerMock.params[3])
assert.True(t, strings.Contains(runnerMock.params[4], filepath.Join("glob/kubectl/dir1/depl.yaml")))
assert.Equal(t, "patch", runnerMock.params[5])
assert.Equal(t, "--local", runnerMock.params[6])
assert.Equal(t, "--output=yaml", runnerMock.params[7])
assert.Equal(t, `--patch={"spec":{"template":{"spec":{"containers":[{"name":"myContainer","image":"myregistry.com/myFancyContainer:1337"}]}}}}`, runnerMock.params[8])
assert.True(t, strings.Contains(runnerMock.params[9], filepath.Join("glob/kubectl/dir2/depl.yaml")))
})
t.Run("missing ContainerName", func(t *testing.T) {
t.Parallel()
@ -229,7 +263,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
t.Parallel()
gitUtils := &gitUtilsMock{failOnClone: true}
err := runGitopsUpdateDeployment(validConfiguration, &gitOpsExecRunnerMock{}, gitUtils, &filesMock{})
err := runGitopsUpdateDeployment(validConfiguration, &gitOpsExecRunnerMock{expectedYaml: expectedYaml}, gitUtils, &filesMock{})
assert.EqualError(t, err, "repository could not get prepared: failed to plain clone repository: error on clone")
})
@ -269,7 +303,7 @@ func TestRunGitopsUpdateDeploymentWithKubectl(t *testing.T) {
t.Parallel()
fileUtils := &filesMock{failOnWrite: true}
err := runGitopsUpdateDeployment(validConfiguration, &gitOpsExecRunnerMock{}, &gitUtilsMock{}, fileUtils)
err := runGitopsUpdateDeployment(validConfiguration, &gitOpsExecRunnerMock{expectedYaml: expectedYaml}, &gitUtilsMock{}, fileUtils)
assert.EqualError(t, err, "failed to write file: error appeared")
})
@ -333,12 +367,13 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, "---\n"+expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "This is the commit message", gitUtilsMock.commitMessage)
assert.Equal(t, "helm", runnerMock.executable)
assert.Equal(t, "template", runnerMock.params[0])
assert.Equal(t, "myFancyDeployment", runnerMock.params[1])
assert.Equal(t, filepath.Join(".", "helm"), runnerMock.params[2])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "helm"), runnerMock.params[2])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[3])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[4])
assert.Equal(t, "--values", runnerMock.params[5])
@ -357,12 +392,13 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, "---\n"+expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "Updated myregistry.com/registry/containers/myFancyContainer to version 1337", gitUtilsMock.commitMessage)
assert.Equal(t, "helm", runnerMock.executable)
assert.Equal(t, "template", runnerMock.params[0])
assert.Equal(t, "myFancyDeployment", runnerMock.params[1])
assert.Equal(t, filepath.Join(".", "helm"), runnerMock.params[2])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "helm"), runnerMock.params[2])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[3])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[4])
assert.Equal(t, "--values", runnerMock.params[5])
@ -381,11 +417,12 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, "---\n"+expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "helm", runnerMock.executable)
assert.Equal(t, "template", runnerMock.params[0])
assert.Equal(t, "myFancyDeployment", runnerMock.params[1])
assert.Equal(t, filepath.Join(".", "helm"), runnerMock.params[2])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "helm"), runnerMock.params[2])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[3])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[4])
assert.Equal(t, "--values", runnerMock.params[5])
@ -404,14 +441,45 @@ func TestRunGitopsUpdateDeploymentWithHelm(t *testing.T) {
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, &filesMock{})
assert.NoError(t, err)
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, "---\n"+expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "helm", runnerMock.executable)
assert.Equal(t, "template", runnerMock.params[0])
assert.Equal(t, "myFancyDeployment", runnerMock.params[1])
assert.Equal(t, filepath.Join(".", "helm"), runnerMock.params[2])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "helm"), runnerMock.params[2])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[3])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[4])
})
t.Run("successful run with glob", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{}
runnerMock.expectedYaml = expectedYaml
var configuration = *validConfiguration
configuration.ChartPath = "glob/helm/dir*/helm"
configuration.HelmValues = nil
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, fsMock)
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, "---\n"+expectedYaml+"---\n"+expectedYaml, gitUtilsMock.savedFiles[0])
assert.Equal(t, "This is the commit message", gitUtilsMock.commitMessage)
assert.Equal(t, "helm", runnerMock.executable)
assert.Equal(t, "template", runnerMock.params[0])
assert.Equal(t, "myFancyDeployment", runnerMock.params[1])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "glob/helm/dir1/helm"), runnerMock.params[2])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[3])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[4])
assert.Equal(t, "template", runnerMock.params[5])
assert.Equal(t, "myFancyDeployment", runnerMock.params[6])
assert.Equal(t, filepath.Join(gitUtilsMock.temporaryDirectory, "glob/helm/dir2/helm"), runnerMock.params[7])
assert.Equal(t, "--set=image.repository=myregistry.com/registry/containers/myFancyContainer", runnerMock.params[8])
assert.Equal(t, "--set=image.tag=1337", runnerMock.params[9])
})
t.Run("erroneous URL", func(t *testing.T) {
t.Parallel()
@ -559,7 +627,8 @@ func TestRunGitopsUpdateDeploymentWithKustomize(t *testing.T) {
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, fsMock)
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Equal(t, expectedKustomize, gitUtilsMock.savedFile)
assert.Len(t, gitUtilsMock.savedFiles, 1)
assert.Equal(t, expectedKustomize, gitUtilsMock.savedFiles[0])
assert.Equal(t, "This is the commit message", gitUtilsMock.commitMessage)
assert.Equal(t, "kustomize", runnerMock.executable)
assert.Equal(t, "edit", runnerMock.params[0])
@ -567,6 +636,32 @@ func TestRunGitopsUpdateDeploymentWithKustomize(t *testing.T) {
assert.Equal(t, "image", runnerMock.params[2])
assert.Equal(t, "myFancyDeployment=registry/containers/myFancyContainer:1337", runnerMock.params[3])
})
t.Run("successful run with glob", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{}
runnerMock.expectedYaml = expectedKustomize
var configuration = *validConfiguration
configuration.FilePath = "glob/kustomize/**/*.yaml"
err := runGitopsUpdateDeployment(&configuration, runnerMock, gitUtilsMock, fsMock)
assert.NoError(t, err)
assert.Equal(t, validConfiguration.BranchName, gitUtilsMock.changedBranch)
assert.Len(t, gitUtilsMock.savedFiles, 2)
assert.Equal(t, expectedKustomize, gitUtilsMock.savedFiles[0])
assert.Equal(t, expectedKustomize, gitUtilsMock.savedFiles[1])
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])
assert.Equal(t, "edit", runnerMock.params[4])
assert.Equal(t, "set", runnerMock.params[5])
assert.Equal(t, "image", runnerMock.params[6])
assert.Equal(t, "myFancyDeployment=registry/containers/myFancyContainer:1337", runnerMock.params[7])
})
t.Run("error on kustomize execution", func(t *testing.T) {
t.Parallel()
@ -595,6 +690,35 @@ func TestRunGitopsUpdateDeploymentWithKustomize(t *testing.T) {
})
}
func TestRunGitopsUpdateDeploymentWithGlobbing(t *testing.T) {
var validConfiguration = &gitopsUpdateDeploymentOptions{
Tool: toolKubectl,
ContainerName: "yes",
DeploymentName: "myFancyDeployment",
}
t.Run("globbing fails", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{failOnGlob: true}
err := runGitopsUpdateDeployment(validConfiguration, runnerMock, gitUtilsMock, fsMock)
assert.EqualError(t, err, "unable to expand globbing pattern: error appeared")
})
t.Run("globbing finds 0 files", func(t *testing.T) {
t.Parallel()
gitUtilsMock := &gitUtilsMock{skipClone: true}
runnerMock := &gitOpsExecRunnerMock{}
fsMock := &filesMock{}
var config = *validConfiguration
config.FilePath = "xxx"
err := runGitopsUpdateDeployment(&config, runnerMock, gitUtilsMock, fsMock)
assert.EqualError(t, err, "no matching files found for provided globbing pattern")
})
}
type gitOpsExecRunnerMock struct {
out io.Writer
params []string
@ -621,15 +745,21 @@ func (e *gitOpsExecRunnerMock) RunExecutable(executable string, params ...string
return errors.New("error happened")
}
e.executable = executable
e.params = params
e.params = append(e.params, params...)
if executable == "kustomize" {
return fileUtils.FileWrite(filepath.Join(e.dir, "kustomization.yaml"), []byte(e.expectedYaml), 0755)
} else {
_, err := e.out.Write([]byte(e.expectedYaml))
return err
}
}
type filesMock struct {
failOnCreation bool
failOnDeletion bool
failOnWrite bool
failOnGlob bool
path string
}
@ -655,8 +785,15 @@ func (f *filesMock) RemoveAll(path string) error {
return piperutils.Files{}.RemoveAll(path)
}
func (f *filesMock) Glob(pattern string) (matches []string, err error) {
if f.failOnGlob {
return nil, errors.New("error appeared")
}
return piperutils.Files{}.Glob(pattern)
}
type gitUtilsMock struct {
savedFile string
savedFiles []string
changedBranch string
commitMessage string
temporaryDirectory string
@ -664,6 +801,7 @@ type gitUtilsMock struct {
failOnChangeBranch bool
failOnCommit bool
failOnPush bool
skipClone bool
}
func (gitUtilsMock) GetWorktree() (*git.Worktree, error) {
@ -678,19 +816,21 @@ func (v *gitUtilsMock) ChangeBranch(branchName string) error {
return nil
}
func (v *gitUtilsMock) CommitSingleFile(newFile string, commitMessage string, _ string) (plumbing.Hash, error) {
func (v *gitUtilsMock) CommitFiles(newFiles []string, commitMessage string, _ string) (plumbing.Hash, error) {
if v.failOnCommit {
return [20]byte{}, errors.New("error on commit")
}
v.commitMessage = commitMessage
for _, newFile := range newFiles {
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)
}
v.savedFile = string(fileContent)
v.savedFiles = append(v.savedFiles, string(fileContent))
}
return [20]byte{123}, nil
}
@ -702,22 +842,33 @@ func (v gitUtilsMock) PushChangesToRepository(string, string) error {
}
func (v *gitUtilsMock) PlainClone(_, _, _, directory string) error {
if v.skipClone {
return nil
}
if v.failOnClone {
return errors.New("error on clone")
}
v.temporaryDirectory = directory
err := piperutils.Files{}.MkdirAll(filepath.Join(directory, "dir1/dir2"), 0755)
if err != nil {
return err
}
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "dir1/dir2/depl.yaml"), []byte(existingYaml), 0755)
if err != nil {
return err
}
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/kubectl/dir1"), 0755)
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/kubectl/dir2"), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "glob/kubectl/dir1/depl.yaml"), []byte(existingYaml), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "glob/kubectl/dir2/depl.yaml"), []byte(existingYaml), 0755)
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "helm"), 0755)
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/helm/dir1/helm"), 0755)
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/helm/dir2/helm"), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "kustomization.yaml"), []byte(existingKustomize), 0755)
if err != nil {
return err
}
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/kustomize/dir1"), 0755)
err = piperutils.Files{}.MkdirAll(filepath.Join(directory, "glob/kustomize/dir2"), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "glob/kustomize/dir1/kustomization.yaml"), []byte(existingKustomize), 0755)
err = piperutils.Files{}.FileWrite(filepath.Join(directory, "glob/kustomize/dir2/kustomization.yaml"), []byte(existingKustomize), 0755)
return nil
}

View File

@ -7,8 +7,9 @@ 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, 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 *helm* the whole template is generated into a single file (`filePath`) and uploaded into the repository.
For *kustomize* the `images` section will be update with the current image.
@ -81,9 +82,9 @@ spec:
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`.
* `kubectl` - path to the `deployment.yaml` that should be patched. Supports globbing.
* `helm` - path where the helm chart will be generated into. Here no globbing is supported.
* `kustomize` - path to the `kustomization.yaml`. Supports globbing.
scope:
- PARAMETERS
- STAGES
@ -130,7 +131,7 @@ spec:
aliases:
- name: helmChartPath
type: string
description: Defines the chart path for deployments using helm.
description: Defines the chart path for deployments using helm. Globbing is supported to merge multiple charts into one resource.yaml that will be commited.
scope:
- PARAMETERS
- STAGES