Merge branch 'feat/update-chart-v2' into feat/add-reloader-enterprise-in-v2-chart

This commit is contained in:
Safwan
2026-07-17 12:10:20 +05:00
10 changed files with 561 additions and 28 deletions
@@ -61,7 +61,7 @@ helm uninstall {{RELEASE_NAME}} -n {{NAMESPACE}}
| `reloader.resourceLabelSelector` | List of comma separated label selectors, if multiple are provided they are combined with the AND operator | string | `""` |
| `reloader.logFormat` | Set type of log format. Value could be either `json` or `""` | string | `""` |
| `reloader.watchGlobally` | Allow Reloader to watch in all namespaces (`true`) or just in a single namespace (`false`) | boolean | `true` |
| `reloader.namespaces` | Explicit namespaces to watch (scoped mode). When non-empty and `reloader.watchGlobally` is `false`, Reloader watches exactly these namespaces and the chart creates a namespace-scoped Role + RoleBinding in each (no ClusterRole). The release namespace is always included automatically. Accepts either a YAML list (`["team-a","team-b"]`) or a comma-separated string (`"team-a,team-b"`). | list/string | `[]` |
| `reloader.namespaces` | Explicit namespaces to watch (scoped mode). When non-empty and `reloader.watchGlobally` is `false`, Reloader watches exactly these namespaces and the chart creates a namespace-scoped Role + RoleBinding in each (no ClusterRole). The release namespace is not watched for reloads unless you list it explicitly; the chart only grants it a minimal Role for Reloader's internal meta-info ConfigMap (and leader-election in HA). Accepts either a YAML list (`["team-a","team-b"]`) or a comma-separated string (`"team-a,team-b"`). | list/string | `[]` |
| `reloader.enableHA` | Enable leadership election allowing you to run multiple replicas | boolean | `false` |
| `reloader.enablePProf` | Enables pprof for profiling | boolean | `false` |
| `reloader.pprofAddr` | Address to start pprof server on | string | `:6060` |
@@ -91,7 +91,10 @@ Create the namespace selector if it does not watch globally
{{/*
Namespaces to watch in scoped mode: exactly the user-supplied reloader.namespaces,
trimmed, de-duped and sorted. The release namespace is intentionally NOT added here
— Reloader watches only what the user asked for (an empty result means global mode).
— Reloader watches only what the user asked for. An empty result is not necessarily
global mode: with watchGlobally=false it becomes single-namespace mode (the release
namespace, injected via --namespaces by reloader-effectiveNamespaces-csv); only with
watchGlobally=true does empty mean watch-all.
Returns a JSON-encoded list; consumers use mustFromJson to iterate.
*/}}
{{- define "reloader-watchNamespaces" -}}
@@ -116,13 +119,45 @@ Comma-joined form of reloader-watchNamespaces, for the --namespaces CLI flag.
{{- include "reloader-watchNamespaces" . | mustFromJson | join "," -}}
{{- end -}}
{{/*
The effective watched namespaces for the --namespaces CLI flag — the single
chart-side source of truth for watch scope, so the binary never has to fall back
to the KUBERNETES_NAMESPACE env:
- scoped mode -> the cleaned reloader.namespaces list
- single-namespace mode -> the release namespace (watchGlobally=false, no list)
- global mode -> empty (no --namespaces flag; watch all)
Returns a comma-joined string ("" in global mode).
*/}}
{{- define "reloader-effectiveNamespaces-csv" -}}
{{- $watch := include "reloader-watchNamespaces" . | mustFromJson -}}
{{- if $watch -}}
{{- $watch | join "," -}}
{{- else if not .Values.reloader.watchGlobally -}}
{{- .Values.namespace | default .Release.Namespace -}}
{{- end -}}
{{- end -}}
{{/*
Whether Reloader runs in scoped mode. This is the single source of truth for the
scoped-vs-global decision: it is true only when the cleaned watch list
(reloader-watchNamespaces) is non-empty. Gate on this rather than the raw
.Values.reloader.namespaces, which is truthy even for values like " , " that trim
to an empty list (those must fall through to global/single-namespace mode).
Returns "true" (truthy) or "" (falsy).
*/}}
{{- define "reloader-isScoped" -}}
{{- if include "reloader-watchNamespaces" . | mustFromJson -}}
true
{{- end -}}
{{- end -}}
{{/*
Fails the render on an inconsistent namespace configuration: reloader.namespaces
(scoped mode) requires reloader.watchGlobally=false. Included from deployment.yaml
so it is validated once regardless of which templates render.
*/}}
{{- define "reloader-validate-namespaces" -}}
{{- if and .Values.reloader.watchGlobally .Values.reloader.namespaces -}}
{{- if and .Values.reloader.watchGlobally (include "reloader-isScoped" .) -}}
{{- fail "reloader.namespaces is set but reloader.watchGlobally is true; set reloader.watchGlobally=false to use scoped namespace mode." -}}
{{- end -}}
{{- end -}}
@@ -131,9 +166,9 @@ so it is validated once regardless of which templates render.
RBAC rules Reloader needs in its own (release) namespace, independent of the
watched namespaces. Reloader publishes an internal meta-info ConfigMap there in
every mode, so configmap write access is always granted. In scoped mode the
release namespace is not covered by the watch RBAC, so leader-election events
(and leases under HA) are granted here too; in global/single mode those are
already covered by the ClusterRole or the single-namespace Role.
release namespace is not covered by the watch RBAC, so under HA the leader-election
leases and the events it emits are granted here too; in global/single mode those
are already covered by the ClusterRole or the single-namespace Role.
Expects the root context ($) as its argument.
*/}}
{{- define "reloader-release-rules" }}
@@ -146,7 +181,15 @@ Expects the root context ($) as its argument.
- create
- update
- patch
{{- if .Values.reloader.namespaces }}
{{- if and (include "reloader-isScoped" .) .Values.reloader.enableHA }}
- apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- create
- get
- update
- apiGroups:
- ""
- "events.k8s.io"
@@ -156,16 +199,6 @@ Expects the root context ($) as its argument.
- create
- patch
- update
{{- if .Values.reloader.enableHA }}
- apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- create
- get
- update
{{- end }}
{{- end }}
{{- end -}}
@@ -144,12 +144,6 @@ spec:
fieldRef:
fieldPath: {{ $value | quote}}
{{- end }}
{{- end }}
{{- if and (eq .Values.reloader.watchGlobally false) (not .Values.reloader.namespaces) }}
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- end }}
- name: RELOADER_NAMESPACE
@@ -216,7 +210,7 @@ spec:
{{- . | toYaml | nindent 10 }}
{{- end }}
{{- end }}
{{- if or (.Values.reloader.logFormat) (.Values.reloader.logLevel) (.Values.reloader.ignoreSecrets) (and .Values.reloader.ignoreNamespaces .Values.reloader.watchGlobally) (.Values.reloader.namespaces) (include "reloader-namespaceSelector" .) (.Values.reloader.resourceLabelSelector) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (eq .Values.reloader.reloadOnDelete true) (ne .Values.reloader.reloadStrategy "default") (.Values.reloader.enableHA) (.Values.reloader.autoReloadAll) (.Values.reloader.ignoreJobs) (.Values.reloader.ignoreCronJobs) (.Values.reloader.enableCSIIntegration)}}
{{- if or (.Values.reloader.logFormat) (.Values.reloader.logLevel) (.Values.reloader.ignoreSecrets) (and .Values.reloader.ignoreNamespaces .Values.reloader.watchGlobally) (include "reloader-effectiveNamespaces-csv" .) (include "reloader-namespaceSelector" .) (.Values.reloader.resourceLabelSelector) (.Values.reloader.ignoreConfigMaps) (.Values.reloader.custom_annotations) (eq .Values.reloader.isArgoRollouts true) (eq .Values.reloader.reloadOnCreate true) (eq .Values.reloader.reloadOnDelete true) (ne .Values.reloader.reloadStrategy "default") (.Values.reloader.enableHA) (.Values.reloader.autoReloadAll) (.Values.reloader.ignoreJobs) (.Values.reloader.ignoreCronJobs) (.Values.reloader.enableCSIIntegration)}}
args:
{{- if .Values.reloader.logFormat }}
- "--log-format={{ .Values.reloader.logFormat }}"
@@ -237,8 +231,8 @@ spec:
{{- else if .Values.reloader.ignoreCronJobs }}
- "--ignored-workload-types=cronjobs"
{{- end }}
{{- if .Values.reloader.namespaces }}
- "--namespaces={{ include "reloader-watchNamespaces-csv" . }}"
{{- if (include "reloader-effectiveNamespaces-csv" .) }}
- "--namespaces={{ include "reloader-effectiveNamespaces-csv" . }}"
{{- end }}
{{- if and .Values.reloader.ignoreNamespaces .Values.reloader.watchGlobally }}
- "--namespaces-to-ignore={{ .Values.reloader.ignoreNamespaces }}"
@@ -3,7 +3,7 @@
{{- if not (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
{{- end }}
{{- if .Values.reloader.namespaces }}
{{- if (include "reloader-isScoped" .) }}
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
apiVersion: {{ $apiVersion }}
kind: Role
@@ -3,7 +3,7 @@
{{- if not (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
{{- end }}
{{- if .Values.reloader.namespaces }}
{{- if (include "reloader-isScoped" .) }}
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
apiVersion: {{ $apiVersion }}
kind: RoleBinding
@@ -0,0 +1,134 @@
suite: Namespace watch flags & validation
templates:
- deployment.yaml
release:
name: reloader
namespace: reloader-ns
tests:
# ---------------------------------------------------------------- scoped mode
- it: scoped mode passes the watched namespaces via --namespaces
set:
reloader:
watchGlobally: false
namespaces:
- team-a
- team-b
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces=team-a,team-b"
- it: namespaces are de-duplicated and sorted for the --namespaces flag
set:
reloader:
watchGlobally: false
namespaces:
- team-b
- team-a
- team-a
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces=team-a,team-b"
- it: a comma-separated string value is accepted for reloader.namespaces
set:
reloader:
watchGlobally: false
namespaces: "team-a,team-b"
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces=team-a,team-b"
# ----------------------------------------------------------- single-ns mode
- it: single-namespace mode passes the release namespace via --namespaces
set:
reloader:
watchGlobally: false
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces=reloader-ns"
- it: whitespace-only namespaces fall through to single-namespace mode
set:
reloader:
watchGlobally: false
namespaces:
- " "
- " "
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces=reloader-ns"
- it: KUBERNETES_NAMESPACE env is no longer injected in single-namespace mode
set:
reloader:
watchGlobally: false
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: KUBERNETES_NAMESPACE
any: true
# ---------------------------------------------------------------- global mode
- it: global mode does not pass a --namespaces flag
set:
reloader:
watchGlobally: true
logLevel: info
asserts:
- notContains:
path: spec.template.spec.containers[0].args
content: "--namespaces=reloader-ns"
- notContains:
path: spec.template.spec.containers[0].args
content: "--namespaces=team-a"
- it: namespaces-to-ignore is only emitted in global mode
set:
reloader:
watchGlobally: true
ignoreNamespaces: "kube-system"
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: "--namespaces-to-ignore=kube-system"
- it: namespaces-to-ignore is dropped in single-namespace mode
set:
reloader:
watchGlobally: false
ignoreNamespaces: "kube-system"
asserts:
- notContains:
path: spec.template.spec.containers[0].args
content: "--namespaces-to-ignore=kube-system"
# ------------------------------------------------------------- validation
- it: fails to render when reloader.namespaces is set but watchGlobally is true
set:
reloader:
watchGlobally: true
namespaces:
- team-a
asserts:
- failedTemplate:
errorMessage: "reloader.namespaces is set but reloader.watchGlobally is true; set reloader.watchGlobally=false to use scoped namespace mode."
- it: does not fail validation for whitespace-only namespaces with watchGlobally true
set:
reloader:
watchGlobally: true
namespaces:
- " "
asserts:
# whitespace trims to an empty watch list, so this is global mode, not scoped -> renders fine
- hasDocuments:
count: 1
- notContains:
path: spec.template.spec.containers[0].args
content: "--namespaces=reloader-ns"
@@ -0,0 +1,234 @@
suite: Namespace-scoped RBAC
templates:
- role.yaml
- rolebinding.yaml
- clusterrole.yaml
release:
name: reloader
namespace: reloader-ns
tests:
# ---------------------------------------------------------------- scoped mode
- it: scoped mode renders a Role + RoleBinding per watched namespace plus the metadata role, and no ClusterRole
set:
reloader:
watchGlobally: false
namespaces:
- team-a
- team-b
asserts:
# 2 watched Roles + 1 metadata Role
- hasDocuments:
count: 3
template: role.yaml
- hasDocuments:
count: 3
template: rolebinding.yaml
- hasDocuments:
count: 0
template: clusterrole.yaml
- it: scoped mode grants the namespaced workload rules in a watched namespace
set:
reloader:
watchGlobally: false
namespaces:
- team-a
- team-b
template: role.yaml
documentSelector:
path: metadata.namespace
value: team-a
asserts:
- isKind:
of: Role
- equal:
path: rules[0].apiGroups[0]
value: ""
- contains:
path: rules[0].resources
content: secrets
- contains:
path: rules[0].resources
content: configmaps
- contains:
path: rules
content:
apiGroups:
- "apps"
resources:
- deployments
- daemonsets
- statefulsets
verbs:
- list
- get
- watch
- update
- patch
- it: scoped RoleBinding in a watched namespace binds the release ServiceAccount to the namespaced Role
set:
reloader:
watchGlobally: false
namespaces:
- team-a
- team-b
template: rolebinding.yaml
documentSelector:
path: metadata.namespace
value: team-b
asserts:
- isKind:
of: RoleBinding
- equal:
path: roleRef.name
value: reloader-reloader-role
- equal:
path: subjects[0].namespace
value: reloader-ns
# ------------------------------------------------------------- metadata role
- it: metadata role is created in the release namespace with configmap write in scoped mode
set:
reloader:
watchGlobally: false
namespaces:
- team-a
template: role.yaml
documentSelector:
path: metadata.name
value: reloader-reloader-metadata-role
asserts:
- equal:
path: metadata.namespace
value: reloader-ns
- contains:
path: rules[0].resources
content: configmaps
- contains:
path: rules[0].verbs
content: create
- it: metadata role also grants leases and events in the release namespace under HA + scoped mode
set:
reloader:
watchGlobally: false
enableHA: true
namespaces:
- team-a
template: role.yaml
documentSelector:
path: metadata.name
value: reloader-reloader-metadata-role
asserts:
- contains:
path: rules
content:
apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- create
- get
- update
- it: metadata role does NOT grant leases when HA is disabled in scoped mode
set:
reloader:
watchGlobally: false
enableHA: false
namespaces:
- team-a
template: role.yaml
documentSelector:
path: metadata.name
value: reloader-reloader-metadata-role
asserts:
- notContains:
path: rules
content:
apiGroups:
- "coordination.k8s.io"
resources:
- leases
verbs:
- create
- get
- update
# ----------------------------------------------------------- single-ns mode
- it: single-namespace mode (watchGlobally=false, no list) renders one Role in the release namespace and no ClusterRole
set:
reloader:
watchGlobally: false
asserts:
# 1 single Role + 1 metadata Role
- hasDocuments:
count: 2
template: role.yaml
- hasDocuments:
count: 0
template: clusterrole.yaml
- equal:
path: metadata.namespace
value: reloader-ns
template: role.yaml
documentIndex: 0
# ---------------------------------------------------------------- global mode
- it: global mode renders a ClusterRole and only the metadata Role (no watched Roles)
set:
reloader:
watchGlobally: true
asserts:
- hasDocuments:
count: 1
template: clusterrole.yaml
- isKind:
of: ClusterRole
template: clusterrole.yaml
# only the metadata role remains from role.yaml
- hasDocuments:
count: 1
template: role.yaml
- equal:
path: metadata.name
value: reloader-reloader-metadata-role
template: role.yaml
documentIndex: 0
# -------------------------------------------------------- input normalization
- it: whitespace-only namespaces fall through to single-namespace mode (no watched Roles)
set:
reloader:
watchGlobally: false
namespaces:
- " "
- " "
asserts:
# cleaned list is empty -> single Role + metadata Role, both in release ns
- hasDocuments:
count: 2
template: role.yaml
- equal:
path: metadata.namespace
value: reloader-ns
template: role.yaml
documentIndex: 0
- it: rbac disabled renders no Roles, RoleBindings, or ClusterRole
set:
reloader:
watchGlobally: false
rbac:
enabled: false
namespaces:
- team-a
asserts:
- hasDocuments:
count: 0
template: role.yaml
- hasDocuments:
count: 0
template: rolebinding.yaml