diff --git a/argocd/argocd/argocd/values.ftl.yaml b/argocd/argocd/argocd/values.ftl.yaml index 85f22b52c..73212f3f7 100644 --- a/argocd/argocd/argocd/values.ftl.yaml +++ b/argocd/argocd/argocd/values.ftl.yaml @@ -53,7 +53,7 @@ argo-cd: # Unfortunately, as of argocd 2.6 this leads to failing notifications # https://github.com/argoproj/argo-cd/issues/11252 params: - application.namespaces: "*" + application.namespaces: "${config.application.namePrefix}argocd" server.insecure: true # tls terminated in ingress # Repo credential templates are created dynamically in groovy, so they are not stored in git diff --git a/argocd/argocd/operator/argocd.ftl.yaml b/argocd/argocd/operator/argocd.ftl.yaml index 6506db8dd..1f52009bc 100644 --- a/argocd/argocd/operator/argocd.ftl.yaml +++ b/argocd/argocd/operator/argocd.ftl.yaml @@ -151,6 +151,7 @@ spec: - name: bitnami type: helm url: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami +<#if !config.application.clusterAdmin> resourceInclusions: | - apiGroups: - "batch" @@ -250,4 +251,5 @@ spec: - "Probe" clusters: - "https://kubernetes.default.svc" - - "${config.features.argocd.resourceInclusionsCluster}" \ No newline at end of file + - "${config.features.argocd.resourceInclusionsCluster}" + \ No newline at end of file diff --git a/docs/configuration.schema.json b/docs/configuration.schema.json index 06ed0ea52..398dc8280 100644 --- a/docs/configuration.schema.json +++ b/docs/configuration.schema.json @@ -50,6 +50,10 @@ "type" : [ "string", "null" ], "description" : "the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana, vault and mailhog take precedence." }, + "clusterAdmin" : { + "type" : [ "boolean", "null" ], + "description" : "Binds ArgoCD controllers to cluster-admin ClusterRole" + }, "destroy" : { "type" : [ "boolean", "null" ], "description" : "Unroll playground" @@ -132,6 +136,10 @@ "type" : [ "boolean", "null" ], "description" : "Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project" }, + "multitenancyExamples" : { + "type" : [ "boolean", "null" ], + "description" : "Deploy multi tenancy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project" + }, "namespaces" : { "description" : "Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging", "type" : [ "array", "null" ], diff --git a/examples/example-apps-via-content-loader/argocd/argocd/projects/example-apps.ftl.yaml b/examples/example-apps-via-content-loader/argocd/argocd/projects/example-apps.ftl.yaml index 23901dd58..42cd7f116 100644 --- a/examples/example-apps-via-content-loader/argocd/argocd/projects/example-apps.ftl.yaml +++ b/examples/example-apps-via-content-loader/argocd/argocd/projects/example-apps.ftl.yaml @@ -4,9 +4,6 @@ metadata: name: example-apps namespace: ${config.application.namePrefix}argocd annotations: -<#if config.features.mail.active?? && config.features.mail.active> - notifications.argoproj.io/subscribe.email: ${config.features.argocd.emailToUser} - spec: description: Contains examples of end-user applications destinations: @@ -21,11 +18,9 @@ spec: # allow to only see application resources from the specified namespace sourceNamespaces: - - '${config.application.namePrefix}example-apps-staging' - - '${config.application.namePrefix}example-apps-production' - <#if config.features.argocd.operator> - - '${config.application.namePrefix}argocd' - + - ${config.application.namePrefix}example-apps-staging + - ${config.application.namePrefix}example-apps-production + - ${config.application.namePrefix}argocd # Allow all namespaced-scoped resources to be created @@ -34,4 +29,4 @@ spec: kind: '*' # Deny all cluster-scoped resources from being created. Least privilege. - clusterResourceWhitelist: \ No newline at end of file + clusterResourceWhitelist: diff --git a/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/Chart.yaml b/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/Chart.yaml deleted file mode 100644 index a585d698d..000000000 --- a/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -version: 13.2.21 -name: nginx -dependencies: - - name: nginx - version: 13.2.21 - repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami \ No newline at end of file diff --git a/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/values.ftl.yaml b/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/values.ftl.yaml deleted file mode 100644 index 554b3a5ca..000000000 --- a/examples/example-apps-via-content-loader/argocd/example-apps/apps/nginx-helm-umbrella/values.ftl.yaml +++ /dev/null @@ -1,43 +0,0 @@ -<#assign DockerImageParser=statics['com.cloudogu.gitops.utils.DockerImageParser']> -nginx: -<#if config.content.variables.images.nginx?has_content> -<#assign nginxImageObject = DockerImageParser.parse(config.content.variables.images.nginx)> - image: - registry: ${nginxImageObject.registry} - repository: ${nginxImageObject.repository} - tag: ${nginxImageObject.tag} -<#else> - image: - repository: bitnamilegacy/nginx - - - <#if config.registry.createImagePullSecrets == true> - - global: - imagePullSecrets: - - proxy-registry - - service: - ports: - http: 80 - type: <#if config.application.remote>LoadBalancer<#else>ClusterIP -<#if config.application.podResources == true> - resources: - limits: - cpu: 100m - memory: 30Mi - requests: - cpu: 30m - memory: 15Mi - - -<#if config.content.variables.nginx.baseDomain?has_content> - ingress: - enabled: true - pathType: Prefix - <#if config.application.urlSeparatorHyphen> - hostname: production-nginx-helm-umbrella-${config.content.variables.nginx.baseDomain} - <#else> - hostname: production.nginx-helm-umbrella.${config.content.variables.nginx.baseDomain} - - diff --git a/examples/example-apps-via-content-loader/config.yaml b/examples/example-apps-via-content-loader/config.yaml index ce4f90854..44689aa01 100644 --- a/examples/example-apps-via-content-loader/config.yaml +++ b/examples/example-apps-via-content-loader/config.yaml @@ -44,4 +44,4 @@ content: yamllint: "cytopia/yamllint:1.25-0.7" nginx: "" petclinic: "eclipse-temurin:17-jre-alpine" - maven: "" + maven: "" \ No newline at end of file diff --git a/examples/init-multi-tenancy/argocd/cluster-resources/argocd/mt-appset.ftl.yaml b/examples/init-multi-tenancy/argocd/cluster-resources/argocd/mt-appset.ftl.yaml new file mode 100644 index 000000000..c68318500 --- /dev/null +++ b/examples/init-multi-tenancy/argocd/cluster-resources/argocd/mt-appset.ftl.yaml @@ -0,0 +1,38 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: gop-multi-tenancy + namespace: ${config.application.namePrefix}argocd +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - git: + repoURL: ${scm.repoUrl}argocd/tenant-configs.git + revision: HEAD + files: + - path: "tenants/*/config.yaml" + template: + metadata: + name: gop-tenant-{{.config.application.namePrefix}} + spec: + project: argocd + sources: + - repoURL: ${scm.repoUrl}argocd/tenant-configs.git + targetRevision: HEAD + ref: valuesRef + - repoURL: ${scm.repoUrl}3rd-party-dependencies/gop-helm.git + path: . + targetRevision: HEAD + helm: + valueFiles: + - $valuesRef/tenants/globalValues.yaml + - $valuesRef/{{ .path.path }}/config.yaml + destination: + server: https://kubernetes.default.svc + namespace: ${config.application.namePrefix}argocd + syncPolicy: + automated: + selfHeal: true + prune: true \ No newline at end of file diff --git a/examples/init-multi-tenancy/argocd/tenant-configs/tenants/globalValues.yaml b/examples/init-multi-tenancy/argocd/tenant-configs/tenants/globalValues.yaml new file mode 100644 index 000000000..e0e0d9679 --- /dev/null +++ b/examples/init-multi-tenancy/argocd/tenant-configs/tenants/globalValues.yaml @@ -0,0 +1,25 @@ +image: + tag: latest +config: + application: + baseUrl: "http://localhost" + insecure: true + username: "admin" + password: "admin" + namePrefix: "" + namespaceIsolation: true + skipCrds: true + jenkins: + active: true + features: + monitoring: + active: false + argocd: + active: true + operator: true + env: [ ] + resourceInclusionsCluster: "https://10.43.0.1:443" + ingressNginx: + active: false +multiTenant: + useDedicatedInstance: true \ No newline at end of file diff --git a/examples/init-multi-tenancy/argocd/tenant-configs/tenants/tenant1/config.yaml b/examples/init-multi-tenancy/argocd/tenant-configs/tenants/tenant1/config.yaml new file mode 100644 index 000000000..bf078b650 --- /dev/null +++ b/examples/init-multi-tenancy/argocd/tenant-configs/tenants/tenant1/config.yaml @@ -0,0 +1,8 @@ +config: + application: + baseUrl: "http://tenant1.localhost" + namePrefix: "tenant1" + registry: + active: true + content: + examples: true \ No newline at end of file diff --git a/examples/init-multi-tenancy/managementConfig.yaml b/examples/init-multi-tenancy/managementConfig.yaml new file mode 100644 index 000000000..76588607f --- /dev/null +++ b/examples/init-multi-tenancy/managementConfig.yaml @@ -0,0 +1,38 @@ +application: + baseUrl: "http://localhost" + insecure: true + username: "admin" + password: "admin" + "yes": true + namePrefix: "" + namespaceIsolation: false + skipCrds: true + clusterAdmin: true +jenkins: + active: false +features: + monitoring: + active: false + argocd: + active: true + operator: false + env: [] + resourceInclusionsCluster: "https://10.43.0.1:443" + ingressNginx: + active: true +content: + repos: + - url: https://github.com/cloudogu/gop-helm + target: 3rd-party-dependencies/gop-helm + overwriteMode: RESET + - url: https://github.com/cloudogu/gitops-playground +# - url: file:///home/cl-pc-0087/src/gitops-playground + path: examples/init-multi-tenancy + ref: feature/init-multi-tenancy + templating: true + type: FOLDER_BASED + overwriteMode: UPGRADE + + namespaces: + - tenant1-argocd + variables: diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy index 1b48725fd..2d32c8a19 100644 --- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy +++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy @@ -189,10 +189,12 @@ class GitopsPlaygroundCli { String configFilePath = cliParams.application.configFile String configMapName = cliParams.application.configMap Boolean contentExamples = cliParams.content.examples + Boolean multiTenancyExamples = cliParams.content.multitenancyExamples Map configFile = [:] Map configMap = [:] Map contentExamplesFile = [:] + Map multiTenancyContentExamplesFile = [:] if (configFilePath) { log.debug("Reading config file ${configFilePath}") @@ -205,14 +207,20 @@ class GitopsPlaygroundCli { configMap = validateConfig(configValues) } - if(contentExamples) { + if (contentExamples) { String contentExamplesConfigPath = "examples/example-apps-via-content-loader/config.yaml" log.debug("Adding example-apps-via-content-loader configuration from '${contentExamplesConfigPath}'") contentExamplesFile = validateConfig(new File(contentExamplesConfigPath).text) } + if (multiTenancyExamples) { + String multiTenancyContentExamplesConfigPath = "examples/init-multi-tenancy/managementConfig.yaml" + log.debug("Adding multi tenancy example-apps config loader from '${multiTenancyContentExamplesConfigPath}'") + multiTenancyContentExamplesFile = validateConfig(new File(multiTenancyContentExamplesConfigPath).text) + } + // Last one takes precedence - def configPrecedence = [configMap, configFile, contentExamplesFile] + def configPrecedence = [configMap, configFile, contentExamplesFile, multiTenancyContentExamplesFile] Map mergedConfigs = [:] configPrecedence.each { deepMerge(it, mergedConfigs) diff --git a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy index 53e69110d..d1060ec4f 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/Config.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/Config.groovy @@ -88,6 +88,10 @@ class Config { @JsonPropertyDescription(CONTENT_EXAMPLES_DESCRIPTION) Boolean examples = false + @Option(names = ['--multi-tenancy-examples'], description = CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION) + @JsonPropertyDescription(CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION) + Boolean multitenancyExamples = false + @JsonPropertyDescription(CONTENT_NAMESPACES_DESCRIPTION) List namespaces = [] @@ -399,6 +403,10 @@ class Config { @JsonPropertyDescription(NETPOLS_DESCRIPTION) Boolean netpols = false + @Option(names = ['--cluster-admin'], description = CLUSTER_ADMIN_DESCRIPTION) + @JsonPropertyDescription(CLUSTER_ADMIN_DESCRIPTION) + Boolean clusterAdmin = false + static class NamespaceSchema { LinkedHashSet dedicatedNamespaces = new LinkedHashSet<>() LinkedHashSet tenantNamespaces = new LinkedHashSet<>() diff --git a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy index 834c1055d..d319da958 100644 --- a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy +++ b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy @@ -29,6 +29,8 @@ interface ConfigConstants { // ContentLoader String CONTENT_EXAMPLES_DESCRIPTION = 'Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project' + String CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION = "Deploy multi tenancy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project" + String CONTENT_NAMESPACES_DESCRIPTION = 'Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging' String CONTENT_REPO_DESCRIPTION = "ContentLoader repos to push into target environment" String CONTENT_REPO_URL_DESCRIPTION = "URL of the content repo. Mandatory for each type." @@ -88,6 +90,7 @@ interface ConfigConstants { String NAMESPACE_ISOLATION_DESCRIPTION = 'Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions.' String MIRROR_REPOS_DESCRIPTION = 'Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments.' String NETPOLS_DESCRIPTION = 'Sets Network Policies' + String CLUSTER_ADMIN_DESCRIPTION = 'Binds ArgoCD controllers to cluster-admin ClusterRole' String OPENSHIFT_DESCRIPTION = 'When set, openshift specific resources and configurations are applied' // group metrics diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy index 58dc1cf81..75ac53c4d 100644 --- a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy +++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy @@ -328,6 +328,20 @@ class ArgoCD extends Feature { .withSubfolder(OPERATOR_RBAC_PATH) .generate() } + + if(config.application.clusterAdmin) { + new RbacDefinition(Role.Variant.CLUSTER_ADMIN) + .withName("argocd-cluster-admin") + .withNamespace(namespace) + .withServiceAccountsFrom( + namespace, + ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"] + ) + .withConfig(config) + .withRepo(argocdRepoInitializationAction.repo) + .withSubfolder(OPERATOR_RBAC_PATH) + .generate() + } } } diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy index adbba381d..10ab03fe6 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy @@ -63,19 +63,37 @@ class RbacDefinition { throw new IllegalStateException("SCMM repo must be set using withRepo() before calling generate()") } - def role = new Role(name, namespace, variant, config) - def binding = new RoleBinding(name, namespace, name, serviceAccounts) - log.trace("Generating RBAC for name='${name}', namespace='${namespace}', subfolder='${subfolder}'") - def outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile() + File outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile() outputDir.mkdirs() + generateRole(outputDir) + + generateRoleBinding(outputDir) + } + + private void generateRole(File outputDir) { + if(variant == Role.Variant.CLUSTER_ADMIN) { + log.trace("Skipping creation of ClusterRole cluster-admin") + return + } + + def role = new Role(name, namespace, variant, config) + templater.template( role.getTemplateFile(), role.getOutputFile(outputDir), role.toTemplateParams() ) + } + + private void generateRoleBinding(File outputDir) { + String roleName = name + if(variant == Role.Variant.CLUSTER_ADMIN) { + roleName = "cluster-admin" + } + def binding = new RoleBinding(name, namespace, roleName, serviceAccounts) templater.template( binding.getTemplateFile(), diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy index 150d29ab1..9e31cf129 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy @@ -21,7 +21,8 @@ class Role { } enum Variant { - ARGOCD("templates/kubernetes/rbac/argocd-role.ftl.yaml") + ARGOCD("templates/kubernetes/rbac/argocd-role.ftl.yaml"), + CLUSTER_ADMIN("") final String templatePath @@ -39,10 +40,16 @@ class Role { } File getTemplateFile() { + if(variant == Variant.CLUSTER_ADMIN) { + throw new IllegalStateException("cluster-admin role shall not be created") + } return new File(variant.getTemplatePath()) } File getOutputFile(File outputDir) { + if(variant == Variant.CLUSTER_ADMIN) { + throw new IllegalStateException("cluster-admin role shall not be created") + } String filename = "role-${name}-${namespace}.yaml" return new File(outputDir, filename) } diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy index fc5794b88..5b7cd36f9 100644 --- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy +++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy @@ -2,8 +2,10 @@ package com.cloudogu.gitops.kubernetes.rbac class RoleBinding { String name + String kind String namespace String roleName + String roleKind List serviceAccounts RoleBinding(String name, String namespace, String roleName, List serviceAccounts) { @@ -13,16 +15,25 @@ class RoleBinding { if (!serviceAccounts || serviceAccounts.isEmpty()) throw new IllegalArgumentException("At least one service account is required") this.name = name + this.kind = "RoleBinding" this.namespace = namespace this.roleName = roleName + this.roleKind = "Role" this.serviceAccounts = serviceAccounts + + if(roleName == "cluster-admin") { + this.kind = "ClusterRoleBinding" + this.roleKind = "ClusterRole" + } } Map toTemplateParams() { return [ name : name, + kind : kind, namespace : namespace, roleName : roleName, + roleKind : roleKind, serviceAccounts: serviceAccounts.collect { it.toMap() } ] } diff --git a/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy index dd13fe5d3..10f6bc5c8 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/ScmManagerSetupTest.groovy @@ -108,7 +108,6 @@ class ScmManagerSetupTest { assertThat(env['INSTALL_ARGOCD']).isEqualTo('true') assertThat(env['NAME_PREFIX']).isEqualTo('foo-') assertThat(env['INSECURE']).isEqualTo('false') - assertThat(env['CONTENT_EXAMPLES']).isEqualTo('false') assertThat(env['SKIP_PLUGINS']).isEqualTo('true') assertThat(env['SKIP_RESTART']).isEqualTo('true') } diff --git a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy index 0cbe270c1..243b922b3 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy @@ -65,7 +65,12 @@ class ArgoCDTest { useDedicatedInstance: false ], content: [ - examples: true + examples: true, + variables: [ + images: [ + buildImages + [petclinic: 'petclinic-value'] + ] + ] ], features: [ argocd : [ diff --git a/templates/kubernetes/rbac/rolebinding.ftl.yaml b/templates/kubernetes/rbac/rolebinding.ftl.yaml index 78e7d462a..384ecdde7 100644 --- a/templates/kubernetes/rbac/rolebinding.ftl.yaml +++ b/templates/kubernetes/rbac/rolebinding.ftl.yaml @@ -1,5 +1,5 @@ apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding +kind: ${kind} metadata: name: ${name} namespace: ${namespace} @@ -10,6 +10,6 @@ subjects: namespace: ${sa.namespace} roleRef: - kind: Role + kind: ${roleKind} name: ${roleName} apiGroup: rbac.authorization.k8s.io