mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-20 20:46:30 +00:00
resolved comments
This commit is contained in:
@@ -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
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
apivalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
"github.com/stakater/Reloader/internal/pkg/workload"
|
||||
)
|
||||
@@ -97,6 +98,19 @@ func (c *Config) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
// Watched namespaces must be valid DNS-1123 labels; an invalid entry would
|
||||
// otherwise fail deep inside the controller-runtime cache with an opaque error.
|
||||
for _, ns := range c.WatchedNamespaces {
|
||||
if msgs := apivalidation.IsDNS1123Label(ns); len(msgs) > 0 {
|
||||
errs = append(
|
||||
errs, ValidationError{
|
||||
Field: "WatchedNamespaces",
|
||||
Message: fmt.Sprintf("invalid namespace %q: %s", ns, strings.Join(msgs, "; ")),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
c.IgnoredResources = normalizeToLower(c.IgnoredResources)
|
||||
|
||||
// Normalize ignored workloads to canonical Kind values (e.g., "cronjobs" -> "CronJob")
|
||||
|
||||
@@ -194,6 +194,40 @@ func TestConfig_Validate_InvalidIgnoredWorkload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_WatchedNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespaces []string
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil is valid (global mode)", nil, false},
|
||||
{"empty is valid (global mode)", []string{}, false},
|
||||
{"single valid label", []string{"team-a"}, false},
|
||||
{"multiple valid labels", []string{"team-a", "team-b", "kube-system"}, false},
|
||||
{"uppercase is invalid", []string{"Team-A"}, true},
|
||||
{"underscore is invalid", []string{"team_a"}, true},
|
||||
{"trailing dash is invalid", []string{"team-"}, true},
|
||||
{"one invalid among valid", []string{"team-a", "Bad_NS!"}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name, func(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.WatchedNamespaces = tt.namespaces
|
||||
|
||||
err := cfg.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr && !strings.Contains(err.Error(), "WatchedNamespaces") {
|
||||
t.Errorf("error should mention WatchedNamespaces, got: %v", err)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Validate_MultipleErrors(t *testing.T) {
|
||||
cfg := NewDefault()
|
||||
cfg.ReloadStrategy = "invalid"
|
||||
|
||||
@@ -113,3 +113,93 @@ var _ = Describe("Watch Namespaces (scoped mode) Flag Tests", Serial, func() {
|
||||
Expect(reloaded).To(BeFalse(), "Deployment in an unwatched namespace should NOT reload")
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Watch Multiple Namespaces (scoped mode) Flag Tests", Serial, func() {
|
||||
var (
|
||||
deploymentName string
|
||||
configMapName string
|
||||
watchedA string
|
||||
watchedB string
|
||||
unwatchedNS string
|
||||
adapter *utils.DeploymentAdapter
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
deploymentName = utils.RandName("deploy")
|
||||
configMapName = utils.RandName("cm")
|
||||
watchedA = "watched-a-" + utils.RandName("ns")
|
||||
watchedB = "watched-b-" + utils.RandName("ns")
|
||||
unwatchedNS = "unwatched-" + utils.RandName("ns")
|
||||
adapter = utils.NewDeploymentAdapter(kubeClient)
|
||||
|
||||
// Both watched namespaces must exist before install: in scoped mode the
|
||||
// chart creates a Role/RoleBinding in each.
|
||||
Expect(utils.CreateNamespace(ctx, kubeClient, watchedA)).To(Succeed())
|
||||
Expect(utils.CreateNamespace(ctx, kubeClient, watchedB)).To(Succeed())
|
||||
Expect(utils.CreateNamespace(ctx, kubeClient, unwatchedNS)).To(Succeed())
|
||||
|
||||
err := deployReloaderWithFlags(map[string]string{
|
||||
"reloader.watchGlobally": "false",
|
||||
"reloader.namespaces": fmt.Sprintf("{%s,%s}", watchedA, watchedB),
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(waitForReloaderReady()).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
for _, ns := range []string{watchedA, watchedB, unwatchedNS} {
|
||||
_ = utils.DeleteDeployment(ctx, kubeClient, ns, deploymentName)
|
||||
_ = utils.DeleteConfigMap(ctx, kubeClient, ns, configMapName)
|
||||
}
|
||||
_ = undeployReloader()
|
||||
for _, ns := range []string{watchedA, watchedB, unwatchedNS} {
|
||||
_ = utils.DeleteNamespace(ctx, kubeClient, ns)
|
||||
}
|
||||
})
|
||||
|
||||
It("should reload workloads across all watched namespaces but not an unwatched one", func() {
|
||||
// Set up an annotated Deployment + ConfigMap in every namespace (both
|
||||
// watched ones and the unwatched control).
|
||||
allNS := []string{watchedA, watchedB, unwatchedNS}
|
||||
for _, ns := range allNS {
|
||||
By("Creating a ConfigMap and Deployment in " + ns)
|
||||
_, err := utils.CreateConfigMap(ctx, kubeClient, ns, configMapName,
|
||||
map[string]string{"key": "initial"}, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = utils.CreateDeployment(ctx, kubeClient, ns, deploymentName,
|
||||
utils.WithConfigMapEnvFrom(configMapName),
|
||||
utils.WithAnnotations(utils.BuildAutoTrueAnnotation()),
|
||||
)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(adapter.WaitReady(ctx, ns, deploymentName, utils.WorkloadReadyTimeout)).To(Succeed())
|
||||
}
|
||||
|
||||
// Capture reload baselines before triggering, then update every ConfigMap.
|
||||
priorReload := map[string]string{}
|
||||
for _, ns := range allNS {
|
||||
pv, err := adapter.GetPodTemplateAnnotation(ctx, ns, deploymentName, utils.AnnotationLastReloadedFrom)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
priorReload[ns] = pv
|
||||
}
|
||||
for _, ns := range allNS {
|
||||
By("Updating the ConfigMap in " + ns)
|
||||
Expect(utils.UpdateConfigMap(ctx, kubeClient, ns, configMapName,
|
||||
map[string]string{"key": "updated"})).To(Succeed())
|
||||
}
|
||||
|
||||
By("Verifying workloads in BOTH watched namespaces reloaded")
|
||||
for _, ns := range []string{watchedA, watchedB} {
|
||||
reloaded, err := adapter.WaitReloadedFrom(ctx, ns, deploymentName,
|
||||
utils.AnnotationLastReloadedFrom, priorReload[ns], utils.ReloadTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reloaded).To(BeTrue(), "Deployment in watched namespace %s should reload", ns)
|
||||
}
|
||||
|
||||
By("Verifying the workload in the unwatched namespace did NOT reload")
|
||||
reloaded, err := adapter.WaitReloadedFrom(ctx, unwatchedNS, deploymentName,
|
||||
utils.AnnotationLastReloadedFrom, priorReload[unwatchedNS], utils.ShortTimeout)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(reloaded).To(BeFalse(), "Deployment in unwatched namespace %s should NOT reload", unwatchedNS)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user