diff --git a/hack/kubernetes-oidc-bindings.bats b/hack/kubernetes-oidc-bindings.bats
new file mode 100644
index 000000000..f37273802
--- /dev/null
+++ b/hack/kubernetes-oidc-bindings.bats
@@ -0,0 +1,187 @@
+#!/usr/bin/env bats
+# -----------------------------------------------------------------------------
+# packages/apps/kubernetes: the OIDC bootstrap Job must not carry the users[]
+# list inside its container command.
+#
+# The Job's script reaches the container as one `sh -c` argument, and Linux caps
+# one exec argument at 128 KiB (MAX_ARG_STRLEN). With a ClusterRoleBinding per
+# user written into that script, about 240 users take it past the cap: the
+# container cannot start, the post-install/post-upgrade hook fails, and the
+# tenant cluster's HelmRelease stays unready. The bindings reach the Job through
+# a mounted ConfigMap instead, so the script is the same size for any list.
+#
+# The last three tests execute the rendered script against a fake kubectl, since
+# no assertion on the script's text says what the prune step does with the list:
+# a condition forced either way passes every helm-unittest case.
+#
+# Requires: helm, yq (mikefarah v4+), jq, like hack/admin-kubeconfig-invariant.bats.
+# -----------------------------------------------------------------------------
+
+load test_helper
+
+USERS=300
+
+# render_users
renders the chart in System mode with
+# generated users and leaves one JSON document per object in /rendered.json.
+render_users() {
+ jq -n --argjson n "$2" '
+ {oidc: {users: [range($n) | {email: "user\(.)@example.org", role: (if . % 7 == 0 then "admin" else "view" end)}]}}' \
+ > "$1/users.yaml"
+ helm template bindings packages/apps/kubernetes \
+ --namespace tenant-test \
+ --values packages/apps/kubernetes/tests/values/oidc-system.yaml \
+ --values "$1/users.yaml" \
+ > "$1/rendered.yaml"
+ yq --output-format=json eval-all '.' "$1/rendered.yaml" > "$1/rendered.json"
+}
+
+# render_job renders the chart in CustomConfig mode and unpacks
+# what the bootstrap Job runs and mounts into : script.sh is its `sh -c`
+# script, bindings/ holds the two files of the mounted ConfigMap. -j keeps an
+# empty value an empty file, as the kubelet writes it.
+render_job() {
+ jq -n --argjson users "$2" '
+ {_cluster: {"oidc-enabled": "false"},
+ oidc: {mode: "CustomConfig",
+ customConfig: {config: "apiVersion: apiserver.config.k8s.io/v1beta1\nkind: AuthenticationConfiguration\njwt: []\n"},
+ users: $users}}' > "$1/values.yaml"
+ helm template bindings packages/apps/kubernetes \
+ --namespace tenant-test \
+ --values packages/apps/kubernetes/tests/values/oidc-system.yaml \
+ --values "$1/values.yaml" \
+ > "$1/rendered.yaml"
+ yq --output-format=json eval-all '.' "$1/rendered.yaml" > "$1/rendered.json"
+ jq -s -j '.[] | select(.kind == "Job" and .metadata.name == "bindings-oidc-bootstrap")
+ | .spec.template.spec.containers[0].command[2]' "$1/rendered.json" > "$1/script.sh"
+ mkdir -p "$1/bindings"
+ jq -s -j '.[] | select(.kind == "ConfigMap" and .metadata.name == "bindings-oidc-bindings")
+ | .data["crbs.yaml"]' "$1/rendered.json" > "$1/bindings/crbs.yaml"
+ jq -s -j '.[] | select(.kind == "ConfigMap" and .metadata.name == "bindings-oidc-bindings")
+ | .data["desired.txt"]' "$1/rendered.json" > "$1/bindings/desired.txt"
+}
+
+# fake_kubectl puts a kubectl first on PATH that logs every call and
+# answers the script's list of existing bindings with .
+fake_kubectl() {
+ mkdir -p "$1/bin"
+ printf '%s' "$2" > "$1/existing"
+ cat > "$1/bin/kubectl" <<'KEOF'
+#!/bin/sh
+echo "$*" >> "$KLOG"
+case "$*" in
+ *"get clusterrolebindings"*) cat "$EXISTING" ;;
+esac
+KEOF
+ chmod +x "$1/bin/kubectl"
+}
+
+# run_job runs the unpacked script as the container does and leaves its
+# status in $rc and its output in /out. Run by bash, which has the pipefail
+# the script sets and which is what the unit lane runs under; the container's own
+# shell is not exercised.
+run_job() {
+ : > "$1/kubectl.log"
+ echo kubeconfig > "$1/kubeconfig"
+ rc=0
+ env PATH="$1/bin:$PATH" KLOG="$1/kubectl.log" EXISTING="$1/existing" HOME="$1" \
+ KUBECONFIG_TENANT="$1/kubeconfig" RELEASE=bindings RELEASE_NAMESPACE=tenant-test \
+ OIDC_MODE=CustomConfig OIDC_BINDINGS_DIR="$1/bindings" \
+ bash "$1/script.sh" > "$1/out" 2>&1 || rc=$?
+}
+
+@test "the OIDC bootstrap Job script stays under the exec argument cap and carries no user" {
+ dir=$(mktemp -d)
+ render_users "$dir" "$USERS"
+ jq -s -r '.[] | select(.kind == "Job" and .metadata.name == "bindings-oidc-bootstrap")
+ | .spec.template.spec.containers[0].command[2]' "$dir/rendered.json" > "$dir/script"
+ size=$(wc -c < "$dir/script")
+ [ "$size" -gt 0 ]
+ [ "$size" -lt 131072 ]
+ if grep -q -E 'user[0-9]+@example\.org' "$dir/script"; then
+ echo "the Job script still carries users[] entries" >&2
+ false
+ fi
+ rm -rf "$dir"
+}
+
+@test "every user keeps one ClusterRoleBinding with its old name and role in the bindings ConfigMap" {
+ dir=$(mktemp -d)
+ render_users "$dir" "$USERS"
+ jq -s -r '.[] | select(.kind == "ConfigMap" and .metadata.name == "bindings-oidc-bindings")
+ | .data["crbs.yaml"]' "$dir/rendered.json" > "$dir/crbs.yaml"
+ jq -s -r '.[] | select(.kind == "ConfigMap" and .metadata.name == "bindings-oidc-bindings")
+ | .data["desired.txt"]' "$dir/rendered.json" > "$dir/desired.txt"
+ [ -s "$dir/crbs.yaml" ]
+ yq --output-format=json eval-all '.' "$dir/crbs.yaml" | jq -s '.' > "$dir/crbs.json"
+
+ [ "$(jq 'length' "$dir/crbs.json")" -eq "$USERS" ]
+ [ "$(jq -r '[.[].subjects[0].name] | unique | length' "$dir/crbs.json")" -eq "$USERS" ]
+ [ "$(jq -r '[.[] | select(.roleRef.name == "cluster-admin")] | length' "$dir/crbs.json")" -eq $(( (USERS + 6) / 7 )) ]
+
+ # The name is what the prune step and every binding already in a tenant cluster
+ # go by, so it must not move: cozystack-oidc- plus the first 16 hex digits of
+ # sha256("-").
+ for i in 0 1 6 7; do
+ email="user$i@example.org"
+ name="cozystack-oidc-$(printf '%s' "bindings-$email" | sha256sum | cut -c1-16)"
+ [ "$(jq -r --arg e "$email" '.[] | select(.subjects[0].name == $e) | .metadata.name' "$dir/crbs.json")" = "$name" ]
+ grep -q -x -F "$name" "$dir/desired.txt"
+ done
+
+ # The list the prune step compares against is exactly the applied names.
+ [ "$(jq -r '.[].metadata.name' "$dir/crbs.json" | sort)" = "$(grep . "$dir/desired.txt" | sort)" ]
+ rm -rf "$dir"
+}
+
+@test "the Job applies the mounted bindings and deletes only the bindings no user has" {
+ dir=$(mktemp -d)
+ render_job "$dir" '[{"email":"alice@example.org","role":"admin"},{"email":"bob@example.org","role":"view"}]'
+ alice="cozystack-oidc-$(printf '%s' 'bindings-alice@example.org' | sha256sum | cut -c1-16)"
+ bob="cozystack-oidc-$(printf '%s' 'bindings-bob@example.org' | sha256sum | cut -c1-16)"
+ fake_kubectl "$dir" "$alice cozystack-oidc-000000000000dead $bob cozystack-oidc-000000000000beef"
+ run_job "$dir"
+ if [ "$rc" -ne 0 ]; then
+ cat "$dir/out"
+ false
+ fi
+ grep -q -F -- "apply --filename $dir/bindings/crbs.yaml" "$dir/kubectl.log"
+ grep -q -F -- "delete clusterrolebinding cozystack-oidc-000000000000dead --ignore-not-found" "$dir/kubectl.log"
+ grep -q -F -- "delete clusterrolebinding cozystack-oidc-000000000000beef --ignore-not-found" "$dir/kubectl.log"
+ if grep -F -e "delete clusterrolebinding $alice" -e "delete clusterrolebinding $bob" "$dir/kubectl.log"; then
+ echo "the Job deleted a binding a user still has" >&2
+ false
+ fi
+ [ "$(grep -c 'delete clusterrolebinding' "$dir/kubectl.log")" -eq 2 ]
+ rm -rf "$dir"
+}
+
+@test "a release with no users applies nothing and deletes every binding it left behind" {
+ dir=$(mktemp -d)
+ render_job "$dir" '[]'
+ fake_kubectl "$dir" "cozystack-oidc-000000000000dead cozystack-oidc-000000000000beef"
+ run_job "$dir"
+ if [ "$rc" -ne 0 ]; then
+ cat "$dir/out"
+ false
+ fi
+ if grep -q -F -- ' apply ' "$dir/kubectl.log"; then
+ echo "the Job ran kubectl apply with no bindings to apply" >&2
+ false
+ fi
+ [ "$(grep -c 'delete clusterrolebinding' "$dir/kubectl.log")" -eq 2 ]
+ rm -rf "$dir"
+}
+
+@test "the Job stops before deleting anything when the desired list is not mounted" {
+ dir=$(mktemp -d)
+ render_job "$dir" '[{"email":"alice@example.org","role":"admin"}]'
+ fake_kubectl "$dir" "cozystack-oidc-000000000000dead cozystack-oidc-000000000000beef"
+ rm "$dir/bindings/desired.txt"
+ run_job "$dir"
+ [ "$rc" -ne 0 ]
+ if grep -q -E 'delete clusterrolebinding| apply ' "$dir/kubectl.log"; then
+ cat "$dir/kubectl.log"
+ false
+ fi
+ rm -rf "$dir"
+}
diff --git a/packages/apps/kubernetes/templates/oidc-rbac-bindings.yaml b/packages/apps/kubernetes/templates/oidc-rbac-bindings.yaml
new file mode 100644
index 000000000..a8517da4f
--- /dev/null
+++ b/packages/apps/kubernetes/templates/oidc-rbac-bindings.yaml
@@ -0,0 +1,42 @@
+{{- /*
+ The ClusterRoleBindings the OIDC bootstrap Job (oidc-rbac-job.yaml) applies
+ inside the tenant cluster, one per `oidc.users` entry, and the list of their
+ names it prunes against. They travel in a ConfigMap the Job mounts and not as
+ text inside its script: the script reaches the container as a single `sh -c`
+ argument, Linux caps one argument at 128 KiB, and a binding per user crosses
+ that cap at about 240 users, after which the container cannot start.
+
+ A release-owned object, not a hook resource, so it exists before the
+ post-install and post-upgrade hooks run. It is rendered only where users are
+ bound; at `mode: None` the Job takes its early branch and the volume is
+ optional.
+*/}}
+{{- if ne .Values.oidc.mode "None" }}
+{{- $desired := "" }}
+{{- $bindings := "" }}
+{{- range .Values.oidc.users }}
+{{- $name := printf "cozystack-oidc-%s" (printf "%s-%s" $.Release.Name .email | sha256sum | trunc 16) }}
+{{- $role := "view" }}
+{{- if eq .role "admin" }}
+{{- $role = "cluster-admin" }}
+{{- end }}
+{{- $desired = printf "%s%s\n" $desired $name }}
+{{- $binding := dict
+ "apiVersion" "rbac.authorization.k8s.io/v1"
+ "kind" "ClusterRoleBinding"
+ "metadata" (dict "name" $name "labels" (dict "app.kubernetes.io/managed-by" "cozystack-oidc" "app.kubernetes.io/instance" $.Release.Name))
+ "roleRef" (dict "apiGroup" "rbac.authorization.k8s.io" "kind" "ClusterRole" "name" $role)
+ "subjects" (list (dict "kind" "User" "apiGroup" "rbac.authorization.k8s.io" "name" .email)) }}
+{{- $bindings = printf "%s---\n%s\n" $bindings (toYaml $binding) }}
+{{- end }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Release.Name }}-oidc-bindings
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "kubernetes.labels" . | nindent 4 }}
+data:
+ {{- dict "crbs.yaml" $bindings "desired.txt" $desired | toYaml | nindent 2 }}
+{{- end }}
diff --git a/packages/apps/kubernetes/templates/oidc-rbac-job.yaml b/packages/apps/kubernetes/templates/oidc-rbac-job.yaml
index 8672ce124..8556d092b 100644
--- a/packages/apps/kubernetes/templates/oidc-rbac-job.yaml
+++ b/packages/apps/kubernetes/templates/oidc-rbac-job.yaml
@@ -25,7 +25,9 @@
- `mode: System | CustomConfig`:
- Apply one ClusterRoleBinding per `users[]` entry inside the tenant
cluster (`admin` → `cluster-admin`, `view` → `view`, subject = the
- literal email matched against the `email` claim).
+ literal email matched against the `email` claim). The bindings are
+ rendered by oidc-rbac-bindings.yaml into a ConfigMap this Job mounts,
+ not into the script below.
- Prune stale CRBs labelled by this release.
- `mode: System`: patch the pre-rendered `-oidc-kubeconfig`
Secret with a real kubeconfig wired to `kubectl oidc-login`
@@ -168,6 +170,8 @@ spec:
value: {{ $issuer | quote }}
- name: OIDC_SERVER_URL
value: {{ printf "https://%s:443" $externalHost | quote }}
+ - name: OIDC_BINDINGS_DIR
+ value: /etc/oidc-bindings
resources:
requests:
cpu: 10m
@@ -254,46 +258,32 @@ spec:
sleep 5
done
+ # A missing list must stop the Job here: grep on a file that is not
+ # there exits 2, `! grep` is then true, and the prune below would
+ # delete every binding of the release.
+ BINDINGS=$OIDC_BINDINGS_DIR
+ if [ ! -f "$BINDINGS/desired.txt" ]; then
+ echo "$BINDINGS/desired.txt is not mounted (ConfigMap $RELEASE-oidc-bindings); refusing to prune against a list that is not there" >&2
+ exit 1
+ fi
+
echo "Applying ${OIDC_MODE} ClusterRoleBindings to the tenant cluster..."
- DESIRED_FILE=$(mktemp)
- {{- range .Values.oidc.users }}
- {{- $hash := printf "%s-%s" $.Release.Name .email | sha256sum | trunc 16 }}
- {{- $crbName := printf "cozystack-oidc-%s" $hash }}
- {{- $role := "view" }}
- {{- if eq .role "admin" }}
- {{- $role = "cluster-admin" }}
- {{- end }}
- echo "{{ $crbName }}" >> "$DESIRED_FILE"
- cat <<'EOF' | kubectl --kubeconfig "$KUBECONFIG_TENANT" apply --filename -
- apiVersion: rbac.authorization.k8s.io/v1
- kind: ClusterRoleBinding
- metadata:
- name: {{ $crbName }}
- labels:
- app.kubernetes.io/managed-by: cozystack-oidc
- app.kubernetes.io/instance: {{ $.Release.Name }}
- roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: {{ $role }}
- subjects:
- - kind: User
- apiGroup: rbac.authorization.k8s.io
- name: {{ .email | quote }}
- EOF
- {{- end }}
+ # kubectl apply fails on a file with no objects, and a release with
+ # no users has none.
+ if [ -s "$BINDINGS/crbs.yaml" ]; then
+ kubectl --kubeconfig "$KUBECONFIG_TENANT" apply --filename "$BINDINGS/crbs.yaml"
+ fi
echo "Pruning stale ClusterRoleBindings labelled by this release..."
EXISTING=$(kubectl --kubeconfig "$KUBECONFIG_TENANT" get clusterrolebindings \
--selector=app.kubernetes.io/managed-by=cozystack-oidc,app.kubernetes.io/instance=$RELEASE \
--output=jsonpath='{.items[*].metadata.name}')
for crb in $EXISTING; do
- if ! grep -x -q -F "$crb" "$DESIRED_FILE"; then
+ if ! grep -x -q -F "$crb" "$BINDINGS/desired.txt"; then
echo " removing $crb"
kubectl --kubeconfig "$KUBECONFIG_TENANT" delete clusterrolebinding "$crb" --ignore-not-found
fi
done
- rm -f "$DESIRED_FILE"
if [ "$OIDC_MODE" = "System" ]; then
echo "Patching the OIDC kubeconfig Secret on the management cluster..."
@@ -349,6 +339,9 @@ spec:
- name: kubeconfig
mountPath: /etc/kubernetes/kubeconfig
readOnly: true
+ - name: oidc-bindings
+ mountPath: /etc/oidc-bindings
+ readOnly: true
- name: tmp
mountPath: /tmp
volumes:
@@ -356,6 +349,10 @@ spec:
secret:
secretName: {{ .Release.Name }}-admin-kubeconfig
optional: true
+ - name: oidc-bindings
+ configMap:
+ name: {{ .Release.Name }}-oidc-bindings
+ optional: true
- name: tmp
emptyDir: {}
{{- /*
diff --git a/packages/apps/kubernetes/tests/oidc_test.yaml b/packages/apps/kubernetes/tests/oidc_test.yaml
index 0623063e7..8511e392c 100644
--- a/packages/apps/kubernetes/tests/oidc_test.yaml
+++ b/packages/apps/kubernetes/tests/oidc_test.yaml
@@ -2561,7 +2561,73 @@ tests:
value: "true"
documentIndex: 3
- - it: bootstrap Job command builds a CRB for each users[] entry
+ - it: the bindings ConfigMap holds a CRB for each users[] entry
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-bindings.yaml
+ set:
+ _cluster.oidc-enabled: "true"
+ _cluster.root-host: example.org
+ oidc.mode: System
+ oidc.users:
+ - email: alice@example.org
+ role: admin
+ - email: bob@example.org
+ role: view
+ asserts:
+ - isKind:
+ of: ConfigMap
+ - equal:
+ path: metadata.name
+ value: kubernetes-app-oidc-bindings
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'name: alice@example.org'
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'name: bob@example.org'
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'app.kubernetes.io/managed-by: cozystack-oidc'
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'app.kubernetes.io/instance: kubernetes-app'
+
+ - it: the binding names and roles stay what the Job script used to render
+ # The prune step and every binding already in a tenant cluster go by these
+ # names: cozystack-oidc- plus the first 16 hex digits of
+ # sha256("-"). A different name leaves the old binding
+ # behind next to the new one.
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-bindings.yaml
+ set:
+ _cluster.oidc-enabled: "true"
+ _cluster.root-host: example.org
+ oidc.mode: System
+ oidc.users:
+ - email: alice@example.org
+ role: admin
+ - email: bob@example.org
+ role: view
+ asserts:
+ - equal:
+ path: data["desired.txt"]
+ value: "cozystack-oidc-0001a28136ca1a02\ncozystack-oidc-ec8dfb1cbc75e699\n"
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'name: cozystack-oidc-0001a28136ca1a02\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: cluster-admin\nsubjects:\n- apiGroup: rbac.authorization.k8s.io\n kind: User\n name: alice@example.org\n'
+ - matchRegex:
+ path: data["crbs.yaml"]
+ pattern: 'name: cozystack-oidc-ec8dfb1cbc75e699\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: view\nsubjects:\n- apiGroup: rbac.authorization.k8s.io\n kind: User\n name: bob@example.org\n'
+
+ - it: the bootstrap Job mounts the bindings ConfigMap and carries no users[] entry in its script
+ # The script is one `sh -c` argument and Linux caps one argument at 128 KiB:
+ # a binding per user in it stops the container from starting at about 240
+ # users. hack/kubernetes-oidc-bindings.bats renders 300 users to hold the
+ # size itself.
release:
name: kubernetes-app
namespace: tenant-foo
@@ -2575,23 +2641,130 @@ tests:
role: admin
- email: bob@example.org
role: view
+ asserts:
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: oidc-bindings
+ configMap:
+ name: kubernetes-app-oidc-bindings
+ optional: true
+ documentIndex: 3
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: oidc-bindings
+ mountPath: /etc/oidc-bindings
+ readOnly: true
+ documentIndex: 3
+ # The script reads the directory from the environment, which is also how
+ # hack/kubernetes-oidc-bindings.bats points it at a scratch directory.
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: OIDC_BINDINGS_DIR
+ value: /etc/oidc-bindings
+ documentIndex: 3
+ - notMatchRegex:
+ path: spec.template.spec.containers[0].command[2]
+ pattern: 'alice@example\.org'
+ documentIndex: 3
+ - notMatchRegex:
+ path: spec.template.spec.containers[0].command[2]
+ pattern: 'bob@example\.org'
+ documentIndex: 3
+
+ - it: the bootstrap Job stops before pruning when the desired list is not mounted
+ # `! grep -F` on a file that is not there is true, so a missing mount would
+ # delete every binding of the release.
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-job.yaml
+ set:
+ _cluster.oidc-enabled: "true"
+ _cluster.root-host: example.org
+ oidc.mode: System
asserts:
- matchRegex:
path: spec.template.spec.containers[0].command[2]
- pattern: 'name: "alice@example.org"'
+ pattern: '(?s)if \[ ! -f "\$BINDINGS/desired\.txt" \]; then.*?exit 1\n\s*fi.*Pruning stale ClusterRoleBindings'
documentIndex: 3
+
+ - it: the bootstrap Job applies the bindings file only when it has objects in it
+ # kubectl apply exits non-zero on a file with no objects, and a release
+ # with no users has none.
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-job.yaml
+ set:
+ _cluster.oidc-enabled: "true"
+ _cluster.root-host: example.org
+ oidc.mode: System
+ asserts:
- matchRegex:
path: spec.template.spec.containers[0].command[2]
- pattern: 'name: "bob@example.org"'
+ pattern: 'if \[ -s "\$BINDINGS/crbs\.yaml" \]; then\n\s+kubectl --kubeconfig "\$KUBECONFIG_TENANT" apply --filename "\$BINDINGS/crbs\.yaml"\n\s*fi'
documentIndex: 3
+
+ - it: a release with no users renders an empty bindings ConfigMap
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-bindings.yaml
+ set:
+ _cluster.oidc-enabled: "true"
+ _cluster.root-host: example.org
+ oidc.mode: System
+ oidc.users: []
+ asserts:
+ - equal:
+ path: data["crbs.yaml"]
+ value: ""
+ - equal:
+ path: data["desired.txt"]
+ value: ""
+
+ - it: CustomConfig mode renders the bindings ConfigMap for its users
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-bindings.yaml
+ set:
+ _cluster.oidc-enabled: "false"
+ oidc.mode: CustomConfig
+ oidc.customConfig.config: |
+ apiVersion: apiserver.config.k8s.io/v1beta1
+ kind: AuthenticationConfiguration
+ jwt:
+ - issuer:
+ url: https://idp.example.com
+ audiences: [my-cluster]
+ oidc.users:
+ - email: alice@example.org
+ role: admin
+ asserts:
+ - isKind:
+ of: ConfigMap
- matchRegex:
- path: spec.template.spec.containers[0].command[2]
- pattern: 'name: cluster-admin'
- documentIndex: 3
- - matchRegex:
- path: spec.template.spec.containers[0].command[2]
- pattern: 'name: view'
- documentIndex: 3
+ path: data["crbs.yaml"]
+ pattern: 'name: alice@example.org'
+
+ - it: mode=None renders no bindings ConfigMap even when users are set
+ # The Job takes its early branch at mode None and never reads the file.
+ release:
+ name: kubernetes-app
+ namespace: tenant-foo
+ template: templates/oidc-rbac-bindings.yaml
+ set:
+ oidc.mode: None
+ oidc.users:
+ - email: alice@example.org
+ role: admin
+ asserts:
+ - hasDocuments:
+ count: 0
- it: mode=System aborts the Job instead of leaving the OIDC kubeconfig Secret as the install-time placeholder when the tenant CA is missing
# A placeholder kubeconfig cannot authenticate, so the release must not go Ready with it.