mirror of
https://github.com/stakater/Reloader.git
synced 2026-08-27 14:37:17 +00:00
wip: port csi provider to v2 sa-8436
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user