Skip to content

Commit 807737a

Browse files
mdrollnihussmann
andauthored
Feature/appconfigurator validation migration (#309)
* add mechanism for feature-specific validation and move validation logic into respective classes * move hook mechanism to gop cli and rename validation to a proper name * decouple common feature config from the feature list --------- Co-authored-by: Niklas Hußmann <niklas.hussmann-extern@cloudogu.com>
1 parent d60b87e commit 807737a

File tree

8 files changed

+173
-136
lines changed

8 files changed

+173
-136
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 preConfigInit. Optional.
89+
* Feature should throw RuntimeException to stop immediately.
90+
*/
91+
void preConfigInit(Config configToSet) { }
92+
93+
/**
94+
* Hook for postConfigInit. Optional.
95+
* Feature should throw RuntimeException to stop immediately.
96+
*/
97+
void postConfigInit(Config configToSet) { }
8998
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import ch.qos.logback.classic.encoder.PatternLayoutEncoder
77
import ch.qos.logback.classic.spi.ILoggingEvent
88
import ch.qos.logback.core.ConsoleAppender
99
import com.cloudogu.gitops.Application
10+
import com.cloudogu.gitops.Feature
1011
import com.cloudogu.gitops.config.ApplicationConfigurator
12+
import com.cloudogu.gitops.config.CommonFeatureConfig
1113
import com.cloudogu.gitops.config.Config
1214
import com.cloudogu.gitops.config.schema.JsonSchemaValidator
1315
import com.cloudogu.gitops.destroy.Destroyer
@@ -60,7 +62,12 @@ class GitopsPlaygroundCli {
6062
return ReturnCode.SUCCESS
6163
}
6264

65+
def context = createApplicationContext()
66+
Application app = context.getBean(Application)
67+
6368
def config = readConfigs(args)
69+
runHook(app, 'preConfigInit', config)
70+
6471
if (config.application.outputConfigFile) {
6572
println(config.toYaml(false))
6673
return ReturnCode.SUCCESS
@@ -70,8 +77,9 @@ class GitopsPlaygroundCli {
7077
// eg a simple docker run .. --help should not fail with connection refused
7178
config = applicationConfigurator.initConfig(config)
7279
log.debug("Actual config: ${config.toYaml(true)}")
80+
runHook(app, 'postConfigInit', config)
7381

74-
def context = createApplicationContext()
82+
context = createApplicationContext()
7583
register(config, context)
7684

7785
if (config.application.destroy) {
@@ -87,7 +95,7 @@ class GitopsPlaygroundCli {
8795
if (!confirm("Applying gitops playground to kubernetes cluster '${k8sClient.currentContext}'.", config)) {
8896
return ReturnCode.NOT_CONFIRMED
8997
}
90-
Application app = context.getBean(Application)
98+
app = context.getBean(Application)
9199
app.start()
92100

93101
printWelcomeScreen()
@@ -208,8 +216,6 @@ class GitopsPlaygroundCli {
208216
log.debug("Writing CLI params into config")
209217
Config mergedConfig = Config.fromMap(mergedConfigs)
210218
new CommandLine(mergedConfig).parseArgs(args)
211-
212-
applicationConfigurator.validateConfig(mergedConfig)
213219

214220
return mergedConfig
215221
}
@@ -241,4 +247,15 @@ class GitopsPlaygroundCli {
241247
|----------------------------------------------------------------------------------------------|
242248
'''
243249
}
250+
251+
static void runHook(Application app, String methodName, def config) {
252+
([new CommonFeatureConfig(), *app.features]).each { feature ->
253+
// Executing only the method if the derived feature class has implemented the passed methodName
254+
def mm = feature.metaClass.getMetaMethod(methodName, config)
255+
if (mm && mm.declaringClass.theClass != Feature) {
256+
log.debug("Executing ${methodName} hook on feature ${feature.class.name}")
257+
mm.invoke(feature, config)
258+
}
259+
}
260+
}
244261
}

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

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ package com.cloudogu.gitops.config
44
import com.cloudogu.gitops.utils.FileSystemUtils
55
import groovy.util.logging.Slf4j
66

7-
import static com.cloudogu.gitops.config.Config.ContentRepoType
8-
import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema
9-
107
@Slf4j
118
class ApplicationConfigurator {
129

@@ -32,8 +29,6 @@ class ApplicationConfigurator {
3229

3330
addFeatureConfig(newConfig)
3431

35-
validateEnvConfigForArgoCDOperator(newConfig)
36-
3732
evaluateBaseUrl(newConfig)
3833

3934
setResourceInclusionsCluster(newConfig)
@@ -258,95 +253,6 @@ class ApplicationConfigurator {
258253
return newUrl
259254
}
260255

261-
/**
262-
* Make sure that config does not contain contradictory values.
263-
* Throws RuntimeException which meaningful message, if invalid.
264-
*/
265-
void validateConfig(Config configToSet) {
266-
validateScmmAndJenkinsAreBothSet(configToSet)
267-
validateMirrorReposHelmChartFolderSet(configToSet)
268-
validateContent(configToSet)
269-
}
270-
271-
private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
272-
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
273-
// This should only happen when run outside the image, i.e. during development
274-
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
275-
"Either run inside the official container image or setting env var " +
276-
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
277-
}
278-
}
279-
280-
static void validateContent(Config config) {
281-
config.content.repos.each { repo ->
282-
283-
if (!repo.url) {
284-
throw new RuntimeException("content.repos requires a url parameter.")
285-
}
286-
if (repo.target) {
287-
if (repo.target.count('/') == 0) {
288-
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
289-
}
290-
}
291-
292-
switch (repo.type) {
293-
case ContentRepoType.COPY:
294-
if (!repo.target) {
295-
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
296-
}
297-
break
298-
case ContentRepoType.FOLDER_BASED:
299-
if (repo.target) {
300-
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
301-
}
302-
if (repo.targetRef) {
303-
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
304-
}
305-
break
306-
case ContentRepoType.MIRROR:
307-
if (!repo.target) {
308-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
309-
}
310-
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
311-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
312-
}
313-
if (repo.templating) {
314-
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
315-
}
316-
break
317-
}
318-
}
319-
}
320-
321-
private void validateScmmAndJenkinsAreBothSet(Config configToSet) {
322-
if (configToSet.jenkins.active &&
323-
(configToSet.scm.scmManager.url && !configToSet.jenkins.url ||
324-
!configToSet.scm.scmManager.url && configToSet.jenkins.url)) {
325-
throw new RuntimeException('When setting jenkins URL, scmm URL must also be set and the other way round')
326-
}
327-
}
328-
329-
// Validate that the env list has proper maps with 'name' and 'value'
330-
private static void validateEnvConfigForArgoCDOperator(Config configToSet) {
331-
// Exit early if not in operator mode or if env list is empty
332-
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
333-
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
334-
return
335-
}
336-
337-
List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>
338-
339-
log.info("Validating env list in features.argocd.env with {} entries.", env.size())
340-
341-
env.each { map ->
342-
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
343-
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
344-
}
345-
}
346-
347-
log.info("Env list validation for features.argocd.env completed successfully.")
348-
}
349-
350256
private void setResourceInclusionsCluster(Config configToSet) {
351257
// Return early if NOT deploying via operator
352258
if (!configToSet.features.argocd.operator) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.cloudogu.gitops.config
2+
3+
import com.cloudogu.gitops.Feature
4+
import groovy.util.logging.Slf4j
5+
6+
@Slf4j
7+
class CommonFeatureConfig extends Feature {
8+
@Override
9+
void preConfigInit(Config configToSet) {
10+
validateConfig(configToSet)
11+
}
12+
13+
/**
14+
* Make sure that config does not contain contradictory values.
15+
* Throws RuntimeException which meaningful message, if invalid.
16+
*/
17+
void validateConfig(Config configToSet) {
18+
validateMirrorReposHelmChartFolderSet(configToSet)
19+
}
20+
21+
private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
22+
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
23+
// This should only happen when run outside the image, i.e. during development
24+
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
25+
"Either run inside the official container image or setting env var " +
26+
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
27+
}
28+
}
29+
30+
@Override
31+
boolean isEnabled() {
32+
return false
33+
}
34+
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,48 @@ class ContentLoader extends Feature {
7474

7575
}
7676

77+
@Override
78+
void preConfigInit(Config configToSet) {
79+
config.content.repos.each { repo ->
80+
81+
if (!repo.url) {
82+
throw new RuntimeException("content.repos requires a url parameter.")
83+
}
84+
if (repo.target) {
85+
if (repo.target.count('/') == 0) {
86+
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
87+
}
88+
}
89+
90+
switch (repo.type) {
91+
case ContentRepoType.COPY:
92+
if (!repo.target) {
93+
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
94+
}
95+
break
96+
case ContentRepoType.FOLDER_BASED:
97+
if (repo.target) {
98+
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
99+
}
100+
if (repo.targetRef) {
101+
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
102+
}
103+
break
104+
case ContentRepoType.MIRROR:
105+
if (!repo.target) {
106+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
107+
}
108+
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
109+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
110+
}
111+
if (repo.templating) {
112+
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
113+
}
114+
break
115+
}
116+
}
117+
}
118+
77119
void createImagePullSecrets() {
78120
if (config.registry.createImagePullSecrets) {
79121
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
@@ -71,6 +71,27 @@ class ArgoCD extends Feature {
7171
config.features.argocd.active
7272
}
7373

74+
@Override
75+
void postConfigInit(Config configToSet) {
76+
// Exit early if not in operator mode or if env list is empty
77+
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
78+
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
79+
return
80+
}
81+
82+
List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>
83+
84+
log.info("Validating env list in features.argocd.env with {} entries.", env.size())
85+
86+
env.each { map ->
87+
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
88+
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
89+
}
90+
}
91+
92+
log.info("Env list validation for features.argocd.env completed successfully.")
93+
}
94+
7495
@Override
7596
void enable() {
7697

0 commit comments

Comments
 (0)