Skip to content

Commit

Permalink
Improve ClusterClass reconcile with RuntimeExtensions at scale
Browse files Browse the repository at this point in the history
Signed-off-by: killianmuldoon <[email protected]>
  • Loading branch information
killianmuldoon committed Sep 1, 2023
1 parent bd336eb commit a57ade9
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 4 deletions.
14 changes: 11 additions & 3 deletions internal/controllers/clusterclass/clusterclass_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ func uniqueObjectRefKey(ref *corev1.ObjectReference) string {
// of the ExtensionConfig.
func (r *Reconciler) extensionConfigToClusterClass(ctx context.Context, o client.Object) []reconcile.Request {
res := []ctrl.Request{}

log := ctrl.LoggerFrom(ctx)
ext, ok := o.(*runtimev1.ExtensionConfig)
if !ok {
panic(fmt.Sprintf("Expected an ExtensionConfig but got a %T", o))
Expand All @@ -418,8 +418,16 @@ func (r *Reconciler) extensionConfigToClusterClass(ctx context.Context, o client
}
for _, patch := range clusterClass.Spec.Patches {
if patch.External != nil && patch.External.DiscoverVariablesExtension != nil {
res = append(res, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: clusterClass.Namespace, Name: clusterClass.Name}})
break
extName, err := runtimeclient.ExtensionNameFromHandlerName(*patch.External.DiscoverVariablesExtension)
if err != nil {
log.Error(err, "failed to reconcile ClusterClass for ExtensionConfig")
continue
}
if extName == ext.Name {
res = append(res, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: clusterClass.Namespace, Name: clusterClass.Name}})
// Once we've added the ClusterClass once we can break here.
break
}
}
}
}
Expand Down
73 changes: 73 additions & 0 deletions internal/controllers/clusterclass/clusterclass_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ import (
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/component-base/featuregate/testing"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
"sigs.k8s.io/cluster-api/feature"
Expand Down Expand Up @@ -545,3 +549,72 @@ func TestReconciler_reconcileVariables(t *testing.T) {
})
}
}

func TestReconciler_extensionConfigToClusterClass(t *testing.T) {
firstExtConfig := &runtimev1.ExtensionConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "runtime1",
},
TypeMeta: metav1.TypeMeta{
Kind: "ExtensionConfig",
APIVersion: runtimev1.GroupVersion.String(),
},
Spec: runtimev1.ExtensionConfigSpec{
NamespaceSelector: &metav1.LabelSelector{},
},
}
secondExtConfig := &runtimev1.ExtensionConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "runtime2",
},
TypeMeta: metav1.TypeMeta{
Kind: "ExtensionConfig",
APIVersion: runtimev1.GroupVersion.String(),
},
Spec: runtimev1.ExtensionConfigSpec{
NamespaceSelector: &metav1.LabelSelector{},
},
}

// These ClusterClasses will be reconciled as they both reference the passed ExtensionConfig `runtime1`.
onePatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc1").
WithPatches([]clusterv1.ClusterClassPatch{
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}}}).
Build()
twoPatchClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc2").
WithPatches([]clusterv1.ClusterClassPatch{
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime1")}},
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.runtime2")}}}).
Build()

// This ClusterClasses will not be reconciled as it does not reference the passed ExtensionConfig `runtime1`.
notReconciledClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "cc3").
WithPatches([]clusterv1.ClusterClassPatch{
{External: &clusterv1.ExternalPatchDefinition{DiscoverVariablesExtension: pointer.String("discover-variables.other-runtime-class")}}}).
Build()

t.Run("test", func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithObjects(onePatchClusterClass, notReconciledClusterClass, twoPatchClusterClass).Build()
r := &Reconciler{
Client: fakeClient,
}

// Expect both onePatchClusterClass and twoPatchClusterClass to trigger a reconcile as both reference ExtensionCopnfig `runtime1`.
firstExtConfigExpected := []reconcile.Request{
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: onePatchClusterClass.Namespace, Name: onePatchClusterClass.Name}},
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}},
}
if got := r.extensionConfigToClusterClass(context.Background(), firstExtConfig); !reflect.DeepEqual(got, firstExtConfigExpected) {
t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, firstExtConfigExpected)
}

// Expect only twoPatchClusterClass to trigger a reconcile as it's the only class with a reference to ExtensionCopnfig `runtime2`.
secondExtConfigExpected := []reconcile.Request{
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: twoPatchClusterClass.Namespace, Name: twoPatchClusterClass.Name}},
}
if got := r.extensionConfigToClusterClass(context.Background(), secondExtConfig); !reflect.DeepEqual(got, secondExtConfigExpected) {
t.Errorf("extensionConfigToClusterClass() = %v, want %v", got, secondExtConfigExpected)
}

})
}
23 changes: 22 additions & 1 deletion internal/runtime/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,14 @@ func (c *client) Discover(ctx context.Context, extensionConfig *runtimev1.Extens
modifiedExtensionConfig.Status.Handlers = []runtimev1.ExtensionHandler{}

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

return selector.Matches(labels.Set(ns.GetLabels())), nil
}

// NameForHandler constructs a canonical name for a registered runtime extension handler.
func NameForHandler(handler runtimehooksv1.ExtensionHandler, extensionConfig *runtimev1.ExtensionConfig) (string, error) {
if extensionConfig == nil {
return "", errors.New("extensionConfig was nil")
}
return fmt.Sprint(handler.Name + "." + extensionConfig.Name), nil
}

// ExtensionNameFromHandlerName extracts the extension name from the canonical name of a registered runtime extension handler.
func ExtensionNameFromHandlerName(registeredHandlerName string) (string, error) {
parts := strings.Split(registeredHandlerName, ".")
if len(parts) != 2 {
return "", errors.Errorf("registered handler name %s was not in the expected format (`HANDLER_NAME.EXTENSION_NAME)", registeredHandlerName)
}
return parts[1], nil
}
68 changes: 68 additions & 0 deletions internal/runtime/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1334,3 +1334,71 @@ func newUnstartedTLSServer(handler http.Handler) *httptest.Server {
}
return srv
}

func TestNameForHandler(t *testing.T) {
tests := []struct {
name string
handler runtimehooksv1.ExtensionHandler
extensionConfig *runtimev1.ExtensionConfig
want string
wantErr bool
}{
{
name: "return well formatted name",
handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
extensionConfig: &runtimev1.ExtensionConfig{ObjectMeta: metav1.ObjectMeta{Name: "runtime1"}},
want: "discover-variables.runtime1",
},
{
name: "return well formatted name",
handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
extensionConfig: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NameForHandler(tt.handler, tt.extensionConfig)
if (err != nil) != tt.wantErr {
t.Errorf("NameForHandler() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("NameForHandler() got = %v, want %v", got, tt.want)
}
})
}
}

func TestExtensionNameFromHandlerName(t *testing.T) {
tests := []struct {
name string
registeredHandlerName string
want string
wantErr bool
}{
{
name: "Get name from correctly formatted handler name",
registeredHandlerName: "discover-variables.runtime1",
want: "runtime1",
},
{
name: "error from incorrectly formatted handler name",
// Two periods make this name badly formed.
registeredHandlerName: "discover-variables.runtime.1",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ExtensionNameFromHandlerName(tt.registeredHandlerName)
if (err != nil) != tt.wantErr {
t.Errorf("ExtensionNameFromHandlerName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ExtensionNameFromHandlerName() got = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit a57ade9

Please sign in to comment.