Compare commits

...

53 Commits

Author SHA1 Message Date
github-actions[bot]
ca0a22e87b Bump version to 1.4.7 (#1001)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: msafwankarim <66724151+msafwankarim@users.noreply.github.com>
2025-08-25 16:52:35 +05:00
M Ahmad Mujtaba
19a76258d0 Merge pull request #998 from stakater/fix-broken-test
fixed broken test
2025-08-22 15:22:40 +05:00
Felix Tonnvik
aa481d9568 fixed broken test 2025-08-19 11:06:08 +02:00
Muhammad Safwan Karim
177d2756a8 Moved some functions to common package (#988)
* separate methods

* basic refactoring

* moved common code to util package to use it in gateway

* common check for argo rollouts

* made code compilable with latest changes on master

* Moved options to separate package and created CommandLineOptions instance that will be in sync with options values.

* reverted extra changes

* initialize CommandLineOptions with default options in module init

* wait for paused at annotation before checking deployment paused

* moved things around to fix things

* reverted unnecessary changes

* reverted rolling_upgrade changes

* reverted extra change

* additional checks in reloader

* refactor: ShouldReloadInternal method. It will be called by Reloader
ShouldReload has some additional resource/namespace filter checks which are not needed for Reloader

* added test cases

* moved config to sharable packae

* moved resource selector and label selctor methods

* fixed pipeline

* removed map.yaml

* removed vague comment
2025-08-15 15:51:47 +05:00
Felix
9b2af6f9b7 Merge pull request #989 from praddy26/fix-operator-pod-crash
fix: handle operator crash when using workloadRef
2025-08-15 12:06:09 +02:00
Felix
7c4899a7eb Merge pull request #981 from nitinverma9/add-dnsConfig-support-for-deployment
reloader: Add in support for dnsConfig for pod reloader.
2025-08-15 12:04:30 +02:00
Abdul Aziz
54d44858f8 Update README.md (#993)
* Update README.md

* Update README.md

Updated sponsor badge

* Update README.md

Fix line breaks for lint
2025-08-14 07:39:49 +02:00
Abdul Aziz
6304a9e5ab Update index.md (#995)
Add sponsor link at the bottom
2025-08-14 07:39:29 +02:00
Pradeep Lakshmi Narasimha
1e6a6ec2d9 fix: handle operator crash when using workloadRef 2025-08-14 07:55:56 +05:30
Nitin Verma
42cd7e71a2 reloader: Add in support for dnsConfig for pod reloader. This is supported as a common standard in most public helm charts, so decided to create a PR here. 2025-08-04 07:52:57 +01:00
M Ahmad Mujtaba
1107fee109 fix applied for issue-PR-975 (#987)
Co-authored-by: mahmadmujtaba <ahmad.mujtaba@stakater.com>
2025-08-01 15:54:32 +05:00
Muhammad Safwan Karim
9e33dac9ef Bump chart (#986)
* Bump chart

* empty
2025-07-30 13:20:23 +05:00
Muhammad Safwan Karim
517fd33fb1 Added pause deployment annotation overrides (#983)
* added pprof flag

* updated chart

* fixed linting and spelling

* custom_annotation for pause deployments related annotaitons
2025-07-29 19:50:13 +05:00
Muhammad Safwan Karim
1e46d44c7c Added pprof flag (#980)
* added pprof flag

* updated chart

* fixed linting and spelling
2025-07-29 15:49:30 +05:00
Muhammad Safwan Karim
49409dce54 Extracted some functions to public package to reuse them in gateway (#966)
* separate methods

* basic refactoring

* moved common code to util package to use it in gateway

* common check for argo rollouts

* made code compilable with latest changes on master

* Moved options to separate package and created CommandLineOptions instance that will be in sync with options values.

* reverted extra changes

* initialize CommandLineOptions with default options in module init

* wait for paused at annotation before checking deployment paused

* moved things around to fix things

* reverted unnecessary changes

* reverted rolling_upgrade changes

* reverted extra change
2025-07-29 11:50:02 +05:00
Muhammad Safwan Karim
9039956c32 Merge pull request #970 from stakater/sa-7499-expose-meta-info
Publish a configmap that contains build/version info and reloader flags
2025-07-22 14:46:44 +05:00
Safwan
82eb8d8b87 reverted reloader.yaml deployment name 2025-07-22 14:40:46 +05:00
Safwan
7af0728990 comments resolved 2025-07-22 13:47:50 +05:00
Safwan
d9b36a56b5 Removed is dirty flag 2025-07-21 17:40:50 +05:00
Safwan
dcf4b0d0f6 merged deployment pause related changes 2025-07-21 15:14:30 +05:00
Safwan
b8b7cdb610 Merge branch 'master' into sa-7499-expose-meta-info 2025-07-21 14:28:07 +05:00
Safwan
d2580930e4 reverted testing changes 2025-07-21 14:25:02 +05:00
Safwan
e0150145ec updated PR workflow 2025-07-21 13:50:21 +05:00
Safwan
52ac7e307d skip e2e test 2025-07-21 13:30:00 +05:00
Safwan
e311fe2fff updated pull request workflow 2025-07-21 13:05:25 +05:00
Safwan
dec3410b7f updated work flows 2025-07-21 12:58:11 +05:00
Safwan
1e1f094516 resolved comments and stuff 2025-07-18 19:27:26 +05:00
Muhammad Safwan Karim
a9566fa672 Merge pull request #958 from s10/feature/pause
Add pausing deployments during upgrades
2025-07-18 15:38:33 +05:00
Muhammad Safwan Karim
51ee0a19bb Merge pull request #967 from pinkavaj/pi-k8s-labels-simple
Use labels suggested by Kubernetes and Helm best practices
2025-07-18 15:23:25 +05:00
Safwan
19918e6fa8 fix error 2025-07-17 13:05:40 +05:00
Safwan
95cea97d34 make parsetime exportable 2025-07-17 13:04:47 +05:00
Safwan
d22a0f25de changed types to bool where needed 2025-07-17 13:03:42 +05:00
Safwan
2d2c35fcf4 discard error 2025-07-16 20:09:19 +05:00
Safwan
cf600f7761 type safety where applicable in meta info 2025-07-16 19:37:02 +05:00
Safwan
01f62cf823 renamed struct fields 2025-07-16 18:29:26 +05:00
Safwan
85bd7a075c updated docker file 2025-07-16 18:08:24 +05:00
Safwan
3999edb7a3 Merge branch 'master' into sa-7499-expose-meta-info 2025-07-16 17:33:38 +05:00
Safwan
f69773c588 refactoring 2025-07-15 20:08:23 +05:00
Safwan
8b257a3f0c publish configmap with meta info 2025-07-15 19:16:26 +05:00
Muhammad Safwan Karim
93936d12c1 Merge pull request #933 from yo-ga/fix/specific-namespace
Ignore namespaceSelector when not watch globally
2025-07-11 17:14:19 +05:00
Muhammad Safwan Karim
2d810f9824 Merge pull request #865 from ChristianCiach/job-owner-ref
Add OwnerReference to generated Jobs
2025-07-09 16:22:51 +05:00
Christian Ciach
99c45b3ca3 Add test for CronJob owner references 2025-07-09 12:45:24 +02:00
Christian Ciach
81315adc9b Add conventional annotation to triggered jobs 2025-07-09 12:38:12 +02:00
Christian Ciach
ad0407517d Add OwnerReference to generated Jobs 2025-07-09 12:38:12 +02:00
Vladislav Gusev
cafa14f0e7 Add pausing deployments during upgrades 2025-07-08 16:59:42 +03:00
Muhammad Safwan Karim
5089955691 Merge pull request #928 from jamie-stinson/master
feat: add ignore annotation support for skipping reloads
2025-07-08 14:41:09 +05:00
Jiří Pinkava
379c428283 Use labels suggested by Kubernetes and Helm best practices
This improves the compatibility and makes it easier to work with various tools,
which take advantake from the assumption of existence of those labels.

Keep the old labels for selectors for backward compatibility.

Also note there is slight change in behaviour of PDB which might
be backward-incompatible in some rare cases.
2025-07-07 22:28:54 +02:00
Yoga Yu
e8cb97a6d1 Merge branch 'master' into fix/specific-namespace 2025-07-04 10:37:34 +08:00
Jamie
58ad781c0c fix: multiple blank lines lint 2025-07-03 16:04:49 +01:00
Jamie
be09beac29 feat: add ignore annotation support for skipping reloads 2025-07-03 14:06:44 +01:00
Yoga Yu
023425d4e1 chore: update doc 2025-06-08 21:54:13 +08:00
Yoga Yu
8c2f2e574c feat: ignore namespaceSelector when not necessory 2025-06-08 21:49:03 +08:00
Yoga Yu
b6d538dca8 fix: ignore filter when in-ns 2025-06-07 01:40:47 +08:00
43 changed files with 2089 additions and 365 deletions

View File

@@ -63,6 +63,11 @@ jobs:
check-latest: true
cache: true
- name: Create timestamp
id: prep
run: echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
# Get highest tag and remove any suffixes with '-'
- name: Get Highest tag
id: highest_tag
@@ -104,6 +109,7 @@ jobs:
kind create cluster
kubectl cluster-info
- name: Test
run: make test
@@ -135,7 +141,12 @@ jobs:
file: ${{ env.DOCKER_FILE_PATH }}
pull: true
push: false
build-args: BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }}
build-args: |
VERSION=merge-${{ steps.generate_tag.outputs.GIT_TAG }}
COMMIT=${{github.event.pull_request.head.sha}}
BUILD_DATE=${{ steps.prep.outputs.created }}
BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }}
cache-to: type=inline
platforms: linux/amd64,linux/arm,linux/arm64
tags: |

View File

@@ -91,6 +91,10 @@ jobs:
with:
username: ${{ secrets.STAKATER_DOCKERHUB_USERNAME }}
password: ${{ secrets.STAKATER_DOCKERHUB_PASSWORD }}
- name: Create timestamp
id: prep
run: echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Generate image repository path for Docker registry
run: |
@@ -148,7 +152,11 @@ jobs:
file: ${{ env.DOCKER_FILE_PATH }}
pull: true
push: true
build-args: BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }}
build-args: |
VERSION=merge-${{ github.event.number }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ steps.prep.outputs.created }}
BUILD_PARAMETERS=${{ env.BUILD_PARAMETERS }}
cache-to: type=inline
platforms: linux/amd64,linux/arm,linux/arm64
tags: |

View File

@@ -79,6 +79,10 @@ jobs:
id: generate_tag
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Create timestamp
id: prep
run: echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -106,6 +110,10 @@ jobs:
platforms: linux/amd64,linux/arm,linux/arm64
tags: |
${{ env.DOCKER_IMAGE_REPOSITORY }}:${{ steps.generate_tag.outputs.RELEASE_VERSION }}
build-args: |
VERSION=${{ steps.generate_tag.outputs.RELEASE_VERSION }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ steps.prep.outputs.created }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
@@ -152,6 +160,10 @@ jobs:
platforms: linux/amd64,linux/arm,linux/arm64
tags: |
${{ env.GHCR_IMAGE_REPOSITORY }}:${{ steps.generate_tag.outputs.RELEASE_VERSION }},${{ env.GHCR_IMAGE_REPOSITORY }}:latest
build-args: |
VERSION=${{ steps.generate_tag.outputs.RELEASE_VERSION }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ steps.prep.outputs.created }}
labels: |
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}

View File

@@ -9,6 +9,10 @@ ARG TARGETARCH
ARG GOPROXY
ARG GOPRIVATE
ARG COMMIT
ARG VERSION
ARG BUILD_DATE
WORKDIR /workspace
# Copy the Go Modules manifests
@@ -30,7 +34,10 @@ RUN CGO_ENABLED=0 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
GO111MODULE=on \
go build -mod=mod -a -o manager main.go
go build -ldflags="-s -w -X github.com/stakater/Reloader/pkg/common.Version=${VERSION} \
-X github.com/stakater/Reloader/pkg/common.Commit=${COMMIT} \
-X github.com/stakater/Reloader/pkg/common.BuildDate=${BUILD_DATE}" \
-installsuffix 'static' -mod=mod -a -o manager ./
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details

View File

@@ -2,6 +2,7 @@
<img src="assets/web/reloader.jpg" alt="Reloader" width="40%"/>
</p>
[![💖 Sponsor Our Work](https://img.shields.io/badge/Sponsor%20Our%20Work-FF8C00?style=flat-square&logo=github-sponsors&logoColor=white)](https://github.com/sponsors/stakater?utm_source=github&utm_medium=readme&utm_campaign=reloader)
[![Go Report Card](https://goreportcard.com/badge/github.com/stakater/reloader?style=flat-square)](https://goreportcard.com/report/github.com/stakater/reloader)
[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/stakater/reloader)
[![Release](https://img.shields.io/github/release/stakater/reloader.svg?style=flat-square)](https://github.com/stakater/reloader/releases/latest)
@@ -153,6 +154,21 @@ This pattern allows fine-grained reload control — workloads only restart if th
1. ✅ You want to reload a workload only if it references a ConfigMap or Secret that has been explicitly tagged with `reloader.stakater.com/match: "true"`.
1. ✅ Use this when you want full control over which shared or system-wide resources trigger reloads. Great in multi-tenant clusters or shared configs.
### ⛔ Resource-Level Ignore Annotation
When you need to prevent specific ConfigMaps or Secrets from triggering any reloads, use the ignore annotation on the resource itself:
```yaml
apiVersion: v1
kind: ConfigMap # or Secret
metadata:
name: my-config
annotations:
reloader.stakater.com/ignore: "true"
```
This instructs Reloader to skip all reload logic for that resource across all workloads.
### 4. ⚙️ Workload-Specific Rollout Strategy
By default, Reloader uses the **rollout** strategy — it updates the pod template to trigger a new rollout. This works well in most cases, but it can cause problems if you're using GitOps tools like ArgoCD, which detect this as configuration drift.
@@ -203,6 +219,25 @@ reloader:
ALERT_ADDITIONAL_INFO: "Triggered by Reloader in staging environment"
```
### 7. ⏸️ Pause Deployments
This feature allows you to pause rollouts for a deployment for a specified duration, helping to prevent multiple restarts when several ConfigMaps or Secrets are updated in quick succession.
| Annotation | Applies To | Description |
|---------------------------------------------------------|--------------|-----------------------------------------------------------------------------|
| `deployment.reloader.stakater.com/pause-period: "5m"` | Deployment | Pauses reloads for the specified period (e.g., `5m`, `1h`) |
#### How it works
1. Add the `deployment.reloader.stakater.com/pause-period` annotation to your Deployment, specifying the pause duration (e.g., `"5m"` for five minutes).
1. When a watched ConfigMap or Secret changes, Reloader will still trigger a reload event, but if the deployment is paused, the rollout will have no effect until the pause period has elapsed.
1. This avoids repeated restarts if multiple resources are updated close together.
#### Use when
1. ✅ Your deployment references multiple ConfigMaps or Secrets that may be updated at the same time.
1. ✅ You want to minimize unnecessary rollouts and reduce downtime caused by back-to-back configuration changes.
## 🚀 Installation
### 1. 📦 Helm
@@ -321,6 +356,15 @@ These flags allow you to redefine annotation keys used in your workloads or reso
| `--search-match-annotation` | Overrides `reloader.stakater.com/match` |
| `--secret-annotation` | Overrides `secret.reloader.stakater.com/reload` |
| `--configmap-annotation` | Overrides `configmap.reloader.stakater.com/reload` |
| `--pause-deployment-annotation` | Overrides `deployment.reloader.stakater.com/pause-period` |
| `--pause-deployment-time-annotation` | Overrides `deployment.reloader.stakater.com/paused-at` |
### 5. 🕷️ Debugging
| Flag | Description |
|--- |-------------|
| `--enable-pprof` | Enables `pprof` for profiling |
| `--pprof-addr` | Address to start `pprof` server on. Default is `:6060` |
## Compatibility

View File

@@ -1 +1 @@
1.1.0
1.4.7

View File

@@ -1,8 +1,8 @@
apiVersion: v1
name: reloader
description: Reloader chart that runs on kubernetes
version: 2.1.5
appVersion: v1.4.5
version: 2.2.1
appVersion: v1.4.6
keywords:
- Reloader
- kubernetes

View File

@@ -52,11 +52,13 @@ helm uninstall {{RELEASE_NAME}} -n {{NAMESPACE}}
| `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.namespaceSelector` | List of comma separated k8s label selectors for namespaces selection. 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.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.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` |
| `reloader.readOnlyRootFileSystem` | Enforce readOnlyRootFilesystem | boolean | `false` |
| `reloader.legacy.rbac` | | boolean | `false` |
| `reloader.matchLabels` | Pod labels to match | map | `{}` |
@@ -82,7 +84,10 @@ helm uninstall {{RELEASE_NAME}} -n {{NAMESPACE}}
| `reloader.deployment.resources` | Set container requests and limits (e.g. CPU or memory) | map | `{}` |
| `reloader.deployment.pod.annotations` | Set annotations for pod | map | `{}` |
| `reloader.deployment.priorityClassName` | Set priority class for pod in cluster | string | `""` |
| `reloader.deployment.volumeMounts` | Mount volume | array | `[]` |
| `reloader.deployment.volumes` | Add volume to a pod | array | `[]` |
| `reloader.deployment.dnsConfig` | dns configuration for pods | map | `{}` |
### Other Reloader Parameters
| Parameter | Description | Type | Default |

View File

@@ -27,12 +27,20 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "reloader-labels.chart" -}}
{{- define "reloader-match-labels.chart" -}}
app: {{ template "reloader-fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
release: {{ .Release.Name | quote }}
{{- end -}}
{{- define "reloader-labels.chart" -}}
{{ include "reloader-match-labels.chart" . }}
app.kubernetes.io/name: {{ template "reloader-name" . }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
heritage: {{ .Release.Service | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end -}}
{{/*
@@ -45,10 +53,10 @@ podAntiAffinity:
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
- key: app.kubernetes.io/instance
operator: In
values:
- {{ template "reloader-fullname" . }}
- {{ .Release.Name | quote }}
topologyKey: "kubernetes.io/hostname"
{{- end -}}
@@ -70,3 +78,12 @@ Create the annotations to support helm3
meta.helm.sh/release-namespace: {{ .Release.Namespace | quote }}
meta.helm.sh/release-name: {{ .Release.Name | quote }}
{{- end -}}
{{/*
Create the namespace selector if it does not watch globally
*/}}
{{- define "reloader-namespaceSelector" -}}
{{- if and .Values.reloader.watchGlobally .Values.reloader.namespaceSelector -}}
{{ .Values.reloader.namespaceSelector }}
{{- end -}}
{{- end -}}

View File

@@ -31,7 +31,7 @@ rules:
- list
- get
- watch
{{- if .Values.reloader.namespaceSelector }}
{{- if (include "reloader-namespaceSelector" .) }}
- apiGroups:
- ""
resources:

View File

@@ -25,8 +25,7 @@ spec:
revisionHistoryLimit: {{ .Values.reloader.deployment.revisionHistoryLimit }}
selector:
matchLabels:
app: {{ template "reloader-fullname" . }}
release: {{ .Release.Name | quote }}
{{ include "reloader-match-labels.chart" . | indent 6 }}
{{- if .Values.reloader.matchLabels }}
{{ tpl (toYaml .Values.reloader.matchLabels) . | indent 6 }}
{{- end }}
@@ -72,6 +71,10 @@ spec:
{{- if .Values.reloader.deployment.priorityClassName }}
priorityClassName: {{ .Values.reloader.deployment.priorityClassName }}
{{- end }}
{{- with .Values.reloader.deployment.dnsConfig }}
dnsConfig:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
{{- if .Values.global.imageRegistry }}
- image: "{{ .Values.global.imageRegistry }}/{{ .Values.image.name }}:{{ .Values.image.tag }}"
@@ -144,6 +147,15 @@ spec:
fieldRef:
fieldPath: metadata.namespace
{{- end }}
- name: RELOADER_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: RELOADER_DEPLOYMENT_NAME
value: {{ template "reloader-fullname" . }}
{{- if .Values.reloader.enableHA }}
- name: POD_NAME
valueFrom:
@@ -198,7 +210,7 @@ spec:
{{- . | toYaml | nindent 10 }}
{{- end }}
{{- end }}
{{- if or (.Values.reloader.logFormat) (.Values.reloader.logLevel) (.Values.reloader.ignoreSecrets) (.Values.reloader.ignoreNamespaces) (.Values.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)}}
{{- 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)}}
args:
{{- if .Values.reloader.logFormat }}
- "--log-format={{ .Values.reloader.logFormat }}"
@@ -215,12 +227,18 @@ spec:
{{- if .Values.reloader.ignoreNamespaces }}
- "--namespaces-to-ignore={{ .Values.reloader.ignoreNamespaces }}"
{{- end }}
{{- if .Values.reloader.namespaceSelector }}
- "--namespace-selector={{ .Values.reloader.namespaceSelector }}"
{{- if (include "reloader-namespaceSelector" .) }}
- "--namespace-selector=\"{{ include "reloader-namespaceSelector" . }}\""
{{- end }}
{{- if .Values.reloader.resourceLabelSelector }}
- "--resource-label-selector={{ .Values.reloader.resourceLabelSelector }}"
{{- end }}
{{- if .Values.reloader.enablePProf }}
- "--enable-pprof"
{{- if and .Values.reloader.pprofAddr }}
- "--pprof-addr={{ .Values.reloader.pprofAddr }}"
{{- end }}
{{- end }}
{{- if .Values.reloader.custom_annotations }}
{{- if .Values.reloader.custom_annotations.configmap }}
- "--configmap-annotation"
@@ -249,6 +267,14 @@ spec:
{{- if .Values.reloader.custom_annotations.match }}
- "--search-match-annotation"
- "{{ .Values.reloader.custom_annotations.match }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.pausePeriod }}
- "--pause-deployment-annotation"
- "{{ .Values.reloader.custom_annotations.pausePeriod }}"
{{- end }}
{{- if .Values.reloader.custom_annotations.pauseTime }}
- "--pause-deployment-annotation"
- "{{ .Values.reloader.custom_annotations.pauseTime }}"
{{- end }}
{{- if .Values.reloader.webhookUrl }}
- "--webhook-url"

View File

@@ -14,8 +14,7 @@ metadata:
spec:
podSelector:
matchLabels:
app: {{ template "reloader-fullname" . }}
release: {{ .Release.Name | quote }}
{{ include "reloader-match-labels.chart" . | indent 6 }}
{{- if .Values.reloader.matchLabels }}
{{ tpl (toYaml .Values.reloader.matchLabels) . | indent 6 }}
{{- end }}

View File

@@ -13,5 +13,5 @@ spec:
{{- end }}
selector:
matchLabels:
app: {{ template "reloader-fullname" . }}
{{ include "reloader-match-labels.chart" . | nindent 6 }}
{{- end }}

View File

@@ -56,5 +56,5 @@ spec:
- {{ .Release.Namespace }}
selector:
matchLabels:
{{ include "reloader-labels.chart" . | nindent 6 }}
{{ include "reloader-match-labels.chart" . | nindent 6 }}
{{- end }}

View File

@@ -101,3 +101,34 @@ rules:
- create
- patch
{{- end }}
---
{{- if .Values.reloader.rbac.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
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" . }}-metadata-role
namespace: {{ .Values.namespace | default .Release.Namespace }}
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- list
- get
- watch
- create
- update
{{- end }}

View File

@@ -27,3 +27,30 @@ subjects:
name: {{ template "reloader-serviceAccountName" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
{{- end }}
---
{{- if .Values.reloader.rbac.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
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" . }}-metadata-role-binding
namespace: {{ .Values.namespace | default .Release.Namespace }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ template "reloader-fullname" . }}-metadata-role
subjects:
- kind: ServiceAccount
name: {{ template "reloader-serviceAccountName" . }}
namespace: {{ .Values.namespace | default .Release.Namespace }}
{{- end }}

View File

@@ -56,5 +56,5 @@ spec:
- {{ .Release.Namespace }}
selector:
matchLabels:
{{ include "reloader-labels.chart" . | nindent 6 }}
{{ include "reloader-match-labels.chart" . | nindent 6 }}
{{- end }}

View File

@@ -17,7 +17,7 @@ fullnameOverride: ""
image:
name: stakater/reloader
repository: ghcr.io/stakater/reloader
tag: v1.4.5
tag: v1.4.6
# digest: sha256:1234567
pullPolicy: IfNotPresent
@@ -41,6 +41,10 @@ reloader:
watchGlobally: true
# Set to true to enable leadership election allowing you to run multiple replicas
enableHA: false
# Set to true to enable pprof for profiling
enablePProf: false
# Address to start pprof server on. Default is ":6060"
pprofAddr: ":6060"
# Set to true if you have a pod security policy that enforces readOnlyRootFilesystem
readOnlyRootFileSystem: false
legacy:
@@ -49,6 +53,19 @@ reloader:
# Set to true to expose a prometheus counter of reloads by namespace (this metric may have high cardinality in clusters with many namespaces)
enableMetricsByNamespace: false
deployment:
# Specifies the deployment DNS configuration.
dnsConfig: {}
# nameservers:
# - 1.2.3.4
# searches:
# - ns1.svc.cluster-domain.example
# - my.dns.search.suffix
# options:
# - name: ndots
# value: "1"
# - name: attempts
# value: "3"
# If you wish to run multiple replicas set reloader.enableHA = true
replicas: 1
@@ -67,6 +84,9 @@ reloader:
# - key: "node-role.kubernetes.io/infra-worker"
# operator: "Exists"
affinity: {}
volumeMounts: []
volumes: []
securityContext:
runAsNonRoot: true
@@ -99,14 +119,14 @@ reloader:
# whenUnsatisfiable: DoNotSchedule
# labelSelector:
# matchLabels:
# app: my-app
# app.kubernetes.io/instance: my-app
topologySpreadConstraints: []
annotations: {}
labels:
provider: stakater
group: com.stakater.platform
version: v1.4.5
version: v1.4.6
# Support for extra environment variables.
env:
# Open supports Key value pair as environment variables.
@@ -337,8 +357,4 @@ reloader:
# are applied during the life of a Pod. Possible values are "Off", "Initial", "Recreate", and "Auto".
updateMode: Auto
volumeMounts: []
volumes: []
webhookUrl: ""

View File

@@ -6,3 +6,4 @@ resources:
- manifests/clusterrolebinding.yaml
- manifests/serviceaccount.yaml
- manifests/deployment.yaml
- manifests/role.yaml

View File

@@ -17,7 +17,7 @@ spec:
app: reloader-reloader
spec:
containers:
- image: "ghcr.io/stakater/reloader:v1.1.0"
- image: "ghcr.io/stakater/reloader:v1.4.7"
imagePullPolicy: IfNotPresent
name: reloader-reloader
env:
@@ -31,6 +31,13 @@ spec:
resourceFieldRef:
resource: limits.memory
divisor: '1'
- name: RELOADER_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: RELOADER_DEPLOYMENT_NAME
value: reloader-reloader
ports:
- name: http
containerPort: 9090

View File

@@ -0,0 +1,32 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: reloader-reloader-metadata-role
namespace: default
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- list
- get
- watch
- create
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: reloader-reloader-metadata-rolebinding
namespace: default
subjects:
- kind: ServiceAccount
name: reloader-reloader
namespace: default
roleRef:
kind: Role
name: reloader-reloader-metadata-role
apiGroup: rbac.authorization.k8s.io

View File

@@ -5,6 +5,23 @@ metadata:
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: reloader-reloader-metadata-role
namespace: default
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- list
- get
- watch
- create
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: reloader-reloader-role
@@ -64,6 +81,20 @@ rules:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: reloader-reloader-metadata-rolebinding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: reloader-reloader-metadata-role
subjects:
- kind: ServiceAccount
name: reloader-reloader
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: reloader-reloader-role-binding
@@ -104,7 +135,13 @@ spec:
resourceFieldRef:
divisor: "1"
resource: limits.memory
image: "ghcr.io/stakater/reloader:latest"
- name: RELOADER_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: RELOADER_DEPLOYMENT_NAME
value: reloader-reloader
image: ghcr.io/stakater/reloader:v1.4.7
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5

View File

@@ -10,3 +10,17 @@ These are the key features of Reloader:
1. Restart pod in a `rollout` on change in linked/related `ConfigMaps` or `Secrets`
This site contains more details on how Reloader works. For an overview, please see the repository's [README file](https://github.com/stakater/Reloader/blob/master/README.md).
---
<div align="center">
[![💖 Sponsor our work](https://img.shields.io/badge/Sponsor%20Our%20Work-FF8C00?style=for-the-badge&logo=github-sponsors&logoColor=white)](https://github.com/sponsors/stakater?utm_source=docs&utm_medium=footer&utm_campaign=reloader)
<p>
Your support funds maintenance, security updates, and new features for Reloader, plus continued investment in other open source tools.
</p>
</div>
---

View File

@@ -16,6 +16,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
patchtypes "k8s.io/apimachinery/pkg/types"
"maps"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
)
@@ -443,11 +445,21 @@ func PatchDeployment(clients kube.Clients, namespace string, resource runtime.Ob
// CreateJobFromCronjob performs rolling upgrade on cronjob
func CreateJobFromCronjob(clients kube.Clients, namespace string, resource runtime.Object) error {
cronJob := resource.(*batchv1.CronJob)
annotations := make(map[string]string)
annotations["cronjob.kubernetes.io/instantiate"] = "manual"
maps.Copy(annotations, cronJob.Spec.JobTemplate.Annotations)
job := &batchv1.Job{
ObjectMeta: cronJob.Spec.JobTemplate.ObjectMeta,
Spec: cronJob.Spec.JobTemplate.Spec,
ObjectMeta: meta_v1.ObjectMeta{
GenerateName: cronJob.Name + "-",
Namespace: cronJob.Namespace,
Annotations: annotations,
Labels: cronJob.Spec.JobTemplate.Labels,
OwnerReferences: []meta_v1.OwnerReference{*meta_v1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
},
Spec: cronJob.Spec.JobTemplate.Spec,
}
job.GenerateName = cronJob.Name + "-"
_, err := clients.KubernetesClient.BatchV1().Jobs(namespace).Create(context.TODO(), job, meta_v1.CreateOptions{FieldManager: "Reloader"})
return err
}

View File

@@ -415,13 +415,26 @@ func TestPatchResources(t *testing.T) {
func TestCreateJobFromCronjob(t *testing.T) {
fixtures := newTestFixtures()
cronJob, err := createTestCronJobWithAnnotations(clients, fixtures.namespace, "1")
runtimeObj, err := createTestCronJobWithAnnotations(clients, fixtures.namespace, "1")
assert.NoError(t, err)
err = callbacks.CreateJobFromCronjob(clients, fixtures.namespace, cronJob.(*batchv1.CronJob))
cronJob := runtimeObj.(*batchv1.CronJob)
err = callbacks.CreateJobFromCronjob(clients, fixtures.namespace, cronJob)
assert.NoError(t, err)
err = deleteTestCronJob(clients, fixtures.namespace, "test-cronjob")
jobList, err := clients.KubernetesClient.BatchV1().Jobs(fixtures.namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
ownerFound := false
for _, job := range jobList.Items {
if isControllerOwner("CronJob", cronJob.Name, job.OwnerReferences) {
ownerFound = true
break
}
}
assert.Truef(t, ownerFound, "Missing CronJob owner reference")
err = deleteTestCronJob(clients, fixtures.namespace, cronJob.Name)
assert.NoError(t, err)
}
@@ -749,3 +762,12 @@ func createTestJobWithAnnotations(clients kube.Clients, namespace, version strin
func deleteTestJob(clients kube.Clients, namespace, name string) error {
return clients.KubernetesClient.BatchV1().Jobs(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
}
func isControllerOwner(kind, name string, ownerRefs []metav1.OwnerReference) bool {
for _, ownerRef := range ownerRefs {
if *ownerRef.Controller && ownerRef.Kind == kind && ownerRef.Name == name {
return true
}
}
return false
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"strings"
@@ -14,12 +15,12 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"github.com/stakater/Reloader/internal/pkg/controller"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
)
@@ -33,27 +34,7 @@ func NewReloaderCommand() *cobra.Command {
}
// options
cmd.PersistentFlags().BoolVar(&options.AutoReloadAll, "auto-reload-all", false, "Auto reload all resources")
cmd.PersistentFlags().StringVar(&options.ConfigmapUpdateOnChangeAnnotation, "configmap-annotation", "configmap.reloader.stakater.com/reload", "annotation to detect changes in configmaps, specified by name")
cmd.PersistentFlags().StringVar(&options.SecretUpdateOnChangeAnnotation, "secret-annotation", "secret.reloader.stakater.com/reload", "annotation to detect changes in secrets, specified by name")
cmd.PersistentFlags().StringVar(&options.ReloaderAutoAnnotation, "auto-annotation", "reloader.stakater.com/auto", "annotation to detect changes in secrets/configmaps")
cmd.PersistentFlags().StringVar(&options.ConfigmapReloaderAutoAnnotation, "configmap-auto-annotation", "configmap.reloader.stakater.com/auto", "annotation to detect changes in configmaps")
cmd.PersistentFlags().StringVar(&options.SecretReloaderAutoAnnotation, "secret-auto-annotation", "secret.reloader.stakater.com/auto", "annotation to detect changes in secrets")
cmd.PersistentFlags().StringVar(&options.AutoSearchAnnotation, "auto-search-annotation", "reloader.stakater.com/search", "annotation to detect changes in configmaps or secrets tagged with special match annotation")
cmd.PersistentFlags().StringVar(&options.SearchMatchAnnotation, "search-match-annotation", "reloader.stakater.com/match", "annotation to mark secrets or configmaps to match the search")
cmd.PersistentFlags().StringVar(&options.LogFormat, "log-format", "", "Log format to use (empty string for text, or JSON)")
cmd.PersistentFlags().StringVar(&options.LogLevel, "log-level", "info", "Log level to use (trace, debug, info, warning, error, fatal and panic)")
cmd.PersistentFlags().StringVar(&options.WebhookUrl, "webhook-url", "", "webhook to trigger instead of performing a reload")
cmd.PersistentFlags().StringSlice("resources-to-ignore", []string{}, "list of resources to ignore (valid options 'configMaps' or 'secrets')")
cmd.PersistentFlags().StringSlice("namespaces-to-ignore", []string{}, "list of namespaces to ignore")
cmd.PersistentFlags().StringSlice("namespace-selector", []string{}, "list of key:value labels to filter on for namespaces")
cmd.PersistentFlags().StringSlice("resource-label-selector", []string{}, "list of key:value labels to filter on for configmaps and secrets")
cmd.PersistentFlags().StringVar(&options.IsArgoRollouts, "is-Argo-Rollouts", "false", "Add support for argo rollouts")
cmd.PersistentFlags().StringVar(&options.ReloadStrategy, constants.ReloadStrategyFlag, constants.EnvVarsReloadStrategy, "Specifies the desired reload strategy")
cmd.PersistentFlags().StringVar(&options.ReloadOnCreate, "reload-on-create", "false", "Add support to watch create events")
cmd.PersistentFlags().StringVar(&options.ReloadOnDelete, "reload-on-delete", "false", "Add support to watch delete events")
cmd.PersistentFlags().BoolVar(&options.EnableHA, "enable-ha", false, "Adds support for running multiple replicas via leadership election")
cmd.PersistentFlags().BoolVar(&options.SyncAfterRestart, "sync-after-restart", false, "Sync add events after reloader restarts")
util.ConfigureReloaderFlags(cmd)
return cmd
}
@@ -122,15 +103,18 @@ func getHAEnvs() (string, string) {
}
func startReloader(cmd *cobra.Command, args []string) {
common.GetCommandLineOptions()
err := configureLogging(options.LogFormat, options.LogLevel)
if err != nil {
logrus.Warn(err)
}
logrus.Info("Starting Reloader")
isGlobal := false
currentNamespace := os.Getenv("KUBERNETES_NAMESPACE")
if len(currentNamespace) == 0 {
currentNamespace = v1.NamespaceAll
isGlobal = true
logrus.Warnf("KUBERNETES_NAMESPACE is unset, will detect changes in all namespaces.")
}
@@ -140,22 +124,22 @@ func startReloader(cmd *cobra.Command, args []string) {
logrus.Fatal(err)
}
ignoredResourcesList, err := getIgnoredResourcesList(cmd)
ignoredResourcesList, err := util.GetIgnoredResourcesList()
if err != nil {
logrus.Fatal(err)
}
ignoredNamespacesList, err := getIgnoredNamespacesList(cmd)
if err != nil {
logrus.Fatal(err)
ignoredNamespacesList := options.NamespacesToIgnore
namespaceLabelSelector := ""
if isGlobal {
namespaceLabelSelector, err = common.GetNamespaceLabelSelector(options.NamespaceSelectors)
if err != nil {
logrus.Fatal(err)
}
}
namespaceLabelSelector, err := getNamespaceLabelSelector(cmd)
if err != nil {
logrus.Fatal(err)
}
resourceLabelSelector, err := getResourceLabelSelector(cmd)
resourceLabelSelector, err := common.GetResourceLabelSelector(options.ResourceSelectors)
if err != nil {
logrus.Fatal(err)
}
@@ -207,107 +191,19 @@ func startReloader(cmd *cobra.Command, args []string) {
go leadership.RunLeaderElection(lock, ctx, cancel, podName, controllers)
}
common.PublishMetaInfoConfigmap(clientset)
if options.EnablePProf {
go startPProfServer()
}
leadership.SetupLivenessEndpoint()
logrus.Fatal(http.ListenAndServe(constants.DefaultHttpListenAddr, nil))
}
func getIgnoredNamespacesList(cmd *cobra.Command) (util.List, error) {
return getStringSliceFromFlags(cmd, "namespaces-to-ignore")
}
func getNamespaceLabelSelector(cmd *cobra.Command) (string, error) {
slice, err := getStringSliceFromFlags(cmd, "namespace-selector")
if err != nil {
logrus.Fatal(err)
}
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
namespaceLabelSelector := strings.Join(slice[:], ",")
_, err = labels.Parse(namespaceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return namespaceLabelSelector, nil
}
func getResourceLabelSelector(cmd *cobra.Command) (string, error) {
slice, err := getStringSliceFromFlags(cmd, "resource-label-selector")
if err != nil {
logrus.Fatal(err)
}
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
resourceLabelSelector := strings.Join(slice[:], ",")
_, err = labels.Parse(resourceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return resourceLabelSelector, nil
}
func getStringSliceFromFlags(cmd *cobra.Command, flag string) ([]string, error) {
slice, err := cmd.Flags().GetStringSlice(flag)
if err != nil {
return nil, err
}
return slice, nil
}
func getIgnoredResourcesList(cmd *cobra.Command) (util.List, error) {
ignoredResourcesList, err := getStringSliceFromFlags(cmd, "resources-to-ignore")
if err != nil {
return nil, err
}
for _, v := range ignoredResourcesList {
if v != "configMaps" && v != "secrets" {
return nil, fmt.Errorf("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not '%s'", v)
}
}
if len(ignoredResourcesList) > 1 {
return nil, errors.New("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not both")
}
return ignoredResourcesList, nil
func startPProfServer() {
logrus.Infof("Starting pprof server on %s", options.PProfAddr)
if err := http.ListenAndServe(options.PProfAddr, nil); err != nil {
logrus.Errorf("Failed to start pprof server: %v", err)
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -94,7 +95,7 @@ func TestControllerUpdatingConfigmapShouldCreatePodAnnotationInDeployment(t *tes
// Verifying deployment update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -147,7 +148,7 @@ func TestControllerUpdatingConfigmapShouldAutoCreatePodAnnotationInDeployment(t
// Verifying deployment update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -212,7 +213,7 @@ func TestControllerCreatingConfigmapShouldCreatePodAnnotationInDeployment(t *tes
// Verifying deployment update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -271,7 +272,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDeploymentUsingArs(t *testing
// Verifying deployment update
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -326,7 +327,7 @@ func TestControllerUpdatingConfigmapLabelsShouldNotCreateOrCreatePodAnnotationIn
// Verifying deployment update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.google.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -390,7 +391,7 @@ func TestControllerCreatingSecretShouldCreatePodAnnotationInDeployment(t *testin
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -443,7 +444,7 @@ func TestControllerUpdatingSecretShouldCreatePodAnnotationInDeployment(t *testin
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -501,7 +502,7 @@ func TestControllerUpdatingSecretShouldUpdatePodAnnotationInDeployment(t *testin
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -552,7 +553,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateOrUpdatePodAnnotationInDep
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -604,7 +605,7 @@ func TestControllerUpdatingConfigmapShouldCreatePodAnnotationInDaemonSet(t *test
// Verifying DaemonSet update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -667,7 +668,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDaemonSetUsingArs(t *testing.
// Verifying DaemonSet update
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -720,7 +721,7 @@ func TestControllerUpdatingSecretShouldCreatePodAnnotationInDaemonSet(t *testing
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -779,7 +780,7 @@ func TestControllerUpdatingSecretShouldUpdatePodAnnotationInDaemonSet(t *testing
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -830,7 +831,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateOrUpdatePodAnnotationInDae
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -882,7 +883,7 @@ func TestControllerUpdatingConfigmapShouldCreatePodAnnotationInStatefulSet(t *te
// Verifying StatefulSet update
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -941,7 +942,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateStatefulSetUsingArs(t *testin
// Verifying StatefulSet update
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -994,7 +995,7 @@ func TestControllerUpdatingSecretShouldCreatePodAnnotationInStatefulSet(t *testi
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1046,7 +1047,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInDeployment(t *testing.T) {
// Verifying deployment update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1099,7 +1100,7 @@ func TestControllerUpdatingConfigmapShouldAutoCreateEnvInDeployment(t *testing.T
// Verifying deployment update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1164,7 +1165,7 @@ func TestControllerCreatingConfigmapShouldCreateEnvInDeployment(t *testing.T) {
// Verifying deployment update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1223,7 +1224,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDeploymentUsingErs(t *testing
// Verifying deployment update
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1278,7 +1279,7 @@ func TestControllerUpdatingConfigmapLabelsShouldNotCreateOrUpdateEnvInDeployment
// Verifying deployment update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.google.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1342,7 +1343,7 @@ func TestControllerCreatingSecretShouldCreateEnvInDeployment(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1395,7 +1396,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInDeployment(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1453,7 +1454,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInDeployment(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1504,7 +1505,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateOrUpdateEnvInDeployment(t
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1556,7 +1557,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInDaemonSet(t *testing.T) {
// Verifying DaemonSet update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1619,7 +1620,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateDaemonSetUsingErs(t *testing.
// Verifying DaemonSet update
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1672,7 +1673,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInDaemonSet(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1731,7 +1732,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInDaemonSet(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1782,7 +1783,7 @@ func TestControllerUpdatingSecretLabelsShouldNotCreateOrUpdateEnvInDaemonSet(t *
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, data)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -1834,7 +1835,7 @@ func TestControllerUpdatingConfigmapShouldCreateEnvInStatefulSet(t *testing.T) {
// Verifying StatefulSet update
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1893,7 +1894,7 @@ func TestControllerForUpdatingConfigmapShouldUpdateStatefulSetUsingErs(t *testin
// Verifying StatefulSet update
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, configmapName, "aurorasolutions.io")
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -1946,7 +1947,7 @@ func TestControllerUpdatingSecretShouldCreateEnvInStatefulSet(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been created")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, newData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -2004,7 +2005,7 @@ func TestControllerUpdatingSecretShouldUpdateEnvInStatefulSet(t *testing.T) {
// Verifying Upgrade
logrus.Infof("Verifying env var has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,
@@ -2062,7 +2063,7 @@ func TestControllerUpdatingSecretShouldUpdatePodAnnotationInStatefulSet(t *testi
// Verifying Upgrade
logrus.Infof("Verifying pod annotation has been updated")
shaData := testutil.ConvertResourceToSHA(testutil.SecretResourceType, namespace, secretName, updatedData)
config := util.Config{
config := common.Config{
Namespace: namespace,
ResourceName: secretName,
SHAValue: shaData,

View File

@@ -4,7 +4,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
)
@@ -33,13 +33,13 @@ func (r ResourceCreatedHandler) Handle() error {
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceCreatedHandler) GetConfig() (util.Config, string) {
func (r ResourceCreatedHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config util.Config
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = util.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = util.GetSecretConfig(r.Resource.(*v1.Secret))
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -42,20 +42,20 @@ func (r ResourceDeleteHandler) Handle() error {
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceDeleteHandler) GetConfig() (util.Config, string) {
func (r ResourceDeleteHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config util.Config
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
config = util.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
config = util.GetSecretConfig(r.Resource.(*v1.Secret))
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}
return config, oldSHAData
}
func invokeDeleteStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func invokeDeleteStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
if options.ReloadStrategy == constants.AnnotationsReloadStrategy {
return removePodAnnotations(upgradeFuncs, item, config, autoReload)
}
@@ -63,12 +63,12 @@ func invokeDeleteStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runti
return removeContainerEnvVars(upgradeFuncs, item, config, autoReload)
}
func removePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func removePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
config.SHAValue = testutil.GetSHAfromEmptyData()
return updatePodAnnotations(upgradeFuncs, item, config, autoReload)
}
func removeContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func removeContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
envVar := getEnvVarName(config.ResourceName, config.Type)
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)

View File

@@ -1,11 +1,9 @@
package handler
import (
"github.com/stakater/Reloader/internal/pkg/util"
)
import "github.com/stakater/Reloader/pkg/common"
// ResourceHandler handles the creation and update of resources
type ResourceHandler interface {
Handle() error
GetConfig() (util.Config, string)
GetConfig() (common.Config, string)
}

View File

@@ -0,0 +1,242 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
app "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
patchtypes "k8s.io/apimachinery/pkg/types"
)
// Keeps track of currently active timers
var activeTimers = make(map[string]*time.Timer)
// Returns unique key for the activeTimers map
func getTimerKey(namespace, deploymentName string) string {
return fmt.Sprintf("%s/%s", namespace, deploymentName)
}
// Checks if a deployment is currently paused
func IsPaused(deployment *app.Deployment) bool {
return deployment.Spec.Paused
}
// Deployment paused by reloader ?
func IsPausedByReloader(deployment *app.Deployment) bool {
if IsPaused(deployment) {
pausedAtAnnotationValue := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
return pausedAtAnnotationValue != ""
}
return false
}
// Returns the time, the deployment was paused by reloader, nil otherwise
func GetPauseStartTime(deployment *app.Deployment) (*time.Time, error) {
if !IsPausedByReloader(deployment) {
return nil, nil
}
pausedAtStr := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
parsedTime, err := time.Parse(time.RFC3339, pausedAtStr)
if err != nil {
return nil, err
}
return &parsedTime, nil
}
// ParsePauseDuration parses the pause interval value and returns a time.Duration
func ParsePauseDuration(pauseIntervalValue string) (time.Duration, error) {
pauseDuration, err := time.ParseDuration(pauseIntervalValue)
if err != nil {
logrus.Warnf("Failed to parse pause interval value '%s': %v", pauseIntervalValue, err)
return 0, err
}
return pauseDuration, nil
}
// Pauses a deployment for a specified duration and creates a timer to resume it
// after the specified duration
func PauseDeployment(deployment *app.Deployment, clients kube.Clients, namespace, pauseIntervalValue string) (*app.Deployment, error) {
deploymentName := deployment.Name
pauseDuration, err := ParsePauseDuration(pauseIntervalValue)
if err != nil {
return nil, err
}
if !IsPaused(deployment) {
logrus.Infof("Pausing Deployment '%s' in namespace '%s' for %s", deploymentName, namespace, pauseDuration)
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
pausePatch, err := CreatePausePatch()
if err != nil {
logrus.Errorf("Failed to create pause patch for deployment '%s': %v", deploymentName, err)
return deployment, err
}
err = deploymentFuncs.PatchFunc(clients, namespace, deployment, patchtypes.StrategicMergePatchType, pausePatch)
if err != nil {
logrus.Errorf("Failed to patch deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return deployment, err
}
updatedDeployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
CreateResumeTimer(deployment, clients, namespace, pauseDuration)
return updatedDeployment, err
}
if !IsPausedByReloader(deployment) {
logrus.Infof("Deployment '%s' in namespace '%s' already paused", deploymentName, namespace)
return deployment, nil
}
// Deployment has already been paused by reloader, check for timer
logrus.Debugf("Deployment '%s' in namespace '%s' is already paused by reloader", deploymentName, namespace)
timerKey := getTimerKey(namespace, deploymentName)
_, timerExists := activeTimers[timerKey]
if !timerExists {
logrus.Warnf("Timer does not exist for already paused deployment '%s' in namespace '%s', creating new one",
deploymentName, namespace)
HandleMissingTimer(deployment, pauseDuration, clients, namespace)
}
return deployment, nil
}
// Handles the case where missing timers for deployments that have been paused by reloader.
// Could occur after new leader election or reloader restart
func HandleMissingTimer(deployment *app.Deployment, pauseDuration time.Duration, clients kube.Clients, namespace string) {
deploymentName := deployment.Name
pauseStartTime, err := GetPauseStartTime(deployment)
if err != nil {
logrus.Errorf("Error parsing pause start time for deployment '%s' in namespace '%s': %v. Resuming deployment immediately",
deploymentName, namespace, err)
ResumeDeployment(deployment, namespace, clients)
return
}
if pauseStartTime == nil {
return
}
elapsedPauseTime := time.Since(*pauseStartTime)
remainingPauseTime := pauseDuration - elapsedPauseTime
if remainingPauseTime <= 0 {
logrus.Infof("Pause period for deployment '%s' in namespace '%s' has expired. Resuming immediately",
deploymentName, namespace)
ResumeDeployment(deployment, namespace, clients)
return
}
logrus.Infof("Creating missing timer for already paused deployment '%s' in namespace '%s' with remaining time %s",
deploymentName, namespace, remainingPauseTime)
CreateResumeTimer(deployment, clients, namespace, remainingPauseTime)
}
// CreateResumeTimer creates a timer to resume the deployment after the specified duration
func CreateResumeTimer(deployment *app.Deployment, clients kube.Clients, namespace string, pauseDuration time.Duration) {
deploymentName := deployment.Name
timerKey := getTimerKey(namespace, deployment.Name)
// Check if there's an existing timer for this deployment
if _, exists := activeTimers[timerKey]; exists {
logrus.Debugf("Timer already exists for deployment '%s' in namespace '%s', Skipping creation",
deploymentName, namespace)
return
}
// Create and store the new timer
timer := time.AfterFunc(pauseDuration, func() {
ResumeDeployment(deployment, namespace, clients)
})
// Add the new timer to the map
activeTimers[timerKey] = timer
logrus.Debugf("Created pause timer for deployment '%s' in namespace '%s' with duration %s",
deploymentName, namespace, pauseDuration)
}
// ResumeDeployment resumes a deployment that has been paused by reloader
func ResumeDeployment(deployment *app.Deployment, namespace string, clients kube.Clients) {
deploymentName := deployment.Name
currentDeployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
if err != nil {
logrus.Errorf("Failed to get deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return
}
if !IsPausedByReloader(currentDeployment) {
logrus.Infof("Deployment '%s' in namespace '%s' not paused by Reloader. Skipping resume", deploymentName, namespace)
return
}
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
resumePatch, err := CreateResumePatch()
if err != nil {
logrus.Errorf("Failed to create resume patch for deployment '%s': %v", deploymentName, err)
return
}
// Remove the timer
timerKey := getTimerKey(namespace, deploymentName)
if timer, exists := activeTimers[timerKey]; exists {
timer.Stop()
delete(activeTimers, timerKey)
logrus.Debugf("Removed pause timer for deployment '%s' in namespace '%s'", deploymentName, namespace)
}
err = deploymentFuncs.PatchFunc(clients, namespace, currentDeployment, patchtypes.StrategicMergePatchType, resumePatch)
if err != nil {
logrus.Errorf("Failed to resume deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
return
}
logrus.Infof("Successfully resumed deployment '%s' in namespace '%s'", deploymentName, namespace)
}
func CreatePausePatch() ([]byte, error) {
patchData := map[string]interface{}{
"spec": map[string]interface{}{
"paused": true,
},
"metadata": map[string]interface{}{
"annotations": map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Format(time.RFC3339),
},
},
}
return json.Marshal(patchData)
}
func CreateResumePatch() ([]byte, error) {
patchData := map[string]interface{}{
"spec": map[string]interface{}{
"paused": false,
},
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
options.PauseDeploymentTimeAnnotation: nil,
},
},
}
return json.Marshal(patchData)
}

View File

@@ -0,0 +1,392 @@
package handler
import (
"context"
"fmt"
"testing"
"time"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/pkg/kube"
"github.com/stretchr/testify/assert"
app "k8s.io/api/apps/v1"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
testclient "k8s.io/client-go/kubernetes/fake"
)
func TestIsPaused(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
paused bool
}{
{
name: "paused deployment",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
paused: true,
},
{
name: "unpaused deployment",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
paused: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := IsPaused(test.deployment)
assert.Equal(t, test.paused, result)
})
}
}
func TestIsPausedByReloader(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
pausedByReloader bool
}{
{
name: "paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Format(time.RFC3339),
},
},
},
pausedByReloader: true,
},
{
name: "not paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
},
pausedByReloader: false,
},
{
name: "not paused",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
pausedByReloader: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pausedByReloader := IsPausedByReloader(test.deployment)
assert.Equal(t, test.pausedByReloader, pausedByReloader)
})
}
}
func TestGetPauseStartTime(t *testing.T) {
now := time.Now()
nowStr := now.Format(time.RFC3339)
tests := []struct {
name string
deployment *appsv1.Deployment
pausedByReloader bool
expectedStartTime time.Time
}{
{
name: "valid pause time",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: true,
},
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: nowStr,
},
},
},
pausedByReloader: true,
expectedStartTime: now,
},
{
name: "not paused by reloader",
deployment: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
pausedByReloader: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualStartTime, err := GetPauseStartTime(test.deployment)
assert.NoError(t, err)
if !test.pausedByReloader {
assert.Nil(t, actualStartTime)
} else {
assert.NotNil(t, actualStartTime)
assert.WithinDuration(t, test.expectedStartTime, *actualStartTime, time.Second)
}
})
}
}
func TestParsePauseDuration(t *testing.T) {
tests := []struct {
name string
pauseIntervalValue string
expectedDuration time.Duration
invalidDuration bool
}{
{
name: "valid duration",
pauseIntervalValue: "10s",
expectedDuration: 10 * time.Second,
invalidDuration: false,
},
{
name: "valid minute duration",
pauseIntervalValue: "2m",
expectedDuration: 2 * time.Minute,
invalidDuration: false,
},
{
name: "invalid duration",
pauseIntervalValue: "invalid",
expectedDuration: 0,
invalidDuration: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualDuration, err := ParsePauseDuration(test.pauseIntervalValue)
if test.invalidDuration {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expectedDuration, actualDuration)
}
})
}
}
func TestHandleMissingTimerSimple(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
shouldBePaused bool // Should be unpaused after HandleMissingTimer ?
}{
{
name: "deployment paused by reloader, pause period has expired and no timer",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-1",
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Add(-6 * time.Minute).Format(time.RFC3339),
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
shouldBePaused: false,
},
{
name: "deployment paused by reloader, pause period expires in the future and no timer",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-2",
Annotations: map[string]string{
options.PauseDeploymentTimeAnnotation: time.Now().Add(1 * time.Minute).Format(time.RFC3339),
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
shouldBePaused: true,
},
}
for _, test := range tests {
// Clean up any timers at the end of the test
defer func() {
for key, timer := range activeTimers {
timer.Stop()
delete(activeTimers, key)
}
}()
t.Run(test.name, func(t *testing.T) {
fakeClient := testclient.NewSimpleClientset()
clients := kube.Clients{
KubernetesClient: fakeClient,
}
_, err := fakeClient.AppsV1().Deployments("default").Create(
context.TODO(),
test.deployment,
metav1.CreateOptions{})
assert.NoError(t, err, "Expected no error when creating deployment")
pauseDuration, _ := ParsePauseDuration(test.deployment.Annotations[options.PauseDeploymentAnnotation])
HandleMissingTimer(test.deployment, pauseDuration, clients, "default")
updatedDeployment, _ := fakeClient.AppsV1().Deployments("default").Get(context.TODO(), test.deployment.Name, metav1.GetOptions{})
assert.Equal(t, test.shouldBePaused, updatedDeployment.Spec.Paused,
"Deployment should have correct paused state after timer expiration")
if test.shouldBePaused {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.NotEmpty(t, pausedAtAnnotationValue,
"Pause annotation should be present and contain a value when deployment is paused")
}
})
}
}
func TestPauseDeployment(t *testing.T) {
tests := []struct {
name string
deployment *appsv1.Deployment
expectedError bool
expectedPaused bool
expectedAnnotation bool // Should have pause time annotation
pauseInterval string
}{
{
name: "deployment without pause annotation",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Annotations: map[string]string{},
},
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
expectedError: true,
expectedPaused: false,
expectedAnnotation: false,
pauseInterval: "",
},
{
name: "deployment already paused but not by reloader",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Annotations: map[string]string{
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: true,
},
},
expectedError: false,
expectedPaused: true,
expectedAnnotation: false,
pauseInterval: "5m",
},
{
name: "deployment unpaused that needs to be paused by reloader",
deployment: &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment-3",
Annotations: map[string]string{
options.PauseDeploymentAnnotation: "5m",
},
},
Spec: appsv1.DeploymentSpec{
Paused: false,
},
},
expectedError: false,
expectedPaused: true,
expectedAnnotation: true,
pauseInterval: "5m",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeClient := testclient.NewSimpleClientset()
clients := kube.Clients{
KubernetesClient: fakeClient,
}
_, err := fakeClient.AppsV1().Deployments("default").Create(
context.TODO(),
test.deployment,
metav1.CreateOptions{})
assert.NoError(t, err, "Expected no error when creating deployment")
updatedDeployment, err := PauseDeployment(test.deployment, clients, "default", test.pauseInterval)
if test.expectedError {
assert.Error(t, err, "Expected an error pausing the deployment")
return
} else {
assert.NoError(t, err, "Expected no error pausing the deployment")
}
assert.Equal(t, test.expectedPaused, updatedDeployment.Spec.Paused,
"Deployment should have correct paused state after pause")
if test.expectedAnnotation {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.NotEmpty(t, pausedAtAnnotationValue,
"Pause annotation should be present and contain a value when deployment is paused")
} else {
pausedAtAnnotationValue := updatedDeployment.Annotations[options.PauseDeploymentTimeAnnotation]
assert.Empty(t, pausedAtAnnotationValue,
"Pause annotation should not be present when deployment has not been paused by reloader")
}
})
}
}
// Simple helper function for test cases
func FindDeploymentByName(deployments []runtime.Object, deploymentName string) (*app.Deployment, error) {
for _, deployment := range deployments {
accessor, err := meta.Accessor(deployment)
if err != nil {
return nil, fmt.Errorf("error getting accessor for item: %v", err)
}
if accessor.GetName() == deploymentName {
deploymentObj, ok := deployment.(*app.Deployment)
if !ok {
return nil, fmt.Errorf("failed to cast to Deployment")
}
return deploymentObj, nil
}
}
return nil, fmt.Errorf("deployment '%s' not found", deploymentName)
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
)
@@ -36,15 +37,15 @@ func (r ResourceUpdatedHandler) Handle() error {
}
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
func (r ResourceUpdatedHandler) GetConfig() (util.Config, string) {
func (r ResourceUpdatedHandler) GetConfig() (common.Config, string) {
var oldSHAData string
var config util.Config
var config common.Config
if _, ok := r.Resource.(*v1.ConfigMap); ok {
oldSHAData = util.GetSHAfromConfigmap(r.OldResource.(*v1.ConfigMap))
config = util.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap))
} else if _, ok := r.Resource.(*v1.Secret); ok {
oldSHAData = util.GetSHAfromSecret(r.OldResource.(*v1.Secret).Data)
config = util.GetSecretConfig(r.Resource.(*v1.Secret))
config = common.GetSecretConfig(r.Resource.(*v1.Secret))
} else {
logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource)
}

View File

@@ -7,9 +7,6 @@ import (
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
"github.com/parnurzeal/gorequest"
"github.com/prometheus/client_golang/prometheus"
@@ -20,7 +17,9 @@ import (
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
app "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
@@ -139,7 +138,7 @@ func GetArgoRolloutRollingUpgradeFuncs() callbacks.RollingUpgradeFuncs {
}
}
func sendUpgradeWebhook(config util.Config, webhookUrl string) error {
func sendUpgradeWebhook(config common.Config, webhookUrl string) error {
logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s', Sending webhook to '%s'",
config.ResourceName, config.Type, config.Namespace, webhookUrl)
@@ -170,7 +169,7 @@ func sendWebhook(url string) (string, []error) {
return buffer.String(), nil
}
func doRollingUpgrade(config util.Config, collectors metrics.Collectors, recorder record.EventRecorder, invoke invokeStrategy) error {
func doRollingUpgrade(config common.Config, collectors metrics.Collectors, recorder record.EventRecorder, invoke invokeStrategy) error {
clients := kube.GetClients()
err := rollingUpgrade(clients, config, GetDeploymentRollingUpgradeFuncs(), collectors, recorder, invoke)
@@ -204,7 +203,7 @@ func doRollingUpgrade(config util.Config, collectors metrics.Collectors, recorde
return nil
}
func rollingUpgrade(clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
func rollingUpgrade(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
err := PerformAction(clients, config, upgradeFuncs, collectors, recorder, strategy)
if err != nil {
logrus.Errorf("Rolling upgrade for '%s' failed with error = %v", config.ResourceName, err)
@@ -213,7 +212,7 @@ func rollingUpgrade(clients kube.Clients, config util.Config, upgradeFuncs callb
}
// PerformAction invokes the deployment if there is any change in configmap or secret data
func PerformAction(clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
func PerformAction(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy) error {
items := upgradeFuncs.ItemsFunc(clients, config.Namespace)
for _, item := range items {
@@ -250,7 +249,7 @@ func retryOnConflict(backoff wait.Backoff, fn func(_ bool) error) error {
return err
}
func upgradeResource(clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy, resource runtime.Object, fetchResource bool) error {
func upgradeResource(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, recorder record.EventRecorder, strategy invokeStrategy, resource runtime.Object, fetchResource bool) error {
accessor, err := meta.Accessor(resource)
if err != nil {
return err
@@ -263,125 +262,76 @@ func upgradeResource(clients kube.Clients, config util.Config, upgradeFuncs call
return err
}
}
// find correct annotation and update the resource
annotations := upgradeFuncs.AnnotationsFunc(resource)
annotationValue, found := annotations[config.Annotation]
searchAnnotationValue, foundSearchAnn := annotations[options.AutoSearchAnnotation]
reloaderEnabledValue, foundAuto := annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue, foundTypedAuto := annotations[config.TypedAutoAnnotation]
excludeConfigmapAnnotationValue, foundExcludeConfigmap := annotations[options.ConfigmapExcludeReloaderAnnotation]
excludeSecretAnnotationValue, foundExcludeSecret := annotations[options.SecretExcludeReloaderAnnotation]
podAnnotations := upgradeFuncs.PodAnnotationsFunc(resource)
result := common.ShouldReload(config, upgradeFuncs.ResourceType, annotations, podAnnotations, common.GetCommandLineOptions())
if !found && !foundAuto && !foundTypedAuto && !foundSearchAnn {
annotations = upgradeFuncs.PodAnnotationsFunc(resource)
annotationValue = annotations[config.Annotation]
searchAnnotationValue = annotations[options.AutoSearchAnnotation]
reloaderEnabledValue = annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue = annotations[config.TypedAutoAnnotation]
}
isResourceExcluded := false
switch config.Type {
case constants.ConfigmapEnvVarPostfix:
if foundExcludeConfigmap {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeConfigmapAnnotationValue)
}
case constants.SecretEnvVarPostfix:
if foundExcludeSecret {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeSecretAnnotationValue)
}
}
if isResourceExcluded {
if !result.ShouldReload {
logrus.Debugf("No changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace)
return nil
}
strategyResult := InvokeStrategyResult{constants.NotUpdated, nil}
reloaderEnabled, _ := strconv.ParseBool(reloaderEnabledValue)
typedAutoAnnotationEnabled, _ := strconv.ParseBool(typedAutoAnnotationEnabledValue)
if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && options.AutoReloadAll {
strategyResult = strategy(upgradeFuncs, resource, config, true)
strategyResult := strategy(upgradeFuncs, resource, config, result.AutoReload)
if strategyResult.Result != constants.Updated {
return nil
}
if strategyResult.Result != constants.Updated && annotationValue != "" {
values := strings.Split(annotationValue, ",")
for _, value := range values {
value = strings.TrimSpace(value)
re := regexp.MustCompile("^" + value + "$")
if re.Match([]byte(config.ResourceName)) {
strategyResult = strategy(upgradeFuncs, resource, config, false)
if strategyResult.Result == constants.Updated {
break
}
}
}
}
// find correct annotation and update the resource
pauseInterval, foundPauseInterval := annotations[options.PauseDeploymentAnnotation]
if strategyResult.Result != constants.Updated && searchAnnotationValue == "true" {
matchAnnotationValue := config.ResourceAnnotations[options.SearchMatchAnnotation]
if matchAnnotationValue == "true" {
strategyResult = strategy(upgradeFuncs, resource, config, true)
}
}
if strategyResult.Result == constants.Updated {
var err error
if upgradeFuncs.SupportsPatch && strategyResult.Patch != nil {
err = upgradeFuncs.PatchFunc(clients, config.Namespace, resource, strategyResult.Patch.Type, strategyResult.Patch.Bytes)
if foundPauseInterval {
deployment, ok := resource.(*app.Deployment)
if !ok {
logrus.Warnf("Annotation '%s' only applicable for deployments", options.PauseDeploymentAnnotation)
} else {
err = upgradeFuncs.UpdateFunc(clients, config.Namespace, resource)
_, err = PauseDeployment(deployment, clients, config.Namespace, pauseInterval)
if err != nil {
logrus.Errorf("Failed to pause deployment '%s' in namespace '%s': %v", resourceName, config.Namespace, err)
return err
}
}
}
if err != nil {
message := fmt.Sprintf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err)
logrus.Errorf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err)
if upgradeFuncs.SupportsPatch && strategyResult.Patch != nil {
err = upgradeFuncs.PatchFunc(clients, config.Namespace, resource, strategyResult.Patch.Type, strategyResult.Patch.Bytes)
} else {
err = upgradeFuncs.UpdateFunc(clients, config.Namespace, resource)
}
collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Inc()
collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "false", "namespace": config.Namespace}).Inc()
if recorder != nil {
recorder.Event(resource, v1.EventTypeWarning, "ReloadFail", message)
}
return err
} else {
message := fmt.Sprintf("Changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace)
message += fmt.Sprintf(", Updated '%s' of type '%s' in namespace '%s'", resourceName, upgradeFuncs.ResourceType, config.Namespace)
if err != nil {
message := fmt.Sprintf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err)
logrus.Errorf("Update for '%s' of type '%s' in namespace '%s' failed with error %v", resourceName, upgradeFuncs.ResourceType, config.Namespace, err)
logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s'; updated '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace)
collectors.Reloaded.With(prometheus.Labels{"success": "false"}).Inc()
collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "false", "namespace": config.Namespace}).Inc()
if recorder != nil {
recorder.Event(resource, v1.EventTypeWarning, "ReloadFail", message)
}
return err
} else {
message := fmt.Sprintf("Changes detected in '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace)
message += fmt.Sprintf(", Updated '%s' of type '%s' in namespace '%s'", resourceName, upgradeFuncs.ResourceType, config.Namespace)
collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Inc()
collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": config.Namespace}).Inc()
alert_on_reload, ok := os.LookupEnv("ALERT_ON_RELOAD")
if recorder != nil {
recorder.Event(resource, v1.EventTypeNormal, "Reloaded", message)
}
if ok && alert_on_reload == "true" {
msg := fmt.Sprintf(
"Reloader detected changes in *%s* of type *%s* in namespace *%s*. Hence reloaded *%s* of type *%s* in namespace *%s*",
config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace)
alert.SendWebhookAlert(msg)
}
logrus.Infof("Changes detected in '%s' of type '%s' in namespace '%s'; updated '%s' of type '%s' in namespace '%s'", config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace)
collectors.Reloaded.With(prometheus.Labels{"success": "true"}).Inc()
collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": config.Namespace}).Inc()
alert_on_reload, ok := os.LookupEnv("ALERT_ON_RELOAD")
if recorder != nil {
recorder.Event(resource, v1.EventTypeNormal, "Reloaded", message)
}
if ok && alert_on_reload == "true" {
msg := fmt.Sprintf(
"Reloader detected changes in *%s* of type *%s* in namespace *%s*. Hence reloaded *%s* of type *%s* in namespace *%s*",
config.ResourceName, config.Type, config.Namespace, resourceName, upgradeFuncs.ResourceType, config.Namespace)
alert.SendWebhookAlert(msg)
}
}
return nil
}
func checkIfResourceIsExcluded(resourceName, excludedResources string) bool {
if excludedResources == "" {
return false
}
excludedResourcesList := strings.Split(excludedResources, ",")
for _, excludedResource := range excludedResourcesList {
if strings.TrimSpace(excludedResource) == resourceName {
return true
}
}
return false
}
func getVolumeMountName(volumes []v1.Volume, mountType string, volumeName string) string {
for i := range volumes {
if mountType == constants.ConfigmapEnvVarPostfix {
@@ -453,7 +403,7 @@ func getContainerWithEnvReference(containers []v1.Container, resourceName string
return nil
}
func getContainerUsingResource(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) *v1.Container {
func getContainerUsingResource(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) *v1.Container {
volumes := upgradeFuncs.VolumesFunc(item)
containers := upgradeFuncs.ContainersFunc(item)
initContainers := upgradeFuncs.InitContainersFunc(item)
@@ -467,7 +417,11 @@ func getContainerUsingResource(upgradeFuncs callbacks.RollingUpgradeFuncs, item
container = getContainerWithVolumeMount(initContainers, volumeMountName)
if container != nil {
// if configmap/secret is being used in init container then return the first Pod container to save reloader env
return &containers[0]
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
} else if container != nil {
return container
@@ -480,13 +434,21 @@ func getContainerUsingResource(upgradeFuncs callbacks.RollingUpgradeFuncs, item
container = getContainerWithEnvReference(initContainers, config.ResourceName, config.Type)
if container != nil {
// if configmap/secret is being used in init container then return the first Pod container to save reloader env
return &containers[0]
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
}
// Get the first container if the annotation is related to specified configmap or secret i.e. configmap.reloader.stakater.com/reload
if container == nil && !autoReload {
return &containers[0]
if len(containers) > 0 {
return &containers[0]
}
// No containers available, return nil to avoid crash
return nil
}
return container
@@ -502,16 +464,16 @@ type InvokeStrategyResult struct {
Patch *Patch
}
type invokeStrategy func(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult
type invokeStrategy func(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult
func invokeReloadStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func invokeReloadStrategy(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
if options.ReloadStrategy == constants.AnnotationsReloadStrategy {
return updatePodAnnotations(upgradeFuncs, item, config, autoReload)
}
return updateContainerEnvVars(upgradeFuncs, item, config, autoReload)
}
func updatePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func updatePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)
if container == nil {
return InvokeStrategyResult{constants.NoContainerFound, nil}
@@ -519,7 +481,7 @@ func updatePodAnnotations(upgradeFuncs callbacks.RollingUpgradeFuncs, item runti
// Generate reloaded annotations. Attaching this to the item's annotation will trigger a rollout
// Note: the data on this struct is purely informational and is not used for future updates
reloadSource := util.NewReloadSourceFromConfig(config, []string{container.Name})
reloadSource := common.NewReloadSourceFromConfig(config, []string{container.Name})
annotations, patch, err := createReloadedAnnotations(&reloadSource, upgradeFuncs)
if err != nil {
logrus.Errorf("Failed to create reloaded annotations for %s! error = %v", config.ResourceName, err)
@@ -546,7 +508,7 @@ func getReloaderAnnotationKey() string {
)
}
func createReloadedAnnotations(target *util.ReloadSource, upgradeFuncs callbacks.RollingUpgradeFuncs) (map[string]string, []byte, error) {
func createReloadedAnnotations(target *common.ReloadSource, upgradeFuncs callbacks.RollingUpgradeFuncs) (map[string]string, []byte, error) {
if target == nil {
return nil, nil, errors.New("target is required")
}
@@ -581,7 +543,7 @@ func getEnvVarName(resourceName string, typeName string) string {
return constants.EnvVarPrefix + util.ConvertToEnvVarName(resourceName) + "_" + typeName
}
func updateContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config util.Config, autoReload bool) InvokeStrategyResult {
func updateContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item runtime.Object, config common.Config, autoReload bool) InvokeStrategyResult {
envVar := getEnvVarName(config.ResourceName, config.Type)
container := getContainerUsingResource(upgradeFuncs, item, config, autoReload)

View File

@@ -7,6 +7,7 @@ import (
"testing"
"time"
argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/prometheus/client_golang/prometheus"
promtestutil "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/sirupsen/logrus"
@@ -16,8 +17,10 @@ import (
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -52,6 +55,9 @@ var (
arsConfigmapWithConfigMapAutoAnnotation = "testconfigmapwithconfigmapautoannotationdeployment-handler-" + testutil.RandSeq(5)
arsSecretWithExcludeSecretAnnotation = "testsecretwithsecretexcludeannotationdeployment-handler-" + testutil.RandSeq(5)
arsConfigmapWithExcludeConfigMapAnnotation = "testconfigmapwithconfigmapexcludeannotationdeployment-handler-" + testutil.RandSeq(5)
arsConfigmapWithIgnoreAnnotation = "testconfigmapWithIgnoreAnnotation-handler-" + testutil.RandSeq(5)
arsSecretWithIgnoreAnnotation = "testsecretWithIgnoreAnnotation-handler-" + testutil.RandSeq(5)
arsConfigmapWithPausedDeployment = "testconfigmapWithPausedDeployment-handler-" + testutil.RandSeq(5)
ersNamespace = "test-handler-" + testutil.RandSeq(5)
ersConfigmapName = "testconfigmap-handler-" + testutil.RandSeq(5)
@@ -75,6 +81,9 @@ var (
ersConfigmapWithConfigMapAutoAnnotation = "testconfigmapwithconfigmapautoannotationdeployment-handler-" + testutil.RandSeq(5)
ersSecretWithSecretExcludeAnnotation = "testsecretwithsecretexcludeannotationdeployment-handler-" + testutil.RandSeq(5)
ersConfigmapWithConfigMapExcludeAnnotation = "testconfigmapwithconfigmapexcludeannotationdeployment-handler-" + testutil.RandSeq(5)
ersConfigmapWithIgnoreAnnotation = "testconfigmapWithIgnoreAnnotation-handler-" + testutil.RandSeq(5)
ersSecretWithIgnoreAnnotation = "testsecretWithIgnoreAnnotation-handler-" + testutil.RandSeq(5)
ersConfigmapWithPausedDeployment = "testconfigmapWithPausedDeployment-handler-" + testutil.RandSeq(5)
)
func TestMain(m *testing.M) {
@@ -203,6 +212,12 @@ func setupArs() {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating configmap for testing pausing deployments
_, err = testutil.CreateConfigMap(clients.KubernetesClient, arsNamespace, arsConfigmapWithPausedDeployment, "www.google.com")
if err != nil {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating secret used with secret auto annotation
_, err = testutil.CreateSecret(clients.KubernetesClient, arsNamespace, arsSecretWithExcludeSecretAnnotation, data)
if err != nil {
@@ -215,6 +230,35 @@ func setupArs() {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating configmap with ignore annotation
_, err = testutil.CreateConfigMap(clients.KubernetesClient, arsNamespace, arsConfigmapWithIgnoreAnnotation, "www.google.com")
if err != nil {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Patch with ignore annotation
cmClient := clients.KubernetesClient.CoreV1().ConfigMaps(arsNamespace)
patch := []byte(`{"metadata":{"annotations":{"reloader.stakater.com/ignore":"true"}}}`)
_, _ = cmClient.Patch(context.TODO(), arsConfigmapWithIgnoreAnnotation, patchtypes.MergePatchType, patch, metav1.PatchOptions{})
// Creating secret with ignore annotation
_, err = testutil.CreateSecret(clients.KubernetesClient, arsNamespace, arsSecretWithIgnoreAnnotation, data)
if err != nil {
logrus.Errorf("Error in secret creation: %v", err)
}
secretClient := clients.KubernetesClient.CoreV1().Secrets(arsNamespace)
_, _ = secretClient.Patch(context.TODO(), arsSecretWithIgnoreAnnotation, patchtypes.MergePatchType, patch, metav1.PatchOptions{})
// Creating Deployment referencing configmap with ignore annotation
_, err = testutil.CreateDeployment(clients.KubernetesClient, arsConfigmapWithIgnoreAnnotation, arsNamespace, true)
if err != nil {
logrus.Errorf("Error in Deployment with configmap ignore annotation creation: %v", err)
}
// Creating Deployment referencing secret with ignore annotation
_, err = testutil.CreateDeployment(clients.KubernetesClient, arsSecretWithIgnoreAnnotation, arsNamespace, true)
if err != nil {
logrus.Errorf("Error in Deployment with secret ignore annotation creation: %v", err)
}
// Creating Deployment with configmap
_, err = testutil.CreateDeployment(clients.KubernetesClient, arsConfigmapName, arsNamespace, true)
if err != nil {
@@ -424,6 +468,12 @@ func setupArs() {
if err != nil {
logrus.Errorf("Error in Deployment with both annotations: %v", err)
}
// Creating Deployment with pause annotation
_, err = testutil.CreateDeploymentWithAnnotations(clients.KubernetesClient, arsConfigmapWithPausedDeployment, arsNamespace, map[string]string{options.PauseDeploymentAnnotation: "10s"}, false)
if err != nil {
logrus.Errorf("Error in Deployment with configmap creation: %v", err)
}
}
func teardownArs() {
@@ -625,6 +675,12 @@ func teardownArs() {
logrus.Errorf("Error while deleting statefulSet with secret as env var source %v", statefulSetError)
}
// Deleting Deployment with pause annotation
deploymentError = testutil.DeleteDeployment(clients.KubernetesClient, arsNamespace, arsConfigmapWithPausedDeployment)
if deploymentError != nil {
logrus.Errorf("Error while deleting deployment with configmap %v", deploymentError)
}
// Deleting Configmap
err := testutil.DeleteConfigMap(clients.KubernetesClient, arsNamespace, arsConfigmapName)
if err != nil {
@@ -655,7 +711,7 @@ func teardownArs() {
logrus.Errorf("Error while deleting the configmap %v", err)
}
// Deleting Configmap used projected volume in init containers
// Deleting secret used in projected volume in init containers
err = testutil.DeleteSecret(clients.KubernetesClient, arsNamespace, arsProjectedSecretWithInitContainer)
if err != nil {
logrus.Errorf("Error while deleting the secret %v", err)
@@ -738,6 +794,12 @@ func teardownArs() {
logrus.Errorf("Error while deleting the configmap used with configmap auto annotations: %v", err)
}
// Deleting configmap for testing pausing deployments
err = testutil.DeleteConfigMap(clients.KubernetesClient, arsNamespace, arsConfigmapWithPausedDeployment)
if err != nil {
logrus.Errorf("Error while deleting the configmap: %v", err)
}
// Deleting namespace
testutil.DeleteNamespace(arsNamespace, clients.KubernetesClient)
@@ -797,6 +859,12 @@ func setupErs() {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating configmap for testing pausing deployments
_, err = testutil.CreateConfigMap(clients.KubernetesClient, ersNamespace, ersConfigmapWithPausedDeployment, "www.google.com")
if err != nil {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating secret
_, err = testutil.CreateSecret(clients.KubernetesClient, ersNamespace, ersSecretWithInitEnv, data)
if err != nil {
@@ -854,6 +922,34 @@ func setupErs() {
logrus.Errorf("Error in configmap creation: %v", err)
}
// Creating configmap with ignore annotation
_, err = testutil.CreateConfigMap(clients.KubernetesClient, ersNamespace, ersConfigmapWithIgnoreAnnotation, "www.google.com")
if err != nil {
logrus.Errorf("Error in configmap creation: %v", err)
}
cmClient := clients.KubernetesClient.CoreV1().ConfigMaps(ersNamespace)
patch := []byte(`{"metadata":{"annotations":{"reloader.stakater.com/ignore":"true"}}}`)
_, _ = cmClient.Patch(context.TODO(), ersConfigmapWithIgnoreAnnotation, patchtypes.MergePatchType, patch, metav1.PatchOptions{})
// Creating secret with ignore annotation
_, err = testutil.CreateSecret(clients.KubernetesClient, ersNamespace, ersSecretWithIgnoreAnnotation, data)
if err != nil {
logrus.Errorf("Error in secret creation: %v", err)
}
secretClient := clients.KubernetesClient.CoreV1().Secrets(ersNamespace)
_, _ = secretClient.Patch(context.TODO(), ersSecretWithIgnoreAnnotation, patchtypes.MergePatchType, patch, metav1.PatchOptions{})
// Creating Deployment referencing configmap with ignore annotation
_, err = testutil.CreateDeployment(clients.KubernetesClient, ersConfigmapWithIgnoreAnnotation, ersNamespace, true)
if err != nil {
logrus.Errorf("Error in Deployment with configmap ignore annotation creation: %v", err)
}
// Creating Deployment referencing secret with ignore annotation
_, err = testutil.CreateDeployment(clients.KubernetesClient, ersSecretWithIgnoreAnnotation, ersNamespace, true)
if err != nil {
logrus.Errorf("Error in Deployment with secret ignore annotation creation: %v", err)
}
// Creating Deployment with configmap
_, err = testutil.CreateDeployment(clients.KubernetesClient, ersConfigmapName, ersNamespace, true)
if err != nil {
@@ -973,6 +1069,12 @@ func setupErs() {
logrus.Errorf("Error in Deployment with configmap and with configmap exclude annotation: %v", err)
}
// Creating Deployment with pause annotation
_, err = testutil.CreateDeploymentWithAnnotations(clients.KubernetesClient, ersConfigmapWithPausedDeployment, ersNamespace, map[string]string{options.PauseDeploymentAnnotation: "10s"}, false)
if err != nil {
logrus.Errorf("Error in Deployment with configmap creation: %v", err)
}
// Creating DaemonSet with configmap
_, err = testutil.CreateDaemonSet(clients.KubernetesClient, ersConfigmapName, ersNamespace, true)
if err != nil {
@@ -1257,6 +1359,12 @@ func teardownErs() {
logrus.Errorf("Error while deleting statefulSet with secret as env var source %v", statefulSetError)
}
// Deleting Deployment for testing pausing deployments
deploymentError = testutil.DeleteDeployment(clients.KubernetesClient, ersNamespace, ersConfigmapWithPausedDeployment)
if deploymentError != nil {
logrus.Errorf("Error while deleting deployment with configmap %v", deploymentError)
}
// Deleting Configmap
err := testutil.DeleteConfigMap(clients.KubernetesClient, ersNamespace, ersConfigmapName)
if err != nil {
@@ -1287,7 +1395,7 @@ func teardownErs() {
logrus.Errorf("Error while deleting the configmap %v", err)
}
// Deleting Configmap used projected volume in init containers
// Deleting secret used in projected volume in init containers
err = testutil.DeleteSecret(clients.KubernetesClient, ersNamespace, ersProjectedSecretWithInitContainer)
if err != nil {
logrus.Errorf("Error while deleting the secret %v", err)
@@ -1370,18 +1478,24 @@ func teardownErs() {
logrus.Errorf("Error while deleting the configmap used with configmap exclude annotation: %v", err)
}
// Deleting ConfigMap for testing pausing deployments
err = testutil.DeleteConfigMap(clients.KubernetesClient, ersNamespace, ersConfigmapWithPausedDeployment)
if err != nil {
logrus.Errorf("Error while deleting the configmap: %v", err)
}
// Deleting namespace
testutil.DeleteNamespace(ersNamespace, clients.KubernetesClient)
}
func getConfigWithAnnotations(resourceType string, name string, shaData string, annotation string, typedAutoAnnotation string) util.Config {
func getConfigWithAnnotations(resourceType string, name string, shaData string, annotation string, typedAutoAnnotation string) common.Config {
ns := ersNamespace
if options.ReloadStrategy == constants.AnnotationsReloadStrategy {
ns = arsNamespace
}
return util.Config{
return common.Config{
Namespace: ns,
ResourceName: name,
SHAValue: shaData,
@@ -1398,7 +1512,7 @@ func getCollectors() metrics.Collectors {
var labelSucceeded = prometheus.Labels{"success": "true"}
var labelFailed = prometheus.Labels{"success": "false"}
func testRollingUpgradeInvokeDeleteStrategyArs(t *testing.T, clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
func testRollingUpgradeInvokeDeleteStrategyArs(t *testing.T, clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
err := PerformAction(clients, config, upgradeFuncs, collectors, nil, invokeDeleteStrategy)
time.Sleep(5 * time.Second)
if err != nil {
@@ -1416,7 +1530,7 @@ func testRollingUpgradeInvokeDeleteStrategyArs(t *testing.T, clients kube.Client
}
}
func testRollingUpgradeWithPatchAndInvokeDeleteStrategyArs(t *testing.T, clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
func testRollingUpgradeWithPatchAndInvokeDeleteStrategyArs(t *testing.T, clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
err := PerformAction(clients, config, upgradeFuncs, collectors, nil, invokeDeleteStrategy)
upgradeFuncs.PatchFunc = func(client kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
@@ -2136,7 +2250,7 @@ func TestRollingUpgradeForDeploymentWithSecretExcludeAnnotationUsingArs(t *testi
logrus.Infof("Verifying deployment did not update")
updated := testutil.VerifyResourceAnnotationUpdate(clients, config, deploymentFuncs)
if updated {
t.Errorf("Deployment which had to be exluded was updated")
t.Errorf("Deployment which had to be excluded was updated")
}
}
@@ -2737,7 +2851,66 @@ func TestFailedRollingUpgradeUsingArs(t *testing.T) {
}
}
func testRollingUpgradeInvokeDeleteStrategyErs(t *testing.T, clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
func TestIgnoreAnnotationNoReloadUsingArs(t *testing.T) {
options.ReloadStrategy = constants.AnnotationsReloadStrategy
envVarPostfix := constants.ConfigmapEnvVarPostfix
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, arsNamespace, arsConfigmapWithIgnoreAnnotation, "www.stakater.com")
config := getConfigWithAnnotations(envVarPostfix, arsConfigmapWithIgnoreAnnotation, shaData, options.ConfigmapUpdateOnChangeAnnotation, options.ConfigmapReloaderAutoAnnotation)
config.ResourceAnnotations = map[string]string{"reloader.stakater.com/ignore": "true"}
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
collectors := getCollectors()
err := PerformAction(clients, config, deploymentFuncs, collectors, nil, invokeReloadStrategy)
if err != nil {
t.Errorf("Rolling upgrade failed for Deployment with Configmap and ignore annotation using ARS")
}
// Ensure deployment is NOT updated
updated := testutil.VerifyResourceAnnotationUpdate(clients, config, deploymentFuncs)
if updated {
t.Errorf("Deployment was updated but should not have been")
}
// Ensure counters remain zero
if promtestutil.ToFloat64(collectors.Reloaded.With(labelSucceeded)) != 0 {
t.Errorf("Reload counter should not have increased")
}
if promtestutil.ToFloat64(collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": arsNamespace})) != 0 {
t.Errorf("Reload counter by namespace should not have increased")
}
}
func TestIgnoreAnnotationNoReloadUsingErs(t *testing.T) {
options.ReloadStrategy = constants.EnvVarsReloadStrategy
envVarPostfix := constants.ConfigmapEnvVarPostfix
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, ersNamespace, ersConfigmapWithIgnoreAnnotation, "www.stakater.com")
config := getConfigWithAnnotations(envVarPostfix, ersConfigmapWithIgnoreAnnotation, shaData, options.ConfigmapUpdateOnChangeAnnotation, options.ConfigmapReloaderAutoAnnotation)
config.ResourceAnnotations = map[string]string{"reloader.stakater.com/ignore": "true"}
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
collectors := getCollectors()
err := PerformAction(clients, config, deploymentFuncs, collectors, nil, invokeReloadStrategy)
if err != nil {
t.Errorf("Rolling upgrade failed for Deployment with Configmap and ignore annotation using ERS")
}
// Ensure deployment is NOT updated
updated := testutil.VerifyResourceEnvVarUpdate(clients, config, envVarPostfix, deploymentFuncs)
if updated {
t.Errorf("Deployment was updated but should not have been (ERS)")
}
// Ensure counters remain zero
if promtestutil.ToFloat64(collectors.Reloaded.With(labelSucceeded)) != 0 {
t.Errorf("Reload counter should not have increased (ERS)")
}
if promtestutil.ToFloat64(collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": ersNamespace})) != 0 {
t.Errorf("Reload counter by namespace should not have increased (ERS)")
}
}
func testRollingUpgradeInvokeDeleteStrategyErs(t *testing.T, clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
err := PerformAction(clients, config, upgradeFuncs, collectors, nil, invokeDeleteStrategy)
time.Sleep(5 * time.Second)
if err != nil {
@@ -2754,7 +2927,7 @@ func testRollingUpgradeInvokeDeleteStrategyErs(t *testing.T, clients kube.Client
}
}
func testRollingUpgradeWithPatchAndInvokeDeleteStrategyErs(t *testing.T, clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
func testRollingUpgradeWithPatchAndInvokeDeleteStrategyErs(t *testing.T, clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) {
assert.NotEmpty(t, upgradeFuncs.PatchTemplatesFunc().DeleteEnvVarTemplate)
err := PerformAction(clients, config, upgradeFuncs, collectors, nil, invokeDeleteStrategy)
@@ -3933,3 +4106,183 @@ func TestFailedRollingUpgradeUsingErs(t *testing.T) {
t.Errorf("Counter by namespace was not increased")
}
}
func TestPausingDeploymentUsingErs(t *testing.T) {
options.ReloadStrategy = constants.EnvVarsReloadStrategy
testPausingDeployment(t, options.ReloadStrategy, ersConfigmapWithPausedDeployment, ersNamespace)
}
func TestPausingDeploymentUsingArs(t *testing.T) {
options.ReloadStrategy = constants.AnnotationsReloadStrategy
testPausingDeployment(t, options.ReloadStrategy, arsConfigmapWithPausedDeployment, arsNamespace)
}
func testPausingDeployment(t *testing.T, reloadStrategy string, testName string, namespace string) {
options.ReloadStrategy = reloadStrategy
envVarPostfix := constants.ConfigmapEnvVarPostfix
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, testName, "pause.stakater.com")
config := getConfigWithAnnotations(envVarPostfix, testName, shaData, options.ConfigmapUpdateOnChangeAnnotation, options.ConfigmapReloaderAutoAnnotation)
deploymentFuncs := GetDeploymentRollingUpgradeFuncs()
collectors := getCollectors()
_ = PerformAction(clients, config, deploymentFuncs, collectors, nil, invokeReloadStrategy)
// Wait for deployment to have paused-at annotation
logrus.Infof("Waiting for deployment %s to have paused-at annotation", testName)
err := waitForDeploymentPausedAtAnnotation(clients, deploymentFuncs, config.Namespace, testName, 30*time.Second)
if err != nil {
t.Errorf("Failed to wait for deployment paused-at annotation: %v", err)
}
if promtestutil.ToFloat64(collectors.Reloaded.With(labelSucceeded)) != 1 {
t.Errorf("Counter was not increased")
}
if promtestutil.ToFloat64(collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": namespace})) != 1 {
t.Errorf("Counter by namespace was not increased")
}
logrus.Infof("Verifying deployment has been paused")
items := deploymentFuncs.ItemsFunc(clients, config.Namespace)
deploymentPaused, err := isDeploymentPaused(items, testName)
if err != nil {
t.Errorf("%s", err.Error())
}
if !deploymentPaused {
t.Errorf("Deployment has not been paused")
}
shaData = testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, namespace, testName, "pause-changed.stakater.com")
config = getConfigWithAnnotations(envVarPostfix, testName, shaData, options.ConfigmapUpdateOnChangeAnnotation, options.ConfigmapReloaderAutoAnnotation)
_ = PerformAction(clients, config, deploymentFuncs, collectors, nil, invokeReloadStrategy)
if promtestutil.ToFloat64(collectors.Reloaded.With(labelSucceeded)) != 2 {
t.Errorf("Counter was not increased")
}
if promtestutil.ToFloat64(collectors.ReloadedByNamespace.With(prometheus.Labels{"success": "true", "namespace": namespace})) != 2 {
t.Errorf("Counter by namespace was not increased")
}
logrus.Infof("Verifying deployment is still paused")
items = deploymentFuncs.ItemsFunc(clients, config.Namespace)
deploymentPaused, err = isDeploymentPaused(items, testName)
if err != nil {
t.Errorf("%s", err.Error())
}
if !deploymentPaused {
t.Errorf("Deployment should still be paused")
}
logrus.Infof("Verifying deployment has been resumed after pause interval")
time.Sleep(11 * time.Second)
items = deploymentFuncs.ItemsFunc(clients, config.Namespace)
deploymentPaused, err = isDeploymentPaused(items, testName)
if err != nil {
t.Errorf("%s", err.Error())
}
if deploymentPaused {
t.Errorf("Deployment should have been resumed after pause interval")
}
}
func isDeploymentPaused(deployments []runtime.Object, deploymentName string) (bool, error) {
deployment, err := FindDeploymentByName(deployments, deploymentName)
if err != nil {
return false, err
}
return IsPaused(deployment), nil
}
// waitForDeploymentPausedAtAnnotation waits for a deployment to have the pause-period annotation
func waitForDeploymentPausedAtAnnotation(clients kube.Clients, deploymentFuncs callbacks.RollingUpgradeFuncs, namespace, deploymentName string, timeout time.Duration) error {
start := time.Now()
for time.Since(start) < timeout {
items := deploymentFuncs.ItemsFunc(clients, namespace)
deployment, err := FindDeploymentByName(items, deploymentName)
if err == nil {
annotations := deployment.GetAnnotations()
if annotations != nil {
if _, exists := annotations[options.PauseDeploymentTimeAnnotation]; exists {
return nil
}
}
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for deployment %s to have pause-period annotation", deploymentName)
}
// MockArgoRolloutWithEmptyContainers creates a mock Argo Rollout with no containers
// This simulates the scenario where Argo Rollouts with workloadRef return empty containers
func MockArgoRolloutWithEmptyContainers(namespace, name string) *runtime.Object {
rollout := &argorolloutv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: argorolloutv1alpha1.RolloutSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{}, // Empty containers slice
InitContainers: []v1.Container{}, // Empty init containers slice
Volumes: []v1.Volume{}, // Empty volumes slice
},
},
},
}
var obj runtime.Object = rollout
return &obj
}
// TestGetContainerUsingResourceWithArgoRolloutEmptyContainers tests with real Argo Rollout functions
func TestGetContainerUsingResourceWithArgoRolloutEmptyContainers(t *testing.T) {
namespace := "test-namespace"
resourceName := "test-configmap"
// Use real Argo Rollout functions but mock the containers function
rolloutFuncs := GetArgoRolloutRollingUpgradeFuncs()
originalContainersFunc := rolloutFuncs.ContainersFunc
originalInitContainersFunc := rolloutFuncs.InitContainersFunc
// Override to return empty containers (simulating workloadRef scenario)
rolloutFuncs.ContainersFunc = func(item runtime.Object) []v1.Container {
return []v1.Container{} // Empty like workloadRef rollouts
}
rolloutFuncs.InitContainersFunc = func(item runtime.Object) []v1.Container {
return []v1.Container{} // Empty like workloadRef rollouts
}
// Restore original functions after test
defer func() {
rolloutFuncs.ContainersFunc = originalContainersFunc
rolloutFuncs.InitContainersFunc = originalInitContainersFunc
}()
// Use proper Argo Rollout object instead of Pod
mockRollout := MockArgoRolloutWithEmptyContainers(namespace, "test-rollout")
config := common.Config{
Namespace: namespace,
ResourceName: resourceName,
Type: constants.ConfigmapEnvVarPostfix,
SHAValue: "test-sha",
}
// Test both autoReload scenarios using subtests as suggested by Felix
for _, autoReload := range []bool{true, false} {
t.Run(fmt.Sprintf("autoReload_%t", autoReload), func(t *testing.T) {
// This tests the actual fix in the context of Argo Rollouts
result := getContainerUsingResource(rolloutFuncs, *mockRollout, config, autoReload)
if result != nil {
t.Errorf("Expected nil when using real Argo Rollout functions with empty containers (workloadRef scenario), got %v", result)
}
})
}
}

View File

@@ -16,7 +16,7 @@ import (
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/testutil"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
)
@@ -159,7 +159,7 @@ func TestRunLeaderElectionWithControllers(t *testing.T) {
// Verifying deployment update
logrus.Infof("Verifying pod envvars has been created")
shaData := testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com")
config := util.Config{
config := common.Config{
Namespace: testutil.Namespace,
ResourceName: configmapName,
SHAValue: shaData,
@@ -186,7 +186,7 @@ func TestRunLeaderElectionWithControllers(t *testing.T) {
// Verifying that the deployment was not updated as leadership has been lost
logrus.Infof("Verifying pod envvars has not been updated")
shaData = testutil.ConvertResourceToSHA(testutil.ConfigmapResourceType, testutil.Namespace, configmapName, "www.stakater.com/new")
config = util.Config{
config = common.Config{
Namespace: testutil.Namespace,
ResourceName: configmapName,
SHAValue: shaData,

View File

@@ -22,6 +22,8 @@ var (
SecretUpdateOnChangeAnnotation = "secret.reloader.stakater.com/reload"
// ReloaderAutoAnnotation is an annotation to detect changes in secrets/configmaps
ReloaderAutoAnnotation = "reloader.stakater.com/auto"
// IgnoreResourceAnnotation is an annotation to ignore changes in secrets/configmaps
IgnoreResourceAnnotation = "reloader.stakater.com/ignore"
// ConfigmapReloaderAutoAnnotation is an annotation to detect changes in configmaps
ConfigmapReloaderAutoAnnotation = "configmap.reloader.stakater.com/auto"
// SecretReloaderAutoAnnotation is an annotation to detect changes in secrets
@@ -38,6 +40,12 @@ var (
SearchMatchAnnotation = "reloader.stakater.com/match"
// RolloutStrategyAnnotation is an annotation to define rollout update strategy
RolloutStrategyAnnotation = "reloader.stakater.com/rollout-strategy"
// PauseDeploymentAnnotation is an annotation to define the time period to pause a deployment after
// a configmap/secret change has been detected. Valid values are described here: https://pkg.go.dev/time#ParseDuration
// only positive values are allowed
PauseDeploymentAnnotation = "deployment.reloader.stakater.com/pause-period"
// Annotation set by reloader to indicate that the deployment has been paused
PauseDeploymentTimeAnnotation = "deployment.reloader.stakater.com/paused-at"
// LogFormat is the log format to use (json, or empty string for default)
LogFormat = ""
// LogLevel is the log level to use (trace, debug, info, warning, error, fatal and panic)
@@ -55,6 +63,19 @@ var (
EnableHA = false
// Url to send a request to instead of triggering a reload
WebhookUrl = ""
// ResourcesToIgnore is a list of resources to ignore when watching for changes
ResourcesToIgnore = []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
NamespaceSelectors = []string{}
// ResourceSelectors is a list of resource selectors to watch for changes
ResourceSelectors = []string{}
// EnablePProf enables pprof for profiling
EnablePProf = false
// PProfAddr is the address to start pprof server on
// Default is :6060
PProfAddr = ":6060"
)
func ToArgoRolloutStrategy(s string) ArgoRolloutStrategy {

View File

@@ -21,6 +21,7 @@ import (
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
"github.com/stakater/Reloader/pkg/common"
"github.com/stakater/Reloader/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
@@ -733,7 +734,7 @@ func GetResourceSHAFromAnnotation(podAnnotations map[string]string) string {
return ""
}
var last util.ReloadSource
var last common.ReloadSource
bytes := []byte(annotationJson)
err := json.Unmarshal(bytes, &last)
if err != nil {
@@ -794,6 +795,26 @@ func CreateDeployment(client kubernetes.Interface, deploymentName string, namesp
return deployment, err
}
// CreateDeployment creates a deployment in given namespace and returns the Deployment
func CreateDeploymentWithAnnotations(client kubernetes.Interface, deploymentName string, namespace string, additionalAnnotations map[string]string, volumeMount bool) (*appsv1.Deployment, error) {
logrus.Infof("Creating Deployment")
deploymentClient := client.AppsV1().Deployments(namespace)
var deploymentObj *appsv1.Deployment
if volumeMount {
deploymentObj = GetDeployment(namespace, deploymentName)
} else {
deploymentObj = GetDeploymentWithEnvVars(namespace, deploymentName)
}
for annotationKey, annotationValue := range additionalAnnotations {
deploymentObj.Annotations[annotationKey] = annotationValue
}
deployment, err := deploymentClient.Create(context.TODO(), deploymentObj, metav1.CreateOptions{})
time.Sleep(3 * time.Second)
return deployment, err
}
// CreateDeploymentConfig creates a deploymentConfig in given namespace and returns the DeploymentConfig
func CreateDeploymentConfig(client appsclient.Interface, deploymentName string, namespace string, volumeMount bool) (*openshiftv1.DeploymentConfig, error) {
logrus.Infof("Creating DeploymentConfig")
@@ -1038,7 +1059,7 @@ func RandSeq(n int) string {
}
// VerifyResourceEnvVarUpdate verifies whether the rolling upgrade happened or not
func VerifyResourceEnvVarUpdate(clients kube.Clients, config util.Config, envVarPostfix string, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
func VerifyResourceEnvVarUpdate(clients kube.Clients, config common.Config, envVarPostfix string, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
items := upgradeFuncs.ItemsFunc(clients, config.Namespace)
for _, i := range items {
containers := upgradeFuncs.ContainersFunc(i)
@@ -1084,7 +1105,7 @@ func VerifyResourceEnvVarUpdate(clients kube.Clients, config util.Config, envVar
}
// VerifyResourceEnvVarRemoved verifies whether the rolling upgrade happened or not and all Envvars SKAKATER_name_CONFIGMAP/SECRET are removed
func VerifyResourceEnvVarRemoved(clients kube.Clients, config util.Config, envVarPostfix string, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
func VerifyResourceEnvVarRemoved(clients kube.Clients, config common.Config, envVarPostfix string, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
items := upgradeFuncs.ItemsFunc(clients, config.Namespace)
for _, i := range items {
containers := upgradeFuncs.ContainersFunc(i)
@@ -1133,7 +1154,7 @@ func VerifyResourceEnvVarRemoved(clients kube.Clients, config util.Config, envVa
}
// VerifyResourceAnnotationUpdate verifies whether the rolling upgrade happened or not
func VerifyResourceAnnotationUpdate(clients kube.Clients, config util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
func VerifyResourceAnnotationUpdate(clients kube.Clients, config common.Config, upgradeFuncs callbacks.RollingUpgradeFuncs) bool {
items := upgradeFuncs.ItemsFunc(clients, config.Namespace)
for _, i := range items {
podAnnotations := upgradeFuncs.PodAnnotationsFunc(i)

View File

@@ -3,10 +3,15 @@ package util
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/crypto"
"github.com/stakater/Reloader/internal/pkg/options"
v1 "k8s.io/api/core/v1"
)
@@ -54,8 +59,6 @@ func GetSHAfromSecret(data map[string][]byte) string {
type List []string
type Map map[string]string
func (l *List) Contains(s string) bool {
for _, v := range *l {
if v == s {
@@ -64,3 +67,48 @@ func (l *List) Contains(s string) bool {
}
return false
}
func ConfigureReloaderFlags(cmd *cobra.Command) {
cmd.PersistentFlags().BoolVar(&options.AutoReloadAll, "auto-reload-all", false, "Auto reload all resources")
cmd.PersistentFlags().StringVar(&options.ConfigmapUpdateOnChangeAnnotation, "configmap-annotation", "configmap.reloader.stakater.com/reload", "annotation to detect changes in configmaps, specified by name")
cmd.PersistentFlags().StringVar(&options.SecretUpdateOnChangeAnnotation, "secret-annotation", "secret.reloader.stakater.com/reload", "annotation to detect changes in secrets, specified by name")
cmd.PersistentFlags().StringVar(&options.ReloaderAutoAnnotation, "auto-annotation", "reloader.stakater.com/auto", "annotation to detect changes in secrets/configmaps")
cmd.PersistentFlags().StringVar(&options.ConfigmapReloaderAutoAnnotation, "configmap-auto-annotation", "configmap.reloader.stakater.com/auto", "annotation to detect changes in configmaps")
cmd.PersistentFlags().StringVar(&options.SecretReloaderAutoAnnotation, "secret-auto-annotation", "secret.reloader.stakater.com/auto", "annotation to detect changes in secrets")
cmd.PersistentFlags().StringVar(&options.AutoSearchAnnotation, "auto-search-annotation", "reloader.stakater.com/search", "annotation to detect changes in configmaps or secrets tagged with special match annotation")
cmd.PersistentFlags().StringVar(&options.SearchMatchAnnotation, "search-match-annotation", "reloader.stakater.com/match", "annotation to mark secrets or configmaps to match the search")
cmd.PersistentFlags().StringVar(&options.PauseDeploymentAnnotation, "pause-deployment-annotation", "deployment.reloader.stakater.com/pause-period", "annotation to define the time period to pause a deployment after a configmap/secret change has been detected")
cmd.PersistentFlags().StringVar(&options.PauseDeploymentTimeAnnotation, "pause-deployment-time-annotation", "deployment.reloader.stakater.com/paused-at", "annotation to indicate when a deployment was paused by Reloader")
cmd.PersistentFlags().StringVar(&options.LogFormat, "log-format", "", "Log format to use (empty string for text, or JSON)")
cmd.PersistentFlags().StringVar(&options.LogLevel, "log-level", "info", "Log level to use (trace, debug, info, warning, error, fatal and panic)")
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.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")
cmd.PersistentFlags().StringVar(&options.IsArgoRollouts, "is-Argo-Rollouts", "false", "Add support for argo rollouts")
cmd.PersistentFlags().StringVar(&options.ReloadStrategy, constants.ReloadStrategyFlag, constants.EnvVarsReloadStrategy, "Specifies the desired reload strategy")
cmd.PersistentFlags().StringVar(&options.ReloadOnCreate, "reload-on-create", "false", "Add support to watch create events")
cmd.PersistentFlags().StringVar(&options.ReloadOnDelete, "reload-on-delete", "false", "Add support to watch delete events")
cmd.PersistentFlags().BoolVar(&options.EnableHA, "enable-ha", false, "Adds support for running multiple replicas via leadership election")
cmd.PersistentFlags().BoolVar(&options.SyncAfterRestart, "sync-after-restart", false, "Sync add events after reloader restarts")
cmd.PersistentFlags().BoolVar(&options.EnablePProf, "enable-pprof", false, "Enable pprof for profiling")
cmd.PersistentFlags().StringVar(&options.PProfAddr, "pprof-addr", ":6060", "Address to start pprof server on. Default is :6060")
}
func GetIgnoredResourcesList() (List, error) {
ignoredResourcesList := options.ResourcesToIgnore // getStringSliceFromFlags(cmd, "resources-to-ignore")
for _, v := range ignoredResourcesList {
if v != "configMaps" && v != "secrets" {
return nil, fmt.Errorf("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not '%s'", v)
}
}
if len(ignoredResourcesList) > 1 {
return nil, errors.New("'resources-to-ignore' only accepts 'configMaps' or 'secrets', not both")
}
return ignoredResourcesList, nil
}

328
pkg/common/common.go Normal file
View File

@@ -0,0 +1,328 @@
package common
import (
"context"
"os"
"regexp"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/options"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
)
type Map map[string]string
type ReloadCheckResult struct {
ShouldReload bool
AutoReload bool
}
// ReloaderOptions contains all configurable options for the Reloader controller.
// These options control how Reloader behaves when watching for changes in ConfigMaps and Secrets.
type ReloaderOptions struct {
// AutoReloadAll enables automatic reloading of all resources when their corresponding ConfigMaps/Secrets are updated
AutoReloadAll bool `json:"autoReloadAll"`
// ConfigmapUpdateOnChangeAnnotation is the annotation key used to detect changes in ConfigMaps specified by name
ConfigmapUpdateOnChangeAnnotation string `json:"configmapUpdateOnChangeAnnotation"`
// SecretUpdateOnChangeAnnotation is the annotation key used to detect changes in Secrets specified by name
SecretUpdateOnChangeAnnotation string `json:"secretUpdateOnChangeAnnotation"`
// ReloaderAutoAnnotation is the annotation key used to detect changes in any referenced ConfigMaps or Secrets
ReloaderAutoAnnotation string `json:"reloaderAutoAnnotation"`
// IgnoreResourceAnnotation is the annotation key used to ignore resources from being watched
IgnoreResourceAnnotation string `json:"ignoreResourceAnnotation"`
// ConfigmapReloaderAutoAnnotation is the annotation key used to detect changes in ConfigMaps only
ConfigmapReloaderAutoAnnotation string `json:"configmapReloaderAutoAnnotation"`
// SecretReloaderAutoAnnotation is the annotation key used to detect changes in Secrets only
SecretReloaderAutoAnnotation string `json:"secretReloaderAutoAnnotation"`
// ConfigmapExcludeReloaderAnnotation is the annotation key containing comma-separated list of ConfigMaps to exclude from watching
ConfigmapExcludeReloaderAnnotation string `json:"configmapExcludeReloaderAnnotation"`
// SecretExcludeReloaderAnnotation is the annotation key containing comma-separated list of Secrets to exclude from watching
SecretExcludeReloaderAnnotation string `json:"secretExcludeReloaderAnnotation"`
// AutoSearchAnnotation is the annotation key used to detect changes in ConfigMaps/Secrets tagged with SearchMatchAnnotation
AutoSearchAnnotation string `json:"autoSearchAnnotation"`
// SearchMatchAnnotation is the annotation key used to tag ConfigMaps/Secrets to be found by AutoSearchAnnotation
SearchMatchAnnotation string `json:"searchMatchAnnotation"`
// RolloutStrategyAnnotation is the annotation key used to define the rollout update strategy for workloads
RolloutStrategyAnnotation string `json:"rolloutStrategyAnnotation"`
// PauseDeploymentAnnotation is the annotation key used to define the time period to pause a deployment after
PauseDeploymentAnnotation string `json:"pauseDeploymentAnnotation"`
// PauseDeploymentTimeAnnotation is the annotation key used to indicate when a deployment was paused by Reloader
PauseDeploymentTimeAnnotation string `json:"pauseDeploymentTimeAnnotation"`
// LogFormat specifies the log format to use (json, or empty string for default text format)
LogFormat string `json:"logFormat"`
// LogLevel specifies the log level to use (trace, debug, info, warning, error, fatal, panic)
LogLevel string `json:"logLevel"`
// IsArgoRollouts indicates whether support for Argo Rollouts is enabled
IsArgoRollouts bool `json:"isArgoRollouts"`
// ReloadStrategy specifies the strategy used to trigger resource reloads (env-vars or annotations)
ReloadStrategy string `json:"reloadStrategy"`
// ReloadOnCreate indicates whether to trigger reloads when ConfigMaps/Secrets are created
ReloadOnCreate bool `json:"reloadOnCreate"`
// ReloadOnDelete indicates whether to trigger reloads when ConfigMaps/Secrets are deleted
ReloadOnDelete bool `json:"reloadOnDelete"`
// SyncAfterRestart indicates whether to sync add events after Reloader restarts (only works when ReloadOnCreate is true)
SyncAfterRestart bool `json:"syncAfterRestart"`
// EnableHA indicates whether High Availability mode is enabled with leader election
EnableHA bool `json:"enableHA"`
// WebhookUrl is the URL to send webhook notifications to instead of performing reloads
WebhookUrl string `json:"webhookUrl"`
// ResourcesToIgnore is a list of resource types to ignore (e.g., "configmaps" or "secrets")
ResourcesToIgnore []string `json:"resourcesToIgnore"`
// NamespaceSelectors is a list of label selectors to filter namespaces to watch
NamespaceSelectors []string `json:"namespaceSelectors"`
// ResourceSelectors is a list of label selectors to filter ConfigMaps and Secrets to watch
ResourceSelectors []string `json:"resourceSelectors"`
// NamespacesToIgnore is a list of namespace names to ignore when watching for changes
NamespacesToIgnore []string `json:"namespacesToIgnore"`
// EnablePProf enables pprof for profiling
EnablePProf bool `json:"enablePProf"`
// PProfAddr is the address to start pprof server on
PProfAddr string `json:"pprofAddr"`
}
var CommandLineOptions *ReloaderOptions
func PublishMetaInfoConfigmap(clientset kubernetes.Interface) {
namespace := os.Getenv("RELOADER_NAMESPACE")
if namespace == "" {
logrus.Warn("RELOADER_NAMESPACE is not set, skipping meta info configmap creation")
return
}
metaInfo := &MetaInfo{
BuildInfo: *NewBuildInfo(),
ReloaderOptions: *GetCommandLineOptions(),
DeploymentInfo: metav1.ObjectMeta{
Name: os.Getenv("RELOADER_DEPLOYMENT_NAME"),
Namespace: namespace,
},
}
configMap := metaInfo.ToConfigMap()
if _, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMap.Name, metav1.GetOptions{}); err == nil {
logrus.Info("Meta info configmap already exists, updating it")
_, err = clientset.CoreV1().ConfigMaps(namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
if err != nil {
logrus.Warn("Failed to update existing meta info configmap: ", err)
}
return
}
_, err := clientset.CoreV1().ConfigMaps(namespace).Create(context.Background(), configMap, metav1.CreateOptions{})
if err != nil {
logrus.Warn("Failed to create meta info configmap: ", err)
}
}
func GetNamespaceLabelSelector(slice []string) (string, error) {
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
namespaceLabelSelector := strings.Join(slice[:], ",")
_, err := labels.Parse(namespaceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return namespaceLabelSelector, nil
}
func GetResourceLabelSelector(slice []string) (string, error) {
for i, kv := range slice {
// Legacy support for ":" as a delimiter and "*" for wildcard.
if strings.Contains(kv, ":") {
split := strings.Split(kv, ":")
if split[1] == "*" {
slice[i] = split[0]
} else {
slice[i] = split[0] + "=" + split[1]
}
}
// Convert wildcard to valid apimachinery operator
if strings.Contains(kv, "=") {
split := strings.Split(kv, "=")
if split[1] == "*" {
slice[i] = split[0]
}
}
}
resourceLabelSelector := strings.Join(slice[:], ",")
_, err := labels.Parse(resourceLabelSelector)
if err != nil {
logrus.Fatal(err)
}
return resourceLabelSelector, nil
}
// ShouldReload checks if a resource should be reloaded based on its annotations and the provided options.
func ShouldReload(config Config, resourceType string, annotations Map, podAnnotations Map, options *ReloaderOptions) ReloadCheckResult {
ignoreResourceAnnotatonValue := config.ResourceAnnotations[options.IgnoreResourceAnnotation]
if ignoreResourceAnnotatonValue == "true" {
return ReloadCheckResult{
ShouldReload: false,
}
}
annotationValue, found := annotations[config.Annotation]
searchAnnotationValue, foundSearchAnn := annotations[options.AutoSearchAnnotation]
reloaderEnabledValue, foundAuto := annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue, foundTypedAuto := annotations[config.TypedAutoAnnotation]
excludeConfigmapAnnotationValue, foundExcludeConfigmap := annotations[options.ConfigmapExcludeReloaderAnnotation]
excludeSecretAnnotationValue, foundExcludeSecret := annotations[options.SecretExcludeReloaderAnnotation]
if !found && !foundAuto && !foundTypedAuto && !foundSearchAnn {
annotations = podAnnotations
annotationValue = annotations[config.Annotation]
searchAnnotationValue = annotations[options.AutoSearchAnnotation]
reloaderEnabledValue = annotations[options.ReloaderAutoAnnotation]
typedAutoAnnotationEnabledValue = annotations[config.TypedAutoAnnotation]
}
isResourceExcluded := false
switch config.Type {
case constants.ConfigmapEnvVarPostfix:
if foundExcludeConfigmap {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeConfigmapAnnotationValue)
}
case constants.SecretEnvVarPostfix:
if foundExcludeSecret {
isResourceExcluded = checkIfResourceIsExcluded(config.ResourceName, excludeSecretAnnotationValue)
}
}
if isResourceExcluded {
return ReloadCheckResult{
ShouldReload: false,
}
}
reloaderEnabled, _ := strconv.ParseBool(reloaderEnabledValue)
typedAutoAnnotationEnabled, _ := strconv.ParseBool(typedAutoAnnotationEnabledValue)
if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && options.AutoReloadAll {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: true,
}
}
values := strings.Split(annotationValue, ",")
for _, value := range values {
value = strings.TrimSpace(value)
re := regexp.MustCompile("^" + value + "$")
if re.Match([]byte(config.ResourceName)) {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: false,
}
}
}
if searchAnnotationValue == "true" {
matchAnnotationValue := config.ResourceAnnotations[options.SearchMatchAnnotation]
if matchAnnotationValue == "true" {
return ReloadCheckResult{
ShouldReload: true,
AutoReload: true,
}
}
}
return ReloadCheckResult{
ShouldReload: false,
}
}
func checkIfResourceIsExcluded(resourceName, excludedResources string) bool {
if excludedResources == "" {
return false
}
excludedResourcesList := strings.Split(excludedResources, ",")
for _, excludedResource := range excludedResourcesList {
if strings.TrimSpace(excludedResource) == resourceName {
return true
}
}
return false
}
func init() {
GetCommandLineOptions()
}
func GetCommandLineOptions() *ReloaderOptions {
if CommandLineOptions == nil {
CommandLineOptions = &ReloaderOptions{}
}
CommandLineOptions.AutoReloadAll = options.AutoReloadAll
CommandLineOptions.ConfigmapUpdateOnChangeAnnotation = options.ConfigmapUpdateOnChangeAnnotation
CommandLineOptions.SecretUpdateOnChangeAnnotation = options.SecretUpdateOnChangeAnnotation
CommandLineOptions.ReloaderAutoAnnotation = options.ReloaderAutoAnnotation
CommandLineOptions.IgnoreResourceAnnotation = options.IgnoreResourceAnnotation
CommandLineOptions.ConfigmapReloaderAutoAnnotation = options.ConfigmapReloaderAutoAnnotation
CommandLineOptions.SecretReloaderAutoAnnotation = options.SecretReloaderAutoAnnotation
CommandLineOptions.ConfigmapExcludeReloaderAnnotation = options.ConfigmapExcludeReloaderAnnotation
CommandLineOptions.SecretExcludeReloaderAnnotation = options.SecretExcludeReloaderAnnotation
CommandLineOptions.AutoSearchAnnotation = options.AutoSearchAnnotation
CommandLineOptions.SearchMatchAnnotation = options.SearchMatchAnnotation
CommandLineOptions.RolloutStrategyAnnotation = options.RolloutStrategyAnnotation
CommandLineOptions.PauseDeploymentAnnotation = options.PauseDeploymentAnnotation
CommandLineOptions.PauseDeploymentTimeAnnotation = options.PauseDeploymentTimeAnnotation
CommandLineOptions.LogFormat = options.LogFormat
CommandLineOptions.LogLevel = options.LogLevel
CommandLineOptions.ReloadStrategy = options.ReloadStrategy
CommandLineOptions.SyncAfterRestart = options.SyncAfterRestart
CommandLineOptions.EnableHA = options.EnableHA
CommandLineOptions.WebhookUrl = options.WebhookUrl
CommandLineOptions.ResourcesToIgnore = options.ResourcesToIgnore
CommandLineOptions.NamespaceSelectors = options.NamespaceSelectors
CommandLineOptions.ResourceSelectors = options.ResourceSelectors
CommandLineOptions.NamespacesToIgnore = options.NamespacesToIgnore
CommandLineOptions.IsArgoRollouts = parseBool(options.IsArgoRollouts)
CommandLineOptions.ReloadOnCreate = parseBool(options.ReloadOnCreate)
CommandLineOptions.ReloadOnDelete = parseBool(options.ReloadOnDelete)
CommandLineOptions.EnablePProf = options.EnablePProf
CommandLineOptions.PProfAddr = options.PProfAddr
return CommandLineOptions
}
func parseBool(value string) bool {
if value == "" {
return false
}
result, err := strconv.ParseBool(value)
if err != nil {
return false // Default to false if parsing fails
}
return result
}

View File

@@ -1,12 +1,13 @@
package util
package common
import (
"github.com/stakater/Reloader/internal/pkg/constants"
"github.com/stakater/Reloader/internal/pkg/options"
"github.com/stakater/Reloader/internal/pkg/util"
v1 "k8s.io/api/core/v1"
)
//Config contains rolling upgrade configuration parameters
// Config contains rolling upgrade configuration parameters
type Config struct {
Namespace string
ResourceName string
@@ -15,6 +16,7 @@ type Config struct {
TypedAutoAnnotation string
SHAValue string
Type string
Labels map[string]string
}
// GetConfigmapConfig provides utility config for configmap
@@ -25,8 +27,9 @@ func GetConfigmapConfig(configmap *v1.ConfigMap) Config {
ResourceAnnotations: configmap.Annotations,
Annotation: options.ConfigmapUpdateOnChangeAnnotation,
TypedAutoAnnotation: options.ConfigmapReloaderAutoAnnotation,
SHAValue: GetSHAfromConfigmap(configmap),
SHAValue: util.GetSHAfromConfigmap(configmap),
Type: constants.ConfigmapEnvVarPostfix,
Labels: configmap.Labels,
}
}
@@ -38,7 +41,8 @@ func GetSecretConfig(secret *v1.Secret) Config {
ResourceAnnotations: secret.Annotations,
Annotation: options.SecretUpdateOnChangeAnnotation,
TypedAutoAnnotation: options.SecretReloaderAutoAnnotation,
SHAValue: GetSHAfromSecret(secret.Data),
SHAValue: util.GetSHAfromSecret(secret.Data),
Type: constants.SecretEnvVarPostfix,
Labels: secret.Labels,
}
}

129
pkg/common/metainfo.go Normal file
View File

@@ -0,0 +1,129 @@
package common
import (
"encoding/json"
"fmt"
"runtime"
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Version, Commit, and BuildDate are set during the build process
// using the -X linker flag to inject these values into the binary.
// They provide metadata about the build version, commit hash, build date, and whether there are
// uncommitted changes in the source code at the time of build.
// This information is useful for debugging and tracking the specific build of the Reloader binary.
var Version = "dev"
var Commit = "unknown"
var BuildDate = "unknown"
const (
MetaInfoConfigmapName = "reloader-meta-info"
MetaInfoConfigmapLabelKey = "reloader.stakater.com/meta-info"
MetaInfoConfigmapLabelValue = "reloader-oss"
)
// MetaInfo contains comprehensive metadata about the Reloader instance.
// This includes build information, configuration options, and deployment details.
type MetaInfo struct {
// BuildInfo contains information about the build version, commit, and compilation details
BuildInfo BuildInfo `json:"buildInfo"`
// ReloaderOptions contains all the configuration options and flags used by this Reloader instance
ReloaderOptions ReloaderOptions `json:"reloaderOptions"`
// DeploymentInfo contains metadata about the Kubernetes deployment of this Reloader instance
DeploymentInfo metav1.ObjectMeta `json:"deploymentInfo"`
}
// BuildInfo contains information about the build and version of the Reloader binary.
// This includes Go version, release version, commit details, and build timestamp.
type BuildInfo struct {
// GoVersion is the version of Go used to compile the binary
GoVersion string `json:"goVersion"`
// ReleaseVersion is the version tag or branch of the Reloader release
ReleaseVersion string `json:"releaseVersion"`
// CommitHash is the Git commit hash of the source code used to build this binary
CommitHash string `json:"commitHash"`
// CommitTime is the timestamp of the Git commit used to build this binary
CommitTime time.Time `json:"commitTime"`
}
func NewBuildInfo() *BuildInfo {
metaInfo := &BuildInfo{
GoVersion: runtime.Version(),
ReleaseVersion: Version,
CommitHash: Commit,
CommitTime: ParseUTCTime(BuildDate),
}
return metaInfo
}
func (m *MetaInfo) ToConfigMap() *v1.ConfigMap {
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: MetaInfoConfigmapName,
Namespace: m.DeploymentInfo.Namespace,
Labels: map[string]string{
MetaInfoConfigmapLabelKey: MetaInfoConfigmapLabelValue,
},
},
Data: map[string]string{
"buildInfo": toJson(m.BuildInfo),
"reloaderOptions": toJson(m.ReloaderOptions),
"deploymentInfo": toJson(m.DeploymentInfo),
},
}
}
func NewMetaInfo(configmap *v1.ConfigMap) (*MetaInfo, error) {
var buildInfo BuildInfo
if val, ok := configmap.Data["buildInfo"]; ok {
err := json.Unmarshal([]byte(val), &buildInfo)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal buildInfo: %w", err)
}
}
var reloaderOptions ReloaderOptions
if val, ok := configmap.Data["reloaderOptions"]; ok {
err := json.Unmarshal([]byte(val), &reloaderOptions)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal reloaderOptions: %w", err)
}
}
var deploymentInfo metav1.ObjectMeta
if val, ok := configmap.Data["deploymentInfo"]; ok {
err := json.Unmarshal([]byte(val), &deploymentInfo)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal deploymentInfo: %w", err)
}
}
return &MetaInfo{
BuildInfo: buildInfo,
ReloaderOptions: reloaderOptions,
DeploymentInfo: deploymentInfo,
}, nil
}
func toJson(data interface{}) string {
jsonData, err := json.Marshal(data)
if err != nil {
return ""
}
return string(jsonData)
}
func ParseUTCTime(value string) time.Time {
if value == "" {
return time.Time{} // Return zero time if value is empty
}
t, err := time.Parse(time.RFC3339, value)
if err != nil {
return time.Time{} // Return zero time if parsing fails
}
return t
}

View File

@@ -1,4 +1,4 @@
package util
package common
import "time"