Skip to content
15 changes: 12 additions & 3 deletions src/main/groovy/com/cloudogu/gitops/Feature.groovy
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.cloudogu.gitops

import com.cloudogu.gitops.utils.MapUtils
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.utils.TemplatingEngine
import groovy.util.logging.Slf4j
import groovy.yaml.YamlSlurper

import java.nio.file.Path

/**
* A single tool to be deployed by GOP.
*
Expand Down Expand Up @@ -86,4 +84,15 @@ abstract class Feature {
*/
protected void validate() { }

/**
* Hook for preConfigInit. Optional.
* Feature should throw RuntimeException to stop immediately.
*/
void preConfigInit(Config configToSet) { }

/**
* Hook for postConfigInit. Optional.
* Feature should throw RuntimeException to stop immediately.
*/
void postConfigInit(Config configToSet) { }
}
25 changes: 21 additions & 4 deletions src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.ConsoleAppender
import com.cloudogu.gitops.Application
import com.cloudogu.gitops.Feature
import com.cloudogu.gitops.config.ApplicationConfigurator
import com.cloudogu.gitops.config.CommonFeatureConfig
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.schema.JsonSchemaValidator
import com.cloudogu.gitops.destroy.Destroyer
Expand Down Expand Up @@ -60,7 +62,12 @@ class GitopsPlaygroundCli {
return ReturnCode.SUCCESS
}

def context = createApplicationContext()
Application app = context.getBean(Application)

def config = readConfigs(args)
runHook(app, 'preConfigInit', config)

if (config.application.outputConfigFile) {
println(config.toYaml(false))
return ReturnCode.SUCCESS
Expand All @@ -70,8 +77,9 @@ class GitopsPlaygroundCli {
// eg a simple docker run .. --help should not fail with connection refused
config = applicationConfigurator.initConfig(config)
log.debug("Actual config: ${config.toYaml(true)}")
runHook(app, 'postConfigInit', config)

def context = createApplicationContext()
context = createApplicationContext()
register(config, context)

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

printWelcomeScreen()
Expand Down Expand Up @@ -208,8 +216,6 @@ class GitopsPlaygroundCli {
log.debug("Writing CLI params into config")
Config mergedConfig = Config.fromMap(mergedConfigs)
new CommandLine(mergedConfig).parseArgs(args)

applicationConfigurator.validateConfig(mergedConfig)

return mergedConfig
}
Expand Down Expand Up @@ -241,4 +247,15 @@ class GitopsPlaygroundCli {
|----------------------------------------------------------------------------------------------|
'''
}

static void runHook(Application app, String methodName, def config) {
([new CommonFeatureConfig(), *app.features]).each { feature ->
// Executing only the method if the derived feature class has implemented the passed methodName
def mm = feature.metaClass.getMetaMethod(methodName, config)
if (mm && mm.declaringClass.theClass != Feature) {
log.debug("Executing ${methodName} hook on feature ${feature.class.name}")
mm.invoke(feature, config)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ package com.cloudogu.gitops.config
import com.cloudogu.gitops.utils.FileSystemUtils
import groovy.util.logging.Slf4j

import static com.cloudogu.gitops.config.Config.ContentRepoType
import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema

@Slf4j
class ApplicationConfigurator {

Expand All @@ -32,8 +29,6 @@ class ApplicationConfigurator {

addFeatureConfig(newConfig)

validateEnvConfigForArgoCDOperator(newConfig)

evaluateBaseUrl(newConfig)

setResourceInclusionsCluster(newConfig)
Expand Down Expand Up @@ -258,95 +253,6 @@ class ApplicationConfigurator {
return newUrl
}

/**
* Make sure that config does not contain contradictory values.
* Throws RuntimeException which meaningful message, if invalid.
*/
void validateConfig(Config configToSet) {
validateScmmAndJenkinsAreBothSet(configToSet)
validateMirrorReposHelmChartFolderSet(configToSet)
validateContent(configToSet)
}

private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
// This should only happen when run outside the image, i.e. during development
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
"Either run inside the official container image or setting env var " +
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
}
}

static void validateContent(Config config) {
config.content.repos.each { repo ->

if (!repo.url) {
throw new RuntimeException("content.repos requires a url parameter.")
}
if (repo.target) {
if (repo.target.count('/') == 0) {
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
}
}

switch (repo.type) {
case ContentRepoType.COPY:
if (!repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
}
break
case ContentRepoType.FOLDER_BASED:
if (repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
}
if (repo.targetRef) {
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
}
break
case ContentRepoType.MIRROR:
if (!repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
}
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
}
if (repo.templating) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
}
break
}
}
}

private void validateScmmAndJenkinsAreBothSet(Config configToSet) {
if (configToSet.jenkins.active &&
(configToSet.scm.scmManager.url && !configToSet.jenkins.url ||
!configToSet.scm.scmManager.url && configToSet.jenkins.url)) {
throw new RuntimeException('When setting jenkins URL, scmm URL must also be set and the other way round')
}
}

// Validate that the env list has proper maps with 'name' and 'value'
private static void validateEnvConfigForArgoCDOperator(Config configToSet) {
// Exit early if not in operator mode or if env list is empty
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
return
}

List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>

log.info("Validating env list in features.argocd.env with {} entries.", env.size())

env.each { map ->
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
}
}

log.info("Env list validation for features.argocd.env completed successfully.")
}

private void setResourceInclusionsCluster(Config configToSet) {
// Return early if NOT deploying via operator
if (!configToSet.features.argocd.operator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.cloudogu.gitops.config

import com.cloudogu.gitops.Feature
import groovy.util.logging.Slf4j

@Slf4j
class CommonFeatureConfig extends Feature {
@Override
void preConfigInit(Config configToSet) {
validateConfig(configToSet)
}

/**
* Make sure that config does not contain contradictory values.
* Throws RuntimeException which meaningful message, if invalid.
*/
void validateConfig(Config configToSet) {
validateMirrorReposHelmChartFolderSet(configToSet)
}

private void validateMirrorReposHelmChartFolderSet(Config configToSet) {
if (configToSet.application.mirrorRepos && !configToSet.application.localHelmChartFolder) {
// This should only happen when run outside the image, i.e. during development
throw new RuntimeException("Missing config for localHelmChartFolder.\n" +
"Either run inside the official container image or setting env var " +
"LOCAL_HELM_CHART_FOLDER='charts' after running 'scripts/downloadHelmCharts.sh' from the repo")
}
}

@Override
boolean isEnabled() {
return false
}
}
42 changes: 42 additions & 0 deletions src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,48 @@ class ContentLoader extends Feature {

}

@Override
void preConfigInit(Config configToSet) {
config.content.repos.each { repo ->

if (!repo.url) {
throw new RuntimeException("content.repos requires a url parameter.")
}
if (repo.target) {
if (repo.target.count('/') == 0) {
throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
}
}

switch (repo.type) {
case ContentRepoType.COPY:
if (!repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
}
break
case ContentRepoType.FOLDER_BASED:
if (repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
}
if (repo.targetRef) {
throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
}
break
case ContentRepoType.MIRROR:
if (!repo.target) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
}
if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
}
if (repo.templating) {
throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
}
break
}
}
}

void createImagePullSecrets() {
if (config.registry.createImagePullSecrets) {
String registryUsername = config.registry.readOnlyUsername ?: config.registry.username
Expand Down
21 changes: 21 additions & 0 deletions src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ class ArgoCD extends Feature {
config.features.argocd.active
}

@Override
void postConfigInit(Config configToSet) {
// Exit early if not in operator mode or if env list is empty
if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
return
}

List<Map> env = configToSet.features.argocd.env as List<Map<String, String>>

log.info("Validating env list in features.argocd.env with {} entries.", env.size())

env.each { map ->
if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
}
}

log.info("Env list validation for features.argocd.env completed successfully.")
}

@Override
void enable() {

Expand Down
Loading