Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 74 additions & 25 deletions hack/sync-rbac-rules.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,114 @@
#!/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")
if idx == -1:
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()
120 changes: 117 additions & 3 deletions helm/temporal-worker-controller/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) }}
Expand Down
8 changes: 4 additions & 4 deletions helm/temporal-worker-controller/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down