Skip to content

Commit 713fbfa

Browse files
Improve ClusterClass reconcile with RuntimeExtensions at scale
Signed-off-by: killianmuldoon <[email protected]>
1 parent bd336eb commit 713fbfa

File tree

4 files changed

+173
-4
lines changed

4 files changed

+173
-4
lines changed

internal/controllers/clusterclass/clusterclass_controller.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ func uniqueObjectRefKey(ref *corev1.ObjectReference) string {
398398
// of the ExtensionConfig.
399399
func (r *Reconciler) extensionConfigToClusterClass(ctx context.Context, o client.Object) []reconcile.Request {
400400
res := []ctrl.Request{}
401-
401+
log := ctrl.LoggerFrom(ctx)
402402
ext, ok := o.(*runtimev1.ExtensionConfig)
403403
if !ok {
404404
panic(fmt.Sprintf("Expected an ExtensionConfig but got a %T", o))
@@ -418,8 +418,16 @@ func (r *Reconciler) extensionConfigToClusterClass(ctx context.Context, o client
418418
}
419419
for _, patch := range clusterClass.Spec.Patches {
420420
if patch.External != nil && patch.External.DiscoverVariablesExtension != nil {
421-
res = append(res, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: clusterClass.Namespace, Name: clusterClass.Name}})
422-
break
421+
extName, err := runtimeclient.ExtensionNameFromHandlerName(*patch.External.DiscoverVariablesExtension)
422+
if err != nil {
423+
log.Error(err, "failed to reconcile ClusterClass for ExtensionConfig")
424+
continue
425+
}
426+
if extName == ext.Name {
427+
res = append(res, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: clusterClass.Namespace, Name: clusterClass.Name}})
428+
// Once we've added the ClusterClass once we can break here.
429+
break
430+
}
423431
}
424432
}
425433
}

internal/controllers/clusterclass/clusterclass_controller_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import (
2828
"github.com/pkg/errors"
2929
corev1 "k8s.io/api/core/v1"
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
3132
utilfeature "k8s.io/component-base/featuregate/testing"
3233
"k8s.io/utils/pointer"
3334
"sigs.k8s.io/controller-runtime/pkg/client"
35+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
36+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3437

3538
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
39+
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
3640
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
3741
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
3842
"sigs.k8s.io/cluster-api/feature"
@@ -545,3 +549,71 @@ func TestReconciler_reconcileVariables(t *testing.T) {
545549
})
546550
}
547551
}
552+
553+
func TestReconciler_extensionConfigToClusterClass(t *testing.T) {
554+
firstExtConfig := &runtimev1.ExtensionConfig{
555+
ObjectMeta: metav1.ObjectMeta{
556+
Name: "runtime1",
557+
},
558+
TypeMeta: metav1.TypeMeta{
559+
Kind: "ExtensionConfig",
560+
APIVersion: runtimev1.GroupVersion.String(),
561+
},
562+
Spec: runtimev1.ExtensionConfigSpec{
563+
NamespaceSelector: &metav1.LabelSelector{},
564+
},
565+
}
566+
secondExtConfig := &runtimev1.ExtensionConfig{
567+
ObjectMeta: metav1.ObjectMeta{
568+
Name: "runtime2",
569+
},
570+
TypeMeta: metav1.TypeMeta{
571+
Kind: "ExtensionConfig",
572+
APIVersion: runtimev1.GroupVersion.String(),
573+
},
574+
Spec: runtimev1.ExtensionConfigSpec{
575+
NamespaceSelector: &metav1.LabelSelector{},
576+
},
577+
}
578+
579+
// These ClusterClasses will be reconciled as they both reference the passed ExtensionConfig `runtime1`.
580+
onePatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc1").
581+
WithPatches([]clusterv1.ClusterClassPatch{
582+
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}}}).
583+
Build()
584+
twoPatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc2").
585+
WithPatches([]clusterv1.ClusterClassPatch{
586+
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}},
587+
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime2")}}}).
588+
Build()
589+
590+
// This ClusterClasses will not be reconciled as it does not reference the passed ExtensionConfig `runtime1`.
591+
notReconciledClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc3").
592+
WithPatches([]clusterv1.ClusterClassPatch{
593+
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.other-runtime-class")}}}).
594+
Build()
595+
596+
t.Run("test", func(t *testing.T) {
597+
fakeClient := fake.NewClientBuilder().WithObjects(onePatchClusterClass, notReconciledClusterClass, twoPatchClusterClass).Build()
598+
r := &Reconciler{
599+
Client: fakeClient,
600+
}
601+
602+
// Expect both onePatchClusterClass and twoPatchClusterClass to trigger a reconcile as both reference ExtensionCopnfig `runtime1`.
603+
firstExtConfigExpected := []reconcile.Request{
604+
{NamespacedName: types.NamespacedName{Namespace: onePatchClusterClass.Namespace, Name: onePatchClusterClass.Name}},
605+
{NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}},
606+
}
607+
if got := r.extensionConfigToClusterClass(context.Background(), firstExtConfig); !reflect.DeepEqual(got, firstExtConfigExpected) {
608+
t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, firstExtConfigExpected)
609+
}
610+
611+
// Expect only twoPatchClusterClass to trigger a reconcile as it's the only class with a reference to ExtensionCopnfig `runtime2`.
612+
secondExtConfigExpected := []reconcile.Request{
613+
{NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}},
614+
}
615+
if got := r.extensionConfigToClusterClass(context.Background(), secondExtConfig); !reflect.DeepEqual(got, secondExtConfigExpected) {
616+
t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, secondExtConfigExpected)
617+
}
618+
})
619+
}

internal/runtime/client/client.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,14 @@ func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.Extens
154154
modifiedExtensionConfig.Status.Handlers = []runtimev1.ExtensionHandler{}
155155

156156
for _, handler := range response.Handlers {
157+
handlerName, err := NameForHandler(handler, extensionConfig)
158+
if err != nil {
159+
return nil, errors.Wrapf(err, "failed to discover extension %q", extensionConfig.Name)
160+
}
157161
modifiedExtensionConfig.Status.Handlers = append(
158162
modifiedExtensionConfig.Status.Handlers,
159163
runtimev1.ExtensionHandler{
160-
Name: handler.Name + "." + extensionConfig.Name, // Uniquely identifies a handler of an Extension.
164+
Name: handlerName, // Uniquely identifies a handler of an Extension.
161165
RequestHook: runtimev1.GroupVersionHook{
162166
APIVersion: handler.RequestHook.APIVersion,
163167
Hook: handler.RequestHook.Hook,
@@ -655,3 +659,20 @@ func (c *client) matchNamespace(ctx context.Context, selector labels.Selector, n
655659

656660
return selector.Matches(labels.Set(ns.GetLabels())), nil
657661
}
662+
663+
// NameForHandler constructs a canonical name for a registered runtime extension handler.
664+
func NameForHandler(handler runtimehooksv1.ExtensionHandler, extensionConfig *runtimev1.ExtensionConfig) (string, error) {
665+
if extensionConfig == nil {
666+
return "", errors.New("extensionConfig was nil")
667+
}
668+
return handler.Name + "." + extensionConfig.Name, nil
669+
}
670+
671+
// ExtensionNameFromHandlerName extracts the extension name from the canonical name of a registered runtime extension handler.
672+
func ExtensionNameFromHandlerName(registeredHandlerName string) (string, error) {
673+
parts := strings.Split(registeredHandlerName, ".")
674+
if len(parts) != 2 {
675+
return "", errors.Errorf("registered handler name %s was not in the expected format (`HANDLER_NAME.EXTENSION_NAME)", registeredHandlerName)
676+
}
677+
return parts[1], nil
678+
}

internal/runtime/client/client_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,3 +1334,71 @@ func newUnstartedTLSServer(handler http.Handler) *httptest.Server {
13341334
}
13351335
return srv
13361336
}
1337+
1338+
func TestNameForHandler(t *testing.T) {
1339+
tests := []struct {
1340+
name string
1341+
handler runtimehooksv1.ExtensionHandler
1342+
extensionConfig *runtimev1.ExtensionConfig
1343+
want string
1344+
wantErr bool
1345+
}{
1346+
{
1347+
name: "return well formatted name",
1348+
handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
1349+
extensionConfig: &runtimev1.ExtensionConfig{ObjectMeta: metav1.ObjectMeta{Name: "runtime1"}},
1350+
want: "discover-variables.runtime1",
1351+
},
1352+
{
1353+
name: "return well formatted name",
1354+
handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
1355+
extensionConfig: nil,
1356+
wantErr: true,
1357+
},
1358+
}
1359+
for _, tt := range tests {
1360+
t.Run(tt.name, func(t *testing.T) {
1361+
got, err := NameForHandler(tt.handler, tt.extensionConfig)
1362+
if (err != nil) != tt.wantErr {
1363+
t.Errorf("NameForHandler() error = %v, wantErr %v", err, tt.wantErr)
1364+
return
1365+
}
1366+
if got != tt.want {
1367+
t.Errorf("NameForHandler() got = %v, want %v", got, tt.want)
1368+
}
1369+
})
1370+
}
1371+
}
1372+
1373+
func TestExtensionNameFromHandlerName(t *testing.T) {
1374+
tests := []struct {
1375+
name string
1376+
registeredHandlerName string
1377+
want string
1378+
wantErr bool
1379+
}{
1380+
{
1381+
name: "Get name from correctly formatted handler name",
1382+
registeredHandlerName: "discover-variables.runtime1",
1383+
want: "runtime1",
1384+
},
1385+
{
1386+
name: "error from incorrectly formatted handler name",
1387+
// Two periods make this name badly formed.
1388+
registeredHandlerName: "discover-variables.runtime.1",
1389+
wantErr: true,
1390+
},
1391+
}
1392+
for _, tt := range tests {
1393+
t.Run(tt.name, func(t *testing.T) {
1394+
got, err := ExtensionNameFromHandlerName(tt.registeredHandlerName)
1395+
if (err != nil) != tt.wantErr {
1396+
t.Errorf("ExtensionNameFromHandlerName() error = %v, wantErr %v", err, tt.wantErr)
1397+
return
1398+
}
1399+
if got != tt.want {
1400+
t.Errorf("ExtensionNameFromHandlerName() got = %v, want %v", got, tt.want)
1401+
}
1402+
})
1403+
}
1404+
}

0 commit comments

Comments
 (0)