wip: port csi provider to v2 sa-8436

This commit is contained in:
Safwan
2026-06-22 16:42:01 +05:00
parent 14117194ac
commit 73729c951e
40 changed files with 1821 additions and 40 deletions
+26
View File
@@ -2,6 +2,7 @@ package reload
import (
corev1 "k8s.io/api/core/v1"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
// EventType represents the type of change event.
@@ -54,3 +55,28 @@ func (c SecretChange) GetNamespace() string { return c.Secret.Names
func (c SecretChange) GetAnnotations() map[string]string { return c.Secret.Annotations }
func (c SecretChange) GetResourceType() ResourceType { return ResourceTypeSecret }
func (c SecretChange) ComputeHash(h *Hasher) string { return h.HashSecret(c.Secret) }
// SecretProviderClassChange represents a change event derived from a
// SecretProviderClassPodStatus update. Name/Annotations refer to the resolved
// SecretProviderClass; Status carries the SPCPS status used for hashing.
type SecretProviderClassChange struct {
Name string
Namespace string
Annotations map[string]string
Status csiv1.SecretProviderClassPodStatusStatus
EventType EventType
}
func (c SecretProviderClassChange) IsNil() bool { return c.Name == "" }
func (c SecretProviderClassChange) GetEventType() EventType { return c.EventType }
func (c SecretProviderClassChange) GetName() string { return c.Name }
func (c SecretProviderClassChange) GetNamespace() string { return c.Namespace }
func (c SecretProviderClassChange) GetAnnotations() map[string]string {
return c.Annotations
}
func (c SecretProviderClassChange) GetResourceType() ResourceType {
return ResourceTypeSecretProviderClass
}
func (c SecretProviderClassChange) ComputeHash(h *Hasher) string {
return h.HashSecretProviderClass(c.Status)
}
+51
View File
@@ -0,0 +1,51 @@
package reload
import (
"testing"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
func TestSecretProviderClassChange(t *testing.T) {
status := csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
}
c := SecretProviderClassChange{
Name: "my-spc",
Namespace: "ns1",
Annotations: map[string]string{"k": "v"},
Status: status,
EventType: EventTypeUpdate,
}
if c.IsNil() {
t.Fatal("IsNil() = true, want false")
}
if c.GetName() != "my-spc" {
t.Fatalf("GetName() = %q", c.GetName())
}
if c.GetNamespace() != "ns1" {
t.Fatalf("GetNamespace() = %q", c.GetNamespace())
}
if c.GetResourceType() != ResourceTypeSecretProviderClass {
t.Fatalf("GetResourceType() = %q", c.GetResourceType())
}
if c.GetEventType() != EventTypeUpdate {
t.Fatalf("GetEventType() = %q", c.GetEventType())
}
if c.GetAnnotations()["k"] != "v" {
t.Fatalf("GetAnnotations() missing key")
}
h := NewHasher()
if c.ComputeHash(h) != h.HashSecretProviderClass(status) {
t.Fatalf("ComputeHash mismatch")
}
}
func TestSecretProviderClassChangeIsNil(t *testing.T) {
c := SecretProviderClassChange{Name: ""}
if !c.IsNil() {
t.Fatal("IsNil() = false, want true for empty name")
}
}
+24
View File
@@ -0,0 +1,24 @@
package reload
import (
"testing"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
// TestCSIDependencyAvailable ensures the CSI types are importable and the
// fields this feature depends on exist.
func TestCSIDependencyAvailable(t *testing.T) {
status := csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "spc",
Objects: []csiv1.SecretProviderClassObject{
{ID: "secret/data/foo", Version: "1"},
},
}
if status.SecretProviderClassName != "spc" {
t.Fatalf("unexpected SecretProviderClassName")
}
if len(status.Objects) != 1 || status.Objects[0].ID != "secret/data/foo" {
t.Fatalf("unexpected Objects")
}
}
+14
View File
@@ -10,6 +10,7 @@ import (
"strings"
corev1 "k8s.io/api/core/v1"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
// Hasher computes content hashes for ConfigMaps and Secrets.
@@ -68,6 +69,19 @@ func (h *Hasher) computeSHA(data string) string {
return fmt.Sprintf("%x", hasher.Sum(nil))
}
// HashSecretProviderClass computes a SHA1 hash of a SecretProviderClassPodStatus
// status: the sorted set of object ID=Version entries plus the SPC name.
// This mirrors master's util.GetSHAfromSecretProviderClassPodStatus exactly.
func (h *Hasher) HashSecretProviderClass(status csiv1.SecretProviderClassPodStatusStatus) string {
values := make([]string, 0, len(status.Objects)+1)
for _, obj := range status.Objects {
values = append(values, obj.ID+"="+obj.Version)
}
values = append(values, "SecretProviderClassName="+status.SecretProviderClassName)
sort.Strings(values)
return h.computeSHA(strings.Join(values, ";"))
}
// EmptyHash returns an empty string to signal resource deletion.
func (h *Hasher) EmptyHash() string {
return ""
+43
View File
@@ -4,6 +4,7 @@ import (
"testing"
corev1 "k8s.io/api/core/v1"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
)
func TestHasher_HashConfigMap(t *testing.T) {
@@ -234,3 +235,45 @@ func TestHasher_NilInput(t *testing.T) {
t.Error("nil Secret should return a valid hash")
}
}
func TestHashSecretProviderClass(t *testing.T) {
h := NewHasher()
status := csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{
{ID: "secret/data/b", Version: "2"},
{ID: "secret/data/a", Version: "1"},
},
}
// Expected = SHA1 hex of the sorted, ';'-joined string, matching master.
expectedInput := "SecretProviderClassName=my-spc;secret/data/a=1;secret/data/b=2"
expected := h.computeSHA(expectedInput)
got := h.HashSecretProviderClass(status)
if got != expected {
t.Fatalf("HashSecretProviderClass = %q, want %q", got, expected)
}
// Order independence: shuffling objects must not change the hash.
statusReordered := csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{
{ID: "secret/data/a", Version: "1"},
{ID: "secret/data/b", Version: "2"},
},
}
if h.HashSecretProviderClass(statusReordered) != got {
t.Fatalf("hash not order-independent")
}
}
func TestHashSecretProviderClassEmpty(t *testing.T) {
h := NewHasher()
status := csiv1.SecretProviderClassPodStatusStatus{SecretProviderClassName: "empty"}
got := h.HashSecretProviderClass(status)
want := h.computeSHA("SecretProviderClassName=empty")
if got != want {
t.Fatalf("HashSecretProviderClass(empty) = %q, want %q", got, want)
}
}
+6
View File
@@ -144,6 +144,8 @@ func (m *Matcher) isResourceExcluded(resourceName string, resourceType ResourceT
excludeAnn = m.cfg.Annotations.ConfigmapExclude
case ResourceTypeSecret:
excludeAnn = m.cfg.Annotations.SecretExclude
case ResourceTypeSecretProviderClass:
excludeAnn = m.cfg.Annotations.SecretProviderClassExclude
}
excludeList, ok := annotations[excludeAnn]
@@ -242,6 +244,8 @@ func (m *Matcher) getExplicitAnnotation(resourceType ResourceType) string {
return m.cfg.Annotations.ConfigmapReload
case ResourceTypeSecret:
return m.cfg.Annotations.SecretReload
case ResourceTypeSecretProviderClass:
return m.cfg.Annotations.SecretProviderClassReload
default:
return ""
}
@@ -253,6 +257,8 @@ func (m *Matcher) getTypedAutoAnnotation(resourceType ResourceType) string {
return m.cfg.Annotations.ConfigmapAuto
case ResourceTypeSecret:
return m.cfg.Annotations.SecretAuto
case ResourceTypeSecretProviderClass:
return m.cfg.Annotations.SecretProviderClassAuto
default:
return ""
}
+46
View File
@@ -404,6 +404,52 @@ func TestMatcher_AutoDoesNotIgnoreExplicit(t *testing.T) {
t.Log("✓ Explicit reload annotation works even when auto is enabled")
}
func TestMatcherSecretProviderClassExplicit(t *testing.T) {
cfg := config.NewDefault()
m := NewMatcher(cfg)
res := m.ShouldReload(MatchInput{
ResourceName: "my-spc",
ResourceType: ResourceTypeSecretProviderClass,
WorkloadAnnotations: map[string]string{
"secretproviderclass.reloader.stakater.com/reload": "my-spc",
},
})
if !res.ShouldReload || res.AutoReload {
t.Fatalf("explicit SPC reload: got %+v", res)
}
}
func TestMatcherSecretProviderClassAuto(t *testing.T) {
cfg := config.NewDefault()
m := NewMatcher(cfg)
res := m.ShouldReload(MatchInput{
ResourceName: "my-spc",
ResourceType: ResourceTypeSecretProviderClass,
WorkloadAnnotations: map[string]string{
"secretproviderclass.reloader.stakater.com/auto": "true",
},
})
if !res.ShouldReload || !res.AutoReload {
t.Fatalf("auto SPC reload: got %+v", res)
}
}
func TestMatcherSecretProviderClassExcluded(t *testing.T) {
cfg := config.NewDefault()
m := NewMatcher(cfg)
res := m.ShouldReload(MatchInput{
ResourceName: "my-spc",
ResourceType: ResourceTypeSecretProviderClass,
WorkloadAnnotations: map[string]string{
"secretproviderclass.reloader.stakater.com/auto": "true",
"secretproviderclasses.exclude.reloader.stakater.com/reload": "my-spc",
},
})
if res.ShouldReload {
t.Fatalf("excluded SPC should not reload: got %+v", res)
}
}
// TestMatcher_PrecedenceOrder verifies the correct order of precedence:
// 1. Ignore annotation → skip
// 2. Exclude annotation → skip
+20
View File
@@ -5,6 +5,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/config"
)
@@ -157,3 +158,22 @@ func IgnoreAnnotationPredicate(cfg *config.Config) predicate.Predicate {
func CombinedPredicates(predicates ...predicate.Predicate) predicate.Predicate {
return predicate.And(predicates...)
}
// SecretProviderClassPodStatusPredicates filters SecretProviderClassPodStatus events.
// Create and Delete are ignored (matching master); Update passes only when the
// hashed status (object IDs/versions + SPC name) changes.
func SecretProviderClassPodStatusPredicates(cfg *config.Config, hasher *Hasher) predicate.Predicate {
return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { return false },
DeleteFunc: func(e event.DeleteEvent) bool { return false },
GenericFunc: func(e event.GenericEvent) bool { return false },
UpdateFunc: func(e event.UpdateEvent) bool {
oldObj, okOld := e.ObjectOld.(*csiv1.SecretProviderClassPodStatus)
newObj, okNew := e.ObjectNew.(*csiv1.SecretProviderClassPodStatus)
if !okOld || !okNew {
return false
}
return hasher.HashSecretProviderClass(oldObj.Status) != hasher.HashSecretProviderClass(newObj.Status)
},
}
}
+35
View File
@@ -7,6 +7,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/event"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/config"
)
@@ -934,3 +935,37 @@ func TestLabelsSet(t *testing.T) {
t.Errorf("Get(nonexistent) = %v, want empty string", ls.Get("nonexistent"))
}
}
func TestSecretProviderClassPodStatusPredicates(t *testing.T) {
cfg := config.NewDefault()
p := SecretProviderClassPodStatusPredicates(cfg, NewHasher())
oldObj := &csiv1.SecretProviderClassPodStatus{
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
},
}
newObjChanged := &csiv1.SecretProviderClassPodStatus{
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "2"}},
},
}
newObjSame := oldObj.DeepCopy()
// Create and Delete are always ignored for SPCPS.
if p.Create(event.CreateEvent{Object: oldObj}) {
t.Fatal("CreateFunc should return false")
}
if p.Delete(event.DeleteEvent{Object: oldObj}) {
t.Fatal("DeleteFunc should return false")
}
// Update only when the status hash changes.
if !p.Update(event.UpdateEvent{ObjectOld: oldObj, ObjectNew: newObjChanged}) {
t.Fatal("UpdateFunc should return true on changed status")
}
if p.Update(event.UpdateEvent{ObjectOld: oldObj, ObjectNew: newObjSame}) {
t.Fatal("UpdateFunc should return false on unchanged status")
}
}
+4
View File
@@ -8,6 +8,8 @@ const (
ResourceTypeConfigMap ResourceType = "configmap"
// ResourceTypeSecret represents a Secret resource.
ResourceTypeSecret ResourceType = "secret"
// ResourceTypeSecretProviderClass represents a CSI SecretProviderClass resource.
ResourceTypeSecretProviderClass ResourceType = "secretproviderclass"
)
// Kind returns the capitalized Kubernetes Kind (e.g., "ConfigMap", "Secret").
@@ -17,6 +19,8 @@ func (r ResourceType) Kind() string {
return "ConfigMap"
case ResourceTypeSecret:
return "Secret"
case ResourceTypeSecretProviderClass:
return "SecretProviderClass"
default:
return string(r)
}
@@ -26,3 +26,12 @@ func TestResourceType_Kind(t *testing.T) {
)
}
}
func TestResourceTypeSecretProviderClassKind(t *testing.T) {
if got := ResourceTypeSecretProviderClass.Kind(); got != "SecretProviderClass" {
t.Fatalf("Kind() = %q, want SecretProviderClass", got)
}
if string(ResourceTypeSecretProviderClass) != "secretproviderclass" {
t.Fatalf("value = %q, want secretproviderclass", ResourceTypeSecretProviderClass)
}
}
+4
View File
@@ -83,6 +83,10 @@ func (s *Service) processResource(
usesResource = wl.UsesConfigMap(resourceName)
case ResourceTypeSecret:
usesResource = wl.UsesSecret(resourceName)
case ResourceTypeSecretProviderClass:
// Annotation-only matching (parity with master): the workload's
// annotations alone decide the reload; no volume-uses scan.
usesResource = true
}
input := MatchInput{
+26
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-logr/logr/testr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/testutil"
@@ -1332,6 +1333,31 @@ func TestService_ProcessNilChange(t *testing.T) {
}
}
func TestServiceProcessSecretProviderClassAuto(t *testing.T) {
cfg := config.NewDefault()
svc := NewService(cfg, testr.New(t))
deploy := testutil.NewDeployment("test-deploy", "default", map[string]string{
"secretproviderclass.reloader.stakater.com/auto": "true",
})
workloads := []workload.Workload{workload.NewDeploymentWorkload(deploy)}
change := SecretProviderClassChange{
Name: "my-spc",
Namespace: "default",
Status: csiv1.SecretProviderClassPodStatusStatus{
SecretProviderClassName: "my-spc",
Objects: []csiv1.SecretProviderClassObject{{ID: "a", Version: "1"}},
},
EventType: EventTypeUpdate,
}
decisions := svc.Process(change, workloads)
if len(decisions) != 1 || !decisions[0].ShouldReload {
t.Fatalf("expected auto SPC reload, got %+v", decisions)
}
}
func TestService_ProcessCreateEventDisabled(t *testing.T) {
cfg := config.NewDefault()
cfg.ReloadOnCreate = false
+4
View File
@@ -19,6 +19,8 @@ const (
ConfigmapEnvVarPostfix = "CONFIGMAP"
// SecretEnvVarPostfix is the postfix for Secret environment variables.
SecretEnvVarPostfix = "SECRET"
// SecretProviderClassEnvVarPostfix is the postfix for SecretProviderClass environment variables.
SecretProviderClassEnvVarPostfix = "SECRETPROVIDERCLASS"
)
// Strategy defines how workload restarts are triggered.
@@ -108,6 +110,8 @@ func (s *EnvVarStrategy) envVarName(resourceName string, resourceType ResourceTy
postfix = ConfigmapEnvVarPostfix
case ResourceTypeSecret:
postfix = SecretEnvVarPostfix
case ResourceTypeSecretProviderClass:
postfix = SecretProviderClassEnvVarPostfix
}
return EnvVarPrefix + convertToEnvVarName(resourceName) + "_" + postfix
}
+9
View File
@@ -291,3 +291,12 @@ func TestNewStrategy(t *testing.T) {
}
})
}
func TestEnvVarNameSecretProviderClass(t *testing.T) {
s := NewEnvVarStrategy()
got := s.envVarName("my-vault-spc", ResourceTypeSecretProviderClass)
want := "STAKATER_MY_VAULT_SPC_SECRETPROVIDERCLASS"
if got != want {
t.Fatalf("envVarName = %q, want %q", got, want)
}
}