From be09beac29a61a8561b997adbfcf47c6c59e9774 Mon Sep 17 00:00:00 2001 From: Jamie <32368259+jamie-stinson@users.noreply.github.com> Date: Wed, 21 May 2025 10:18:01 +0100 Subject: [PATCH 1/2] feat: add ignore annotation support for skipping reloads --- README.md | 16 ++++ internal/pkg/handler/upgrade.go | 5 ++ internal/pkg/handler/upgrade_test.go | 120 +++++++++++++++++++++++++++ internal/pkg/options/flags.go | 2 + 4 files changed, 143 insertions(+) diff --git a/README.md b/README.md index 23e4042..ec11ec9 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,22 @@ 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. diff --git a/internal/pkg/handler/upgrade.go b/internal/pkg/handler/upgrade.go index b87a859..ecb4aae 100644 --- a/internal/pkg/handler/upgrade.go +++ b/internal/pkg/handler/upgrade.go @@ -264,6 +264,11 @@ func upgradeResource(clients kube.Clients, config util.Config, upgradeFuncs call } } + ignoreResourceAnnotatonValue := config.ResourceAnnotations[options.IgnoreResourceAnnotation] + if ignoreResourceAnnotatonValue == "true" { + return nil + } + // find correct annotation and update the resource annotations := upgradeFuncs.AnnotationsFunc(resource) annotationValue, found := annotations[config.Annotation] diff --git a/internal/pkg/handler/upgrade_test.go b/internal/pkg/handler/upgrade_test.go index 1d68a3e..5c77cae 100644 --- a/internal/pkg/handler/upgrade_test.go +++ b/internal/pkg/handler/upgrade_test.go @@ -52,6 +52,8 @@ 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) ersNamespace = "test-handler-" + testutil.RandSeq(5) ersConfigmapName = "testconfigmap-handler-" + testutil.RandSeq(5) @@ -75,6 +77,8 @@ 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) ) func TestMain(m *testing.M) { @@ -215,6 +219,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 { @@ -854,6 +887,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 { @@ -2737,6 +2798,65 @@ func TestFailedRollingUpgradeUsingArs(t *testing.T) { } } +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 util.Config, upgradeFuncs callbacks.RollingUpgradeFuncs, collectors metrics.Collectors, envVarPostfix string) { err := PerformAction(clients, config, upgradeFuncs, collectors, nil, invokeDeleteStrategy) time.Sleep(5 * time.Second) diff --git a/internal/pkg/options/flags.go b/internal/pkg/options/flags.go index 081acc3..7c0e14e 100644 --- a/internal/pkg/options/flags.go +++ b/internal/pkg/options/flags.go @@ -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 From 58ad781c0c2bff659d58b731bc207de2c9f3f013 Mon Sep 17 00:00:00 2001 From: Jamie <32368259+jamie-stinson@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:04:49 +0100 Subject: [PATCH 2/2] fix: multiple blank lines lint --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ec11ec9..46b2c49 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,6 @@ 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: