Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions pkg/serviceprovider/apireconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type APIReconciler[T API, C Config] struct {
withWorkloadCluster bool
// secretNamespace is the namespace to watch secrets in on the platform cluster. Used only if the ServiceProviderReconciler also implements SecretWatcher.
secretNamespace string
// configMapNamespace is the namespace to watch ConfigMaps in on the platform cluster. Used only if the ServiceProviderReconciler also implements ConfigMapWatcher.
configMapNamespace string
// emptyObj creates an empty object of the api type
emptyObj func() T
}
Expand Down Expand Up @@ -123,6 +125,13 @@ func (b *APIReconcilerBuilder[T, C]) SecretNamespace(ns string) *APIReconcilerBu
return b
}

// ConfigMapNamespace enables ConfigMap watching in the given namespace on the platform cluster.
// Only used if the ServiceProviderReconciler also implements ConfigMapWatcher.
func (b *APIReconcilerBuilder[T, C]) ConfigMapNamespace(ns string) *APIReconcilerBuilder[T, C] {
b.apiReconciler.configMapNamespace = ns
return b
}

// ProviderConfig sets the provider config.
func (b *APIReconcilerBuilder[T, C]) ProviderConfig(config C) *APIReconcilerBuilder[T, C] {
b.apiReconciler.providerConfig.Store(&config)
Expand Down Expand Up @@ -360,6 +369,22 @@ func (r *APIReconciler[T, C]) SetupWithManager(mgr ctrl.Manager, name string, pr
)
}

// Optional: watch ConfigMaps on the platform cluster if the reconciler implements ConfigMapWatcher
if cw, ok := r.reconciler.(ConfigMapWatcher[C]); ok && r.configMapNamespace != "" {
controller = controller.WatchesRawSource(
source.Kind(
r.platformCluster.Cluster().GetCache(),
&corev1.ConfigMap{},
handler.TypedEnqueueRequestsFromMapFunc(r.mapConfigMapToRequests(cw)),
controllerutil2.ToTypedPredicate[*corev1.ConfigMap](
predicate.NewPredicateFuncs(func(obj client.Object) bool {
return obj.GetNamespace() == r.configMapNamespace
}),
),
),
)
}

return controller.Named(name).Complete(r)
}

Expand All @@ -378,6 +403,21 @@ func (r *APIReconciler[T, C]) mapSecretToRequests(sw SecretWatcher[C]) func(ctx
}
}

// mapConfigMapToRequests returns a typed map function that checks whether a changed ConfigMap
// is referenced by the service provider and, if so, enqueues all ServiceProviderAPI objects.
func (r *APIReconciler[T, C]) mapConfigMapToRequests(cw ConfigMapWatcher[C]) func(ctx context.Context, configMap *corev1.ConfigMap) []reconcile.Request {
return func(ctx context.Context, configMap *corev1.ConfigMap) []reconcile.Request {
var pcVal C
if pc := r.providerConfig.Load(); pc != nil {
pcVal = *pc
}
if !cw.IsReferencedConfigMap(ctx, configMap, pcVal) {
return nil
}
return r.enqueueAllObjects(ctx)
}
}

// enqueueAllObjects lists all ServiceProviderAPI objects and returns a reconcile request for each.
func (r *APIReconciler[T, C]) enqueueAllObjects(ctx context.Context) []reconcile.Request {
var list unstructured.UnstructuredList
Expand Down
103 changes: 102 additions & 1 deletion pkg/serviceprovider/apireconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,18 @@ func (m *MockSecretWatchingReconciler) IsReferencedSecret(_ context.Context, sec
return m.referencedSecrets[secret.Name]
}

// MockConfigMapWatchingReconciler satisfies both ServiceProviderReconciler and ConfigMapWatcher.
type MockConfigMapWatchingReconciler struct {
MockServiceProviderReconciler
referencedConfigMaps map[string]bool
}

var _ ConfigMapWatcher[*fakeProviderConfigImpl] = &MockConfigMapWatchingReconciler{}

func (m *MockConfigMapWatchingReconciler) IsReferencedConfigMap(_ context.Context, configMap *corev1.ConfigMap, _ *fakeProviderConfigImpl) bool {
return m.referencedConfigMaps[configMap.Name]
}

// createFakeClusterWithUnstructuredList creates a fake cluster whose client supports
// listing unstructured objects by intercepting List calls and populating the result
// from the given objects.
Expand Down Expand Up @@ -544,7 +556,7 @@ func TestMapSecretToRequests(t *testing.T) {
referenced: map[string]bool{secretName: true},
providerConfig: &fakeProviderConfigImpl{FakePollInterval: time.Hour},
existingObjs: []client.Object{
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-1", Namespace: testNamespaceName}},
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-1", Namespace: testNamespaceName}}, //nolint:goconst
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-2", Namespace: testNamespaceName}},
},
wantRequests: 2,
Expand Down Expand Up @@ -659,3 +671,92 @@ func TestAPIReconciler_enqueueAllObjects(t *testing.T) {
})
}
}

func TestMapConfigMapToRequests(t *testing.T) {
const configMapName = "my-configmap"
tests := []struct {
name string
configMap *corev1.ConfigMap
referenced map[string]bool
providerConfig *fakeProviderConfigImpl
existingObjs []client.Object
wantRequests int
}{
{
name: "referenced ConfigMap with existing objects triggers reconciliation",
configMap: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: testNamespaceName},
},
referenced: map[string]bool{configMapName: true},
providerConfig: &fakeProviderConfigImpl{FakePollInterval: time.Hour},
existingObjs: []client.Object{
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-1", Namespace: testNamespaceName}},
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-2", Namespace: testNamespaceName}},
},
wantRequests: 2,
},
{
name: "unreferenced ConfigMap does not trigger reconciliation",
configMap: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "other-configmap", Namespace: testNamespaceName},
},
referenced: map[string]bool{configMapName: true},
providerConfig: &fakeProviderConfigImpl{FakePollInterval: time.Hour},
existingObjs: []client.Object{
&fakeApiImpl{ObjectMeta: metav1.ObjectMeta{Name: "obj-1", Namespace: testNamespaceName}},
},
wantRequests: 0,
},
{
name: "referenced ConfigMap with no existing objects returns empty",
configMap: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: testNamespaceName},
},
referenced: map[string]bool{configMapName: true},
providerConfig: &fakeProviderConfigImpl{FakePollInterval: time.Hour},
existingObjs: nil,
wantRequests: 0,
},
{
name: "nil provider config does not panic",
configMap: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: testNamespaceName},
},
referenced: map[string]bool{configMapName: true},
providerConfig: nil,
existingObjs: nil,
wantRequests: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
onboardingCluster := createFakeClusterWithUnstructuredList(t, "onboarding", tt.existingObjs)

mockCW := &MockConfigMapWatchingReconciler{
referencedConfigMaps: tt.referenced,
}

r := &APIReconciler[*fakeApiImpl, *fakeProviderConfigImpl]{
emptyObj: func() *fakeApiImpl { return &fakeApiImpl{} },
onboardingCluster: onboardingCluster,
reconciler: mockCW,
}
r.providerConfig.Store(&tt.providerConfig)

mapFn := r.mapConfigMapToRequests(mockCW)
reqs := mapFn(context.Background(), tt.configMap)
assert.Equal(t, tt.wantRequests, len(reqs))

if tt.wantRequests > 0 {
names := make(map[string]bool)
for _, req := range reqs {
names[req.Name] = true
}
for _, obj := range tt.existingObjs {
assert.True(t, names[obj.GetName()], "expected request for object %s", obj.GetName())
}
}
})
}
}
18 changes: 18 additions & 0 deletions pkg/serviceprovider/configmapwatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package serviceprovider

import (
"context"

corev1 "k8s.io/api/core/v1"
)

// ConfigMapWatcher can optionally be implemented by a Reconciler to trigger reconciliation
// of all API objects when a referenced ConfigMap in the provider namespace changes.
// The watch is set up on the platform cluster and filtered to the namespace configured via ConfigMapNamespace.
type ConfigMapWatcher[PC Config] interface {
// IsReferencedConfigMap returns true if the given ConfigMap should trigger
// reconciliation. pc is the current provider config — it will be the
// zero value (nil for pointer types) if not yet loaded; implementations
// must guard against this.
IsReferencedConfigMap(ctx context.Context, configMap *corev1.ConfigMap, pc PC) bool
}
2 changes: 1 addition & 1 deletion pkg/serviceprovider/secretwatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

// SecretWatcher can optionally be implemented by a Reconciler to trigger reconciliation
// of all API objects when a referenced secret in the provider namespace changes.
// The watch is set up on the platform cluster and filtered to the namespace configured via WithSecretNamespace.
// The watch is set up on the platform cluster and filtered to the namespace configured via SecretNamespace.
type SecretWatcher[PC Config] interface {
// IsReferencedSecret returns true if the given secret should trigger
// reconciliation. pc is the current provider config — it will be the
Expand Down