Skip to content

Commit 9b78e60

Browse files
committed
add mechanism for feature-specific validation and move validation logic into respective classes
1 parent 07092fd commit 9b78e60

File tree

7 files changed

+149
-101
lines changed

7 files changed

+149
-101
lines changed

src/main/groovy/com/cloudogu/gitops/Feature.groovy

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package com.cloudogu.gitops
22

3-
import com.cloudogu.gitops.utils.MapUtils
3+
import com.cloudogu.gitops.config.Config
44
import com.cloudogu.gitops.utils.TemplatingEngine
55
import groovy.util.logging.Slf4j
66
import groovy.yaml.YamlSlurper
77

8-
import java.nio.file.Path
9-
108
/**
119
* A single tool to be deployed by GOP.
1210
*
@@ -86,4 +84,15 @@ abstract class Feature {
8684
*/
8785
protected void validate() { }
8886

87+
/**
88+
* Hook for preConfigValidation. Optional.
89+
* Feature should throw RuntimeException to stop immediately.
90+
*/
91+
void preConfigValidation(Config configToSet) { }
92+
93+
/**
94+
* Hook for postConfigValidation. Optional.
95+
* Feature should throw RuntimeException to stop immediately.
96+
*/
97+
void postConfigValidation(Config configToSet) { }
8998
}

src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.cloudogu.gitops.config.Config
1212
import com.cloudogu.gitops.config.schema.JsonSchemaValidator
1313
import com.cloudogu.gitops.destroy.Destroyer
1414
import com.cloudogu.gitops.utils.CommandExecutor
15+
import com.cloudogu.gitops.utils.FeatureUtils
1516
import com.cloudogu.gitops.utils.FileSystemUtils
1617
import com.cloudogu.gitops.utils.K8sClient
1718
import groovy.util.logging.Slf4j
@@ -20,6 +21,7 @@ import io.micronaut.context.ApplicationContext
2021
import org.slf4j.LoggerFactory
2122
import picocli.CommandLine
2223

24+
2325
import static com.cloudogu.gitops.config.ConfigConstants.APP_NAME
2426
import static com.cloudogu.gitops.utils.MapUtils.deepMerge
2527
/**
@@ -35,7 +37,7 @@ class GitopsPlaygroundCli {
3537
ApplicationConfigurator applicationConfigurator
3638

3739
GitopsPlaygroundCli(K8sClient k8sClient = new K8sClient(new CommandExecutor(), new FileSystemUtils(), null),
38-
ApplicationConfigurator applicationConfigurator = new ApplicationConfigurator()) {
40+
ApplicationConfigurator applicationConfigurator = new ApplicationConfigurator(null)) {
3941
this.k8sClient = k8sClient
4042
this.applicationConfigurator = applicationConfigurator
4143
}
@@ -59,6 +61,9 @@ class GitopsPlaygroundCli {
5961
return ReturnCode.SUCCESS
6062
}
6163

64+
def context = createApplicationContext()
65+
Application app = context.getBean(Application)
66+
6267
def config = readConfigs(args)
6368
if (config.application.outputConfigFile) {
6469
println(config.toYaml(false))
@@ -70,7 +75,7 @@ class GitopsPlaygroundCli {
7075
config = applicationConfigurator.initConfig(config)
7176
log.debug("Actual config: ${config.toYaml(true)}")
7277

73-
def context = createApplicationContext()
78+
FeatureUtils.runHook(app, 'postConfigValidation', config)
7479
register(config, context)
7580

7681
if (config.application.destroy) {
@@ -86,7 +91,6 @@ class GitopsPlaygroundCli {
8691
if (!confirm("Applying gitops playground to kubernetes cluster '${k8sClient.currentContext}'.", config)) {
8792
return ReturnCode.NOT_CONFIRMED
8893
}
89-
Application app = context.getBean(Application)
9094
app.start()
9195

9296
printWelcomeScreen()
@@ -208,7 +212,8 @@ class GitopsPlaygroundCli {
208212
Config mergedConfig = Config.fromMap(mergedConfigs)
209213
new CommandLine(mergedConfig).parseArgs(args)
210214

211-
applicationConfigurator.validateConfig(mergedConfig)
215+
def app = ApplicationContext.run().getBean(Application)
216+
FeatureUtils.runHook(app, 'preConfigValidation', mergedConfig)
212217

213218
return mergedConfig
214219
}

src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.cloudogu.gitops.config
22

3+
34
import com.cloudogu.gitops.utils.FileSystemUtils
45
import groovy.util.logging.Slf4j
56

6-
import static com.cloudogu.gitops.config.Config.ContentRepoType
7-
import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema
8-
97
@Slf4j
108
class ApplicationConfigurator {
119

@@ -21,6 +19,7 @@ class ApplicationConfigurator {
2119
Config initConfig(Config newConfig) {
2220

2321
addAdditionalApplicationConfig(newConfig)
22+
2423
addNamePrefix(newConfig)
2524

2625
addScmmConfig(newConfig)
@@ -31,8 +30,6 @@ class ApplicationConfigurator {
3130

3231
addFeatureConfig(newConfig)
3332

34-
validateEnvConfigForArgoCDOperator(newConfig)
35-
3633
evaluateBaseUrl(newConfig)
3734

3835
setResourceInclusionsCluster(newConfig)
@@ -283,95 +280,6 @@ class ApplicationConfigurator {
283280
return newUrl
284281
}
285282

286-
/**
287-
* Make sure that config does not contain contradictory values.
288-
* Throws RuntimeException which meaningful message, if invalid.
289-
*/
290-
void validateConfig(Config configToSet) {
291-
validateScmmAndJenkinsAreBothSet(configToSet)
292-
validateMirrorReposHelmChartFolderSet(configToSet)
293-
validateContent(configToSet)
294-
}
295-
296-
private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
297-
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
298-
// This should only happen when run outside the image, i.e. during development
299-
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
300-
"Either run inside the official container image or setting env var " +
301-
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
302-
}
303-
}
304-
305-
static void validateContent(Config config) {
306-
config.content.repos.each { repo ->
307-
308-
if (!repo.url) {
309-
throw new RuntimeException("content.repos requires a url parameter.")
310-
}
311-
if (repo.target) {
312-
if (repo.target.count('/') == 0) {
313-
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
314-
}
315-
}
316-
317-
switch (repo.type) {
318-
case ContentRepoType.COPY:
319-
if (!repo.target) {
320-
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
321-
}
322-
break
323-
case ContentRepoType.FOLDER_BASED:
324-
if (repo.target) {
325-
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
326-
}
327-
if (repo.targetRef) {
328-
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
329-
}
330-
break
331-
case ContentRepoType.MIRROR:
332-
if (!repo.target) {
333-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
334-
}
335-
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
336-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
337-
}
338-
if (repo.templating) {
339-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
340-
}
341-
break
342-
}
343-
}
344-
}
345-
346-
private void validateScmmAndJenkinsAreBothSet(Config configToSet) {
347-
if (configToSet.jenkins.active &&
348-
(configToSet.scmm.url && !configToSet.jenkins.url ||
349-
!configToSet.scmm.url && configToSet.jenkins.url)) {
350-
throw new RuntimeException('When setting jenkins URL, scmm URL must also be set and the other way round')
351-
}
352-
}
353-
354-
// Validate that the env list has proper maps with 'name' and 'value'
355-
private static void validateEnvConfigForArgoCDOperator(Config configToSet) {
356-
// Exit early if not in operator mode or if env list is empty
357-
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
358-
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
359-
return
360-
}
361-
362-
List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>
363-
364-
log.info("Validating env list in features.argocd.env with {} entries.", env.size())
365-
366-
env.each { map ->
367-
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
368-
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
369-
}
370-
}
371-
372-
log.info("Env list validation for features.argocd.env completed successfully.")
373-
}
374-
375283
private void setResourceInclusionsCluster(Config configToSet) {
376284
// Return early if NOT deploying via operator
377285
if (!configToSet.features.argocd.operator) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.cloudogu.gitops.config
2+
3+
import com.cloudogu.gitops.Feature
4+
import groovy.util.logging.Slf4j
5+
import io.micronaut.core.annotation.Order
6+
import jakarta.inject.Singleton
7+
8+
@Slf4j
9+
@Singleton
10+
@Order(40)
11+
class ConfigValidator extends Feature {
12+
13+
ConfigValidator() {
14+
log.debug("Doing common pre/post config validation...")
15+
}
16+
17+
@Override
18+
void preConfigValidation(Config configToSet) {
19+
validateScmmAndJenkinsAreBothSet(configToSet)
20+
validateMirrorReposHelmChartFolderSet(configToSet)
21+
}
22+
23+
private void validateScmmAndJenkinsAreBothSet(Config configToSet) {
24+
if (configToSet.jenkins.active &&
25+
(configToSet.scmm.url && !configToSet.jenkins.url ||
26+
!configToSet.scmm.url && configToSet.jenkins.url)) {
27+
throw new RuntimeException('When setting jenkins URL, scmm URL must also be set and the other way round')
28+
}
29+
}
30+
31+
private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
32+
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
33+
// This should only happen when run outside the image, i.e. during development
34+
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
35+
"Either run inside the official container image or setting env var " +
36+
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
37+
}
38+
}
39+
40+
@Override
41+
boolean isEnabled() {
42+
return false
43+
}
44+
}

src/main/groovy/com/cloudogu/gitops/features/Content.groovy

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,48 @@ class Content extends Feature {
7878

7979
}
8080

81+
@Override
82+
void preConfigValidation(Config configToSet) {
83+
config.content.repos.each { repo ->
84+
85+
if (!repo.url) {
86+
throw new RuntimeException("content.repos requires a url parameter.")
87+
}
88+
if (repo.target) {
89+
if (repo.target.count('/') == 0) {
90+
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
91+
}
92+
}
93+
94+
switch (repo.type) {
95+
case ContentRepoType.COPY:
96+
if (!repo.target) {
97+
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
98+
}
99+
break
100+
case ContentRepoType.FOLDER_BASED:
101+
if (repo.target) {
102+
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
103+
}
104+
if (repo.targetRef) {
105+
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
106+
}
107+
break
108+
case ContentRepoType.MIRROR:
109+
if (!repo.target) {
110+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
111+
}
112+
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
113+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
114+
}
115+
if (repo.templating) {
116+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
117+
}
118+
break
119+
}
120+
}
121+
}
122+
81123
void createImagePullSecrets() {
82124
if (config.registry.createImagePullSecrets) {
83125
String registryUsername = config.registry.readOnlyUsername ?: config.registry.username

src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ class ArgoCD extends Feature {
7676
config.features.argocd.active
7777
}
7878

79+
@Override
80+
void postConfigValidation(Config configToSet) {
81+
// Exit early if not in operator mode or if env list is empty
82+
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
83+
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
84+
return
85+
}
86+
87+
List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>
88+
89+
log.info("Validating env list in features.argocd.env with {} entries.", env.size())
90+
91+
env.each { map ->
92+
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
93+
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
94+
}
95+
}
96+
97+
log.info("Env list validation for features.argocd.env completed successfully.")
98+
}
99+
79100
@Override
80101
void enable() {
81102
initRepos()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cloudogu.gitops.utils
2+
3+
import com.cloudogu.gitops.Application
4+
import groovy.util.logging.Slf4j
5+
import com.cloudogu.gitops.Feature
6+
7+
@Slf4j
8+
class FeatureUtils {
9+
static void runHook(Application app, String methodName, def config) {
10+
app.features.each { feature ->
11+
// Executing only the method if the derived feature class has implemented the passed specific hook method
12+
def mm = feature.metaClass.getMetaMethod(methodName, config)
13+
if (mm && mm.declaringClass.theClass != Feature) {
14+
log.debug("Executing ${methodName} hook on feature ${feature.class.name}")
15+
mm.invoke(feature, config)
16+
}
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)