Skip to content

Commit

Permalink
Add discoverVariables call to the ClusterClass reconciler
Browse files Browse the repository at this point in the history
  • Loading branch information
killianmuldoon committed Feb 1, 2023
1 parent e9a7318 commit 4a98ac7
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 16 deletions.
4 changes: 4 additions & 0 deletions api/v1beta1/clusterclass_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@ type ExternalPatchDefinition struct {
// +optional
ValidateExtension *string `json:"validateExtension,omitempty"`

// DiscoverVariablesExtension references an extension which is called to discover variables.
// +optional
DiscoverVariablesExtension *string `json:"discoverVariablesExtension,omitempty"`

// Settings defines key value pairs to be passed to the extensions.
// Values defined here take precedence over the values defined in the
// corresponding ExtensionConfig.
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions api/v1beta1/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmd/clusterctl/client/alpha/rollout_pauser.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (r *rollout) ObjectPauser(proxy cluster.Proxy, ref corev1.ObjectReference)
return errors.Wrapf(err, "failed to fetch %v/%v", ref.Kind, ref.Name)
}
if deployment.Spec.Paused {
return errors.Errorf("MachineDeploymet is already paused: %v/%v\n", ref.Kind, ref.Name) //nolint:revive // MachineDeployment is intentionally capitalized.
return errors.Errorf("MachineDeployment is already paused: %v/%v\n", ref.Kind, ref.Name) //nolint:revive // MachineDeployment is intentionally capitalized.
}
if err := pauseMachineDeployment(proxy, ref.Name, ref.Namespace); err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions controllers/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ type ClusterClassReconciler struct {
Client client.Client
APIReader client.Reader

// RuntimeClient is a client for calling runtime extensions.
RuntimeClient runtimeclient.Client

// WatchFilterValue is the label value used to filter events prior to reconciliation.
WatchFilterValue string

Expand All @@ -212,6 +215,7 @@ func (r *ClusterClassReconciler) SetupWithManager(ctx context.Context, mgr ctrl.
return (&clusterclasscontroller.Reconciler{
Client: r.Client,
APIReader: r.APIReader,
RuntimeClient: r.RuntimeClient,
UnstructuredCachingClient: r.UnstructuredCachingClient,
WatchFilterValue: r.WatchFilterValue,
}).SetupWithManager(ctx, mgr, options)
Expand Down
98 changes: 87 additions & 11 deletions internal/controllers/clusterclass/clusterclass_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,24 @@ import (
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/controllers/external"
runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
"sigs.k8s.io/cluster-api/feature"
tlog "sigs.k8s.io/cluster-api/internal/log"
runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
"sigs.k8s.io/cluster-api/util/annotations"
"sigs.k8s.io/cluster-api/util/conditions"
"sigs.k8s.io/cluster-api/util/conversion"
Expand All @@ -53,6 +61,9 @@ type Reconciler struct {
// WatchFilterValue is the label value used to filter events prior to reconciliation.
WatchFilterValue string

// RuntimeClient is a client for calling runtime extensions.
RuntimeClient runtimeclient.Client

// UnstructuredCachingClient provides a client that forces caching of unstructured objects,
// thus allowing to optimize reads for templates or provider specific objects.
UnstructuredCachingClient client.Client
Expand All @@ -63,8 +74,13 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
For(&clusterv1.ClusterClass{}).
Named("clusterclass").
WithOptions(options).
Watches(
&source.Kind{Type: &runtimev1.ExtensionConfig{}},
handler.EnqueueRequestsFromMapFunc(r.extensionConfigToClusterClass),
).
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
Complete(r)

if err != nil {
return errors.Wrap(err, "failed setting up with a controller manager")
}
Expand Down Expand Up @@ -177,25 +193,57 @@ func (r *Reconciler) reconcile(ctx context.Context, clusterClass *clusterv1.Clus
return ctrl.Result{}, kerrors.NewAggregate(errs)
}

// Ensure the variables are added to the ClusterClass status.
// Add inline variable definitions to the ClusterClass status.
clusterClass.Status.Variables = []clusterv1.ClusterClassStatusVariable{}
for _, variable := range clusterClass.Spec.Variables {
clusterClass.Status.Variables = append(clusterClass.Status.Variables,
clusterv1.ClusterClassStatusVariable{
Name: variable.Name,
Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
{
From: clusterv1.VariableDefinitionFromInline,
Required: variable.Required,
Schema: variable.Schema,
},
}})
clusterClass.Status.Variables = append(clusterClass.Status.Variables, statusVariableFromClusterClassVariable(variable, clusterv1.VariableDefinitionFromInline))
}

// If RuntimeSDK is enabled call the DiscoverVariables hook for all associated Runtime Extensions and add the variables
// to the ClusterClass status.
if feature.Gates.Enabled(feature.RuntimeSDK) {
for _, patch := range clusterClass.Spec.Patches {
if patch.External == nil || patch.External.DiscoverVariablesExtension == nil {
continue
}
req := &runtimehooksv1.DiscoverVariablesRequest{}
req.Settings = patch.External.Settings

resp := &runtimehooksv1.DiscoverVariablesResponse{}
err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp)
if err != nil {
return ctrl.Result{}, err
}
if resp.Status != runtimehooksv1.ResponseStatusSuccess {
return ctrl.Result{}, errors.Errorf("failed to discover variables for ClusterClass %s: %s", clusterClass.Name, resp.Message)
}
if resp.Variables != nil {
for _, variable := range resp.Variables {
// TODO: Variables should be validated and deduplicated base on their provided definition.
clusterClass.Status.Variables = append(clusterClass.Status.Variables, statusVariableFromClusterClassVariable(variable, patch.Name))
}
}
}
}
reconcileConditions(clusterClass, outdatedRefs)

return ctrl.Result{}, nil
}

func statusVariableFromClusterClassVariable(variable clusterv1.ClusterClassVariable, from string) clusterv1.ClusterClassStatusVariable {
return clusterv1.ClusterClassStatusVariable{
Name: variable.Name,
// TODO: In a future iteration this should be false where definitions are equal.
DefintionsConflict: true,
Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
{
From: from,
Required: variable.Required,
Schema: variable.Schema,
},
}}
}

func reconcileConditions(clusterClass *clusterv1.ClusterClass, outdatedRefs map[*corev1.ObjectReference]*corev1.ObjectReference) {
if len(outdatedRefs) > 0 {
var msg []string
Expand Down Expand Up @@ -263,3 +311,31 @@ func (r *Reconciler) reconcileExternal(ctx context.Context, clusterClass *cluste
func uniqueObjectRefKey(ref *corev1.ObjectReference) string {
return fmt.Sprintf("Name:%s, Namespace:%s, Kind:%s, APIVersion:%s", ref.Name, ref.Namespace, ref.Kind, ref.APIVersion)
}

// extensionConfigToClusterClass maps an ExtensionConfigs with the corresponding ClusterClass to reconcile them on updates
// of the ExtensionConfig.
func (r *Reconciler) extensionConfigToClusterClass(o client.Object) []reconcile.Request {
result := []ctrl.Request{}

ext, ok := o.(*runtimev1.ExtensionConfig)
if !ok {
panic(fmt.Sprintf("Expected an ExtensionConfig but got a %T", o))
}

clusterClasses := clusterv1.ClusterClassList{}
selector, err := metav1.LabelSelectorAsSelector(ext.Spec.NamespaceSelector)
if err != nil {
return nil
}
if err := r.Client.List(
context.TODO(),
&clusterClasses,
client.MatchingLabelsSelector{Selector: selector},
); err != nil {
return nil
}
for _, ext := range clusterClasses.Items {
result = append(result, ctrl.Request{NamespacedName: client.ObjectKey{Name: ext.Name}})
}
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,16 @@ func assertStatusVariables(actualClusterClass *clusterv1.ClusterClass) error {
continue
}
found = true
if statusVar.DefintionsConflict {
return errors.Errorf("ClusterClass status %s variable RequiresNamespace does not match. Expected %t , got %t", specVar.Name, false, statusVar.DefintionsConflict)
if !statusVar.DefintionsConflict {
return errors.Errorf("ClusterClass status %s variable DefintionsConflict does not match. Expected %v , got %v", specVar.Name, true, statusVar.DefintionsConflict)
}
if len(statusVar.Definitions) != 1 {
return errors.Errorf("ClusterClass status has multiple definitions for variable %s. Expected a single definition", specVar.Name)
}
// For this test assume there is only one status variable definition, and that it should match the spec.
statusVarDefinition := statusVar.Definitions[0]
if statusVarDefinition.From != clusterv1.VariableDefinitionFromInline {
return errors.Errorf("ClusterClass status variable %s namespace field does not match. Expected %s. Got %s", statusVar.Name, clusterv1.VariableDefinitionFromInline, statusVarDefinition.From)
return errors.Errorf("ClusterClass status variable %s from field does not match. Expected %s. Got %s", statusVar.Name, clusterv1.VariableDefinitionFromInline, statusVarDefinition.From)
}
if specVar.Required != statusVarDefinition.Required {
return errors.Errorf("ClusterClass status variable %s required field does not match. Expecte %v. Got %v", specVar.Name, statusVarDefinition.Required, statusVarDefinition.Required)
Expand All @@ -159,7 +159,7 @@ func assertStatusVariables(actualClusterClass *clusterv1.ClusterClass) error {
}
}
if !found {
return errors.Errorf("ClusterClass does not define variable %s", specVar.Name)
return errors.Errorf("ClusterClass does not have status for variable %s", specVar.Name)
}
}
return nil
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager) {
if err := (&controllers.ClusterClassReconciler{
Client: mgr.GetClient(),
APIReader: mgr.GetAPIReader(),
RuntimeClient: runtimeClient,
UnstructuredCachingClient: unstructuredCachingClient,
WatchFilterValue: watchFilterValue,
}).SetupWithManager(ctx, mgr, concurrency(clusterClassConcurrency)); err != nil {
Expand Down

0 comments on commit 4a98ac7

Please sign in to comment.