diff --git a/hack/sync-rbac-rules.py b/hack/sync-rbac-rules.py index f4a6d616..810fec6c 100644 --- a/hack/sync-rbac-rules.py +++ b/hack/sync-rbac-rules.py @@ -1,23 +1,34 @@ #!/usr/bin/env python3 -"""Sync rules from config/rbac/role.yaml into the Helm ClusterRole template. +"""Sync rules from config/rbac/role.yaml into the Helm RBAC template. -The Helm template must contain: - # GENERATED RULES BEGIN - ... +The Helm template contains two marker pairs: + + # GENERATED RULES BEGIN — full rules for the ClusterRole (default mode) # GENERATED RULES END -markers inside its ClusterRole rules section. Everything between the markers -is replaced with the rules from config/rbac/role.yaml (indented by two spaces). + + # GENERATED RULES (NAMESPACED) BEGIN — rules minus cluster-scoped entries for + # GENERATED RULES (NAMESPACED) END the namespaced Role (restrictWatchNamespaces) + +Both are updated from config/rbac/role.yaml. The namespaced block excludes +rules for cluster-scoped resources (namespaces, subjectaccessreviews) since +those are handled by the separate manager-cluster-role. """ import re import sys ROLE_YAML = "config/rbac/role.yaml" HELM_RBAC = "helm/temporal-worker-controller/templates/rbac.yaml" + BEGIN_MARKER = " # GENERATED RULES BEGIN" END_MARKER = " # GENERATED RULES END" +BEGIN_MARKER_NS = " # GENERATED RULES (NAMESPACED) BEGIN" +END_MARKER_NS = " # GENERATED RULES (NAMESPACED) END" + +CLUSTER_SCOPED_RESOURCES = {"namespaces", "subjectaccessreviews"} def extract_rules_text(path): + """Extract the raw rules text from config/rbac/role.yaml.""" with open(path) as f: content = f.read() idx = content.find("\nrules:\n") @@ -25,41 +36,79 @@ def extract_rules_text(path): print(f"ERROR: 'rules:' not found in {path}", file=sys.stderr) sys.exit(1) rules_body = content[idx + len("\nrules:\n"):] - # Indent lines relative to the `rules:` key in the Helm template. - # controller-gen emits two indentation levels: - # col 0: outer list items (e.g. "- apiGroups:") → add 2 spaces - # col 2: inner list values (e.g. " - events") → add 4 spaces - # col 2: mapping keys (e.g. " resources:") → add 2 spaces - # The extra indent on inner list values matches the style used by the - # hand-authored rules in the Helm template. lines = rules_body.splitlines(keepends=True) result = [] for line in lines: if not line.strip(): result.append(line) elif line.startswith(" - "): - result.append(" " + line) # inner list value: 2 global + 2 extra + result.append(" " + line) else: - result.append(" " + line) # outer list item or mapping key: 2 global + result.append(" " + line) return "".join(result) -def update_helm(path, rules_text): - with open(path) as f: - content = f.read() +def filter_namespaced(rules_text): + """Remove rule blocks that reference cluster-scoped resources. + + Splits on top-level list items (lines starting with ' - ') and drops + any block whose 'resources:' section contains a cluster-scoped resource. + """ + blocks = [] + current = [] + for line in rules_text.splitlines(keepends=True): + if line.startswith(" - ") and current: + blocks.append("".join(current)) + current = [line] + else: + current.append(line) + if current: + blocks.append("".join(current)) + + filtered = [] + for block in blocks: + resources = set() + in_resources = False + for line in block.splitlines(): + stripped = line.strip() + if stripped == "resources:": + in_resources = True + elif in_resources and stripped.startswith("- "): + resources.add(stripped[2:].strip()) + elif in_resources and not stripped.startswith("- "): + in_resources = False + if not CLUSTER_SCOPED_RESOURCES.intersection(resources): + filtered.append(block) + + return "".join(filtered) + + +def replace_between_markers(content, begin, end, replacement): pattern = re.compile( - r"(" + re.escape(BEGIN_MARKER) + r"[^\n]*\n)(.*?)(" + re.escape(END_MARKER) + r")", + r"(" + re.escape(begin) + r"[^\n]*\n)(.*?)(" + re.escape(end) + r")", re.DOTALL, ) if not pattern.search(content): - print(f"ERROR: markers not found in {path}", file=sys.stderr) + print(f"ERROR: markers {begin!r} not found", file=sys.stderr) sys.exit(1) - updated = pattern.sub(r"\g<1>" + rules_text + r"\g<3>", content) - with open(path, "w") as f: - f.write(updated) + return pattern.sub(r"\g<1>" + replacement + r"\g<3>", content) + + +def main(): + rules_text = extract_rules_text(ROLE_YAML) + + with open(HELM_RBAC) as f: + content = f.read() + + content = replace_between_markers(content, BEGIN_MARKER, END_MARKER, rules_text) + content = replace_between_markers( + content, BEGIN_MARKER_NS, END_MARKER_NS, filter_namespaced(rules_text) + ) + + with open(HELM_RBAC, "w") as f: + f.write(content) print(f"Synced RBAC rules from {ROLE_YAML} → {HELM_RBAC}") if __name__ == "__main__": - rules = extract_rules_text(ROLE_YAML) - update_helm(HELM_RBAC, rules) + main() diff --git a/helm/temporal-worker-controller/templates/rbac.yaml b/helm/temporal-worker-controller/templates/rbac.yaml index cf3e9ba2..88706e38 100644 --- a/helm/temporal-worker-controller/templates/rbac.yaml +++ b/helm/temporal-worker-controller/templates/rbac.yaml @@ -58,6 +58,119 @@ subjects: name: {{ .Values.serviceAccount.name | default (printf "%s-service-account" .Release.Name) }} namespace: {{ .Release.Namespace }} --- +{{- if .Values.rbac.restrictWatchNamespaces }} +# Namespace-scoped mode: the manager role is emitted as a namespaced Role in each +# watched namespace. Cluster-scoped rules (namespaces get, subjectaccessreviews +# create) are handled by the separate manager-cluster-role below. +{{- range .Values.rbac.restrictWatchNamespaces }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: rbac + {{- include "temporal-worker-controller.labels" $ | nindent 4 }} + name: {{ $.Release.Name }}-manager-role + namespace: {{ . }} +rules: + # GENERATED RULES (NAMESPACED) BEGIN - do not edit; run 'make manifests' to update + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - deployments/scale + verbs: + - update + - apiGroups: + - temporal.io + resources: + - connections + - temporalconnections + - workerresourcetemplates + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - temporal.io + resources: + - connections/finalizers + - temporalconnections/finalizers + - temporalworkerdeployments/finalizers + - workerdeployments/finalizers + verbs: + - update + - apiGroups: + - temporal.io + resources: + - temporalconnections/status + - temporalworkerdeployments/status + - workerdeployments/status + - workerresourcetemplates/status + verbs: + - get + - patch + - update + - apiGroups: + - temporal.io + resources: + - temporalworkerdeployments + - workerdeployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + # GENERATED RULES (NAMESPACED) END + {{- range $.Values.workerResourceTemplate.allowedResources }} + - apiGroups: + {{- range .apiGroups }} + - {{ . | quote }} + {{- end }} + resources: + {{- range .resources }} + - {{ . | quote }} + {{- end }} + verbs: + - create + - delete + - get + - patch + - update + {{- end }} +{{- end }} +{{- else }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -173,6 +286,7 @@ rules: - patch - update {{- end }} +{{- end }} --- {{- if .Values.rbac.restrictWatchNamespaces }} # Namespace-scoped mode: the manager role is bound only within the watched @@ -226,12 +340,12 @@ metadata: labels: app.kubernetes.io/component: rbac {{- include "temporal-worker-controller.labels" $ | nindent 4 }} - name: {{ $.Release.Name }}-{{ $.Release.Namespace }}-manager-rolebinding + name: {{ $.Release.Name }}-manager-rolebinding namespace: {{ . }} roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ $.Release.Name }}-{{ $.Release.Namespace }}-manager-role + kind: Role + name: {{ $.Release.Name }}-manager-role subjects: - kind: ServiceAccount name: {{ $.Values.serviceAccount.name | default (printf "%s-service-account" $.Release.Name) }} diff --git a/helm/temporal-worker-controller/values.yaml b/helm/temporal-worker-controller/values.yaml index 1c01d447..09f2cba3 100644 --- a/helm/temporal-worker-controller/values.yaml +++ b/helm/temporal-worker-controller/values.yaml @@ -42,10 +42,10 @@ rbac: create: true # restrictWatchNamespaces keeps the namespaces the controller watches and the # namespaces its RBAC grants access to in sync. Empty (the default) watches all - # namespaces with cluster-wide RBAC. When set, the controller watches only these - # namespaces (via WATCH_NAMESPACES) and is granted the manager role through a - # RoleBinding in each listed namespace, plus a minimal cluster-scoped role for the - # two grants that cannot be namespaced (namespaces get, subjectaccessreviews create). + # namespaces with cluster-wide RBAC (ClusterRole + ClusterRoleBinding). When set, + # the manager role is emitted as a namespaced Role (one per listed namespace) bound + # via a RoleBinding, plus a minimal cluster-scoped role for the two grants that + # cannot be namespaced (namespaces get, subjectaccessreviews create). restrictWatchNamespaces: [] serviceAccount: