From 71571698346bd18b3f71faf3aeb2a758c93459c7 Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Wed, 21 Sep 2022 15:58:20 +0200 Subject: [PATCH] ClusterClass: add condition for references with outdated apiVersions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- api/v1alpha4/conversion.go | 7 +++ api/v1alpha4/zz_generated.conversion.go | 16 ++--- api/v1beta1/clusterclass_types.go | 25 +++++++- api/v1beta1/condition_consts.go | 12 ++++ api/v1beta1/zz_generated.deepcopy.go | 23 +++++++ api/v1beta1/zz_generated.openapi.go | 38 +++++++++++- .../cluster.x-k8s.io_clusterclasses.yaml | 52 +++++++++++++++- config/rbac/role.yaml | 11 ++++ .../clusterclass/clusterclass_controller.go | 61 ++++++++++++++++++- 9 files changed, 229 insertions(+), 16 deletions(-) diff --git a/api/v1alpha4/conversion.go b/api/v1alpha4/conversion.go index 72642d04f9e7..dcf15a849aa0 100644 --- a/api/v1alpha4/conversion.go +++ b/api/v1alpha4/conversion.go @@ -115,6 +115,8 @@ func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Workers.MachineDeployments[i].MachineHealthCheck = restored.Spec.Workers.MachineDeployments[i].MachineHealthCheck } + dst.Status = restored.Status + return nil } @@ -339,3 +341,8 @@ func Convert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in *clusterv1.Machi // MachineStatus.CertificatesExpiryDate has been added in v1beta1. return autoConvert_v1beta1_MachineStatus_To_v1alpha4_MachineStatus(in, out, s) } + +func Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in *clusterv1.ClusterClass, out *ClusterClass, s apiconversion.Scope) error { + // ClusterClass.Status has been added in v1beta1. + return autoConvert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in, out, s) +} diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index 10e1913232f5..61633ad2190e 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -75,11 +75,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta1.ClusterClass)(nil), (*ClusterClass)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(a.(*v1beta1.ClusterClass), b.(*ClusterClass), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*ClusterClassList)(nil), (*v1beta1.ClusterClassList)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_ClusterClassList_To_v1beta1_ClusterClassList(a.(*ClusterClassList), b.(*v1beta1.ClusterClassList), scope) }); err != nil { @@ -445,6 +440,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta1.ClusterClass)(nil), (*ClusterClass)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(a.(*v1beta1.ClusterClass), b.(*ClusterClass), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta1.ControlPlaneClass)(nil), (*ControlPlaneClass)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(a.(*v1beta1.ControlPlaneClass), b.(*ControlPlaneClass), scope) }); err != nil { @@ -577,14 +577,10 @@ func autoConvert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in *v1beta1.Clust if err := Convert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(&in.Spec, &out.Spec, s); err != nil { return err } + // WARNING: in.Status requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass is an autogenerated conversion function. -func Convert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in *v1beta1.ClusterClass, out *ClusterClass, s conversion.Scope) error { - return autoConvert_v1beta1_ClusterClass_To_v1alpha4_ClusterClass(in, out, s) -} - func autoConvert_v1alpha4_ClusterClassList_To_v1beta1_ClusterClassList(in *ClusterClassList, out *v1beta1.ClusterClassList, s conversion.Scope) error { out.ListMeta = in.ListMeta if in.Items != nil { diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index d3386811a767..0930631add80 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -26,6 +26,7 @@ import ( // +kubebuilder:object:root=true // +kubebuilder:resource:path=clusterclasses,shortName=cc,scope=Namespaced,categories=cluster-api // +kubebuilder:storageversion +// +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of ClusterClass" // ClusterClass is a template which can be used to create managed topologies. @@ -33,7 +34,8 @@ type ClusterClass struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ClusterClassSpec `json:"spec,omitempty"` + Spec ClusterClassSpec `json:"spec,omitempty"` + Status ClusterClassStatus `json:"status,omitempty"` } // ClusterClassSpec describes the desired state of the ClusterClass. @@ -468,6 +470,27 @@ type LocalObjectTemplate struct { Ref *corev1.ObjectReference `json:"ref"` } +// ANCHOR: ClusterClassStatus + +// ClusterClassStatus defines the observed state of the ClusterClass. +type ClusterClassStatus struct { + // Conditions defines current observed state of the ClusterClass. + // +optional + Conditions Conditions `json:"conditions,omitempty"` +} + +// GetConditions returns the set of conditions for this object. +func (c *ClusterClass) GetConditions() Conditions { + return c.Status.Conditions +} + +// SetConditions sets the conditions on this object. +func (c *ClusterClass) SetConditions(conditions Conditions) { + c.Status.Conditions = conditions +} + +// ANCHOR_END: ClusterClassStatus + // +kubebuilder:object:root=true // ClusterClassList contains a list of Cluster. diff --git a/api/v1beta1/condition_consts.go b/api/v1beta1/condition_consts.go index d404f0e52daa..49f430142079 100644 --- a/api/v1beta1/condition_consts.go +++ b/api/v1beta1/condition_consts.go @@ -282,3 +282,15 @@ const ( // not yet completed because at least one of the lifecycle hooks is blocking. TopologyReconciledHookBlockingReason = "LifecycleHookBlocking" ) + +// Conditions amd condition reasons for ClusterClass. +const ( + // ClusterClassValidationSucceededCondition documents if the validation of the ClusterClass + // succeeded. + ClusterClassValidationSucceededCondition ConditionType = "ClusterClassValidationSucceeded" + + // ClusterClassOutdatedAPIVersionsInReferencesReason (Severity=Warning) documents that + // the ClusterClass contains references which are not using the latest apiVersion + // (according to the labels on the corresponding CRDs). + ClusterClassOutdatedAPIVersionsInReferencesReason = "OutdatedAPIVersionsInReferences" +) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a754d7e349ec..e1e955f0c247 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -103,6 +103,7 @@ func (in *ClusterClass) DeepCopyInto(out *ClusterClass) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClass. @@ -219,6 +220,28 @@ func (in *ClusterClassSpec) DeepCopy() *ClusterClassSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassStatus) DeepCopyInto(out *ClusterClassStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassStatus. +func (in *ClusterClassStatus) DeepCopy() *ClusterClassStatus { + if in == nil { + return nil + } + out := new(ClusterClassStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClassVariable) DeepCopyInto(out *ClusterClassVariable) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index decd991b955d..9b8a83cfdd89 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -37,6 +37,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassList": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassList(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassPatch(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassSpec": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatus": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatus(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariable(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterList": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterList(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ClusterNetwork": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterNetwork(ref), @@ -229,11 +230,17 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClass(ref common.ReferenceC Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassSpec"), }, }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatus"), + }, + }, }, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassSpec"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassSpec", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassStatus"}, } } @@ -408,6 +415,35 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere } } +func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterClassStatus defines the observed state of the ClusterClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions defines current observed state of the ClusterClass.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.Condition"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "sigs.k8s.io/cluster-api/api/v1beta1.Condition"}, + } +} + func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassVariable(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index a365e3e34d79..9053c7d8ba7c 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -1247,7 +1247,57 @@ spec: type: array type: object type: object + status: + description: ClusterClassStatus defines the observed state of the ClusterClass. + properties: + conditions: + description: Conditions defines current observed state of the ClusterClass. + items: + description: Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + type: object type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9a2a8ca3bb28..ce89a490ae9b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -81,6 +81,17 @@ rules: - patch - update - watch +- apiGroups: + - cluster.x-k8s.io + resources: + - clusterclasses + - clusterclasses/status + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - cluster.x-k8s.io resources: diff --git a/internal/controllers/clusterclass/clusterclass_controller.go b/internal/controllers/clusterclass/clusterclass_controller.go index 73c8a9951d1f..e792d5e9e0a3 100644 --- a/internal/controllers/clusterclass/clusterclass_controller.go +++ b/internal/controllers/clusterclass/clusterclass_controller.go @@ -19,6 +19,7 @@ package clusterclass import ( "context" "fmt" + "strings" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -34,12 +35,14 @@ import ( "sigs.k8s.io/cluster-api/controllers/external" tlog "sigs.k8s.io/cluster-api/internal/log" "sigs.k8s.io/cluster-api/util/annotations" + "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/conversion" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/predicates" ) // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io;bootstrap.cluster.x-k8s.io;controlplane.cluster.x-k8s.io,resources=*,verbs=get;list;watch;update;patch -// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusterclasses,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusterclasses;clusterclasses/status,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch // Reconciler reconciles the ClusterClass object. @@ -90,6 +93,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re return ctrl.Result{}, nil } + patchHelper, err := patch.NewHelper(clusterClass, r.Client) + if err != nil { + return ctrl.Result{}, errors.Wrapf(err, "failed to create patch helper for %s", tlog.KObj{Obj: clusterClass}) + } + + defer func() { + if err := patchHelper.Patch(ctx, clusterClass); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, errors.Wrapf(err, "failed to patch %s", tlog.KObj{Obj: clusterClass})}) + return + } + }() + return r.reconcile(ctx, clusterClass) } @@ -124,6 +139,7 @@ func (r *Reconciler) reconcile(ctx context.Context, clusterClass *clusterv1.Clus // classes. errs := []error{} reconciledRefs := sets.NewString() + outdatedRefs := map[*corev1.ObjectReference]*corev1.ObjectReference{} for i := range refs { ref := refs[i] uniqueKey := uniqueObjectRefKey(ref) @@ -133,14 +149,53 @@ func (r *Reconciler) reconcile(ctx context.Context, clusterClass *clusterv1.Clus continue } + reconciledRefs.Insert(uniqueKey) + if err := r.reconcileExternal(ctx, clusterClass, ref); err != nil { errs = append(errs, err) continue } - reconciledRefs.Insert(uniqueKey) + + updatedRef := ref.DeepCopy() + if err := conversion.UpdateReferenceAPIContract(ctx, r.Client, r.APIReader, updatedRef); err != nil { + errs = append(errs, err) + } + + if ref.GroupVersionKind().Version != updatedRef.GroupVersionKind().Version { + outdatedRefs[ref] = updatedRef + } + } + if len(errs) > 0 { + return ctrl.Result{}, kerrors.NewAggregate(errs) + } + + reconcileConditions(clusterClass, outdatedRefs) + + return ctrl.Result{}, nil +} + +func reconcileConditions(clusterClass *clusterv1.ClusterClass, outdatedRefs map[*corev1.ObjectReference]*corev1.ObjectReference) { + if len(outdatedRefs) > 0 { + var msg []string + for currentRef, updatedRef := range outdatedRefs { + msg = append(msg, fmt.Sprintf("%s should be %s", currentRef.GroupVersionKind().String(), updatedRef.GroupVersionKind().String())) + } + conditions.Set( + clusterClass, + conditions.FalseCondition( + clusterv1.ClusterClassValidationSucceededCondition, + clusterv1.ClusterClassOutdatedAPIVersionsInReferencesReason, + clusterv1.ConditionSeverityWarning, + strings.Join(msg, ", "), + ), + ) + return } - return ctrl.Result{}, kerrors.NewAggregate(errs) + conditions.Set( + clusterClass, + conditions.TrueCondition(clusterv1.ClusterClassValidationSucceededCondition), + ) } func (r *Reconciler) reconcileExternal(ctx context.Context, clusterClass *clusterv1.ClusterClass, ref *corev1.ObjectReference) error {