From e57c80a5c9fe22402eebf6d87b7eaf0675ccc75e Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Fri, 21 Jun 2024 13:45:13 +0200 Subject: [PATCH 1/7] Implement ManagedOSVersion controller and finalizer Signed-off-by: Andrea Mazzotti --- api/v1beta1/common_consts.go | 3 + api/v1beta1/machineinventory_types.go | 2 +- api/v1beta1/managedosversion_types.go | 5 +- api/v1beta1/zz_generated.deepcopy.go | 3 +- ...elemental.cattle.io_managedosversions.yaml | 426 ------------------ config/rbac/bases/role.yaml | 1 + controllers/managedosimage_controller.go | 6 + controllers/managedosversion_controller.go | 175 +++++++ .../managedosversion_controller_test.go | 168 +++++++ 9 files changed, 358 insertions(+), 431 deletions(-) create mode 100644 controllers/managedosversion_controller.go create mode 100644 controllers/managedosversion_controller_test.go diff --git a/api/v1beta1/common_consts.go b/api/v1beta1/common_consts.go index 53e1c94b8..89946de63 100644 --- a/api/v1beta1/common_consts.go +++ b/api/v1beta1/common_consts.go @@ -20,6 +20,9 @@ const ( // ElementalManagedLabel label used to put on resources managed by the elemental operator. ElementalManagedLabel = "elemental.cattle.io/managed" + // ElementalManagedOSImageVersionNameLabel label used filter ManagedOSImages referencing a ManagedOSVersion. + ElementalManagedOSImageVersionNameLabel = "elemental.cattle.io/managed-os-version-name" + // ElementalManagedOSVersionChannelLabel is used to filter a set of ManagedOSVersions given the channel they originate from. ElementalManagedOSVersionChannelLabel = "elemental.cattle.io/channel" diff --git a/api/v1beta1/machineinventory_types.go b/api/v1beta1/machineinventory_types.go index 9d4949756..d6f192042 100644 --- a/api/v1beta1/machineinventory_types.go +++ b/api/v1beta1/machineinventory_types.go @@ -21,7 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var ( +const ( MachineInventoryFinalizer = "machineinventory.elemental.cattle.io" PlanSecretType corev1.SecretType = "elemental.cattle.io/plan" PlanTypeAnnotation = "elemental.cattle.io/plan.type" diff --git a/api/v1beta1/managedosversion_types.go b/api/v1beta1/managedosversion_types.go index 0e94fa664..b088234ef 100644 --- a/api/v1beta1/managedosversion_types.go +++ b/api/v1beta1/managedosversion_types.go @@ -28,8 +28,9 @@ import ( ) const ( - containerType = "container" - isoType = "iso" + containerType = "container" + isoType = "iso" + ManagedOSVersionFinalizer = "managedosversion.elemental.cattle.io" ) // +kubebuilder:object:root=true diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 3fe4c1a58..251dce193 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -765,7 +765,7 @@ func (in *ManagedOSVersion) DeepCopyInto(out *ManagedOSVersion) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSVersion. @@ -960,7 +960,6 @@ func (in *ManagedOSVersionSpec) DeepCopy() *ManagedOSVersionSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedOSVersionStatus) DeepCopyInto(out *ManagedOSVersionStatus) { *out = *in - in.BundleStatus.DeepCopyInto(&out.BundleStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedOSVersionStatus. diff --git a/config/crd/bases/elemental.cattle.io_managedosversions.yaml b/config/crd/bases/elemental.cattle.io_managedosversions.yaml index 571c3c298..f7fe5095e 100644 --- a/config/crd/bases/elemental.cattle.io_managedosversions.yaml +++ b/config/crd/bases/elemental.cattle.io_managedosversions.yaml @@ -394,432 +394,6 @@ spec: type: string type: object status: - properties: - conditions: - description: |- - Conditions is a list of Wrangler conditions that describe the state - of the bundle. - items: - properties: - lastTransitionTime: - description: Last time the condition transitioned from one status - to another. - type: string - lastUpdateTime: - description: The last time this condition was updated. - type: string - message: - description: Human-readable message indicating details about - last transition - type: string - reason: - description: The reason for the condition's last transition. - type: string - status: - description: Status of the condition, one of True, False, Unknown. - type: string - type: - description: Type of cluster condition. - type: string - required: - - status - - type - type: object - type: array - display: - description: |- - Display contains the number of ready, desiredready clusters and a - summary state for the bundle's resources. - properties: - readyClusters: - description: |- - ReadyClusters is a string in the form "%d/%d", that describes the - number of clusters that are ready vs. the number of clusters desired - to be ready. - nullable: true - type: string - state: - description: State is a summary state for the bundle, calculated - over the non-ready resources. - nullable: true - type: string - type: object - maxNew: - description: |- - MaxNew is always 50. A bundle change can only stage 50 - bundledeployments at a time. - type: integer - maxUnavailable: - description: |- - MaxUnavailable is the maximum number of unavailable deployments. See - rollout configuration. - type: integer - maxUnavailablePartitions: - description: |- - MaxUnavailablePartitions is the maximum number of unavailable - partitions. The rollout configuration defines a maximum number or - percentage of unavailable partitions. - type: integer - newlyCreated: - description: |- - NewlyCreated is the number of bundle deployments that have been created, - not updated. - type: integer - observedGeneration: - description: ObservedGeneration is the current generation of the bundle. - format: int64 - type: integer - partitions: - description: PartitionStatus lists the status of each partition. - items: - description: PartitionStatus is the status of a single rollout partition. - properties: - count: - description: Count is the number of clusters in the partition. - type: integer - maxUnavailable: - description: MaxUnavailable is the maximum number of unavailable - clusters in the partition. - type: integer - name: - description: Name is the name of the partition. - nullable: true - type: string - summary: - description: Summary is a summary state for the partition, calculated - over its non-ready resources. - properties: - desiredReady: - description: |- - DesiredReady is the number of bundle deployments that should be - ready. - type: integer - errApplied: - description: |- - ErrApplied is the number of bundle deployments that have been synced - from the Fleet controller and the downstream cluster, but with some - errors when deploying the bundle. - type: integer - modified: - description: |- - Modified is the number of bundle deployments that have been deployed - and for which all resources are ready, but where some changes from the - Git repository have not yet been synced. - type: integer - nonReadyResources: - description: |- - NonReadyClusters is a list of states, which is filled for a bundle - that is not ready. - items: - description: |- - NonReadyResource contains information about a bundle that is not ready for a - given state like "ErrApplied". It contains a list of non-ready or modified - resources and their states. - properties: - bundleState: - description: State is the state of the resource, like - e.g. "NotReady" or "ErrApplied". - nullable: true - type: string - message: - description: Message contains information why the - bundle is not ready. - nullable: true - type: string - modifiedStatus: - description: ModifiedStatus lists the state for each - modified resource. - items: - description: |- - ModifiedStatus is used to report the status of a resource that is modified. - It indicates if the modification was a create, a delete or a patch. - properties: - apiVersion: - nullable: true - type: string - delete: - type: boolean - kind: - nullable: true - type: string - missing: - type: boolean - name: - nullable: true - type: string - namespace: - nullable: true - type: string - patch: - nullable: true - type: string - type: object - type: array - name: - description: Name is the name of the resource. - nullable: true - type: string - nonReadyStatus: - description: NonReadyStatus lists the state for each - non-ready resource. - items: - description: NonReadyStatus is used to report the - status of a resource that is not ready. It includes - a summary. - properties: - apiVersion: - nullable: true - type: string - kind: - nullable: true - type: string - name: - nullable: true - type: string - namespace: - nullable: true - type: string - summary: - properties: - error: - type: boolean - message: - items: - type: string - type: array - state: - type: string - transitioning: - type: boolean - type: object - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - nullable: true - type: string - type: object - type: array - type: object - type: array - notReady: - description: |- - NotReady is the number of bundle deployments that have been deployed - where some resources are not ready. - type: integer - outOfSync: - description: |- - OutOfSync is the number of bundle deployments that have been synced - from Fleet controller, but not yet by the downstream agent. - type: integer - pending: - description: |- - Pending is the number of bundle deployments that are being processed - by Fleet controller. - type: integer - ready: - description: |- - Ready is the number of bundle deployments that have been deployed - where all resources are ready. - type: integer - waitApplied: - description: |- - WaitApplied is the number of bundle deployments that have been - synced from Fleet controller and downstream cluster, but are waiting - to be deployed. - type: integer - type: object - unavailable: - description: Unavailable is the number of unavailable clusters - in the partition. - type: integer - type: object - type: array - resourceKey: - description: |- - ResourceKey lists resources, which will likely be deployed. The - actual list of resources on a cluster might differ, depending on the - helm chart, value templating, etc.. - items: - description: ResourceKey lists resources, which will likely be deployed. - properties: - apiVersion: - description: APIVersion is the k8s api version of the resource. - nullable: true - type: string - kind: - description: Kind is the k8s api kind of the resource. - nullable: true - type: string - name: - description: Name is the name of the resource. - nullable: true - type: string - namespace: - description: Namespace is the namespace of the resource. - nullable: true - type: string - type: object - type: array - resourcesSha256Sum: - description: ResourcesSHA256Sum corresponds to the JSON serialization - of the .Spec.Resources field - type: string - summary: - description: |- - Summary contains the number of bundle deployments in each state and - a list of non-ready resources. - properties: - desiredReady: - description: |- - DesiredReady is the number of bundle deployments that should be - ready. - type: integer - errApplied: - description: |- - ErrApplied is the number of bundle deployments that have been synced - from the Fleet controller and the downstream cluster, but with some - errors when deploying the bundle. - type: integer - modified: - description: |- - Modified is the number of bundle deployments that have been deployed - and for which all resources are ready, but where some changes from the - Git repository have not yet been synced. - type: integer - nonReadyResources: - description: |- - NonReadyClusters is a list of states, which is filled for a bundle - that is not ready. - items: - description: |- - NonReadyResource contains information about a bundle that is not ready for a - given state like "ErrApplied". It contains a list of non-ready or modified - resources and their states. - properties: - bundleState: - description: State is the state of the resource, like e.g. - "NotReady" or "ErrApplied". - nullable: true - type: string - message: - description: Message contains information why the bundle - is not ready. - nullable: true - type: string - modifiedStatus: - description: ModifiedStatus lists the state for each modified - resource. - items: - description: |- - ModifiedStatus is used to report the status of a resource that is modified. - It indicates if the modification was a create, a delete or a patch. - properties: - apiVersion: - nullable: true - type: string - delete: - type: boolean - kind: - nullable: true - type: string - missing: - type: boolean - name: - nullable: true - type: string - namespace: - nullable: true - type: string - patch: - nullable: true - type: string - type: object - type: array - name: - description: Name is the name of the resource. - nullable: true - type: string - nonReadyStatus: - description: NonReadyStatus lists the state for each non-ready - resource. - items: - description: NonReadyStatus is used to report the status - of a resource that is not ready. It includes a summary. - properties: - apiVersion: - nullable: true - type: string - kind: - nullable: true - type: string - name: - nullable: true - type: string - namespace: - nullable: true - type: string - summary: - properties: - error: - type: boolean - message: - items: - type: string - type: array - state: - type: string - transitioning: - type: boolean - type: object - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - nullable: true - type: string - type: object - type: array - type: object - type: array - notReady: - description: |- - NotReady is the number of bundle deployments that have been deployed - where some resources are not ready. - type: integer - outOfSync: - description: |- - OutOfSync is the number of bundle deployments that have been synced - from Fleet controller, but not yet by the downstream agent. - type: integer - pending: - description: |- - Pending is the number of bundle deployments that are being processed - by Fleet controller. - type: integer - ready: - description: |- - Ready is the number of bundle deployments that have been deployed - where all resources are ready. - type: integer - waitApplied: - description: |- - WaitApplied is the number of bundle deployments that have been - synced from Fleet controller and downstream cluster, but are waiting - to be deployed. - type: integer - type: object - unavailable: - description: |- - Unavailable is the number of bundle deployments that are not ready or - where the AppliedDeploymentID in the status does not match the - DeploymentID from the spec. - type: integer - unavailablePartitions: - description: UnavailablePartitions is the number of unavailable partitions. - type: integer type: object type: object served: true diff --git a/config/rbac/bases/role.yaml b/config/rbac/bases/role.yaml index 731d0ed35..822ca4079 100644 --- a/config/rbac/bases/role.yaml +++ b/config/rbac/bases/role.yaml @@ -216,6 +216,7 @@ rules: - managedosversions/status verbs: - get + - list - patch - update - apiGroups: diff --git a/controllers/managedosimage_controller.go b/controllers/managedosimage_controller.go index 56e8cd624..b52cc406a 100644 --- a/controllers/managedosimage_controller.go +++ b/controllers/managedosimage_controller.go @@ -188,6 +188,12 @@ func (r *ManagedOSImageReconciler) newFleetBundleResources(ctx context.Context, return nil, fmt.Errorf("failed to get managedOSVersion: %w", err) } m = managedOSVersion.Spec.Metadata + + // Add a label that can be used to List all ManagedOSImages referencing a certain ManagedOSVersion. + if managedOSImage.ObjectMeta.Labels == nil { + managedOSImage.ObjectMeta.Labels = map[string]string{} + } + managedOSImage.ObjectMeta.Labels[elementalv1.ElementalManagedOSImageVersionNameLabel] = managedOSImage.Spec.ManagedOSVersionName } // Entire logic from below is carried from the old code. diff --git a/controllers/managedosversion_controller.go b/controllers/managedosversion_controller.go new file mode 100644 index 000000000..2454b5a7d --- /dev/null +++ b/controllers/managedosversion_controller.go @@ -0,0 +1,175 @@ +/* +Copyright © 2022 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "errors" + "fmt" + "strings" + + elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ManagedOSVersionReconciler reconciles a ManagedOSVersion object. +type ManagedOSVersionReconciler struct { + client.Client + DefaultRegistry string + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedosversions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedosversions/status,verbs=get;update;patch;list +// +kubebuilder:rbac:groups=elemental.cattle.io,resources=managedosimages,verbs=get;list;watch + +func (r *ManagedOSVersionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&elementalv1.ManagedOSVersion{}). + Watches( + &elementalv1.ManagedOSImage{}, + handler.EnqueueRequestsFromMapFunc(r.ManagedOSImageToManagedOSVersion), + ). + Complete(r) +} + +func (r *ManagedOSVersionReconciler) ManagedOSImageToManagedOSVersion(ctx context.Context, obj client.Object) []ctrl.Request { + logger := ctrl.LoggerFrom(ctx) + logger.Info("Enqueueing ManagedOSVersion reconciliation from ManagedOSImage") + + requests := []ctrl.Request{} + + // Verify we are actually handling a ManagedOSImage object + managedOSImage, ok := obj.(*elementalv1.ManagedOSImage) + if !ok { + logger.Error(errors.New("Enqueueing error"), fmt.Sprintf("Expected a ElementalHost object, but got %T", obj)) + return []ctrl.Request{} + } + + // Check the ManagedOSImage was referencing any ManagedOSVersion + if len(managedOSImage.Spec.ManagedOSVersionName) > 0 { + logger.Info("Adding ManagedOSVersion to reconciliation request", "ManagedOSVersion", managedOSImage.Spec.ManagedOSVersionName) + name := client.ObjectKey{Namespace: managedOSImage.Namespace, Name: managedOSImage.Spec.ManagedOSVersionName} + requests = append(requests, ctrl.Request{NamespacedName: name}) + } + + return requests +} + +func (r *ManagedOSVersionReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ ctrl.Result, rerr error) { + logger := ctrl.LoggerFrom(ctx).WithValues("Namespace", req.Namespace).WithValues("ManagedOSVersion", req.Name) + logger.Info("Reconciling ManagedOSVersion") + + // Fetch the ManagedOSVersion + managedOSVersion := &elementalv1.ManagedOSVersion{} + if err := r.Client.Get(ctx, req.NamespacedName, managedOSVersion); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("fetching ManagedOSVersion: %w", err) + } + + // Ensure we patch the latest version otherwise we could erratically overlap with other controllers (e.g. backup and restore) + patchBase := client.MergeFromWithOptions(managedOSVersion.DeepCopy(), client.MergeFromWithOptimisticLock{}) + defer func() { + managedOSVersionStatusCopy := managedOSVersion.Status.DeepCopy() // Patch call will erase the status + + if err := r.Patch(ctx, managedOSVersion, patchBase); err != nil { + rerr = errors.Join(rerr, fmt.Errorf("patching ManagedOSVersion: %w", err)) + } + + managedOSVersion.Status = *managedOSVersionStatusCopy + + // If the object was waiting for deletion and we just removed the finalizer, we will get a not found error + if err := r.Status().Patch(ctx, managedOSVersion, patchBase); err != nil && !apierrors.IsNotFound(err) { + rerr = errors.Join(rerr, fmt.Errorf("patching ManagedOSVersion status: %w", err)) + } + }() + + // The object is not up for deletion + if managedOSVersion.GetDeletionTimestamp().IsZero() { + // The object is not being deleted, so register the finalizer + if !controllerutil.ContainsFinalizer(managedOSVersion, elementalv1.ManagedOSVersionFinalizer) { + controllerutil.AddFinalizer(managedOSVersion, elementalv1.ManagedOSVersionFinalizer) + } + + // Reconcile ManagedOSVersion + result, err := r.reconcileNormal(ctx, managedOSVersion) + if err != nil { + return ctrl.Result{}, fmt.Errorf("reconciling ManagedOSVersion: %w", err) + } + return result, nil + } + + // Object is up for deletion + if controllerutil.ContainsFinalizer(managedOSVersion, elementalv1.ManagedOSVersionFinalizer) { + result, err := r.reconcileDelete(ctx, managedOSVersion) + if err != nil { + return ctrl.Result{}, fmt.Errorf("reconciling ManagedOSVersion deletion: %w", err) + } + return result, err + } + + return ctrl.Result{}, nil +} + +func (r *ManagedOSVersionReconciler) reconcileNormal(ctx context.Context, managedOSVersion *elementalv1.ManagedOSVersion) (ctrl.Result, error) { + logger := ctrl.LoggerFrom(ctx).WithValues("Namespace", managedOSVersion.Namespace).WithValues("ManagedOSVersion", managedOSVersion.Name) + logger.Info("Normal ManagedOSVersion reconcile") + // Nothing to do here + return ctrl.Result{}, nil +} + +func (r *ManagedOSVersionReconciler) reconcileDelete(ctx context.Context, managedOSVersion *elementalv1.ManagedOSVersion) (ctrl.Result, error) { + logger := ctrl.LoggerFrom(ctx).WithValues("Namespace", managedOSVersion.Namespace).WithValues("ManagedOSVersion", managedOSVersion.Name) + logger.Info("Deletion ManagedOSVersion reconcile") + managedOSImages, err := r.getManagedOSImages(ctx, *managedOSVersion) + if err != nil { + return ctrl.Result{}, fmt.Errorf("getting linked ManagedOSImages: %w", err) + } + // If there are ManagedOSImages referencing this ManagedOSVersion, hold on deletion. + if len(managedOSImages.Items) > 0 { + imageNames := []string{} + for _, image := range managedOSImages.Items { + imageNames = append(imageNames, image.Name) + } + logger.Info("ManagedOSVersion still in use by ManagedOSImages", "ManagedOSImages", strings.Join(imageNames, ",")) + return ctrl.Result{}, nil + } + + controllerutil.RemoveFinalizer(managedOSVersion, elementalv1.ManagedOSVersionFinalizer) + return ctrl.Result{}, nil +} + +// getManagedOSImages returns a list of ManagedOSImages referencing the ManagedOSVersion. +func (r *ManagedOSVersionReconciler) getManagedOSImages(ctx context.Context, managedOSVersion elementalv1.ManagedOSVersion) (*elementalv1.ManagedOSImageList, error) { + managedOSImages := &elementalv1.ManagedOSImageList{} + + if err := r.List(ctx, managedOSImages, client.InNamespace(managedOSVersion.Namespace), client.MatchingLabels(map[string]string{ + elementalv1.ElementalManagedOSImageVersionNameLabel: managedOSVersion.Name, + })); err != nil { + return nil, fmt.Errorf("listing ManagedOSImages: %w", err) + } + + return managedOSImages, nil +} diff --git a/controllers/managedosversion_controller_test.go b/controllers/managedosversion_controller_test.go new file mode 100644 index 000000000..a58328aed --- /dev/null +++ b/controllers/managedosversion_controller_test.go @@ -0,0 +1,168 @@ +/* +Copyright © 2022 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + elementalv1 "github.com/rancher/elemental-operator/api/v1beta1" + "github.com/rancher/elemental-operator/pkg/test" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("reconcile managed os version", func() { + var r *ManagedOSVersionReconciler + + var managedOSImage *elementalv1.ManagedOSImage + var referencedManagedOSVersion *elementalv1.ManagedOSVersion + var standaloneManagedOSVersion *elementalv1.ManagedOSVersion + + BeforeEach(func() { + r = &ManagedOSVersionReconciler{ + Client: cl, + Scheme: cl.Scheme(), + } + referencedManagedOSVersion = &elementalv1.ManagedOSVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-referenced", + Namespace: "default", + }, + Spec: elementalv1.ManagedOSVersionSpec{}, + } + managedOSImage = &elementalv1.ManagedOSImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-image", + Namespace: "default", + Labels: map[string]string{ + elementalv1.ElementalManagedOSImageVersionNameLabel: referencedManagedOSVersion.Name, + }, + }, + Spec: elementalv1.ManagedOSImageSpec{ + ManagedOSVersionName: referencedManagedOSVersion.Name, + }, + } + standaloneManagedOSVersion = &elementalv1.ManagedOSVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-standalone", + Namespace: "default", + }, + Spec: elementalv1.ManagedOSVersionSpec{}, + } + + }) + + AfterEach(func() { + Expect(test.CleanupAndWait(ctx, cl, managedOSImage, referencedManagedOSVersion, standaloneManagedOSVersion)).To(Succeed()) + }) + + It("should delete standalone ManagedOSVersion", func() { + Expect(cl.Create(ctx, standaloneManagedOSVersion)).To(Succeed()) + + Eventually(func() error { + _, err := r.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: standaloneManagedOSVersion.Namespace, + Name: standaloneManagedOSVersion.Name, + }, + }) + return err + }).WithTimeout(time.Minute).ShouldNot(HaveOccurred()) + + Expect(cl.Get(ctx, client.ObjectKey{ + Name: standaloneManagedOSVersion.Name, + Namespace: standaloneManagedOSVersion.Namespace, + }, standaloneManagedOSVersion)).To(Succeed()) + Expect(controllerutil.ContainsFinalizer(standaloneManagedOSVersion, elementalv1.ManagedOSVersionFinalizer)).To(BeTrue(), "ManagedOSVersion must contain finalizer after first reconcile") + + // Reconcile after deletion + Expect(cl.Delete(ctx, standaloneManagedOSVersion)) + _, err := r.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: standaloneManagedOSVersion.Namespace, + Name: standaloneManagedOSVersion.Name, + }, + }) + Expect(err).ToNot(HaveOccurred()) + err = cl.Get(ctx, client.ObjectKey{ + Name: standaloneManagedOSVersion.Name, + Namespace: standaloneManagedOSVersion.Namespace, + }, standaloneManagedOSVersion) + Expect(apierrors.IsNotFound(err)).Should(BeTrue(), "Standalone ManagedOSVersion should have been deleted") + }) + It("should hold referenced ManagedOSVersion deletion", func() { + Expect(cl.Create(ctx, referencedManagedOSVersion)).To(Succeed()) + Expect(cl.Create(ctx, managedOSImage)).To(Succeed()) + + // First reconcile + Eventually(func() error { + _, err := r.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: referencedManagedOSVersion.Namespace, + Name: referencedManagedOSVersion.Name, + }, + }) + return err + }).WithTimeout(time.Minute).ShouldNot(HaveOccurred()) + + // Reconcile after deletion + Expect(cl.Delete(ctx, referencedManagedOSVersion)) + _, err := r.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: referencedManagedOSVersion.Namespace, + Name: referencedManagedOSVersion.Name, + }, + }) + Expect(err).ToNot(HaveOccurred()) + // Expect referencedManagedOSVersion to not have been deleted + Expect(cl.Get(ctx, client.ObjectKey{ + Name: referencedManagedOSVersion.Name, + Namespace: referencedManagedOSVersion.Namespace, + }, referencedManagedOSVersion)).Should(Succeed()) + + // Deleted the referencing ManagedOSImage + Expect(cl.Delete(ctx, managedOSImage)) + Eventually(func() bool { + err := cl.Get(ctx, client.ObjectKey{ + Name: managedOSImage.Name, + Namespace: managedOSImage.Namespace, + }, managedOSImage) + return apierrors.IsNotFound(err) + }).WithTimeout(time.Minute).Should(BeTrue(), "ManagedOSImage should have been deleted") + + requestsFromImage := r.ManagedOSImageToManagedOSVersion(ctx, managedOSImage) + for _, request := range requestsFromImage { + _, err = r.Reconcile(ctx, request) + Expect(err).ToNot(HaveOccurred()) + } + + Eventually(func() bool { + err := cl.Get(ctx, client.ObjectKey{ + Name: referencedManagedOSVersion.Name, + Namespace: referencedManagedOSVersion.Namespace, + }, referencedManagedOSVersion) + return apierrors.IsNotFound(err) + }).WithTimeout(time.Minute).Should(BeTrue(), "Referenced ManagedOSVersion should have been deleted") + + }) +}) From 3482584addb46fe36a13aa17d42a9bab048e6bba Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Fri, 21 Jun 2024 13:48:13 +0200 Subject: [PATCH 2/7] Remove unused ManagedOSVersion Status Signed-off-by: Andrea Mazzotti --- api/v1beta1/managedosversion_types.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/v1beta1/managedosversion_types.go b/api/v1beta1/managedosversion_types.go index b088234ef..2906ac674 100644 --- a/api/v1beta1/managedosversion_types.go +++ b/api/v1beta1/managedosversion_types.go @@ -21,7 +21,6 @@ import ( "strings" "github.com/rancher/elemental-operator/pkg/object" - fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" upgradev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -60,9 +59,7 @@ type ManagedOSVersionSpec struct { UpgradeContainer *upgradev1.ContainerSpec `json:"upgradeContainer,omitempty"` } -type ManagedOSVersionStatus struct { - fleetv1.BundleStatus `json:""` //nolint -} +type ManagedOSVersionStatus struct{} // +kubebuilder:object:root=true From e02465090e89d20b90040b82570113c281f5e172 Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Fri, 21 Jun 2024 14:03:40 +0200 Subject: [PATCH 3/7] Update manifests Signed-off-by: Andrea Mazzotti --- .obs/chartfile/crds/templates/crds.yaml | 426 ------------------ .../operator/templates/cluster_role.yaml | 1 + 2 files changed, 1 insertion(+), 426 deletions(-) diff --git a/.obs/chartfile/crds/templates/crds.yaml b/.obs/chartfile/crds/templates/crds.yaml index 71f8940ad..b97c41c04 100644 --- a/.obs/chartfile/crds/templates/crds.yaml +++ b/.obs/chartfile/crds/templates/crds.yaml @@ -3393,432 +3393,6 @@ spec: type: string type: object status: - properties: - conditions: - description: |- - Conditions is a list of Wrangler conditions that describe the state - of the bundle. - items: - properties: - lastTransitionTime: - description: Last time the condition transitioned from one status - to another. - type: string - lastUpdateTime: - description: The last time this condition was updated. - type: string - message: - description: Human-readable message indicating details about - last transition - type: string - reason: - description: The reason for the condition's last transition. - type: string - status: - description: Status of the condition, one of True, False, Unknown. - type: string - type: - description: Type of cluster condition. - type: string - required: - - status - - type - type: object - type: array - display: - description: |- - Display contains the number of ready, desiredready clusters and a - summary state for the bundle's resources. - properties: - readyClusters: - description: |- - ReadyClusters is a string in the form "%d/%d", that describes the - number of clusters that are ready vs. the number of clusters desired - to be ready. - nullable: true - type: string - state: - description: State is a summary state for the bundle, calculated - over the non-ready resources. - nullable: true - type: string - type: object - maxNew: - description: |- - MaxNew is always 50. A bundle change can only stage 50 - bundledeployments at a time. - type: integer - maxUnavailable: - description: |- - MaxUnavailable is the maximum number of unavailable deployments. See - rollout configuration. - type: integer - maxUnavailablePartitions: - description: |- - MaxUnavailablePartitions is the maximum number of unavailable - partitions. The rollout configuration defines a maximum number or - percentage of unavailable partitions. - type: integer - newlyCreated: - description: |- - NewlyCreated is the number of bundle deployments that have been created, - not updated. - type: integer - observedGeneration: - description: ObservedGeneration is the current generation of the bundle. - format: int64 - type: integer - partitions: - description: PartitionStatus lists the status of each partition. - items: - description: PartitionStatus is the status of a single rollout partition. - properties: - count: - description: Count is the number of clusters in the partition. - type: integer - maxUnavailable: - description: MaxUnavailable is the maximum number of unavailable - clusters in the partition. - type: integer - name: - description: Name is the name of the partition. - nullable: true - type: string - summary: - description: Summary is a summary state for the partition, calculated - over its non-ready resources. - properties: - desiredReady: - description: |- - DesiredReady is the number of bundle deployments that should be - ready. - type: integer - errApplied: - description: |- - ErrApplied is the number of bundle deployments that have been synced - from the Fleet controller and the downstream cluster, but with some - errors when deploying the bundle. - type: integer - modified: - description: |- - Modified is the number of bundle deployments that have been deployed - and for which all resources are ready, but where some changes from the - Git repository have not yet been synced. - type: integer - nonReadyResources: - description: |- - NonReadyClusters is a list of states, which is filled for a bundle - that is not ready. - items: - description: |- - NonReadyResource contains information about a bundle that is not ready for a - given state like "ErrApplied". It contains a list of non-ready or modified - resources and their states. - properties: - bundleState: - description: State is the state of the resource, like - e.g. "NotReady" or "ErrApplied". - nullable: true - type: string - message: - description: Message contains information why the - bundle is not ready. - nullable: true - type: string - modifiedStatus: - description: ModifiedStatus lists the state for each - modified resource. - items: - description: |- - ModifiedStatus is used to report the status of a resource that is modified. - It indicates if the modification was a create, a delete or a patch. - properties: - apiVersion: - nullable: true - type: string - delete: - type: boolean - kind: - nullable: true - type: string - missing: - type: boolean - name: - nullable: true - type: string - namespace: - nullable: true - type: string - patch: - nullable: true - type: string - type: object - type: array - name: - description: Name is the name of the resource. - nullable: true - type: string - nonReadyStatus: - description: NonReadyStatus lists the state for each - non-ready resource. - items: - description: NonReadyStatus is used to report the - status of a resource that is not ready. It includes - a summary. - properties: - apiVersion: - nullable: true - type: string - kind: - nullable: true - type: string - name: - nullable: true - type: string - namespace: - nullable: true - type: string - summary: - properties: - error: - type: boolean - message: - items: - type: string - type: array - state: - type: string - transitioning: - type: boolean - type: object - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - nullable: true - type: string - type: object - type: array - type: object - type: array - notReady: - description: |- - NotReady is the number of bundle deployments that have been deployed - where some resources are not ready. - type: integer - outOfSync: - description: |- - OutOfSync is the number of bundle deployments that have been synced - from Fleet controller, but not yet by the downstream agent. - type: integer - pending: - description: |- - Pending is the number of bundle deployments that are being processed - by Fleet controller. - type: integer - ready: - description: |- - Ready is the number of bundle deployments that have been deployed - where all resources are ready. - type: integer - waitApplied: - description: |- - WaitApplied is the number of bundle deployments that have been - synced from Fleet controller and downstream cluster, but are waiting - to be deployed. - type: integer - type: object - unavailable: - description: Unavailable is the number of unavailable clusters - in the partition. - type: integer - type: object - type: array - resourceKey: - description: |- - ResourceKey lists resources, which will likely be deployed. The - actual list of resources on a cluster might differ, depending on the - helm chart, value templating, etc.. - items: - description: ResourceKey lists resources, which will likely be deployed. - properties: - apiVersion: - description: APIVersion is the k8s api version of the resource. - nullable: true - type: string - kind: - description: Kind is the k8s api kind of the resource. - nullable: true - type: string - name: - description: Name is the name of the resource. - nullable: true - type: string - namespace: - description: Namespace is the namespace of the resource. - nullable: true - type: string - type: object - type: array - resourcesSha256Sum: - description: ResourcesSHA256Sum corresponds to the JSON serialization - of the .Spec.Resources field - type: string - summary: - description: |- - Summary contains the number of bundle deployments in each state and - a list of non-ready resources. - properties: - desiredReady: - description: |- - DesiredReady is the number of bundle deployments that should be - ready. - type: integer - errApplied: - description: |- - ErrApplied is the number of bundle deployments that have been synced - from the Fleet controller and the downstream cluster, but with some - errors when deploying the bundle. - type: integer - modified: - description: |- - Modified is the number of bundle deployments that have been deployed - and for which all resources are ready, but where some changes from the - Git repository have not yet been synced. - type: integer - nonReadyResources: - description: |- - NonReadyClusters is a list of states, which is filled for a bundle - that is not ready. - items: - description: |- - NonReadyResource contains information about a bundle that is not ready for a - given state like "ErrApplied". It contains a list of non-ready or modified - resources and their states. - properties: - bundleState: - description: State is the state of the resource, like e.g. - "NotReady" or "ErrApplied". - nullable: true - type: string - message: - description: Message contains information why the bundle - is not ready. - nullable: true - type: string - modifiedStatus: - description: ModifiedStatus lists the state for each modified - resource. - items: - description: |- - ModifiedStatus is used to report the status of a resource that is modified. - It indicates if the modification was a create, a delete or a patch. - properties: - apiVersion: - nullable: true - type: string - delete: - type: boolean - kind: - nullable: true - type: string - missing: - type: boolean - name: - nullable: true - type: string - namespace: - nullable: true - type: string - patch: - nullable: true - type: string - type: object - type: array - name: - description: Name is the name of the resource. - nullable: true - type: string - nonReadyStatus: - description: NonReadyStatus lists the state for each non-ready - resource. - items: - description: NonReadyStatus is used to report the status - of a resource that is not ready. It includes a summary. - properties: - apiVersion: - nullable: true - type: string - kind: - nullable: true - type: string - name: - nullable: true - type: string - namespace: - nullable: true - type: string - summary: - properties: - error: - type: boolean - message: - items: - type: string - type: array - state: - type: string - transitioning: - type: boolean - type: object - uid: - description: |- - UID is a type that holds unique ID values, including UUIDs. Because we - don't ONLY use UUIDs, this is an alias to string. Being a type captures - intent and helps make sure that UIDs and names do not get conflated. - nullable: true - type: string - type: object - type: array - type: object - type: array - notReady: - description: |- - NotReady is the number of bundle deployments that have been deployed - where some resources are not ready. - type: integer - outOfSync: - description: |- - OutOfSync is the number of bundle deployments that have been synced - from Fleet controller, but not yet by the downstream agent. - type: integer - pending: - description: |- - Pending is the number of bundle deployments that are being processed - by Fleet controller. - type: integer - ready: - description: |- - Ready is the number of bundle deployments that have been deployed - where all resources are ready. - type: integer - waitApplied: - description: |- - WaitApplied is the number of bundle deployments that have been - synced from Fleet controller and downstream cluster, but are waiting - to be deployed. - type: integer - type: object - unavailable: - description: |- - Unavailable is the number of bundle deployments that are not ready or - where the AppliedDeploymentID in the status does not match the - DeploymentID from the spec. - type: integer - unavailablePartitions: - description: UnavailablePartitions is the number of unavailable partitions. - type: integer type: object type: object served: true diff --git a/.obs/chartfile/operator/templates/cluster_role.yaml b/.obs/chartfile/operator/templates/cluster_role.yaml index f4a9528c1..aaa3a0f2e 100644 --- a/.obs/chartfile/operator/templates/cluster_role.yaml +++ b/.obs/chartfile/operator/templates/cluster_role.yaml @@ -215,6 +215,7 @@ rules: - managedosversions/status verbs: - get + - list - patch - update - apiGroups: From d856ef2301b71640b4927787edcd65446269e1ff Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Fri, 21 Jun 2024 15:32:59 +0200 Subject: [PATCH 4/7] Start the ManagedOSVersion controller Signed-off-by: Andrea Mazzotti --- cmd/operator/operator/root.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/operator/operator/root.go b/cmd/operator/operator/root.go index 60e03fb9c..690ed500b 100644 --- a/cmd/operator/operator/root.go +++ b/cmd/operator/operator/root.go @@ -315,4 +315,11 @@ func setupReconcilers(mgr ctrl.Manager, config *rootConfig) { setupLog.Error(err, "unable to create reconciler", "controller", "SeedImage") os.Exit(1) } + + if err := (&controllers.ManagedOSVersionReconciler{ + Client: mgr.GetClient(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create reconciler", "controller", "ManagedOSVersion") + os.Exit(1) + } } From 4bc6ff5bd7d5d5aaf7b9f81e67994c96af703a44 Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Tue, 25 Jun 2024 15:50:17 +0200 Subject: [PATCH 5/7] Update api/v1beta1/common_consts.go Co-authored-by: Francesco Giudici Signed-off-by: Andrea Mazzotti --- api/v1beta1/common_consts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1beta1/common_consts.go b/api/v1beta1/common_consts.go index 89946de63..fc54688d4 100644 --- a/api/v1beta1/common_consts.go +++ b/api/v1beta1/common_consts.go @@ -20,7 +20,7 @@ const ( // ElementalManagedLabel label used to put on resources managed by the elemental operator. ElementalManagedLabel = "elemental.cattle.io/managed" - // ElementalManagedOSImageVersionNameLabel label used filter ManagedOSImages referencing a ManagedOSVersion. + // ElementalManagedOSImageVersionNameLabel label is used to filter ManagedOSImages referencing a ManagedOSVersion. ElementalManagedOSImageVersionNameLabel = "elemental.cattle.io/managed-os-version-name" // ElementalManagedOSVersionChannelLabel is used to filter a set of ManagedOSVersions given the channel they originate from. From b7418bf378ecbb2163b87164c4400d2d6f2d60ef Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Tue, 25 Jun 2024 15:50:38 +0200 Subject: [PATCH 6/7] Update controllers/managedosversion_controller.go Co-authored-by: Francesco Giudici Signed-off-by: Andrea Mazzotti --- controllers/managedosversion_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/managedosversion_controller.go b/controllers/managedosversion_controller.go index 2454b5a7d..ffa2d3956 100644 --- a/controllers/managedosversion_controller.go +++ b/controllers/managedosversion_controller.go @@ -62,7 +62,7 @@ func (r *ManagedOSVersionReconciler) ManagedOSImageToManagedOSVersion(ctx contex // Verify we are actually handling a ManagedOSImage object managedOSImage, ok := obj.(*elementalv1.ManagedOSImage) if !ok { - logger.Error(errors.New("Enqueueing error"), fmt.Sprintf("Expected a ElementalHost object, but got %T", obj)) + logger.Error(errors.New("Enqueueing error"), fmt.Sprintf("Expected a ManagedOSImage object, but got %T", obj)) return []ctrl.Request{} } From b80eea1ab4eda9302b58ac21dfdea364716578ae Mon Sep 17 00:00:00 2001 From: Andrea Mazzotti Date: Tue, 25 Jun 2024 15:50:52 +0200 Subject: [PATCH 7/7] Update cmd/operator/operator/root.go Co-authored-by: Francesco Giudici Signed-off-by: Andrea Mazzotti --- cmd/operator/operator/root.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/operator/operator/root.go b/cmd/operator/operator/root.go index 690ed500b..561569ff7 100644 --- a/cmd/operator/operator/root.go +++ b/cmd/operator/operator/root.go @@ -315,7 +315,6 @@ func setupReconcilers(mgr ctrl.Manager, config *rootConfig) { setupLog.Error(err, "unable to create reconciler", "controller", "SeedImage") os.Exit(1) } - if err := (&controllers.ManagedOSVersionReconciler{ Client: mgr.GetClient(), }).SetupWithManager(mgr); err != nil {