diff --git a/class/defaults.yml b/class/defaults.yml index 3ec61b8..161039d 100644 --- a/class/defaults.yml +++ b/class/defaults.yml @@ -1,5 +1,11 @@ parameters: openshift4_console: + images: + oc: + registry: quay.io + repository: appuio/oc + tag: v4.15 + namespace: openshift-console namespace_annotations: openshift.io/node-selector: '' diff --git a/component/main.jsonnet b/component/main.jsonnet index 3a7b223..b627f31 100644 --- a/component/main.jsonnet +++ b/component/main.jsonnet @@ -172,48 +172,6 @@ local consoleRoutePatch = local tls = import 'tls.libsonnet'; -// If we deploy cert-manager Certificates, we annotate namespace -// openshift-config with the `kyvernoAnnotation` defined in `tls.libsonnet` -// through a ResourceLocker patch. This triggers the the Kyverno policy to -// copy the cert-manager TLS secrets into namespace openshift-config. -// -// We add the ResourceLocker patch to ArgoCD sync-wave 5, so it's guaranteed -// to be applied in the cluster after the certificate has been issued and -// before the custom openshift console route config is applied. -// -// NOTE: Due to the current implementation of the resource locker component -// library this prevents other components from also providing ResourceLocker -// patches for the `openshift-config` namespace. -local openshiftConfigNsAnnotationPatch = - local needsPatch = hostname != null && std.length(tls.certs) > 0; - if needsPatch then - local target = kube.Namespace('openshift-config'); - local patch = { - metadata: { - annotations: tls.kyvernoAnnotation, - }, - }; - [ - if obj.kind == 'Patch' then - obj { - metadata+: { - annotations+: { - // Annotate namespace openshift-config before we configure the - // route certificate, see patch above - 'argocd.argoproj.io/sync-wave': '5', - }, - }, - } - else - obj - for obj in - po.Patch( - target, - patch, - patchstrategy='application/merge-patch+json' - ) - ]; - { '00_namespace': kube.Namespace(params.namespace) { metadata+: { @@ -247,6 +205,4 @@ local openshiftConfigNsAnnotationPatch = faviconRoute, [if consoleRoutePatch != null then '20_ingress_config_patch']: consoleRoutePatch, - [if openshiftConfigNsAnnotationPatch != null then '20_openshift_config_ns_annotation_patch']: - openshiftConfigNsAnnotationPatch, } diff --git a/component/scripts/reconcile-console-secret.sh b/component/scripts/reconcile-console-secret.sh new file mode 100755 index 0000000..df182d9 --- /dev/null +++ b/component/scripts/reconcile-console-secret.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail + +test -n "${SECRET_NAME:-}" || (echo "SECRET_NAME is required" && exit 1) + +source_namespace="openshift-console" +target_namespace="openshift-config" + +# # Wait for the secret to be created before trying to get it. +# # TODO: --for=create is included with OCP 4.17 +# kubectl -n "${source_namespace}" wait secret "${SECRET_NAME}" --for=create --timeout=30m +echo "Waiting for secret ${SECRET_NAME} to be created" +while test -z "$(kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" --ignore-not-found -oname)" ; do + printf "." + sleep 1 +done +printf "\n" + +# When using -w flag kubectl returns the secret once on startup and then again when it changes. +kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" -ojson -w | jq -c --unbuffered | while read -r secret ; do + echo "Syncing secret: $(printf "%s" "$secret" | jq -r '.metadata.name')" + + kubectl -n "$target_namespace" apply --server-side -f <(printf "%s" "$secret" | jq '{"apiVersion": .apiVersion, "kind": .kind, "metadata": {"name": .metadata.name}, "type": .type, "data": .data}') +done diff --git a/component/tls.libsonnet b/component/tls.libsonnet index 97ef008..372295b 100644 --- a/component/tls.libsonnet +++ b/component/tls.libsonnet @@ -2,7 +2,6 @@ local cm = import 'lib/cert-manager.libsonnet'; local com = import 'lib/commodore.libjsonnet'; local kap = import 'lib/kapitan.libjsonnet'; local kube = import 'lib/kube.libjsonnet'; -local kyverno = import 'lib/kyverno.libsonnet'; local inv = kap.inventory(); local params = inv.parameters.openshift4_console; @@ -36,52 +35,141 @@ local secrets = std.filter( ] ); -local kyvernoAnnotation = { - 'syn.tools/openshift4-console': 'secret-target-namespace', -}; - local makeCert(c, cert) = - assert - std.member(inv.applications, 'kyverno') : - 'You need to add component `kyverno` to the cluster to be able to deploy cert-manager Certificate resources for the the openshift web console.'; + local sa = kube.ServiceAccount('openshift4-console-sync-' + c) { + metadata+: { + namespace: params.namespace, + }, + }; + local sourceNsRole = kube.Role('openshift4-console-sync-' + c) { + metadata+: { + namespace: params.namespace, + }, + rules: [ + { + apiGroups: [ '' ], + resources: [ 'secrets' ], + verbs: [ 'get', 'list', 'watch' ], + }, + ], + }; + local targetNsRole = kube.Role('openshift4-console-sync-' + c) { + metadata+: { + namespace: 'openshift-config', + }, + rules: [ + { + apiGroups: [ '' ], + resources: [ 'secrets' ], + verbs: [ 'get', 'update', 'patch' ], + }, + ], + }; + [ cm.cert(c) { metadata+: { // Certificate must be deployed in the same namespace as the web // console, otherwise OpenShift won't admit the HTTP01 solver route. - // We copy the resulting secret to namespace 'openshift-config' with - // Kyverno, see below. + // We copy the resulting secret to namespace 'openshift-config', see below. namespace: params.namespace, }, spec+: { secretName: '%s' % c, }, } + com.makeMergeable(cert), - kyverno.ClusterPolicy('openshift4-console-sync-' + c) { - spec: { - rules: [ - { - name: 'Sync "%s" certificate secret to openshift-config' % c, - match: { - resources: { - kinds: [ 'Namespace' ], - // We copy the created TLS secret into all namespaces which - // have the annotation specified in `kyvernoAnnotation`. - annotations: kyvernoAnnotation, - }, + kube.ConfigMap('openshift4-console-sync-' + c) { + metadata+: { + namespace: params.namespace, + }, + data: { + 'reconcile-console-secret.sh': (importstr 'scripts/reconcile-console-secret.sh'), + }, + }, + sa, + sourceNsRole, + targetNsRole, + kube.RoleBinding('openshift4-console-sync-' + c) { + metadata+: { + namespace: sourceNsRole.metadata.namespace, + }, + subjects_: [ sa ], + roleRef_: sourceNsRole, + }, + kube.RoleBinding('openshift4-console-sync-' + c) { + metadata+: { + namespace: targetNsRole.metadata.namespace, + }, + subjects_: [ sa ], + roleRef_: targetNsRole, + }, + kube.Deployment('openshift4-console-sync-' + c) { + metadata+: { + namespace: params.namespace, + }, + spec+: { + strategy: { + type: 'Recreate', + }, + replicas: 1, + selector: { + matchLabels: { + app: 'openshift4-console-sync-' + c, + }, + }, + template+: { + metadata: { + labels: { + app: 'openshift4-console-sync-' + c, }, - generate: { - kind: 'Secret', - name: c, - namespace: '{{request.object.metadata.name}}', - synchronize: true, - clone: { - namespace: params.namespace, - name: c, + }, + spec+: { + serviceAccountName: 'openshift4-console-sync-' + c, + containers: [ + { + name: 'sync', + image: '%(registry)s/%(repository)s:%(tag)s' % params.images.oc, + workingDir: '/export', + env: [ + { + name: 'SECRET_NAME', + value: c, + }, + { + name: 'HOME', + value: '/export', + }, + ], + command: [ + '/scripts/reconcile-console-secret.sh', + ], + volumeMounts: [ + { + name: 'export', + mountPath: '/export', + }, + { + name: 'scripts', + mountPath: '/scripts', + }, + ], }, - }, + ], + volumes: [ + { + name: 'scripts', + configMap: { + name: 'openshift4-console-sync-' + c, + defaultMode: 365, // 365 = 0555 + }, + }, + { + name: 'export', + emptyDir: {}, + }, + ], }, - ], + }, }, }, ]; @@ -104,5 +192,4 @@ local certs = { certs: certs, secrets: secrets, - kyvernoAnnotation: kyvernoAnnotation, } diff --git a/docs/modules/ROOT/pages/references/parameters.adoc b/docs/modules/ROOT/pages/references/parameters.adoc index b72f4aa..07afc51 100644 --- a/docs/modules/ROOT/pages/references/parameters.adoc +++ b/docs/modules/ROOT/pages/references/parameters.adoc @@ -169,8 +169,7 @@ The dictionary values are then directly directly merged into the mostly empty `C OpenShift won't admit the route for the HTTP01 solver pod unless the `Certificate` resources are deployed in the same namespace as the web console. This behavior is caused by a security feature in the OpenShift ingress controller operator to not allow malicious actors to abuse hostnames which are already in use in other namespaces. -However, since OpenShift requires that custom TLS secrets for the OpenShift console are stored in namespace `openshift-config`, we deploy a Kyverno policy to clone the TLS secret created by cert-manager into namespace `openshift-config` for each `Certificate` resource. -Because of that, the component requires that Kyverno is installed on the cluster via the https://hub.syn.tools/kyverno/[Commodore component `kyverno`], when `Certificate` resources are configured in the hierarchy. +However, since OpenShift requires that custom TLS secrets for the OpenShift console are stored in namespace `openshift-config`, we deploy a script to clone the TLS secret created by cert-manager into namespace `openshift-config` for each `Certificate` resource. == Example: Custom hostname in cluster's app domain diff --git a/tests/custom-route-managed-tls.yml b/tests/custom-route-managed-tls.yml index bdc4e17..67e9a6a 100644 --- a/tests/custom-route-managed-tls.yml +++ b/tests/custom-route-managed-tls.yml @@ -1,5 +1,3 @@ -applications: - - kyverno parameters: kapitan: dependencies: @@ -9,9 +7,6 @@ parameters: - type: https source: https://raw.githubusercontent.com/projectsyn/component-patch-operator/v1.2.0/lib/patch-operator.libsonnet output_path: vendor/lib/patch-operator.libsonnet - - type: https - source: https://raw.githubusercontent.com/projectsyn/component-kyverno/v1.4.0/lib/kyverno.libsonnet - output_path: vendor/lib/kyverno.libsonnet patch_operator: patch_serviceaccount: diff --git a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml index 8dbfcd1..4f03ea5 100644 --- a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml +++ b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml @@ -14,27 +14,166 @@ spec: name: letsencrypt-staging secretName: console-cluster-example-org-tls --- -apiVersion: kyverno.io/v1 -kind: ClusterPolicy +apiVersion: v1 +data: + reconcile-console-secret.sh: | + #!/bin/bash + set -euo pipefail + + test -n "${SECRET_NAME:-}" || (echo "SECRET_NAME is required" && exit 1) + + source_namespace="openshift-console" + target_namespace="openshift-config" + + # # Wait for the secret to be created before trying to get it. + # # TODO: --for=create is included with OCP 4.17 + # kubectl -n "${source_namespace}" wait secret "${SECRET_NAME}" --for=create --timeout=30m + echo "Waiting for secret ${SECRET_NAME} to be created" + while test -z "$(kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" --ignore-not-found -oname)" ; do + printf "." + sleep 1 + done + printf "\n" + + # When using -w flag kubectl returns the secret once on startup and then again when it changes. + kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" -ojson -w | jq -c --unbuffered | while read -r secret ; do + echo "Syncing secret: $(printf "%s" "$secret" | jq -r '.metadata.name')" + + kubectl -n "$target_namespace" apply --server-side -f <(printf "%s" "$secret" | jq '{"apiVersion": .apiVersion, "kind": .kind, "metadata": {"name": .metadata.name}, "type": .type, "data": .data}') + done +kind: ConfigMap metadata: annotations: {} labels: name: openshift4-console-sync-console-cluster-example-org-tls name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +rules: + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-config +rules: + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - update + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: openshift4-console-sync-console-cluster-example-org-tls +subjects: + - kind: ServiceAccount + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-config +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: openshift4-console-sync-console-cluster-example-org-tls +subjects: + - kind: ServiceAccount + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: {} + labels: + name: openshift4-console-sync-console-cluster-example-org-tls + name: openshift4-console-sync-console-cluster-example-org-tls + namespace: openshift-console spec: - rules: - - generate: - clone: - name: console-cluster-example-org-tls - namespace: openshift-console - kind: Secret - name: console-cluster-example-org-tls - namespace: '{{request.object.metadata.name}}' - synchronize: true - match: - resources: - annotations: - syn.tools/openshift4-console: secret-target-namespace - kinds: - - Namespace - name: Sync "console-cluster-example-org-tls" certificate secret to openshift-config + minReadySeconds: 30 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: openshift4-console-sync-console-cluster-example-org-tls + strategy: + type: Recreate + template: + metadata: + labels: + app: openshift4-console-sync-console-cluster-example-org-tls + spec: + containers: + - command: + - /scripts/reconcile-console-secret.sh + env: + - name: SECRET_NAME + value: console-cluster-example-org-tls + - name: HOME + value: /export + image: quay.io/appuio/oc:v4.15 + name: sync + volumeMounts: + - mountPath: /export + name: export + - mountPath: /scripts + name: scripts + workingDir: /export + imagePullSecrets: [] + initContainers: [] + serviceAccountName: openshift4-console-sync-console-cluster-example-org-tls + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 365 + name: openshift4-console-sync-console-cluster-example-org-tls + name: scripts + - emptyDir: {} + name: export diff --git a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml deleted file mode 100644 index 46ef949..0000000 --- a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: redhatcop.redhat.io/v1alpha1 -kind: Patch -metadata: - annotations: - argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true - argocd.argoproj.io/sync-wave: '5' - labels: - name: namespace-openshift-config-2c8343f13594d63 - name: namespace-openshift-config-2c8343f13594d63 - namespace: syn-patch-operator -spec: - patches: - namespace-openshift-config-2c8343f13594d63-patch: - patchTemplate: |- - "metadata": - "annotations": - "syn.tools/openshift4-console": "secret-target-namespace" - patchType: application/merge-patch+json - targetObjectRef: - apiVersion: v1 - kind: Namespace - name: openshift-config - serviceAccountRef: - name: patch-sa