diff --git a/argocd/argocd/operator/argocd.ftl.yaml b/argocd/argocd/operator/argocd.ftl.yaml index ecd021394..721cafd0e 100644 --- a/argocd/argocd/operator/argocd.ftl.yaml +++ b/argocd/argocd/operator/argocd.ftl.yaml @@ -103,6 +103,7 @@ spec: server: + insecure: ${isInsecure?c} resources: limits: cpu: "500m" @@ -120,7 +121,42 @@ spec: route: enabled: ${isOpenshift?c} host: "${argocd.host}" + # Enable ingress only if we are not on OpenShift and insecure mode is NOT enabled. + # Note: When insecure mode is enabled, forced HTTP redirect to HTTPS cannot be disabled here(likely due to a bug), + # so we cannot use this ingress for insecure mode. For insecure mode we use a separate file (ingress.ftl.yaml). + ingress: + enabled: ${((!isOpenshift) && (!isInsecure))?c} + initialRepositories: | + - name: argocd + url: ${scmm.repoUrl}argocd/argocd<#if scmm.provider == "gitlab">.git + - name: example-apps + url: ${scmm.repoUrl}argocd/example-apps<#if scmm.provider == "gitlab">.git + - name: cluster-resources + url: ${scmm.repoUrl}argocd/cluster-resources<#if scmm.provider == "gitlab">.git + - name: nginx-helm-jenkins + url: ${scmm.repoUrl}argocd/nginx-helm-jenkins<#if scmm.provider == "gitlab">.git + - name: nginx-helm-umbrella + url: ${scmm.repoUrl}argocd/nginx-helm-umbrella<#if scmm.provider == "gitlab">.git + - name: bitnami + type: helm + url: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + - name: prometheus-community + type: helm + url: https://prometheus-community.github.io/helm-charts + - name: codecentric + type: helm + url: https://codecentric.github.io/helm-charts + - name: ingress-nginx + type: helm + url: https://kubernetes.github.io/ingress-nginx resourceInclusions: | + - apiGroups: + - "batch" + kinds: + - "Job" + clusters: + - "https://kubernetes.default.svc" + - "${argocd.resourceInclusionsCluster}" - apiGroups: - "" kinds: diff --git a/argocd/argocd/operator/ingress.ftl.yaml b/argocd/argocd/operator/ingress.ftl.yaml new file mode 100644 index 000000000..2ddb9d3cb --- /dev/null +++ b/argocd/argocd/operator/ingress.ftl.yaml @@ -0,0 +1,25 @@ + <#if (!isOpenshift && isInsecure)> + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: argocd + namespace: "${namePrefix}argocd" + labels: + app: argocd-server + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + spec: + rules: + - host: "${argocd.host}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: argocd-server + port: + number: 80 + + diff --git a/argocd/argocd/operator/rbac/example-apps-production.ftl.yaml b/argocd/argocd/operator/rbac/example-apps-production.ftl.yaml index 4711830c8..8f70bd579 100644 --- a/argocd/argocd/operator/rbac/example-apps-production.ftl.yaml +++ b/argocd/argocd/operator/rbac/example-apps-production.ftl.yaml @@ -4,6 +4,18 @@ metadata: namespace: "${namePrefix}example-apps-production" name: argocd rules: + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "route.openshift.io" resources: @@ -170,13 +182,13 @@ metadata: subjects: - kind: ServiceAccount name: argocd-argocd-server - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-argocd-application-controller - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-applicationset-controller - namespace: argocd + namespace: "${namePrefix}argocd" roleRef: kind: Role name: argocd diff --git a/argocd/argocd/operator/rbac/example-apps-staging.ftl.yaml b/argocd/argocd/operator/rbac/example-apps-staging.ftl.yaml index 02b55934c..0690aceb7 100644 --- a/argocd/argocd/operator/rbac/example-apps-staging.ftl.yaml +++ b/argocd/argocd/operator/rbac/example-apps-staging.ftl.yaml @@ -4,6 +4,18 @@ metadata: namespace: "${namePrefix}example-apps-staging" name: argocd rules: + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "route.openshift.io" resources: @@ -170,13 +182,13 @@ metadata: subjects: - kind: ServiceAccount name: argocd-argocd-server - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-argocd-application-controller - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-applicationset-controller - namespace: argocd + namespace: "${namePrefix}argocd" roleRef: kind: Role name: argocd diff --git a/argocd/argocd/operator/rbac/ingress-nginx.ftl.yaml b/argocd/argocd/operator/rbac/ingress-nginx.ftl.yaml index e2dc3f442..65927de45 100644 --- a/argocd/argocd/operator/rbac/ingress-nginx.ftl.yaml +++ b/argocd/argocd/operator/rbac/ingress-nginx.ftl.yaml @@ -4,6 +4,18 @@ metadata: namespace: "${namePrefix}ingress-nginx" name: argocd rules: + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "route.openshift.io" resources: @@ -170,13 +182,13 @@ metadata: subjects: - kind: ServiceAccount name: argocd-argocd-server - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-argocd-application-controller - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-applicationset-controller - namespace: argocd + namespace: "${namePrefix}argocd" roleRef: kind: Role name: argocd diff --git a/argocd/argocd/operator/rbac/monitoring.ftl.yaml b/argocd/argocd/operator/rbac/monitoring.ftl.yaml index 20a5850fc..9453db9f7 100644 --- a/argocd/argocd/operator/rbac/monitoring.ftl.yaml +++ b/argocd/argocd/operator/rbac/monitoring.ftl.yaml @@ -4,6 +4,18 @@ metadata: namespace: "${namePrefix}monitoring" name: argocd rules: + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "route.openshift.io" resources: @@ -170,13 +182,13 @@ metadata: subjects: - kind: ServiceAccount name: argocd-argocd-server - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-argocd-application-controller - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-applicationset-controller - namespace: argocd + namespace: "${namePrefix}argocd" roleRef: kind: Role name: argocd diff --git a/argocd/argocd/operator/rbac/secrets.ftl.yaml b/argocd/argocd/operator/rbac/secrets.ftl.yaml index e99bd265e..12171b809 100644 --- a/argocd/argocd/operator/rbac/secrets.ftl.yaml +++ b/argocd/argocd/operator/rbac/secrets.ftl.yaml @@ -4,6 +4,18 @@ metadata: namespace: "${namePrefix}secrets" name: argocd rules: + - apiGroups: + - "batch" + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "route.openshift.io" resources: @@ -170,13 +182,13 @@ metadata: subjects: - kind: ServiceAccount name: argocd-argocd-server - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-argocd-application-controller - namespace: argocd + namespace: "${namePrefix}argocd" - kind: ServiceAccount name: argocd-applicationset-controller - namespace: argocd + namespace: "${namePrefix}argocd" roleRef: kind: Role name: argocd diff --git a/src/main/groovy/com/cloudogu/gitops/utils/TemplatingEngine.groovy b/src/main/groovy/com/cloudogu/gitops/utils/TemplatingEngine.groovy index d6605e632..45bd20e40 100644 --- a/src/main/groovy/com/cloudogu/gitops/utils/TemplatingEngine.groovy +++ b/src/main/groovy/com/cloudogu/gitops/utils/TemplatingEngine.groovy @@ -18,11 +18,17 @@ class TemplatingEngine { */ File replaceTemplate(File templateFile, Map parameters) { def targetFile = new File(templateFile.toString().replace(".ftl", "")) + def rendered = template(templateFile, parameters) - template(templateFile, targetFile, parameters) + // Only write file if template has non-empty output. + // This avoids creating empty files when the entire template is skipped via <#if>. + if (rendered?.trim()) { + targetFile.text = rendered + } else { + targetFile.delete() + } templateFile.delete() - return targetFile } 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 04952a6c8..f9b60ed4e 100644 --- a/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy +++ b/src/test/groovy/com/cloudogu/gitops/features/argocd/ArgoCDTest.groovy @@ -1364,6 +1364,153 @@ class ArgoCDTest { } } + @Test + void 'Operator config sets server insecure to true when insecure is set'() { + config.application.insecure = true + def argoCD = setupOperatorTest() + + argoCD.install() + + def yaml = parseActualYaml(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH).toString()) + assertThat(yaml['spec']['server']['insecure']).isEqualTo(true) + } + + @Test + void 'Operator config sets server_insecure to false when insecure is not set'() { + def argoCD = setupOperatorTest() + + argoCD.install() + + def yaml = parseActualYaml(Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), ArgoCD.OPERATOR_CONFIG_PATH).toString()) + assertThat(yaml['spec']['server']['insecure']).isEqualTo(false) + } + + @Test + void 'Generates correct ingress yaml with expected host when insecure is true and not on OpenShift'() { + config.application.insecure = true + config.features.argocd.url = "http://argocd.localhost" + def argoCD = setupOperatorTest(openshift: false) + + argoCD.install() + + def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should be generated for insecure mode on non-OpenShift") + .exists() + + def ingressYaml = parseActualYaml(ingressFile.toString()) + + def rules = ingressYaml['spec']['rules'] as List + def host = rules[0]['host'] + assertThat(host) + .as("Ingress host should match configured ArgoCD hostname") + .isEqualTo(new URL(config.features.argocd.url).host) + } + + @Test + void 'Does not generate ingress yaml when insecure is false'() { + config.application.insecure = false + def argoCD = setupOperatorTest(openshift: false) + + argoCD.install() + + def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated when insecure is false") + .doesNotExist() + } + + @Test + void 'Does not generate ingress yaml when running on OpenShift'() { + config.application.insecure = true + def argoCD = setupOperatorTest(openshift: true) + + argoCD.install() + + def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated on OpenShift") + .doesNotExist() + } + + @Test + void 'Does not generate ingress yaml when insecure is false and OpenShift is true'() { + config.application.insecure = false + def argoCD = setupOperatorTest(openshift: true) + + argoCD.install() + + def ingressFile = new File(argocdRepo.getAbsoluteLocalRepoTmpDir(), "operator/ingress.yaml") + assertThat(ingressFile) + .as("Ingress file should not be generated when both flags are false") + .doesNotExist() + } + + @Test + void 'RBAC files use ${namePrefix}argocd as serviceAccount namespace'() { + config.application.namePrefix = "test-" + def argoCD = setupOperatorTest() + + argoCD.install() + + def rbacFiles = [ + "monitoring.yaml", + "secrets.yaml", + "ingress-nginx.yaml", + "example-apps-staging.yaml", + "example-apps-production.yaml" + ] + + rbacFiles.each { file -> + def path = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), "${ArgoCD.OPERATOR_RBAC_PATH}/$file").toString() + def raw = parseActualYaml(path) + // Ensure uniform handling whether YAML has one or multiple documents + // Freemarker parser returns a List for multi-doc YAML, but a Map for single-doc files + def documents = raw instanceof List ? raw : [raw] + + documents.findAll { it['kind'] == 'RoleBinding' }.each { roleBinding -> + def subjects = roleBinding['subjects'] as List + def namespaces = subjects.collect { it['namespace'] }.findAll() + assertThat(namespaces) + .as("Subjects in $file should use templated namespace") + .allMatch { it == "test-argocd" } + } + } + } + + @Test + void 'RBAC files have metadata namespace set with namePrefix'() { + config.application.namePrefix = "test-" + def argoCD = setupOperatorTest() + + argoCD.install() + + def rbacFiles = [ + "monitoring.yaml", + "secrets.yaml", + "ingress-nginx.yaml", + "example-apps-staging.yaml", + "example-apps-production.yaml" + ] + + rbacFiles.each { file -> + def path = Path.of(argocdRepo.getAbsoluteLocalRepoTmpDir(), "${ArgoCD.OPERATOR_RBAC_PATH}/$file").toString() + def raw = parseActualYaml(path) + // Ensure uniform handling whether YAML has one or multiple documents + // Freemarker parser returns a List for multi-doc YAML, but a Map for single-doc files + def documents = raw instanceof List ? raw : [raw] + + documents.each { doc -> + def metadata = (doc as Map)['metadata'] as Map + def ns = metadata['namespace'] as String + + assertThat(ns) + .as("metadata.namespace should be prefixed in $file") + .startsWith("test-") + } + } + } + private ArgoCD setupOperatorTest(Map options = [:]) { config.features.argocd.operator = true config.features.argocd.resourceInclusionsCluster = 'https://192.168.0.1:6443'