From 8dfe9debe1db477412506d8a3387fa006006d97f Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Tue, 2 May 2023 12:04:12 +0200 Subject: [PATCH] Implement plain deployments for CronJobs Fixes #31 --- CHANGELOG.md | 3 + README.md | 5 +- .../deployment/plain/Plain.groovy | 25 +++++++- .../cloudogu/gitopsbuildlib/ScriptMock.groovy | 15 +---- .../gitopsbuildlib/deployment/HelmTest.groovy | 49 ++++---------- .../deployment/PlainTest.groovy | 64 +++++++++++++++++-- .../helm/helmrelease/FluxV1ReleaseTest.groovy | 32 +++------- .../helm/helmrelease/HelmReleaseTest.groovy | 20 ++---- 8 files changed, 119 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcf330..78df96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Make `deployments.plain` work with `CronJob + ## [0.4.0](https://github.com/cloudogu/gitops-build-lib/releases/tag/0.4.0) - 2023-03-21 ### Added diff --git a/README.md b/README.md index 63444c9..bdae7fe 100644 --- a/README.md +++ b/README.md @@ -563,7 +563,10 @@ def gitopsConfig = [ In plain pipelines, the library creates the deployment resources by updating the image tag within the deployment. ---- +Note that this works with Kubernetes `Deployment`s, `StatefulSet`s and `CronJob`s. For all other kinds of resources the +library tries to find the `containers` at the following YAML path: `spec.template.spec.containers`. This might fail, of course. If you encounter a case +like this, please create an issue. Eventually, we're planning to provide a `fieldPath` option just like in helm releases. + ### Helm deployment Besides plain k8s resources you can also use helm charts to generate the resources. You can choose between these types diff --git a/src/com/cloudogu/gitopsbuildlib/deployment/plain/Plain.groovy b/src/com/cloudogu/gitopsbuildlib/deployment/plain/Plain.groovy index d8fe2a5..a4ed4dd 100644 --- a/src/com/cloudogu/gitopsbuildlib/deployment/plain/Plain.groovy +++ b/src/com/cloudogu/gitopsbuildlib/deployment/plain/Plain.groovy @@ -3,7 +3,7 @@ package com.cloudogu.gitopsbuildlib.deployment.plain import com.cloudogu.gitopsbuildlib.deployment.Deployment import com.cloudogu.gitopsbuildlib.deployment.SourceType -class Plain extends Deployment{ +class Plain extends Deployment { Plain(def script, def gitopsConfig) { super(script, gitopsConfig) @@ -30,14 +30,35 @@ class Plain extends Deployment{ private updateImage(String stage) { def destinationPath = getDestinationFolder(getFolderStructureStrategy(), stage) + script.echo "About Updating images in plain deployment: ${gitopsConfig.deployments.plain.updateImages}" gitopsConfig.deployments.plain.updateImages.each { + script.echo "Replacing image '${it['imageName']}' in file: ${it['filename']}" def deploymentFilePath = "${destinationPath}/${it['filename']}" def data = script.readYaml file: deploymentFilePath - def containers = data.spec.template.spec.containers + String kind = data.kind + def containers = findContainers(data, kind) + script.echo "Found containers '${containers}' in YAML: ${it['filename']}" def containerName = it['containerName'] def updateContainer = containers.find { it.name == containerName } updateContainer.image = it['imageName'] script.writeYaml file: deploymentFilePath, data: data, overwrite: true + script.echo "Wrote file ${deploymentFilePath} with yaml:\n${data}" + } + } + + def findContainers(def data, String kind) { + //noinspection GroovyFallthrough + switch (kind) { + case 'Deployment': + // Falling through because Deployment and StatefulSet's paths are the same + case 'StatefulSet': + return data.spec.template.spec.containers + case 'CronJob': + return data.spec.jobTemplate.spec.template.spec.containers + default: + script.echo "Warning: Kind '$kind' is unknown, using best effort to find 'containers' in YAML" + // Best effort: Try the same as for Deployment and StatefulSet + return data.spec.template.spec.containers } } } diff --git a/test/com/cloudogu/gitopsbuildlib/ScriptMock.groovy b/test/com/cloudogu/gitopsbuildlib/ScriptMock.groovy index 4ea3ab6..5ead24e 100644 --- a/test/com/cloudogu/gitopsbuildlib/ScriptMock.groovy +++ b/test/com/cloudogu/gitopsbuildlib/ScriptMock.groovy @@ -16,20 +16,7 @@ class ScriptMock { List actualReadYamlArgs = new LinkedList<>() List actualGitArgs = new LinkedList<>() List actualDir = new LinkedList<>() - def configYaml = '''\ ---- -#this part is only for PlainTest regarding updating the image name -spec: - template: - spec: - containers: - - name: 'application' - image: 'oldImageName' -#this part is only for HelmTest regarding changing the yaml values -to: - be: - changed: 'oldValue' -''' + def configYaml = '' List actualWriteYamlArgs = new LinkedList<>() List actualReadFileArgs = new LinkedList<>() List actualWriteFileArgs = new LinkedList<>() diff --git a/test/com/cloudogu/gitopsbuildlib/deployment/HelmTest.groovy b/test/com/cloudogu/gitopsbuildlib/deployment/HelmTest.groovy index 7383e5c..bdbf399 100644 --- a/test/com/cloudogu/gitopsbuildlib/deployment/HelmTest.groovy +++ b/test/com/cloudogu/gitopsbuildlib/deployment/HelmTest.groovy @@ -99,6 +99,15 @@ class HelmTest { def helmHelm = new Helm(scriptMock.mock, getGitopsConfig(helmRepo)) def helmLocal = new Helm(scriptMock.mock, getGitopsConfig(localRepo, 'ARGO')) + @BeforeEach + void init () { + scriptMock.configYaml = ''' +to: + be: + changed: 'oldValue' +''' + } + @Test void 'creating helm release with git repo'() { helmGit.preValidation('staging') @@ -121,15 +130,7 @@ spec: ref: null path: chartPath values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: \'application\' - image: \'oldImageName\' - #this part is only for HelmTest regarding changing the yaml values + to: be: changed: \'oldValue\' @@ -160,15 +161,7 @@ spec: name: chartName version: 1.0 values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: \'application\' - image: \'oldImageName\' - #this part is only for HelmTest regarding changing the yaml values + to: be: changed: \'oldValue\' @@ -200,15 +193,7 @@ spec: ref: null path: chartPath values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: \'application\' - image: \'oldImageName\' - #this part is only for HelmTest regarding changing the yaml values + to: be: changed: \'oldValue\' @@ -242,15 +227,7 @@ spec: name: chartName version: 1.0 values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: \'application\' - image: \'oldImageName\' - #this part is only for HelmTest regarding changing the yaml values + to: be: changed: \'oldValue\' diff --git a/test/com/cloudogu/gitopsbuildlib/deployment/PlainTest.groovy b/test/com/cloudogu/gitopsbuildlib/deployment/PlainTest.groovy index 81d4d13..91e5525 100644 --- a/test/com/cloudogu/gitopsbuildlib/deployment/PlainTest.groovy +++ b/test/com/cloudogu/gitopsbuildlib/deployment/PlainTest.groovy @@ -5,9 +5,10 @@ import com.cloudogu.gitopsbuildlib.deployment.plain.Plain import com.cloudogu.gitopsbuildlib.validation.HelmKubeval import com.cloudogu.gitopsbuildlib.validation.Kubeval import com.cloudogu.gitopsbuildlib.validation.Yamllint -import org.junit.jupiter.api.* -import static org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import static org.assertj.core.api.Assertions.assertThat class PlainTest { @@ -52,14 +53,69 @@ class PlainTest { ], ]) + String deploymentYaml = ''' +kind: Deployment +spec: + template: + spec: + containers: + - name: 'application' + image: 'oldImageName' +''' + + String cronJobYaml = ''' +kind: CronJob +spec: + jobTemplate: + spec: + template: + spec: + containers: + - name: 'application' + image: 'oldImageName' + - name: 'other' + image: 'otherImageName' +''' + + @BeforeEach + void init () { + scriptMock.configYaml = deploymentYaml + } + @Test void 'successful update'() { plain.preValidation('staging') assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') - assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]], to:[be:[changed:oldValue]]], overwrite:true]') + assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:Deployment, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') } + @Test + void 'successful update with statefulSet'() { + scriptMock.configYaml = scriptMock.configYaml.replace('kind: Deployment', 'kind: StatefulSet') + plain.preValidation('staging') + assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') + assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:StatefulSet, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') + } + + @Test + void 'successful update with cronjob'() { + scriptMock.configYaml = cronJobYaml + + plain.preValidation('staging') + assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') + assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:CronJob, spec:[jobTemplate:[spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application], [image:otherImageName, name:other]]]]]]]], overwrite:true]') + } + + @Test + void 'successful update with other resource'() { + scriptMock.configYaml = scriptMock.configYaml.replace('kind: Deployment', 'kind: SomethingElse') + plain.preValidation('staging') + assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml]') + assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:staging/app/deployment.yaml, data:[kind:SomethingElse, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') + assertThat(scriptMock.actualEchoArgs).contains('Warning: Kind \'SomethingElse\' is unknown, using best effort to find \'containers\' in YAML') + } + @Test void 'successful update with ENV_PER_APP and other destinationRootPath '() { plain.gitopsConfig['folderStructureStrategy'] = 'ENV_PER_APP' @@ -67,7 +123,7 @@ class PlainTest { plain.preValidation('staging') assertThat(scriptMock.actualReadYamlArgs[0]).isEqualTo('[file:apps/app/staging/deployment.yaml]') - assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:apps/app/staging/deployment.yaml, data:[spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]], to:[be:[changed:oldValue]]], overwrite:true]') + assertThat(scriptMock.actualWriteYamlArgs[0]).isEqualTo('[file:apps/app/staging/deployment.yaml, data:[kind:Deployment, spec:[template:[spec:[containers:[[image:imageNameReplacedTest, name:application]]]]]], overwrite:true]') } @Test diff --git a/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1ReleaseTest.groovy b/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1ReleaseTest.groovy index d96eb3a..1f95890 100644 --- a/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1ReleaseTest.groovy +++ b/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1ReleaseTest.groovy @@ -1,6 +1,7 @@ package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease import com.cloudogu.gitopsbuildlib.ScriptMock +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import static org.assertj.core.api.Assertions.assertThat @@ -10,6 +11,11 @@ class FluxV1ReleaseTest { def scriptMock = new ScriptMock() def fluxV1Release = new FluxV1Release(scriptMock.mock) + @BeforeEach + void init () { + scriptMock.configYaml = 'a: b' + } + @Test void 'correct helm release with git repo'() { def output = fluxV1Release.create([ @@ -39,18 +45,7 @@ spec: ref: 1.0 path: . values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: 'application' - image: 'oldImageName' - #this part is only for HelmTest regarding changing the yaml values - to: - be: - changed: 'oldValue' + a: b """) } @@ -84,18 +79,7 @@ spec: name: chartName version: 1.0 values: - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: 'application' - image: 'oldImageName' - #this part is only for HelmTest regarding changing the yaml values - to: - be: - changed: 'oldValue' + a: b """) } } diff --git a/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmReleaseTest.groovy b/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmReleaseTest.groovy index af93b92..46f7b69 100644 --- a/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmReleaseTest.groovy +++ b/test/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/HelmReleaseTest.groovy @@ -1,6 +1,7 @@ package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease import com.cloudogu.gitopsbuildlib.ScriptMock +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import static org.assertj.core.api.Assertions.assertThat @@ -10,23 +11,16 @@ class HelmReleaseTest { def scriptMock = new ScriptMock() def repoType = new HelmReleaseUnderTest(scriptMock.mock) + @BeforeEach + void init () { + scriptMock.configYaml = 'a: b' + } + @Test void 'inline yaml test'() { def output = repoType.fileToInlineYaml('filepath') assertThat(scriptMock.actualReadFileArgs[0]).isEqualTo('filepath') - assertThat(output).isEqualTo('''\ - --- - #this part is only for PlainTest regarding updating the image name - spec: - template: - spec: - containers: - - name: 'application\' - image: 'oldImageName' - #this part is only for HelmTest regarding changing the yaml values - to: - be: - changed: 'oldValue\'''') + assertThat(output).isEqualTo(' a: b') } class HelmReleaseUnderTest extends HelmRelease {