mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-19 20:16:28 +00:00
feat: scoped multi-namespace mode (Role per namespace, no ClusterRole)
Add a third RBAC posture between watch-globally (ClusterRole) and single namespace: give Reloader an explicit list of namespaces to watch. The chart creates a namespace-scoped Role + RoleBinding in each listed namespace (no ClusterRole), and one install covers them all. Go: - new --namespaces flag / options.Namespaces - resolveWatchNamespaces() picks list -> KUBERNETES_NAMESPACE -> all - controller creation loops over the watched namespaces - namespaces-to-ignore is now only honored in global mode (watchGlobally=true); in single-namespace and scoped modes the watched set is already explicit Helm: - new reloader.namespaces value (active when watchGlobally=false); accepts either a YAML list or a comma-separated string for consistency with the sibling namespace options - reloader-watchNamespaces helper (release ns always auto-included, deduped) - shared reloader-namespaced-rules template reused per namespace - role.yaml/rolebinding.yaml range over the list; deployment passes --namespaces - --namespaces-to-ignore only rendered when watchGlobally=true - fail guard for watchGlobally=true + namespaces set Tests: unit test for resolveWatchNamespaces; scoped-namespaces e2e case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2cbb7715de
commit
50153d05ea
@@ -56,11 +56,12 @@ helm uninstall {{RELEASE_NAME}} -n {{NAMESPACE}}
|
||||
| `reloader.reloadOnDelete` | Enable reload on delete events. Valid value are either `true` or `false` | boolean | `false` |
|
||||
| `reloader.syncAfterRestart` | Enable sync after Reloader restarts for **Add** events, works only when reloadOnCreate is `true`. Valid value are either `true` or `false` | boolean | `false` |
|
||||
| `reloader.reloadStrategy` | Strategy to trigger resource restart, set to either `default`, `env-vars` or `annotations` | enumeration | `default` |
|
||||
| `reloader.ignoreNamespaces` | List of comma separated namespaces to ignore, if multiple are provided, they are combined with the AND operator | string | `""` |
|
||||
| `reloader.ignoreNamespaces` | List of comma separated namespaces to ignore, if multiple are provided, they are combined with the AND operator. Only honored when `reloader.watchGlobally` is `true`; in single-namespace and scoped (`reloader.namespaces`) modes the watched set is already explicit and this value is ignored. | string | `""` |
|
||||
| `reloader.namespaceSelector` | List of comma separated k8s label selectors for namespaces selection. The parameter only used when `reloader.watchGlobally` is `true`. See [LIST and WATCH filtering](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#list-and-watch-filtering) for more details on label-selector | string | `""` |
|
||||
| `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.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` |
|
||||
|
||||
@@ -88,6 +88,136 @@ Create the namespace selector if it does not watch globally
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Effective set of namespaces to watch in scoped mode: the release namespace
|
||||
(always included so the meta-info ConfigMap, HA leases and events keep working)
|
||||
plus the user-supplied reloader.namespaces, deduped and sorted.
|
||||
Returns a JSON-encoded list; consumers use mustFromJson to iterate.
|
||||
*/}}
|
||||
{{- define "reloader-watchNamespaces" -}}
|
||||
{{- $relNs := .Values.namespace | default .Release.Namespace -}}
|
||||
{{- $ns := .Values.reloader.namespaces | default list -}}
|
||||
{{- if kindIs "string" $ns -}}
|
||||
{{- $ns = splitList "," $ns -}}
|
||||
{{- end -}}
|
||||
{{- $clean := list -}}
|
||||
{{- range $ns -}}
|
||||
{{- $t := . | toString | trim -}}
|
||||
{{- if $t -}}
|
||||
{{- $clean = append $clean $t -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $all := concat (list $relNs) $clean | uniq | sortAlpha -}}
|
||||
{{- $all | toJson -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Comma-joined form of reloader-watchNamespaces, for the --namespaces CLI flag.
|
||||
*/}}
|
||||
{{- define "reloader-watchNamespaces-csv" -}}
|
||||
{{- include "reloader-watchNamespaces" . | mustFromJson | join "," -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The namespaced RBAC rules granted to Reloader in every watched namespace.
|
||||
Shared between the single-namespace Role and the per-namespace scoped Roles so
|
||||
the rule set is defined once. Expects the root context ($) as its argument.
|
||||
*/}}
|
||||
{{- define "reloader-namespaced-rules" }}
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
{{- if .Values.reloader.ignoreSecrets }}{{- else }}
|
||||
- secrets
|
||||
{{- end }}
|
||||
{{- if .Values.reloader.ignoreConfigMaps }}{{- else }}
|
||||
- configmaps
|
||||
{{- end }}
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
{{- if and (.Capabilities.APIVersions.Has "apps.openshift.io/v1") (.Values.reloader.isOpenshift) }}
|
||||
- apiGroups:
|
||||
- "apps.openshift.io"
|
||||
- ""
|
||||
resources:
|
||||
- deploymentconfigs
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
{{- end }}
|
||||
{{- if and (.Capabilities.APIVersions.Has "argoproj.io/v1alpha1") (.Values.reloader.isArgoRollouts) }}
|
||||
- apiGroups:
|
||||
- "argoproj.io"
|
||||
- ""
|
||||
resources:
|
||||
- rollouts
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
{{- end }}
|
||||
- apiGroups:
|
||||
- "apps"
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- statefulsets
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- cronjobs
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- list
|
||||
- get
|
||||
{{- if .Values.reloader.enableHA }}
|
||||
- apiGroups:
|
||||
- "coordination.k8s.io"
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- update
|
||||
{{- end}}
|
||||
{{- if .Values.reloader.enableCSIIntegration }}
|
||||
- apiGroups:
|
||||
- "secrets-store.csi.x-k8s.io"
|
||||
resources:
|
||||
- secretproviderclasspodstatuses
|
||||
- secretproviderclasses
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
{{- end}}
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Normalizes global.imagePullSecrets to a list of objects with name fields.
|
||||
Supports both of these in values.yaml:
|
||||
|
||||
@@ -144,7 +144,7 @@ spec:
|
||||
fieldPath: {{ $value | quote}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.reloader.watchGlobally false }}
|
||||
{{- if and (eq .Values.reloader.watchGlobally false) (not .Values.reloader.namespaces) }}
|
||||
- name: KUBERNETES_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
@@ -213,7 +213,7 @@ spec:
|
||||
{{- . | toYaml | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if or (.Values.reloader.logFormat) (.Values.reloader.logLevel) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (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) (.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)}}
|
||||
args:
|
||||
{{- if .Values.reloader.logFormat }}
|
||||
- "--log-format={{ .Values.reloader.logFormat }}"
|
||||
@@ -234,7 +234,10 @@ spec:
|
||||
{{- else if .Values.reloader.ignoreCronJobs }}
|
||||
- "--ignored-workload-types=cronjobs"
|
||||
{{- end }}
|
||||
{{- if .Values.reloader.ignoreNamespaces }}
|
||||
{{- if .Values.reloader.namespaces }}
|
||||
- "--namespaces={{ include "reloader-watchNamespaces-csv" . }}"
|
||||
{{- end }}
|
||||
{{- if and .Values.reloader.ignoreNamespaces .Values.reloader.watchGlobally }}
|
||||
- "--namespaces-to-ignore={{ .Values.reloader.ignoreNamespaces }}"
|
||||
{{- end }}
|
||||
{{- if (include "reloader-namespaceSelector" .) }}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
{{- if and (not (.Values.reloader.watchGlobally)) (.Values.reloader.rbac.enabled) }}
|
||||
{{- if (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{ else }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
{{- if and .Values.reloader.watchGlobally .Values.reloader.namespaces }}
|
||||
{{- fail "reloader.namespaces is set but reloader.watchGlobally is true; set reloader.watchGlobally=false to use scoped namespace mode." }}
|
||||
{{- end }}
|
||||
{{- if and (not (.Values.reloader.watchGlobally)) (.Values.reloader.rbac.enabled) }}
|
||||
{{- $apiVersion := "rbac.authorization.k8s.io/v1" }}
|
||||
{{- if not (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
|
||||
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
|
||||
{{- end }}
|
||||
{{- if .Values.reloader.namespaces }}
|
||||
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
|
||||
apiVersion: {{ $apiVersion }}
|
||||
kind: Role
|
||||
metadata:
|
||||
annotations:
|
||||
{{ include "reloader-helm3.annotations" $ | indent 4 }}
|
||||
labels:
|
||||
{{ include "reloader-labels.chart" $ | indent 4 }}
|
||||
{{- if $.Values.reloader.rbac.labels }}
|
||||
{{ tpl (toYaml $.Values.reloader.rbac.labels) $ | indent 4 }}
|
||||
{{- end }}
|
||||
{{- if $.Values.reloader.matchLabels }}
|
||||
{{ tpl (toYaml $.Values.reloader.matchLabels) $ | indent 4 }}
|
||||
{{- end }}
|
||||
name: {{ template "reloader-fullname" $ }}-role
|
||||
namespace: {{ $ns }}
|
||||
rules:
|
||||
{{- include "reloader-namespaced-rules" $ }}
|
||||
---
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
apiVersion: {{ $apiVersion }}
|
||||
kind: Role
|
||||
metadata:
|
||||
annotations:
|
||||
@@ -19,98 +44,8 @@ metadata:
|
||||
name: {{ template "reloader-fullname" . }}-role
|
||||
namespace: {{ .Values.namespace | default .Release.Namespace }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
{{- if .Values.reloader.ignoreSecrets }}{{- else }}
|
||||
- secrets
|
||||
{{- include "reloader-namespaced-rules" . }}
|
||||
{{- end }}
|
||||
{{- if .Values.reloader.ignoreConfigMaps }}{{- else }}
|
||||
- configmaps
|
||||
{{- end }}
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
{{- if and (.Capabilities.APIVersions.Has "apps.openshift.io/v1") (.Values.reloader.isOpenshift) }}
|
||||
- apiGroups:
|
||||
- "apps.openshift.io"
|
||||
- ""
|
||||
resources:
|
||||
- deploymentconfigs
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
{{- end }}
|
||||
{{- if and (.Capabilities.APIVersions.Has "argoproj.io/v1alpha1") (.Values.reloader.isArgoRollouts) }}
|
||||
- apiGroups:
|
||||
- "argoproj.io"
|
||||
- ""
|
||||
resources:
|
||||
- rollouts
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
{{- end }}
|
||||
- apiGroups:
|
||||
- "apps"
|
||||
resources:
|
||||
- deployments
|
||||
- daemonsets
|
||||
- statefulsets
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- cronjobs
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- list
|
||||
- get
|
||||
{{- if .Values.reloader.enableHA }}
|
||||
- apiGroups:
|
||||
- "coordination.k8s.io"
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- update
|
||||
{{- end}}
|
||||
{{- if .Values.reloader.enableCSIIntegration }}
|
||||
- apiGroups:
|
||||
- "secrets-store.csi.x-k8s.io"
|
||||
resources:
|
||||
- secretproviderclasspodstatuses
|
||||
- secretproviderclasses
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
{{- end}}
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
{{- if and (not (.Values.reloader.watchGlobally)) (.Values.reloader.rbac.enabled) }}
|
||||
{{- if (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{ else }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
{{- $apiVersion := "rbac.authorization.k8s.io/v1" }}
|
||||
{{- if not (.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1") }}
|
||||
{{- $apiVersion = "rbac.authorization.k8s.io/v1beta1" }}
|
||||
{{- end }}
|
||||
{{- if .Values.reloader.namespaces }}
|
||||
{{- range $ns := (include "reloader-watchNamespaces" . | mustFromJson) }}
|
||||
apiVersion: {{ $apiVersion }}
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
annotations:
|
||||
{{ include "reloader-helm3.annotations" $ | indent 4 }}
|
||||
labels:
|
||||
{{ include "reloader-labels.chart" $ | indent 4 }}
|
||||
{{- if $.Values.reloader.rbac.labels }}
|
||||
{{ tpl (toYaml $.Values.reloader.rbac.labels) $ | indent 4 }}
|
||||
{{- end }}
|
||||
{{- if $.Values.reloader.matchLabels }}
|
||||
{{ tpl (toYaml $.Values.reloader.matchLabels) $ | indent 4 }}
|
||||
{{- end }}
|
||||
name: {{ template "reloader-fullname" $ }}-role-binding
|
||||
namespace: {{ $ns }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ template "reloader-fullname" $ }}-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ template "reloader-serviceAccountName" $ }}
|
||||
namespace: {{ $.Values.namespace | default $.Release.Namespace }}
|
||||
---
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
apiVersion: {{ $apiVersion }}
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
annotations:
|
||||
@@ -27,6 +55,7 @@ subjects:
|
||||
name: {{ template "reloader-serviceAccountName" . }}
|
||||
namespace: {{ .Values.namespace | default .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
{{- if .Values.reloader.rbac.enabled }}
|
||||
|
||||
@@ -45,6 +45,14 @@ reloader:
|
||||
logFormat: "" # json
|
||||
logLevel: info # Log level to use (trace, debug, info, warning, error, fatal and panic)
|
||||
watchGlobally: true
|
||||
# Scoped mode: explicit list of namespaces to watch. When non-empty (and watchGlobally
|
||||
# is false), Reloader watches exactly these namespaces and the chart creates a namespace
|
||||
# scoped Role + RoleBinding in each one — no ClusterRole is created. The release namespace
|
||||
# is always included automatically. Leave empty ([]) for the default single-namespace or
|
||||
# global behavior controlled by watchGlobally.
|
||||
# Accepts either a YAML list (e.g. ["team-a", "team-b"]) or a comma-separated string
|
||||
# (e.g. "team-a,team-b")
|
||||
namespaces: []
|
||||
# Set to true to enable leadership election allowing you to run multiple replicas
|
||||
enableHA: false
|
||||
# Set to true to enable pprof for profiling
|
||||
|
||||
@@ -18,6 +18,13 @@ reloader:
|
||||
ignoreNamespaces: "" # Comma separated list of namespaces to ignore
|
||||
logFormat: "" #json
|
||||
watchGlobally: true
|
||||
# Scoped mode: explicit list of namespaces to watch. When non-empty (and watchGlobally
|
||||
# is false), Reloader watches exactly these namespaces and the chart creates a namespace
|
||||
# scoped Role + RoleBinding in each one — no ClusterRole is created. The release namespace
|
||||
# is always included automatically.
|
||||
# Accepts either a YAML list (e.g. ["team-a", "team-b"]) or a comma-separated string
|
||||
# (e.g. "team-a,team-b")
|
||||
namespaces: []
|
||||
# Set to true if you have a pod security policy that enforces readOnlyRootFilesystem
|
||||
readOnlyRootFileSystem: false
|
||||
legacy:
|
||||
|
||||
@@ -102,6 +102,21 @@ func getHAEnvs() (string, string) {
|
||||
return podName, podNamespace
|
||||
}
|
||||
|
||||
// resolveWatchNamespaces determines the set of namespaces to watch and whether
|
||||
// Reloader runs in global (all-namespaces) mode. Precedence:
|
||||
// 1. an explicit --namespaces list (scoped mode) — watch exactly those namespaces;
|
||||
// 2. the KUBERNETES_NAMESPACE env var (single-namespace mode);
|
||||
// 3. otherwise watch all namespaces (global mode).
|
||||
func resolveWatchNamespaces(namespaces []string, kubernetesNamespace string) ([]string, bool) {
|
||||
if len(namespaces) > 0 {
|
||||
return namespaces, false
|
||||
}
|
||||
if len(kubernetesNamespace) > 0 {
|
||||
return []string{kubernetesNamespace}, false
|
||||
}
|
||||
return []string{v1.NamespaceAll}, true
|
||||
}
|
||||
|
||||
func startReloader(cmd *cobra.Command, args []string) {
|
||||
common.GetCommandLineOptions()
|
||||
err := configureLogging(options.LogFormat, options.LogLevel)
|
||||
@@ -110,12 +125,11 @@ func startReloader(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
logrus.Info("Starting Reloader")
|
||||
isGlobal := false
|
||||
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
|
||||
if len(currentNamespace) == 0 {
|
||||
currentNamespace = v1.NamespaceAll
|
||||
isGlobal = true
|
||||
watchNamespaces, isGlobal := resolveWatchNamespaces(options.Namespaces, os.Getenv("KUBERNETES_NAMESPACE"))
|
||||
if isGlobal {
|
||||
logrus.Warnf("KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
|
||||
} else if len(options.Namespaces) > 0 {
|
||||
logrus.Infof("Watching scoped namespaces: %s", strings.Join(watchNamespaces, ", "))
|
||||
}
|
||||
|
||||
// create the clientset
|
||||
@@ -129,14 +143,20 @@ func startReloader(cmd *cobra.Command, args []string) {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
ignoredNamespacesList := options.NamespacesToIgnore
|
||||
// namespaces-to-ignore and namespace-selector only make sense when watching all
|
||||
// namespaces. In single-namespace and scoped modes the watched set is already
|
||||
// explicit, so both are intentionally left empty.
|
||||
ignoredNamespacesList := []string{}
|
||||
namespaceLabelSelector := ""
|
||||
|
||||
if isGlobal {
|
||||
ignoredNamespacesList = options.NamespacesToIgnore
|
||||
namespaceLabelSelector, err = common.GetNamespaceLabelSelector(options.NamespaceSelectors)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else if len(options.NamespacesToIgnore) > 0 {
|
||||
logrus.Warnf("namespaces-to-ignore is set but is only honored in global mode (watchGlobally=true); ignoring it.")
|
||||
}
|
||||
|
||||
resourceLabelSelector, err := common.GetResourceLabelSelector(options.ResourceSelectors)
|
||||
@@ -159,31 +179,33 @@ func startReloader(cmd *cobra.Command, args []string) {
|
||||
collectors := metrics.SetupPrometheusEndpoint()
|
||||
|
||||
var controllers []*controller.Controller
|
||||
for k := range kube.ResourceMap {
|
||||
if k == constants.SecretProviderClassController && !shouldRunCSIController() {
|
||||
continue
|
||||
}
|
||||
for _, currentNamespace := range watchNamespaces {
|
||||
for k := range kube.ResourceMap {
|
||||
if k == constants.SecretProviderClassController && !shouldRunCSIController() {
|
||||
continue
|
||||
}
|
||||
|
||||
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
|
||||
continue
|
||||
}
|
||||
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, resourceLabelSelector, collectors)
|
||||
if err != nil {
|
||||
logrus.Fatalf("%s", err)
|
||||
}
|
||||
c, err := controller.NewController(clientset, k, currentNamespace, ignoredNamespacesList, namespaceLabelSelector, resourceLabelSelector, collectors)
|
||||
if err != nil {
|
||||
logrus.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
controllers = append(controllers, c)
|
||||
controllers = append(controllers, c)
|
||||
|
||||
// If HA is enabled we only run the controller when
|
||||
if options.EnableHA {
|
||||
continue
|
||||
// If HA is enabled we only run the controller when
|
||||
if options.EnableHA {
|
||||
continue
|
||||
}
|
||||
// Now let's start the controller
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
logrus.Infof("Starting Controller to watch resource type: %s in namespace: %s", k, currentNamespace)
|
||||
go c.Run(1, stop)
|
||||
}
|
||||
// Now let's start the controller
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
logrus.Infof("Starting Controller to watch resource type: %s", k)
|
||||
go c.Run(1, stop)
|
||||
}
|
||||
|
||||
// Run leadership election
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestResolveWatchNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespaces []string
|
||||
kubernetesNamespace string
|
||||
wantNamespaces []string
|
||||
wantGlobal bool
|
||||
}{
|
||||
{
|
||||
name: "scoped mode takes precedence over env",
|
||||
namespaces: []string{"team-a", "team-b"},
|
||||
kubernetesNamespace: "reloader-system",
|
||||
wantNamespaces: []string{"team-a", "team-b"},
|
||||
wantGlobal: false,
|
||||
},
|
||||
{
|
||||
name: "scoped mode with single namespace",
|
||||
namespaces: []string{"team-a"},
|
||||
kubernetesNamespace: "",
|
||||
wantNamespaces: []string{"team-a"},
|
||||
wantGlobal: false,
|
||||
},
|
||||
{
|
||||
name: "single namespace mode from env",
|
||||
namespaces: nil,
|
||||
kubernetesNamespace: "reloader-system",
|
||||
wantNamespaces: []string{"reloader-system"},
|
||||
wantGlobal: false,
|
||||
},
|
||||
{
|
||||
name: "global mode when nothing set",
|
||||
namespaces: nil,
|
||||
kubernetesNamespace: "",
|
||||
wantNamespaces: []string{v1.NamespaceAll},
|
||||
wantGlobal: true,
|
||||
},
|
||||
{
|
||||
name: "empty list falls back to env",
|
||||
namespaces: []string{},
|
||||
kubernetesNamespace: "reloader-system",
|
||||
wantNamespaces: []string{"reloader-system"},
|
||||
wantGlobal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotNamespaces, gotGlobal := resolveWatchNamespaces(tt.namespaces, tt.kubernetesNamespace)
|
||||
assert.Equal(t, tt.wantNamespaces, gotNamespaces)
|
||||
assert.Equal(t, tt.wantGlobal, gotGlobal)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,9 @@ var (
|
||||
ResourcesToIgnore = []string{}
|
||||
// WorkloadTypesToIgnore is a list of workload types to ignore when watching for changes
|
||||
WorkloadTypesToIgnore = []string{}
|
||||
// Namespaces is an explicit list of namespaces to watch (scoped mode). When non-empty,
|
||||
// Reloader watches exactly these namespaces and requires no ClusterRole.
|
||||
Namespaces = []string{}
|
||||
// NamespacesToIgnore is a list of namespace names to ignore when watching for changes
|
||||
NamespacesToIgnore = []string{}
|
||||
// NamespaceSelectors is a list of namespace selectors to watch for changes
|
||||
|
||||
@@ -97,6 +97,7 @@ func ConfigureReloaderFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVar(&options.WebhookUrl, "webhook-url", "", "webhook to trigger instead of performing a reload")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.ResourcesToIgnore, "resources-to-ignore", options.ResourcesToIgnore, "list of resources to ignore (valid options 'configmaps' or 'secrets')")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.WorkloadTypesToIgnore, "ignored-workload-types", options.WorkloadTypesToIgnore, "list of workload types to ignore (valid options: 'jobs', 'cronjobs', or both)")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.Namespaces, "namespaces", options.Namespaces, "explicit list of namespaces to watch (scoped mode; creates no ClusterRole)")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.NamespacesToIgnore, "namespaces-to-ignore", options.NamespacesToIgnore, "list of namespaces to ignore")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.NamespaceSelectors, "namespace-selector", options.NamespaceSelectors, "list of key:value labels to filter on for namespaces")
|
||||
cmd.PersistentFlags().StringSliceVar(&options.ResourceSelectors, "resource-label-selector", options.ResourceSelectors, "list of key:value labels to filter on for configmaps and secrets")
|
||||
|
||||
@@ -108,6 +108,119 @@ var _ = Describe("Watch Globally Flag Tests", Serial, func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("with scoped namespaces list (watchGlobally=false + reloader.namespaces)", func() {
|
||||
var scopedNS string
|
||||
|
||||
BeforeEach(func() {
|
||||
scopedNS = "scoped-" + utils.RandName("ns")
|
||||
Expect(utils.CreateNamespace(ctx, kubeClient, scopedNS)).To(Succeed())
|
||||
Expect(utils.CreateNamespace(ctx, kubeClient, otherNS)).To(Succeed())
|
||||
|
||||
// Watch only scopedNS explicitly; the release namespace (testNamespace)
|
||||
// is auto-included by the chart. otherNS is intentionally left out.
|
||||
err := deployReloaderWithFlags(map[string]string{
|
||||
"reloader.watchGlobally": "false",
|
||||
"reloader.namespaces": "{" + scopedNS + "}",
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(waitForReloaderReady()).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
_ = utils.DeleteDeployment(ctx, kubeClient, scopedNS, deploymentName)
|
||||
_ = utils.DeleteConfigMap(ctx, kubeClient, scopedNS, configMapName)
|
||||
_ = undeployReloader()
|
||||
_ = utils.DeleteNamespace(ctx, kubeClient, scopedNS)
|
||||
_ = utils.DeleteNamespace(ctx, kubeClient, otherNS)
|
||||
})
|
||||
|
||||
It("should reload workloads in a listed namespace", func() {
|
||||
By("Creating a ConfigMap in the listed namespace")
|
||||
_, err := utils.CreateConfigMap(ctx, kubeClient, scopedNS, configMapName,
|
||||
map[string]string{"key": "initial"}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Creating a Deployment in the listed namespace with auto annotation")
|
||||
_, err = utils.CreateDeployment(ctx, kubeClient, scopedNS, deploymentName,
|
||||
utils.WithConfigMapEnvFrom(configMapName),
|
||||
utils.WithAnnotations(utils.BuildAutoTrueAnnotation()),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Waiting for Deployment to be ready")
|
||||
err = adapter.WaitReady(ctx, scopedNS, deploymentName, utils.WorkloadReadyTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Updating the ConfigMap")
|
||||
err = utils.UpdateConfigMap(ctx, kubeClient, scopedNS, configMapName, map[string]string{"key": "updated"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Waiting for Deployment to be reloaded")
|
||||
reloaded, err := adapter.WaitReloaded(ctx, scopedNS, deploymentName,
|
||||
utils.AnnotationLastReloadedFrom, utils.ReloadTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reloaded).To(BeTrue(), "Deployment in a listed namespace should reload")
|
||||
})
|
||||
|
||||
It("should reload workloads in Reloader's auto-included release namespace", func() {
|
||||
By("Creating a ConfigMap in Reloader's namespace")
|
||||
_, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName,
|
||||
map[string]string{"key": "initial"}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Creating a Deployment in Reloader's namespace with auto annotation")
|
||||
_, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName,
|
||||
utils.WithConfigMapEnvFrom(configMapName),
|
||||
utils.WithAnnotations(utils.BuildAutoTrueAnnotation()),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Waiting for Deployment to be ready")
|
||||
err = adapter.WaitReady(ctx, testNamespace, deploymentName, utils.WorkloadReadyTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Updating the ConfigMap")
|
||||
err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, map[string]string{"key": "updated"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Waiting for Deployment to be reloaded (release namespace is auto-included)")
|
||||
reloaded, err := adapter.WaitReloaded(ctx, testNamespace, deploymentName,
|
||||
utils.AnnotationLastReloadedFrom, utils.ReloadTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reloaded).To(BeTrue(), "Deployment in Reloader's auto-included namespace should reload")
|
||||
})
|
||||
|
||||
It("should NOT reload workloads in an unlisted namespace", func() {
|
||||
By("Creating a ConfigMap in an unlisted namespace")
|
||||
_, err := utils.CreateConfigMap(ctx, kubeClient, otherNS, configMapName,
|
||||
map[string]string{"key": "initial"}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Creating a Deployment in an unlisted namespace with auto annotation")
|
||||
_, err = utils.CreateDeployment(ctx, kubeClient, otherNS, deploymentName,
|
||||
utils.WithConfigMapEnvFrom(configMapName),
|
||||
utils.WithAnnotations(utils.BuildAutoTrueAnnotation()),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Waiting for Deployment to be ready")
|
||||
err = adapter.WaitReady(ctx, otherNS, deploymentName, utils.WorkloadReadyTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Updating the ConfigMap in the unlisted namespace")
|
||||
err = utils.UpdateConfigMap(ctx, kubeClient, otherNS, configMapName, map[string]string{"key": "updated"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Verifying Deployment was NOT reloaded (namespace not in the list)")
|
||||
time.Sleep(utils.NegativeTestWait)
|
||||
reloaded, err := adapter.WaitReloaded(ctx, otherNS, deploymentName,
|
||||
utils.AnnotationLastReloadedFrom, utils.ShortTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reloaded).To(BeFalse(), "Deployment in an unlisted namespace should NOT reload")
|
||||
})
|
||||
})
|
||||
|
||||
Context("with watchGlobally=true flag (default)", func() {
|
||||
var globalNS string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user