Skip to content

Commit 0f1d02e

Browse files
Readme update and code refactor
Signed-off-by: faizanahmad055 <faizan.ahmad55@outlook.com>
1 parent 85f1c13 commit 0f1d02e

10 files changed

Lines changed: 104 additions & 39 deletions

File tree

README.md

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
## 🔁 What is Reloader?
1515

16-
Reloader is a Kubernetes controller that automatically triggers rollouts of workloads (like Deployments, StatefulSets, and more) whenever referenced `Secrets` or `ConfigMaps` are updated.
16+
Reloader is a Kubernetes controller that automatically triggers rollouts of workloads (like Deployments, StatefulSets, and more) whenever referenced `Secrets`, `ConfigMaps` or **optionally CSI-mounted secrets** are updated.
1717

1818
In a traditional Kubernetes setup, updating a `Secret` or `ConfigMap` does not automatically restart or redeploy your workloads. This can lead to stale configurations running in production, especially when dealing with dynamic values like credentials, feature flags, or environment configs.
1919

@@ -169,9 +169,11 @@ metadata:
169169
170170
This instructs Reloader to skip all reload logic for that resource across all workloads.
171171
172-
### 4. ⚙️ Workload-Specific Rollout Strategy
172+
### 4. ⚙️ Workload-Specific Rollout Strategy (Argo Rollouts Only)
173173
174-
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.
174+
Note: This is only applicable when using [Argo Rollouts](https://argoproj.github.io/argo-rollouts/). It is ignored for standard Kubernetes Deployments, StatefulSets, or DaemonSets. To use this feature, Argo Rollouts support must be enabled in Reloader (for example via --is-argo-rollouts=true).
175+
176+
By default, Reloader triggers the Argo Rollout controller to perform a standard rollout by updating the pod template. This works well in most cases, however, because this modifies the workload spec, GitOps tools like ArgoCD will detect this as "Configuration Drift" and mark your application as OutOfSync.
175177
176178
To avoid that, you can switch to the **restart** strategy, which simply restarts the pod without changing the pod template.
177179
@@ -189,8 +191,10 @@ metadata:
189191
✅ Use `restart` if:
190192

191193
1. You're using GitOps and want to avoid drift
192-
1. You want a quick restart without changing the workload spec
193-
1. Your platform restricts metadata changes
194+
2. You want a quick restart without changing the workload spec
195+
3. Your platform restricts metadata changes
196+
197+
This setting affects Argo Rollouts behavior, not Argo CD sync settings.
194198

195199
### 5. ❗ Annotation Behavior Rules & Compatibility
196200

@@ -239,6 +243,38 @@ This feature allows you to pause rollouts for a deployment for a specified durat
239243
1. ✅ Your deployment references multiple ConfigMaps or Secrets that may be updated at the same time.
240244
1. ✅ You want to minimize unnecessary rollouts and reduce downtime caused by back-to-back configuration changes.
241245

246+
### 8. 🔐 CSI Secret Provider Support
247+
248+
Reloader supports the [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/), which allows mounting secrets from external secret stores (like AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) directly into pods.
249+
Unlike Kubernetes Secret objects, CSI-mounted secrets do not always trigger native Kubernetes update events. Reloader solves this by watching CSI status resources and restarting affected workloads when mounted secret versions change.
250+
251+
#### How it works
252+
253+
When secret rotation is enabled, the Secrets Store CSI Driver updates a Kubernetes resource called: `SecretProviderClassPodStatus`
254+
255+
This resource reflects the currently mounted secret versions for a pod.
256+
Reloader watches these updates and triggers a rollout when a change is detected.
257+
258+
#### Prerequisites
259+
260+
- Secrets Store CSI Driver must be installed in your cluster
261+
- Secret rotation enabled in the CSI driver.
262+
- Enable CSI integration in Reloader: `--enable-csi-integration=true`
263+
264+
#### Annotations for CSI-mounted Secrets
265+
266+
| Annotation | Description |
267+
|--------------------------------------------|----------------------------------------------------------------------|
268+
| `reloader.stakater.com/auto: "true"` | Reloads workload when CSI-mounted secrets change |
269+
| `secretproviderclass.reloader.stakater.com/reload: "my-spc"` | Reloads when specific SecretProviderClass changes |
270+
271+
#### Notes & Limitations
272+
273+
Reloader reacts to CSI status changes, not direct updates to external secret stores
274+
Secret rotation must be enabled in the CSI driver for updates to be detected
275+
CSI limitations (such as subPath mounts) still apply and may require pod restarts
276+
If secrets are synced to Kubernetes Secret objects, standard Reloader behavior applies and CSI support may not be required
277+
242278
## 🚀 Installation
243279

244280
### 1. 📦 Helm

internal/pkg/callbacks/rolling_upgrade_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func newTestFixtures() testFixtures {
4949

5050
func setupTestClients() kube.Clients {
5151
return kube.Clients{
52-
KubernetesClient: fake.NewSimpleClientset(),
52+
KubernetesClient: fake.NewClientset(),
5353
ArgoRolloutClient: fakeargoclientset.NewSimpleClientset(),
5454
}
5555
}

internal/pkg/cmd/reloader.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,8 @@ func startReloader(cmd *cobra.Command, args []string) {
160160

161161
var controllers []*controller.Controller
162162
for k := range kube.ResourceMap {
163-
if k == "secretproviderclasspodstatuses" {
164-
if !options.EnableCSIIntegration {
165-
logrus.Infof("EnableCSIIntegration is set to false, won't run secretproviderclasspodstatuses controller")
166-
continue
167-
}
168-
if !kube.IsCSIInstalled {
169-
logrus.Infof("Can't run secretproviderclasspodstatuses controller as CSI CRDs are not installed")
170-
continue
171-
}
163+
if k == constants.SecretProviderClassController && !shouldRunCSIController() {
164+
continue
172165
}
173166

174167
if ignoredResourcesList.Contains(k) || (len(namespaceLabelSelector) == 0 && k == "namespaces") {
@@ -218,3 +211,15 @@ func startPProfServer() {
218211
logrus.Errorf("Failed to start pprof server: %v", err)
219212
}
220213
}
214+
215+
func shouldRunCSIController() bool {
216+
if !options.EnableCSIIntegration {
217+
logrus.Info("Skipping secretproviderclasspodstatuses controller: EnableCSIIntegration is disabled")
218+
return false
219+
}
220+
if !kube.IsCSIInstalled {
221+
logrus.Info("Skipping secretproviderclasspodstatuses controller: CSI CRDs not installed")
222+
return false
223+
}
224+
return true
225+
}

internal/pkg/constants/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const (
2424
EnvVarsReloadStrategy = "env-vars"
2525
// AnnotationsReloadStrategy instructs Reloader to add pod template annotations to facilitate a restart
2626
AnnotationsReloadStrategy = "annotations"
27+
// SecretProviderClassController enables support for SecretProviderClassPodStatus resources
28+
SecretProviderClassController = "secretproviderclasspodstatuses"
2729
)
2830

2931
// Leadership election related consts

internal/pkg/controller/controller.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package controller
22

33
import (
44
"fmt"
5+
"slices"
56
"time"
67

78
"github.com/sirupsen/logrus"
9+
"github.com/stakater/Reloader/internal/pkg/constants"
810
"github.com/stakater/Reloader/internal/pkg/handler"
911
"github.com/stakater/Reloader/internal/pkg/metrics"
1012
"github.com/stakater/Reloader/internal/pkg/options"
@@ -21,7 +23,6 @@ import (
2123
"k8s.io/client-go/tools/record"
2224
"k8s.io/client-go/util/workqueue"
2325
"k8s.io/kubectl/pkg/scheme"
24-
"k8s.io/utils/strings/slices"
2526
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
2627
)
2728

@@ -80,13 +81,9 @@ func NewController(
8081
}
8182
}
8283

83-
getterRESTClient := client.CoreV1().RESTClient()
84-
if resource == "secretproviderclasspodstatuses" {
85-
csiClient, err := kube.GetCSIClient()
86-
if err != nil {
87-
logrus.Fatal(err)
88-
}
89-
getterRESTClient = csiClient.SecretsstoreV1().RESTClient()
84+
getterRESTClient, err := getClientForResource(resource, client)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to initialize REST client for %s: %w", resource, err)
9087
}
9188

9289
listWatcher := cache.NewFilteredListWatchFromClient(getterRESTClient, resource, namespace, optionsModifier)
@@ -301,3 +298,14 @@ func (c *Controller) handleErr(err error, key interface{}) {
301298
logrus.Errorf("Dropping key out of the queue: %v", err)
302299
logrus.Debugf("Dropping the key %q out of the queue: %v", key, err)
303300
}
301+
302+
func getClientForResource(resource string, coreClient kubernetes.Interface) (cache.Getter, error) {
303+
if resource == constants.SecretProviderClassController {
304+
csiClient, err := kube.GetCSIClient()
305+
if err != nil {
306+
return nil, fmt.Errorf("failed to get CSI client: %w", err)
307+
}
308+
return csiClient.SecretsstoreV1().RESTClient(), nil
309+
}
310+
return coreClient.CoreV1().RESTClient(), nil
311+
}

internal/pkg/controller/controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2757,7 +2757,7 @@ func TestController_resourceInNamespaceSelector(t *testing.T) {
27572757

27582758
for _, tt := range tests {
27592759
t.Run(tt.name, func(t *testing.T) {
2760-
fakeClient := fake.NewSimpleClientset()
2760+
fakeClient := fake.NewClientset()
27612761
namespace, _ := fakeClient.CoreV1().Namespaces().Create(context.Background(), &tt.fields.namespace, metav1.CreateOptions{})
27622762
logrus.Infof("created fakeClient namespace for testing = %s", namespace.Name)
27632763

internal/pkg/handler/pause_deployment_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func TestHandleMissingTimerSimple(t *testing.T) {
244244
}()
245245

246246
t.Run(test.name, func(t *testing.T) {
247-
fakeClient := testclient.NewSimpleClientset()
247+
fakeClient := testclient.NewClientset()
248248
clients := kube.Clients{
249249
KubernetesClient: fakeClient,
250250
}
@@ -337,7 +337,7 @@ func TestPauseDeployment(t *testing.T) {
337337

338338
for _, test := range tests {
339339
t.Run(test.name, func(t *testing.T) {
340-
fakeClient := testclient.NewSimpleClientset()
340+
fakeClient := testclient.NewClientset()
341341
clients := kube.Clients{
342342
KubernetesClient: fakeClient,
343343
}

internal/pkg/handler/update.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,31 @@ func (r ResourceUpdatedHandler) Handle() error {
3939

4040
// GetConfig gets configurations containing SHA, annotations, namespace and resource name
4141
func (r ResourceUpdatedHandler) GetConfig() (common.Config, string) {
42-
var oldSHAData string
43-
var config common.Config
42+
var (
43+
oldSHAData string
44+
config common.Config
45+
)
46+
4447
switch res := r.Resource.(type) {
4548
case *v1.ConfigMap:
46-
oldSHAData = util.GetSHAfromConfigmap(r.OldResource.(*v1.ConfigMap))
49+
if old, ok := r.OldResource.(*v1.ConfigMap); ok && old != nil {
50+
oldSHAData = util.GetSHAfromConfigmap(old)
51+
}
4752
config = common.GetConfigmapConfig(res)
53+
4854
case *v1.Secret:
49-
oldSHAData = util.GetSHAfromSecret(r.OldResource.(*v1.Secret).Data)
55+
if old, ok := r.OldResource.(*v1.Secret); ok && old != nil {
56+
oldSHAData = util.GetSHAfromSecret(old.Data)
57+
}
5058
config = common.GetSecretConfig(res)
59+
5160
case *csiv1.SecretProviderClassPodStatus:
52-
oldSHAData = util.GetSHAfromSecretProviderClassPodStatus(r.OldResource.(*csiv1.SecretProviderClassPodStatus).Status)
61+
if old, ok := r.OldResource.(*csiv1.SecretProviderClassPodStatus); ok && old != nil && old.Status.Objects != nil {
62+
oldSHAData = util.GetSHAfromSecretProviderClassPodStatus(old.Status)
63+
}
5364
config = common.GetSecretProviderClassPodStatusConfig(res)
5465
default:
55-
logrus.Warnf("Invalid resource: Resource should be 'Secret', 'Configmap' or 'SecretProviderClassPodStatus' but found, %v", r.Resource)
66+
logrus.Warnf("Invalid resource: Resource should be 'Secret', 'Configmap' or 'SecretProviderClassPodStatus' but found, %T", r.Resource)
5667
}
5768
return config, oldSHAData
5869
}

internal/pkg/handler/upgrade.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -634,11 +634,10 @@ func updateEnvVar(container *v1.Container, envVar string, shaData string) consta
634634
}
635635

636636
func secretProviderClassEnvReloaded(containers []v1.Container, envVar string, shaData string) bool {
637-
for i := range containers {
638-
envs := containers[i].Env
639-
for j := range envs {
640-
if envs[j].Name == envVar {
641-
return envs[j].Value == shaData
637+
for _, container := range containers {
638+
for _, env := range container.Env {
639+
if env.Name == envVar {
640+
return env.Value == shaData
642641
}
643642
}
644643
}
@@ -649,7 +648,11 @@ func populateAnnotationsFromSecretProviderClass(clients kube.Clients, config *co
649648
obj, err := clients.CSIClient.SecretsstoreV1().SecretProviderClasses(config.Namespace).Get(context.TODO(), config.ResourceName, metav1.GetOptions{})
650649
annotations := make(map[string]string)
651650
if err != nil {
652-
logrus.Infof("Couldn't find secretproviderclass '%s' in '%s' namespace for typed annotation", config.ResourceName, config.Namespace)
651+
if apierrors.IsNotFound(err) {
652+
logrus.Warnf("SecretProviderClass '%s' not found in namespace '%s'", config.ResourceName, config.Namespace)
653+
} else {
654+
logrus.Errorf("Failed to get SecretProviderClass '%s' in namespace '%s': %v", config.ResourceName, config.Namespace, err)
655+
}
653656
} else if obj.Annotations != nil {
654657
annotations = obj.Annotations
655658
}

internal/pkg/handler/upgrade_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import (
3232

3333
var (
3434
clients = kube.Clients{
35-
KubernetesClient: testclient.NewSimpleClientset(),
35+
KubernetesClient: testclient.NewClientset(),
3636
CSIClient: csitestclient.NewSimpleClientset(),
3737
}
3838

0 commit comments

Comments
 (0)