Skip to content

Commit b15be9f

Browse files
authored
Merge pull request #16 from cloudogu/feature/validator_refactor
Feature/validator refactor
2 parents 1796464 + ed307f8 commit b15be9f

29 files changed

+639
-260
lines changed

README.md

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -682,24 +682,86 @@ node {
682682
deployViaGitops(gitopsConfig)
683683
}
684684
}
685+
```
686+
687+
### Custom validators
688+
689+
The library offers a convenient base class [`com.cloudogu.gitops.gitopsbuildlib.Validator`](src/com/cloudogu/gitopsbuildlib/Validator.groovy).
690+
However, this seems impossible to use with neither dynamic library loading via the `library()` nor with `@Library`,
691+
because the library is loaded after the class is evaluated.
692+
If you need to use `library()` or `@Library` then your validator needs to implement the following three methods:
693+
694+
- `void validate(boolean enabled, String targetDirectory, Map config)`
695+
- `SourceType[] getSupportedSourceTypes()`
696+
- `GitopsTool[] getSupportedGitopsTools()`
697+
698+
```groovy
699+
import com.cloudogu.gitopsbuildlib.deployment.GitopsTool
700+
import com.cloudogu.gitopsbuildlib.deployment.SourceType
701+
702+
class MyValidator extends Validator {
685703
686-
// Simple example that works with dynamic library loading, i.e. the library() step
687-
class MyValidator {
688-
def script
689704
MyValidator(def script) {
690-
this.script = script
705+
super(script)
706+
}
707+
708+
@Override
709+
void validate(boolean enabled, String targetDirectory, Map validatorConfig, Map gitopsConfig) {
710+
script.echo "Enabled: $enabled; targetDirectory: $targetDirectory; validatorConfig: $validatorConfig; gitopsConfig: $gitopsConfig"
691711
}
692712
693-
void validate(boolean enabled, String targetDirectory, Map config) {
694-
script.echo "Enabled: $enabled; targetDirectory: $targetDirectory; config: $config"
713+
@Override
714+
SourceType[] getSupportedSourceTypes() {
715+
return [SourceType.HELM, SourceType.PLAIN]
716+
}
717+
718+
@Override
719+
GitopsTool[] getSupportedGitopsTools() {
720+
return [GitopsTool.ARGO, GitopsTool.FLUX]
695721
}
696722
}
697723
```
698724

699-
### Custom validators
725+
In general a custom validator may implement the Validator class. You therefore have to implement the following methods:
700726

701-
In general a custom validator must provide this method: `validate(boolean enabled, String targetDirectory, Map config)`
727+
`void validate(boolean enabled, String targetDirectory, Map config)`
728+
- Here lies your validation code or the entrypoint for more complex validation processes
702729

703-
The library also offers a convenient base class [`com.cloudogu.gitops.gitopsbuildlib.Validator`](src/com/cloudogu/gitopsbuildlib/Validator.groovy).
704-
However, this seems impossible to use with neither dynamic library loading via the `library()` nor with `@Library`,
705-
because the library is loaded after the class is evaluated.
730+
`SourceType[] getSupportedSourceTypes()`
731+
- This method returns a collection of supported Source Types.
732+
The SourceType determines which resources are going to be validated.
733+
There are two locations where resources can be validated.
734+
They are differentiated by the resource-type of which there are two right now.
735+
- Helm resources
736+
- Plain k8s resources
737+
738+
739+
**Visual representation of the folder structure on the Jenkins agent**
740+
```
741+
├── jenkinsworkdir/
742+
└── .configRepoTempDir/
743+
└── ${stage}/
744+
└── ${application}/
745+
├── extraResources/
746+
├── generatedResources/
747+
├── deployment.yaml
748+
└── ...
749+
└── .helmChartTempDir/
750+
└── chart/
751+
└── ${chartPath}/ (for git repo) or ${chartName}/ (for helm repo)
752+
└── mergedValues.yaml
753+
```
754+
755+
**Helm resources** - `.helmChartTempDir`:
756+
This location is only temporary and is being used for the helm chart to be downloaded and the mergedValues.file (values-shared.yaml + values-${stage}.yaml)
757+
Only Validators which support Helm schemas should operate on this folder
758+
759+
**Plain k8s resources** - `.configRepoTempDir`:
760+
This location is for your plain k8s resources. This folder is also the gitops folder which will be pushed to the scm.
761+
It contains your k8s resources in the root and two extra folders for additional k8s resources:
762+
`extraResources`: Only needed for a Helm deployment if you whish to deploy plain k8s resources in addition to the helm deployment. See: [Important note in Namespaces](#namespaces)
763+
`generatedResources`: If you have files which need to be deployed as a configMap. See: [Extra Files](#extra-files)
764+
765+
`GitopsTool[] getSupportedGitopsTools()`
766+
This determins on which GitopsTool the validator will run. We implemented this feature since Argo already uses `helm template` and `kubeval` internally so we don't need `helm kubeval` since it does exactly the same.
767+
So we defined `HelmKubeval` as only needed to be executed on a `FLUX` operator.

src/com/cloudogu/gitopsbuildlib/deployment/Deployment.groovy

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.cloudogu.gitopsbuildlib.deployment
22

3+
import com.cloudogu.gitopsbuildlib.docker.DockerWrapper
4+
35
abstract class Deployment {
46

57
protected static String getKubectlImage() { 'lachlanevenson/k8s-kubectl:v1.19.3' }
@@ -10,9 +12,12 @@ abstract class Deployment {
1012
protected script
1113
protected Map gitopsConfig
1214

15+
protected DockerWrapper dockerWrapper
16+
1317
Deployment(def script, def gitopsConfig) {
1418
this.script = script
1519
this.gitopsConfig = gitopsConfig
20+
dockerWrapper = new DockerWrapper(this.script)
1621
}
1722

1823
def create(String stage) {
@@ -25,14 +30,7 @@ abstract class Deployment {
2530

2631
abstract preValidation(String stage)
2732
abstract postValidation(String stage)
28-
29-
30-
def validate(String stage) {
31-
gitopsConfig.validators.each { validatorConfig ->
32-
script.echo "Executing validator ${validatorConfig.key}"
33-
validatorConfig.value.validator.validate(validatorConfig.value.enabled, "${stage}/${gitopsConfig.application}", validatorConfig.value.config, gitopsConfig)
34-
}
35-
}
33+
abstract validate(String stage)
3634

3735
def createFoldersAndCopyK8sResources(String stage) {
3836
def sourcePath = gitopsConfig.deployments.sourcePath
@@ -57,7 +55,7 @@ abstract class Deployment {
5755

5856
String createConfigMap(String key, String filePath, String name, String namespace) {
5957
String configMap = ""
60-
withKubectl {
58+
withDockerImage(kubectlImage) {
6159
String kubeScript = "KUBECONFIG=${writeKubeConfig()} kubectl create configmap ${name} " +
6260
"--from-file=${key}=${filePath} " +
6361
"--dry-run=client -o yaml -n ${namespace}"
@@ -67,12 +65,12 @@ abstract class Deployment {
6765
return configMap
6866
}
6967

70-
void withKubectl(Closure body) {
71-
script.cesBuildLib.Docker.new(script).image(kubectlImage)
72-
// Allow accessing WORKSPACE even when we are in a child dir (using "dir() {}")
73-
.inside("${script.pwd().equals(script.env.WORKSPACE) ? '' : "-v ${script.env.WORKSPACE}:${script.env.WORKSPACE}"}") {
74-
body()
75-
}
68+
void withDockerImage(String image, Closure body) {
69+
dockerWrapper.withDockerImage(image, body)
70+
}
71+
72+
void withHelm(Closure body) {
73+
dockerWrapper.withHelm(body)
7674
}
7775

7876
// Dummy kubeConfig, so we can use `kubectl --dry-run=client`
@@ -110,4 +108,15 @@ users:
110108
}
111109
return namespace
112110
}
111+
112+
protected GitopsTool getGitopsTool() {
113+
switch (gitopsConfig.gitopsTool) {
114+
case 'FLUX':
115+
return GitopsTool.FLUX
116+
case 'ARGO':
117+
return GitopsTool.ARGO
118+
default:
119+
return null
120+
}
121+
}
113122
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cloudogu.gitopsbuildlib.deployment
2+
3+
enum GitopsTool {
4+
FLUX('flux'), ARGO('argo')
5+
6+
private final String name
7+
8+
GitopsTool(String name) {
9+
this.name = name
10+
}
11+
12+
String getNameValue() {
13+
return name
14+
}
15+
16+
String toString() {
17+
return name() + " = " + getNameValue()
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cloudogu.gitopsbuildlib.deployment
2+
3+
enum SourceType {
4+
HELM('helm'), PLAIN('plain')
5+
6+
private final String name
7+
8+
SourceType(String name) {
9+
this.name = name
10+
}
11+
12+
String getNameValue() {
13+
return name
14+
}
15+
16+
String toString() {
17+
return name() + " = " + getNameValue()
18+
}
19+
}

src/com/cloudogu/gitopsbuildlib/deployment/helm/Helm.groovy

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package com.cloudogu.gitopsbuildlib.deployment.helm
22

33
import com.cloudogu.gitopsbuildlib.deployment.Deployment
4+
import com.cloudogu.gitopsbuildlib.deployment.GitopsTool
45
import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.ArgoCDRelease
56
import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.FluxV1Release
67
import com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease.HelmRelease
78
import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.GitRepo
89
import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.HelmRepo
910
import com.cloudogu.gitopsbuildlib.deployment.helm.repotype.RepoType
11+
import com.cloudogu.gitopsbuildlib.deployment.SourceType
1012

1113
class Helm extends Deployment {
1214

1315
protected RepoType chartRepo
1416
protected HelmRelease helmRelease
1517

18+
private String helmChartTempDir = ".helmChartTempDir"
19+
private String chartRootDir = "chart"
20+
1621
Helm(def script, def gitopsConfig) {
1722
super(script, gitopsConfig)
1823
this.extraResourcesFolder = "extraResources"
@@ -34,24 +39,30 @@ class Helm extends Deployment {
3439
def application = gitopsConfig.application
3540
def sourcePath = gitopsConfig.deployments.sourcePath
3641

42+
chartRepo.prepareRepo(helmConfig, helmChartTempDir, chartRootDir)
43+
3744
// writing the merged-values.yaml via writeYaml into a file has the advantage, that it gets formatted as valid yaml
3845
// This makes it easier to read in and indent for the inline use in the helmRelease.
3946
// It enables us to reuse the `fileToInlineYaml` function, without writing a complex formatting logic.
40-
script.writeFile file: "${stage}/${application}/mergedValues.yaml", text: chartRepo.mergeValues(helmConfig, ["${script.env.WORKSPACE}/${sourcePath}/values-${stage}.yaml", "${script.env.WORKSPACE}/${sourcePath}/values-shared.yaml"] as String[])
41-
42-
updateYamlValue("${stage}/${application}/mergedValues.yaml", helmConfig)
47+
script.writeFile file: "${script.env.WORKSPACE}/${helmChartTempDir}/mergedValues.yaml", text: mergeValuesFiles(helmConfig, ["${script.env.WORKSPACE}/${sourcePath}/values-${stage}.yaml", "${script.env.WORKSPACE}/${sourcePath}/values-shared.yaml"] as String[])
4348

44-
script.writeFile file: "${stage}/${application}/applicationRelease.yaml", text: helmRelease.create(helmConfig, application, getNamespace(stage), "${stage}/${application}/mergedValues.yaml")
49+
updateYamlValue("${script.env.WORKSPACE}/${helmChartTempDir}/mergedValues.yaml", helmConfig)
4550

46-
// since the values are already inline (helmRelease.yaml) we do not need to commit them into the gitops repo
47-
script.sh "rm ${stage}/${application}/mergedValues.yaml"
51+
script.writeFile file: "${stage}/${application}/applicationRelease.yaml", text: helmRelease.create(helmConfig, application, getNamespace(stage), "${script.env.WORKSPACE}/${helmChartTempDir}/mergedValues.yaml")
4852
}
4953

5054
@Override
5155
def postValidation(String stage) {
52-
def helmConfig = gitopsConfig.deployments.helm
53-
// clean the gitrepo helm chart folder since the helmRelease.yaml ist now created
54-
script.sh "rm -rf chart || true"
56+
// clean the helm chart folder since the validation on this helm chart is done
57+
script.sh "rm -rf ${script.env.WORKSPACE}/${helmChartTempDir} || true"
58+
}
59+
60+
@Override
61+
def validate(String stage) {
62+
gitopsConfig.validators.each { validator ->
63+
validator.value.validator.validate(validator.value.enabled, getGitopsTool(), SourceType.PLAIN, "${stage}/${gitopsConfig.application}", validator.value.config, gitopsConfig)
64+
validator.value.validator.validate(validator.value.enabled, getGitopsTool(), SourceType.HELM, "${script.env.WORKSPACE}/${helmChartTempDir}",validator.value.config, gitopsConfig)
65+
}
5566
}
5667

5768
private void updateYamlValue(String yamlFilePath, Map helmConfig) {
@@ -69,4 +80,29 @@ class Helm extends Deployment {
6980
}
7081
script.writeYaml file: yamlFilePath, data: data, overwrite: true
7182
}
83+
84+
private String mergeValuesFiles(Map helmConfig, String[] valuesFiles) {
85+
String mergedValuesFile = ""
86+
87+
def chartDir = ''
88+
if (helmConfig.containsKey('chartPath') && helmConfig.chartPath) {
89+
chartDir = helmConfig.chartPath
90+
} else if ( helmConfig.containsKey('chartName')) {
91+
chartDir = helmConfig.chartName
92+
}
93+
94+
withHelm {
95+
String helmScript = "helm values ${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}/${chartDir} ${valuesFilesWithParameter(valuesFiles)}"
96+
mergedValuesFile = script.sh returnStdout: true, script: helmScript
97+
}
98+
return mergedValuesFile
99+
}
100+
101+
private String valuesFilesWithParameter(String[] valuesFiles) {
102+
String valuesFilesWithParameter = ""
103+
valuesFiles.each {
104+
valuesFilesWithParameter += "-f $it "
105+
}
106+
return valuesFilesWithParameter
107+
}
72108
}

src/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/ArgoCDRelease.groovy

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease
22

33
import com.cloudogu.gitopsbuildlib.docker.DockerWrapper
44

5-
class ArgoCDRelease extends HelmRelease{
5+
class ArgoCDRelease extends HelmRelease {
66

77
protected DockerWrapper dockerWrapper
88

@@ -13,7 +13,6 @@ class ArgoCDRelease extends HelmRelease{
1313

1414
@Override
1515
String create(Map helmConfig, String application, String namespace, String mergedValuesFile) {
16-
1716
String helmRelease = ""
1817
if (helmConfig.repoType == 'GIT') {
1918
helmRelease = createResourcesFromGitRepo(helmConfig, application, mergedValuesFile)
@@ -24,7 +23,6 @@ class ArgoCDRelease extends HelmRelease{
2423
}
2524

2625
private String createResourcesFromGitRepo(Map helmConfig, String application, String mergedValuesFile) {
27-
2826
def chartPath = ''
2927
if (helmConfig.containsKey('chartPath')) {
3028
chartPath = helmConfig.chartPath
@@ -40,7 +38,7 @@ class ArgoCDRelease extends HelmRelease{
4038
private String createHelmRelease(String chartPath, String application, String mergedValuesFile) {
4139
String helmRelease = ""
4240
dockerWrapper.withHelm {
43-
String templateScript = "helm template ${application} chart/${chartPath} -f ${mergedValuesFile}"
41+
String templateScript = "helm template ${application} ${script.env.WORKSPACE}/.helmChartTempDir/chart/${chartPath} -f ${mergedValuesFile}"
4442
helmRelease = script.sh returnStdout: true, script: templateScript
4543
}
4644

src/com/cloudogu/gitopsbuildlib/deployment/helm/helmrelease/FluxV1Release.groovy

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.cloudogu.gitopsbuildlib.deployment.helm.helmrelease
22

3-
class FluxV1Release extends HelmRelease{
3+
class FluxV1Release extends HelmRelease {
44

55
FluxV1Release(def script) {
66
super(script)
@@ -26,7 +26,6 @@ ${values}
2626
}
2727

2828
private String gitRepoChart(Map helmConfig) {
29-
3029
def chartPath = "."
3130
if (helmConfig.containsKey('chartPath') && helmConfig.chartPath) {
3231
chartPath = helmConfig.chartPath

src/com/cloudogu/gitopsbuildlib/deployment/helm/repotype/GitRepo.groovy

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,24 @@ class GitRepo extends RepoType {
77
}
88

99
@Override
10-
String mergeValues(Map helmConfig, String[] valuesFiles) {
11-
String merge = ""
10+
void prepareRepo(Map helmConfig, String helmChartTempDir, String chartRootDir) {
1211

13-
getHelmChartFromGitRepo(helmConfig)
12+
getHelmChartFromGitRepo(helmConfig, helmChartTempDir, chartRootDir)
1413

1514
def chartPath = ''
1615
if (helmConfig.containsKey('chartPath')) {
1716
chartPath = helmConfig.chartPath
1817
}
1918

2019
withHelm {
21-
script.sh "helm dep update chart/${chartPath}"
22-
String helmScript = "helm values chart/${chartPath} ${valuesFilesWithParameter(valuesFiles)}"
23-
merge = script.sh returnStdout: true, script: helmScript
20+
script.sh "helm dep update ${script.env.WORKSPACE}/.helmChartTempDir/${chartRootDir}/${chartPath}"
2421
}
25-
26-
return merge
2722
}
2823

29-
private getHelmChartFromGitRepo(Map helmConfig) {
24+
private getHelmChartFromGitRepo(Map helmConfig, String helmChartTempDir, String chartRootDir) {
3025
def git
3126

32-
script.dir("chart") {
27+
script.dir("${script.env.WORKSPACE}/${helmChartTempDir}/${chartRootDir}/") {
3328

3429
if (helmConfig.containsKey('credentialsId')) {
3530
git = script.cesBuildLib.Git.new(script, helmConfig.credentialsId)

0 commit comments

Comments
 (0)