Skip to content

Commit 8477c6b

Browse files
committed
adoption: use per-role label keys and indexes to fix multi-secret conflicts
When a SpiceDBCluster references the same secret for multiple credential roles, the previous single-index / single-label approach caused adoption handlers to treat each other's secrets as stale and remove labels. Each credential role now gets its own label key (authzed.com/credential-type-{role}) and its own cache index, so handlers only see secrets that carry their specific label. A shared secret carries all applicable role labels simultaneously, appearing in every relevant index. Role-qualified SSA field managers (spicedb-operator-{role}) prevent the second apply from releasing ownership of the first role's label.
1 parent 9231431 commit 8477c6b

7 files changed

Lines changed: 820 additions & 61 deletions

File tree

pkg/controller/controller.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
corev1 "k8s.io/api/core/v1"
2828
policyv1 "k8s.io/api/policy/v1"
2929
rbacv1 "k8s.io/api/rbac/v1"
30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3031
"k8s.io/apimachinery/pkg/api/meta"
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233
"k8s.io/apimachinery/pkg/labels"
@@ -181,7 +182,6 @@ func NewController(ctx context.Context, registry *typed.Registry, dclient dynami
181182

182183
for _, gvr := range []schema.GroupVersionResource{
183184
appsv1.SchemeGroupVersion.WithResource("deployments"),
184-
corev1.SchemeGroupVersion.WithResource("secrets"),
185185
corev1.SchemeGroupVersion.WithResource("serviceaccounts"),
186186
corev1.SchemeGroupVersion.WithResource("services"),
187187
corev1.SchemeGroupVersion.WithResource("pods"),
@@ -202,6 +202,27 @@ func NewController(ctx context.Context, registry *typed.Registry, dclient dynami
202202
return nil, err
203203
}
204204
}
205+
// Secrets get per-type indexes (one per credential role) so that each
206+
// adoption handler only sees secrets of its own type and never
207+
// misidentifies another role's secret as stale. OwningClusterIndex is
208+
// kept for the legacy SecretRef path and for syncExternalResource, which
209+
// uses it to re-enqueue the owning cluster when any secret changes.
210+
secretsInf := externalInformerFactory.ForResource(corev1.SchemeGroupVersion.WithResource("secrets")).Informer()
211+
if err := secretsInf.AddIndexers(cache.Indexers{
212+
metadata.OwningClusterIndex: metadata.GetClusterKeyFromMeta,
213+
metadata.OwningClusterDatastoreURIIndex: metadata.GetClusterKeyFromMetaForType(metadata.CredentialTypeDatastoreURI),
214+
metadata.OwningClusterPresharedKeyIndex: metadata.GetClusterKeyFromMetaForType(metadata.CredentialTypePresharedKey),
215+
metadata.OwningClusterMigrationSecretsIndex: metadata.GetClusterKeyFromMetaForType(metadata.CredentialTypeMigrationSecrets),
216+
}); err != nil {
217+
return nil, err
218+
}
219+
if _, err := secretsInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
220+
AddFunc: func(obj any) { c.syncExternalResource(obj) },
221+
UpdateFunc: func(_, obj any) { c.syncExternalResource(obj) },
222+
DeleteFunc: func(obj any) { c.syncExternalResource(obj) },
223+
}); err != nil {
224+
return nil, err
225+
}
205226
externalInformerFactories = append(externalInformerFactories, externalInformerFactory)
206227
}
207228

@@ -414,6 +435,40 @@ func credentialSecretNames(cluster *v1alpha1.SpiceDBCluster) []string {
414435
return names
415436
}
416437

438+
type credentialSecret struct {
439+
name string
440+
credType string
441+
}
442+
443+
// credentialSecretsForCluster returns one entry per non-skipped credential ref,
444+
// preserving the credential type for each. Unlike credentialSecretNames, the
445+
// same name may appear more than once when a secret is shared across multiple
446+
// roles — each role runs its own adoption handler with its own type label.
447+
func credentialSecretsForCluster(cluster *v1alpha1.SpiceDBCluster) []credentialSecret {
448+
creds := cluster.Spec.Credentials
449+
if creds == nil && cluster.Spec.SecretRef != "" {
450+
return []credentialSecret{{name: cluster.Spec.SecretRef, credType: ""}}
451+
}
452+
if creds == nil {
453+
return nil
454+
}
455+
roleSecrets := []struct {
456+
ref *v1alpha1.CredentialRef
457+
credType string
458+
}{
459+
{creds.DatastoreURI, metadata.CredentialTypeDatastoreURI},
460+
{creds.PresharedKey, metadata.CredentialTypePresharedKey},
461+
{creds.MigrationSecrets, metadata.CredentialTypeMigrationSecrets},
462+
}
463+
var result []credentialSecret
464+
for _, rs := range roleSecrets {
465+
if rs.ref != nil && !rs.ref.Skip && rs.ref.SecretName != "" {
466+
result = append(result, credentialSecret{name: rs.ref.SecretName, credType: rs.credType})
467+
}
468+
}
469+
return result
470+
}
471+
417472
// syncExternalResource is called when a dependent resource is updated:
418473
// It queues the owning SpiceDBCluster for reconciliation based on the labels.
419474
// No other reconciliation should take place here; we keep a single state
@@ -576,21 +631,35 @@ func (c *Controller) selfPauseCluster(...handler.Handler) handler.Handler {
576631
func (c *Controller) secretAdopter(next ...handler.Handler) handler.Handler {
577632
secretsGVR := corev1.SchemeGroupVersion.WithResource("secrets")
578633
return handler.NewHandlerFromFunc(func(ctx context.Context) {
579-
names := CtxSecretNames.Value(ctx)
580-
if len(names) == 0 {
634+
cluster := CtxCluster.MustValue(ctx)
635+
pairs := credentialSecretsForCluster(cluster)
636+
if len(pairs) == 0 {
581637
handler.Handlers(next).MustOne().Handle(ctx)
582638
return
583639
}
584-
cluster := CtxCluster.MustValue(ctx)
585-
for _, name := range names {
640+
for _, cs := range pairs {
641+
credType := cs.credType
586642
secretCtx := CtxSecretNN.WithValue(ctx, types.NamespacedName{
587-
Name: name,
643+
Name: cs.name,
588644
Namespace: cluster.Namespace,
589645
})
590646
NewSecretAdoptionHandler(
591647
c.Recorder,
592648
func(ctx context.Context) (*corev1.Secret, error) {
593-
return typed.MustListerForKey[*corev1.Secret](c.Registry, typed.NewRegistryKey(DependentFactoryKey(CtxCacheNamespace.Value(ctx)), secretsGVR)).ByNamespace(CtxSecretNN.MustValue(ctx).Namespace).Get(CtxSecretNN.MustValue(ctx).Name)
649+
secret, err := typed.MustListerForKey[*corev1.Secret](c.Registry, typed.NewRegistryKey(DependentFactoryKey(CtxCacheNamespace.Value(ctx)), secretsGVR)).ByNamespace(CtxSecretNN.MustValue(ctx).Namespace).Get(CtxSecretNN.MustValue(ctx).Name)
650+
if err != nil {
651+
return nil, err
652+
}
653+
// If the secret lacks this role's per-type label key, return
654+
// NotFound so the adoption handler applies the full label set.
655+
// This migrates secrets from operator versions that did not
656+
// set per-role labels.
657+
if lk := metadata.LabelKeyForCredentialType(credType); lk != "" {
658+
if _, ok := secret.Labels[lk]; !ok {
659+
return nil, apierrors.NewNotFound(corev1.SchemeGroupVersion.WithResource("secrets").GroupResource(), CtxSecretNN.MustValue(ctx).Name)
660+
}
661+
}
662+
return secret, nil
594663
},
595664
func(ctx context.Context, err error) {
596665
cluster := CtxCluster.MustValue(ctx)
@@ -618,6 +687,7 @@ func (c *Controller) secretAdopter(next ...handler.Handler) handler.Handler {
618687
_, err := c.kclient.CoreV1().Secrets(nn.Namespace).Get(ctx, nn.Name, metav1.GetOptions{})
619688
return err
620689
},
690+
credType,
621691
handler.NoopHandler,
622692
).Handle(secretCtx)
623693
if errors.Is(secretCtx.Err(), context.Canceled) {

pkg/controller/controller_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,87 @@ func TestControllerNamespacing(t *testing.T) {
204204
})
205205
}
206206
}
207+
208+
func TestCredentialSecretsForCluster(t *testing.T) {
209+
ref := func(name string) *v1alpha1.CredentialRef { return &v1alpha1.CredentialRef{SecretName: name} }
210+
skipped := func(name string) *v1alpha1.CredentialRef {
211+
return &v1alpha1.CredentialRef{SecretName: name, Skip: true}
212+
}
213+
214+
tests := []struct {
215+
name string
216+
credentials *v1alpha1.ClusterCredentials
217+
secretRef string
218+
expect []credentialSecret
219+
}{
220+
{
221+
name: "nil credentials with no SecretRef returns nil",
222+
expect: nil,
223+
},
224+
{
225+
name: "nil credentials with SecretRef returns legacy entry with empty type",
226+
secretRef: "my-secret",
227+
expect: []credentialSecret{{name: "my-secret", credType: ""}},
228+
},
229+
{
230+
name: "datastoreURI only",
231+
credentials: &v1alpha1.ClusterCredentials{DatastoreURI: ref("ds-secret")},
232+
expect: []credentialSecret{{name: "ds-secret", credType: metadata.CredentialTypeDatastoreURI}},
233+
},
234+
{
235+
name: "presharedKey only",
236+
credentials: &v1alpha1.ClusterCredentials{PresharedKey: ref("psk-secret")},
237+
expect: []credentialSecret{{name: "psk-secret", credType: metadata.CredentialTypePresharedKey}},
238+
},
239+
{
240+
name: "migrationSecrets only",
241+
credentials: &v1alpha1.ClusterCredentials{MigrationSecrets: ref("mig-secret")},
242+
expect: []credentialSecret{{name: "mig-secret", credType: metadata.CredentialTypeMigrationSecrets}},
243+
},
244+
{
245+
name: "shared secret across two roles returns one entry per role",
246+
credentials: &v1alpha1.ClusterCredentials{
247+
DatastoreURI: ref("shared"),
248+
PresharedKey: ref("shared"),
249+
},
250+
expect: []credentialSecret{
251+
{name: "shared", credType: metadata.CredentialTypeDatastoreURI},
252+
{name: "shared", credType: metadata.CredentialTypePresharedKey},
253+
},
254+
},
255+
{
256+
name: "shared secret across all three roles returns three entries",
257+
credentials: &v1alpha1.ClusterCredentials{
258+
DatastoreURI: ref("shared"),
259+
PresharedKey: ref("shared"),
260+
MigrationSecrets: ref("shared"),
261+
},
262+
expect: []credentialSecret{
263+
{name: "shared", credType: metadata.CredentialTypeDatastoreURI},
264+
{name: "shared", credType: metadata.CredentialTypePresharedKey},
265+
{name: "shared", credType: metadata.CredentialTypeMigrationSecrets},
266+
},
267+
},
268+
{
269+
name: "skipped ref is excluded",
270+
credentials: &v1alpha1.ClusterCredentials{
271+
DatastoreURI: skipped("shared"),
272+
PresharedKey: ref("shared"),
273+
},
274+
expect: []credentialSecret{
275+
{name: "shared", credType: metadata.CredentialTypePresharedKey},
276+
},
277+
},
278+
}
279+
for _, tt := range tests {
280+
t.Run(tt.name, func(t *testing.T) {
281+
cluster := &v1alpha1.SpiceDBCluster{
282+
Spec: v1alpha1.ClusterSpec{
283+
Credentials: tt.credentials,
284+
SecretRef: tt.secretRef,
285+
},
286+
}
287+
require.Equal(t, tt.expect, credentialSecretsForCluster(cluster))
288+
})
289+
}
290+
}

pkg/controller/secret_adoption.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,21 @@ import (
1818

1919
const EventSecretAdoptedBySpiceDBCluster = "SecretAdoptedBySpiceDB"
2020

21-
func NewSecretAdoptionHandler(recorder record.EventRecorder, getFromCache func(ctx context.Context) (*corev1.Secret, error), missingFunc func(ctx context.Context, err error), secretIndexer *typed.Indexer[*corev1.Secret], secretApplyFunc adopt.ApplyFunc[*corev1.Secret, *applycorev1.SecretApplyConfiguration], existsFunc func(ctx context.Context, name types.NamespacedName) error, next handler.Handler) handler.Handler {
21+
func NewSecretAdoptionHandler(recorder record.EventRecorder, getFromCache func(ctx context.Context) (*corev1.Secret, error), missingFunc func(ctx context.Context, err error), secretIndexer *typed.Indexer[*corev1.Secret], secretApplyFunc adopt.ApplyFunc[*corev1.Secret, *applycorev1.SecretApplyConfiguration], existsFunc func(ctx context.Context, name types.NamespacedName) error, credentialType string, next handler.Handler) handler.Handler {
2222
ctxSecret := typedctx.WithDefault[*corev1.Secret](nil)
23+
labels := map[string]string{metadata.OperatorManagedLabelKey: metadata.OperatorManagedLabelValue}
24+
if lk := metadata.LabelKeyForCredentialType(credentialType); lk != "" {
25+
labels[lk] = "true"
26+
}
27+
// Use a role-qualified field manager so that two handlers adopting the same
28+
// shared secret do not release each other's type label via SSA ownership.
29+
fieldManager := metadata.FieldManager
30+
if credentialType != "" {
31+
fieldManager = metadata.FieldManager + "-" + credentialType
32+
}
2333
return handler.NewHandler(&adopt.AdoptionHandler[*corev1.Secret, *applycorev1.SecretApplyConfiguration]{
2434
OperationsContext: QueueOps,
25-
ControllerFieldManager: metadata.FieldManager,
35+
ControllerFieldManager: fieldManager,
2636
AdopteeCtx: CtxSecretNN,
2737
OwnerCtx: CtxClusterNN,
2838
AdoptedCtx: ctxSecret,
@@ -32,8 +42,8 @@ func NewSecretAdoptionHandler(recorder record.EventRecorder, getFromCache func(c
3242
ObjectMissingFunc: missingFunc,
3343
GetFromCache: getFromCache,
3444
Indexer: secretIndexer,
35-
IndexName: metadata.OwningClusterIndex,
36-
Labels: map[string]string{metadata.OperatorManagedLabelKey: metadata.OperatorManagedLabelValue},
45+
IndexName: metadata.IndexNameForCredentialType(credentialType),
46+
Labels: labels,
3747
NewPatch: func(nn types.NamespacedName) *applycorev1.SecretApplyConfiguration {
3848
return applycorev1.Secret(nn.Name, nn.Namespace)
3949
},

0 commit comments

Comments
 (0)