Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) { }
}
29 changes: 24 additions & 5 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 All @@ -21,7 +23,9 @@ import org.slf4j.LoggerFactory
import picocli.CommandLine

import static com.cloudogu.gitops.config.ConfigConstants.APP_NAME
import static com.cloudogu.gitops.utils.MapUtils.deepMerge
import static com.cloudogu.gitops.utils.MapUtils.deepMerge


/**
* Provides the entrypoint to the application as well as all config parameters.
* When changing parameters, make sure to update the Config for the config file as well
Expand Down Expand Up @@ -59,7 +63,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 @@ -69,8 +78,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 @@ -86,7 +96,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 @@ -207,8 +217,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 @@ -240,4 +248,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
@@ -1,11 +1,9 @@
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 @@ -21,6 +19,7 @@ class ApplicationConfigurator {
Config initConfig(Config newConfig) {

addAdditionalApplicationConfig(newConfig)

addNamePrefix(newConfig)

addScmmConfig(newConfig)
Expand All @@ -31,8 +30,6 @@ class ApplicationConfigurator {

addFeatureConfig(newConfig)

validateEnvConfigForArgoCDOperator(newConfig)

evaluateBaseUrl(newConfig)

setResourceInclusionsCluster(newConfig)
Expand Down Expand Up @@ -283,95 +280,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.scmm.url && !configToSet.jenkins.url ||
!configToSet.scmm.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,43 @@
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) {
validateScmmAndJenkinsAreBothSet(configToSet)
validateMirrorReposHelmChartFolderSet(configToSet)
}

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

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/Content.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,48 @@ class Content 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 @@ -76,6 +76,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() {
initRepos()
Expand Down
Loading