From 320b16492c931ffd475fe862c94d6025ed537304 Mon Sep 17 00:00:00 2001 From: zerospiel Date: Mon, 30 Dec 2024 13:13:53 +0100 Subject: [PATCH] Backup implementation part 2 * install velero via flux rather than code * adjust roles for the velero chart * remove unnecessary controller values * rename Backup to ManagementBackup * remove Oneshot parameter from the Spec * add StorageLocation to both Management and ManagementBackup * remove unused types * manage schedules in the ctrl instead of velero * new source runner for schedules * collect the required velero backup spec for the whole backup * label Credential references (clusterIdentities) in order to include them in backup * backup validation webhook * amend backup controller logic with better objects handling * fix bug in providertemplates ctrl when ownerreferences are being updated but requeue is not set * add custom plugins set via mgmt spec * rename k0smotron related provider labels to the correct ones from the k0sproject --- PROJECT | 5 +- api/v1alpha1/backup_types.go | 77 --- api/v1alpha1/indexers.go | 23 + api/v1alpha1/management_backup_types.go | 102 ++++ api/v1alpha1/management_types.go | 20 +- api/v1alpha1/templates_common.go | 7 +- api/v1alpha1/zz_generated.deepcopy.go | 182 ++++--- cmd/main.go | 33 +- config/dev/aws-clusterdeployment.yaml | 2 - config/dev/aws-credentials.yaml | 6 - config/dev/azure-clusterdeployment.yaml | 2 - config/dev/azure-credentials.yaml | 5 - config/dev/eks-clusterdeployment.yaml | 2 - config/dev/vsphere-clusterdeployment.yaml | 2 - config/dev/vsphere-credentials.yaml | 6 - go.mod | 11 +- go.sum | 39 +- hack/templates.sh | 2 - internal/controller/backup/collect.go | 137 ++++++ internal/controller/backup/collect_test.go | 236 ++++++++++ internal/controller/backup/config.go | 60 +++ internal/controller/backup/install.go | 445 ------------------ internal/controller/backup/oneshot.go | 30 -- internal/controller/backup/periodic.go | 135 ++++++ internal/controller/backup/reconcile.go | 242 ++++++++++ internal/controller/backup/schedule.go | 30 -- internal/controller/backup/type.go | 62 --- internal/controller/backup_controller.go | 166 ------- internal/controller/credential_controller.go | 170 ++++++- .../management_backup_controller.go | 162 +++++++ ...o => management_backup_controller_test.go} | 14 +- internal/controller/management_controller.go | 9 + .../multiclusterservice_controller.go | 2 +- .../multiclusterservice_controller_test.go | 6 +- internal/controller/template_controller.go | 10 +- .../controller/template_controller_test.go | 2 +- .../templatechain_controller_test.go | 8 +- internal/credspropagation/common.go | 6 + internal/utils/label.go | 2 +- internal/utils/status/status.go | 6 +- internal/webhook/clusterdeployment_webhook.go | 12 +- internal/webhook/management_webhook.go | 2 +- internal/webhook/management_webhook_test.go | 8 +- internal/webhook/managementbackup_webhook.go | 72 +++ .../webhook/managementbackup_webhook_test.go | 74 +++ templates/cluster/aws-hosted-cp/Chart.yaml | 6 +- .../cluster/aws-standalone-cp/Chart.yaml | 6 +- templates/cluster/azure-hosted-cp/Chart.yaml | 6 +- .../cluster/azure-standalone-cp/Chart.yaml | 6 +- .../openstack-standalone-cp/Chart.yaml | 6 +- .../cluster/vsphere-hosted-cp/Chart.yaml | 6 +- .../cluster/vsphere-standalone-cp/Chart.yaml | 6 +- templates/provider/k0smotron/Chart.yaml | 2 +- .../provider/kcm-templates/files/release.yaml | 2 - .../templates/adopted-cluster-0-0-2.yaml | 2 - .../files/templates/aws-eks-0-0-4.yaml | 2 - .../files/templates/aws-hosted-cp-0-0-4.yaml | 2 - .../templates/aws-standalone-cp-0-0-5.yaml | 2 - .../files/templates/azure-aks-0-0-2.yaml | 2 - .../templates/azure-hosted-cp-0-0-4.yaml | 2 - .../templates/azure-standalone-cp-0-0-5.yaml | 2 - .../files/templates/cert-manager-1-16-2.yaml | 2 - .../templates/cluster-api-provider-aws.yaml | 2 - .../templates/cluster-api-provider-azure.yaml | 2 - .../cluster-api-provider-openstack.yaml | 2 - .../cluster-api-provider-vsphere.yaml | 2 - .../files/templates/cluster-api.yaml | 2 - .../files/templates/dex-0-19-1.yaml | 2 - .../templates/external-secrets-0-11-0.yaml | 2 - .../files/templates/ingress-nginx-4-11-0.yaml | 2 - .../files/templates/ingress-nginx-4-11-3.yaml | 2 - .../files/templates/k0smotron.yaml | 2 - .../kcm-templates/files/templates/kcm.yaml | 2 - .../files/templates/kyverno-3-2-6.yaml | 2 - .../openstack-standalone-cp-0-0-2.yaml | 2 - .../files/templates/projectsveltos.yaml | 2 - .../files/templates/velero-8-1-0.yaml | 2 - .../templates/vsphere-hosted-cp-0-0-5.yaml | 2 - .../vsphere-standalone-cp-0-0-5.yaml | 2 - templates/provider/kcm/Chart.lock | 7 +- templates/provider/kcm/Chart.yaml | 4 + templates/provider/kcm/templates/_helpers.tpl | 8 - ...rdent.mirantis.com_managementbackups.yaml} | 146 +++--- .../k0rdent.mirantis.com_managements.yaml | 19 +- .../provider/kcm/templates/deployment.yaml | 9 +- .../kcm/templates/rbac/controller/roles.yaml | 52 +- ...itor.yaml => managementbackup-editor.yaml} | 6 +- ...ewer.yaml => managementbackup-viewer.yaml} | 6 +- .../provider/kcm/templates/webhooks.yaml | 21 + templates/provider/kcm/values.yaml | 39 +- test/objects/management/management.go | 2 +- 91 files changed, 1761 insertions(+), 1288 deletions(-) delete mode 100644 api/v1alpha1/backup_types.go create mode 100644 api/v1alpha1/management_backup_types.go create mode 100644 internal/controller/backup/collect.go create mode 100644 internal/controller/backup/collect_test.go create mode 100644 internal/controller/backup/config.go delete mode 100644 internal/controller/backup/install.go delete mode 100644 internal/controller/backup/oneshot.go create mode 100644 internal/controller/backup/periodic.go create mode 100644 internal/controller/backup/reconcile.go delete mode 100644 internal/controller/backup/schedule.go delete mode 100644 internal/controller/backup/type.go delete mode 100644 internal/controller/backup_controller.go create mode 100644 internal/controller/management_backup_controller.go rename internal/controller/{backup_controller_test.go => management_backup_controller_test.go} (87%) create mode 100644 internal/webhook/managementbackup_webhook.go create mode 100644 internal/webhook/managementbackup_webhook_test.go rename templates/provider/kcm/templates/crds/{k0rdent.mirantis.com_backups.yaml => k0rdent.mirantis.com_managementbackups.yaml} (65%) rename templates/provider/kcm/templates/rbac/user-facing/{backup-editor.yaml => managementbackup-editor.yaml} (79%) rename templates/provider/kcm/templates/rbac/user-facing/{backup-viewer.yaml => managementbackup-viewer.yaml} (79%) diff --git a/PROJECT b/PROJECT index 7c02eec49..28ea01962 100644 --- a/PROJECT +++ b/PROJECT @@ -110,7 +110,10 @@ resources: controller: true domain: k0rdent.mirantis.com group: k0rdent.mirantis.com - kind: Backup + kind: ManagementBackup path: github.com/K0rdent/kcm/api/v1alpha1 version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/backup_types.go b/api/v1alpha1/backup_types.go deleted file mode 100644 index f05239a7a..000000000 --- a/api/v1alpha1/backup_types.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2024 -// -// 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 v1alpha1 - -import ( - velerov1 "github.com/zerospiel/velero/pkg/apis/velero/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - // Name to label most of the KCM-related components. - // Mostly utilized by the backup feature. - GenericComponentLabelName = "k0rdent.mirantis.com/component" - // Component label value for the KCM-related components. - GenericComponentLabelValueKCM = "kcm" -) - -// BackupSpec defines the desired state of Backup -type BackupSpec struct { - // Oneshot indicates whether the Backup should not be scheduled - // and rather created immediately and only once. - Oneshot bool `json:"oneshot,omitempty"` -} - -// BackupStatus defines the observed state of Backup -type BackupStatus struct { - // Reference to the underlying Velero object being managed. - // Might be either Velero Backup or Schedule. - Reference *corev1.ObjectReference `json:"reference,omitempty"` - // Status of the Velero Schedule for the Management scheduled backups. - // Always absent for the Backups with the .spec.oneshot set to true. - Schedule *velerov1.ScheduleStatus `json:"schedule,omitempty"` - // NextAttempt indicates the time when the next scheduled backup will be performed. - // Always absent for the Backups with the .spec.oneshot set to true. - NextAttempt *metav1.Time `json:"nextAttempt,omitempty"` - // Last Velero Backup that has been created. - LastBackup *velerov1.BackupStatus `json:"lastBackup,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:resource:scope=Cluster - -// Backup is the Schema for the backups API -type Backup struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec BackupSpec `json:"spec,omitempty"` - Status BackupStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// BackupList contains a list of Backup -type BackupList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Backup `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Backup{}, &BackupList{}) -} diff --git a/api/v1alpha1/indexers.go b/api/v1alpha1/indexers.go index 066a7cb97..093a519a1 100644 --- a/api/v1alpha1/indexers.go +++ b/api/v1alpha1/indexers.go @@ -35,6 +35,7 @@ func SetupIndexers(ctx context.Context, mgr ctrl.Manager) error { setupClusterTemplateProvidersIndexer, setupMultiClusterServiceServicesIndexer, setupOwnerReferenceIndexers, + setupManagementBackupScheduledIndexer, } { merr = errors.Join(merr, f(ctx, mgr)) } @@ -237,3 +238,25 @@ func extractOwnerReferences(rawObj client.Object) []string { } return owners } + +// management backup indexers + +// ManagementBackupScheduledIndexKey indexer field name to extract only [ManagementBackup] objects +// that meant to be scheduled. +const ManagementBackupScheduledIndexKey = "k0rdent.scheduled-backup" + +func setupManagementBackupScheduledIndexer(ctx context.Context, mgr ctrl.Manager) error { + return mgr.GetFieldIndexer().IndexField(ctx, &ManagementBackup{}, ManagementBackupScheduledIndexKey, func(o client.Object) []string { + mb, ok := o.(*ManagementBackup) + if !ok { + return nil + } + + v, ok := mb.Annotations[ScheduleBackupAnnotation] + if !ok { + return nil + } + + return []string{v} + }) +} diff --git a/api/v1alpha1/management_backup_types.go b/api/v1alpha1/management_backup_types.go new file mode 100644 index 000000000..7f3f99dce --- /dev/null +++ b/api/v1alpha1/management_backup_types.go @@ -0,0 +1,102 @@ +// Copyright 2024 +// +// 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 v1alpha1 + +import ( + "strconv" + "time" + + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // Name to label most of the KCM-related components. + // Mostly utilized by the backup feature. + GenericComponentNameLabel = "k0rdent.mirantis.com/component" + // Component label value for the KCM-related components. + GenericComponentLabelValueKCM = "kcm" + + // ScheduleBackupAnnotation is an annotation for ease the listing + // of [ManagementBackup]. Indicates that the type of the object is schedule. + ScheduleBackupAnnotation = "k0rdent.mirantis.com/schedule" +) + +// ManagementBackupSpec defines the desired state of ManagementBackup +type ManagementBackupSpec struct { + // StorageLocation is the name of a [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.StorageLocation] + // where the backup should be stored. + StorageLocation string `json:"storageLocation,omitempty"` +} + +// ManagementBackupStatus defines the observed state of ManagementBackup +type ManagementBackupStatus struct { + // NextAttempt indicates the time when the next backup will be created. + // Always absent for a single [ManagementBackup]. + NextAttempt *metav1.Time `json:"nextAttempt,omitempty"` + // Time of the most recently created [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup]. + LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"` + // Most recently [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup] that has been created. + LastBackup *velerov1.BackupStatus `json:"lastBackup,omitempty"` + // Name of most recently created [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup]. + LastBackupName string `json:"lastBackupName,omitempty"` + // Paused indicates if the schedule is currently paused. + Paused bool `json:"paused,omitempty"` +} + +// IsSchedule checks if an instance of [ManagementBackup] is schedulable. +func (s *ManagementBackup) IsSchedule() bool { + if _, err := strconv.ParseBool(s.Annotations[ScheduleBackupAnnotation]); err == nil { + return true + } + + return false +} + +// TimestampedBackupName returns the backup name related to scheduled [ManagementBackup] based on the given timestamp. +func (s *ManagementBackup) TimestampedBackupName(timestamp time.Time) string { + return s.Name + "-" + timestamp.Format("20060102150405") +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,shortName=kcmbackup;mgmtbackup +// +kubebuilder:printcolumn:name="LastBackupStatus",type=string,JSONPath=`.status.lastBackup.phase`,description="Status of last backup run",priority=0 +// +kubebuilder:printcolumn:name="NextBackup",type=string,JSONPath=`.status.nextAttempt`,description="Next scheduled attempt to back up",priority=0 +// +kubebuilder:printcolumn:name="SinceLastBackup",type=date,JSONPath=`.status.lastBackupTime`,description="Time elapsed since last backup run",priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Time elapsed since object creation",priority=0 +// +kubebuilder:printcolumn:name="Paused",type=boolean,JSONPath=`.status.paused`,description="Schedule is on pause",priority=1 + +// ManagementBackup is the Schema for the managementbackups API +type ManagementBackup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ManagementBackupSpec `json:"spec,omitempty"` + Status ManagementBackupStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ManagementBackupList contains a list of ManagementBackup +type ManagementBackupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ManagementBackup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ManagementBackup{}, &ManagementBackupList{}) +} diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index 24c0c2ee1..c528b6213 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -44,7 +44,7 @@ type ManagementSpec struct { // Providers is the list of supported CAPI providers. Providers []Provider `json:"providers,omitempty"` - Backup ManagementBackup `json:"backup,omitempty"` + Backup Backup `json:"backup,omitempty"` } // Core represents a structure describing core Management components. @@ -55,15 +55,19 @@ type Core struct { CAPI Component `json:"capi,omitempty"` } -// ManagementBackup enables a feature to backup KCM objects into a cloud. -type ManagementBackup struct { - // Schedule is a Cron expression defining when to run the scheduled Backup. - // Default value is to backup every 6 hours. +// Backup enables a feature to backup KCM objects into a cloud. +type Backup struct { + // Schedule is a Cron expression defining when to run the scheduled [ManagementBackup]. + // Default value is to backup at minute 0 past every 6th hour (0 */6 * * *). Schedule string `json:"schedule,omitempty"` - // Flag to indicate whether the backup feature is enabled. - // If set to true, [Velero] platform will be installed. - // If set to false, creation or modification of Backups/Restores will be blocked. + // StorageLocation is the name of a [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.StorageLocation] + // where the backup should be stored. It is propagated to the corresponding scheduled [ManagementBackup] + // unless set in the object. + StorageLocation string `json:"storageLocation,omitempty"` + + // Flag to indicate whether the management cluster backup feature is enabled. + // The backup is done using [Velero]. // // [Velero]: https://velero.io Enabled bool `json:"enabled,omitempty"` diff --git a/api/v1alpha1/templates_common.go b/api/v1alpha1/templates_common.go index f7c6d22a2..ee60a189c 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -23,13 +23,10 @@ import ( helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" ) const ( - // ChartAnnotationProviderName is the annotation set on components in a Template. - // This annotations allows to identify all the components belonging to a provider. - ChartAnnotationProviderName = "cluster.x-k8s.io/provider" - chartAnnoCAPIPrefix = "cluster.x-k8s.io/" DefaultRepoName = "kcm-templates" @@ -103,7 +100,7 @@ func getProvidersList(providers Providers, annotations map[string]string) Provid return slices.Compact(res) } - providersFromAnno := annotations[ChartAnnotationProviderName] + providersFromAnno := annotations[clusterapiv1beta1.ProviderNameLabel] if len(providersFromAnno) == 0 { return Providers{} } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3bd853ce1..825d86097 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ import ( "github.com/fluxcd/helm-controller/api/v2" apiv1 "github.com/fluxcd/source-controller/api/v1" "github.com/projectsveltos/addon-controller/api/v1beta1" - velerov1 "github.com/zerospiel/velero/pkg/apis/velero/v1" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -181,10 +181,6 @@ func (in *AvailableUpgrade) DeepCopy() *AvailableUpgrade { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Backup) DeepCopyInto(out *Backup) { *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup. @@ -197,95 +193,6 @@ func (in *Backup) DeepCopy() *Backup { return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Backup) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackupList) DeepCopyInto(out *BackupList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Backup, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupList. -func (in *BackupList) DeepCopy() *BackupList { - if in == nil { - return nil - } - out := new(BackupList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BackupList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec. -func (in *BackupSpec) DeepCopy() *BackupSpec { - if in == nil { - return nil - } - out := new(BackupSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackupStatus) DeepCopyInto(out *BackupStatus) { - *out = *in - if in.Reference != nil { - in, out := &in.Reference, &out.Reference - *out = new(corev1.ObjectReference) - **out = **in - } - if in.Schedule != nil { - in, out := &in.Schedule, &out.Schedule - *out = new(velerov1.ScheduleStatus) - (*in).DeepCopyInto(*out) - } - if in.NextAttempt != nil { - in, out := &in.NextAttempt, &out.NextAttempt - *out = (*in).DeepCopy() - } - if in.LastBackup != nil { - in, out := &in.LastBackup, &out.LastBackup - *out = new(velerov1.BackupStatus) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupStatus. -func (in *BackupStatus) DeepCopy() *BackupStatus { - if in == nil { - return nil - } - out := new(BackupStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterDeployment) DeepCopyInto(out *ClusterDeployment) { *out = *in @@ -817,6 +724,10 @@ func (in *Management) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagementBackup) DeepCopyInto(out *ManagementBackup) { *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementBackup. @@ -829,6 +740,89 @@ func (in *ManagementBackup) DeepCopy() *ManagementBackup { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagementBackup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagementBackupList) DeepCopyInto(out *ManagementBackupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagementBackup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementBackupList. +func (in *ManagementBackupList) DeepCopy() *ManagementBackupList { + if in == nil { + return nil + } + out := new(ManagementBackupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagementBackupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagementBackupSpec) DeepCopyInto(out *ManagementBackupSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementBackupSpec. +func (in *ManagementBackupSpec) DeepCopy() *ManagementBackupSpec { + if in == nil { + return nil + } + out := new(ManagementBackupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagementBackupStatus) DeepCopyInto(out *ManagementBackupStatus) { + *out = *in + if in.NextAttempt != nil { + in, out := &in.NextAttempt, &out.NextAttempt + *out = (*in).DeepCopy() + } + if in.LastBackupTime != nil { + in, out := &in.LastBackupTime, &out.LastBackupTime + *out = (*in).DeepCopy() + } + if in.LastBackup != nil { + in, out := &in.LastBackup, &out.LastBackup + *out = new(velerov1.BackupStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementBackupStatus. +func (in *ManagementBackupStatus) DeepCopy() *ManagementBackupStatus { + if in == nil { + return nil + } + out := new(ManagementBackupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagementList) DeepCopyInto(out *ManagementList) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 5233c528c..b980e5612 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,8 +22,8 @@ import ( hcv2 "github.com/fluxcd/helm-controller/api/v2" sourcev1 "github.com/fluxcd/source-controller/api/v1" sveltosv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1" - velerov1api "github.com/zerospiel/velero/pkg/apis/velero/v1" - velerov2alpha1api "github.com/zerospiel/velero/pkg/apis/velero/v2alpha1" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" @@ -56,8 +56,8 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) // velero deps - utilruntime.Must(velerov1api.AddToScheme(scheme)) - utilruntime.Must(velerov2alpha1api.AddToScheme(scheme)) + utilruntime.Must(velerov1.AddToScheme(scheme)) + utilruntime.Must(velerov2alpha1.AddToScheme(scheme)) utilruntime.Must(apiextv1.AddToScheme(scheme)) utilruntime.Must(apiextv1beta1.AddToScheme(scheme)) // WARN: if snapshot is to be used, then the following resources should also be added to the scheme @@ -90,6 +90,7 @@ func main() { kcmTemplatesChartName string enableTelemetry bool enableWebhook bool + enableVelero bool webhookPort int webhookCertDir string ) @@ -114,6 +115,7 @@ func main() { "The name of the helm chart with KCM Templates.") flag.BoolVar(&enableTelemetry, "enable-telemetry", true, "Collect and send telemetry data.") flag.BoolVar(&enableWebhook, "enable-webhook", true, "Enable admission webhook.") + flag.BoolVar(&enableVelero, "enable-velero", true, "Enable Velero stack for management cluster backups.") flag.IntVar(&webhookPort, "webhook-port", 9443, "Admission webhook port.") flag.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/", "Webhook cert dir, only used when webhook-port is specified.") @@ -284,19 +286,22 @@ func main() { } if err = (&controller.CredentialReconciler{ - Client: mgr.GetClient(), + SystemNamespace: currentNamespace, + Client: mgr.GetClient(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Credential") os.Exit(1) } - // TODO (zerospiel): disabled until the #605 - // if err = (&controller.BackupReconciler{ - // Client: mgr.GetClient(), - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "Backup") - // os.Exit(1) - // } + if enableVelero { + if err = (&controller.ManagementBackupReconciler{ + Client: mgr.GetClient(), + SystemNamespace: currentNamespace, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ManagementBackup") + os.Exit(1) + } + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -367,5 +372,9 @@ func setupWebhooks(mgr ctrl.Manager, currentNamespace string) error { setupLog.Error(err, "unable to create webhook", "webhook", "Release") return err } + if err := (&kcmwebhook.ManagementBackupValidator{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ManagementBackup") + return err + } return nil } diff --git a/config/dev/aws-clusterdeployment.yaml b/config/dev/aws-clusterdeployment.yaml index 2991d51d0..d6b73387c 100644 --- a/config/dev/aws-clusterdeployment.yaml +++ b/config/dev/aws-clusterdeployment.yaml @@ -3,8 +3,6 @@ kind: ClusterDeployment metadata: name: aws-dev namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: template: aws-standalone-cp-0-0-5 credential: aws-cluster-identity-cred diff --git a/config/dev/aws-credentials.yaml b/config/dev/aws-credentials.yaml index e614003d3..d681d2e8c 100644 --- a/config/dev/aws-credentials.yaml +++ b/config/dev/aws-credentials.yaml @@ -4,8 +4,6 @@ kind: AWSClusterStaticIdentity metadata: name: aws-cluster-identity namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: secretRef: aws-cluster-identity-secret allowedNamespaces: @@ -17,8 +15,6 @@ kind: Secret metadata: name: aws-cluster-identity-secret namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm type: Opaque stringData: AccessKeyID: ${AWS_ACCESS_KEY_ID} @@ -30,8 +26,6 @@ kind: Credential metadata: name: aws-cluster-identity-cred namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: description: AWS credentials identityRef: diff --git a/config/dev/azure-clusterdeployment.yaml b/config/dev/azure-clusterdeployment.yaml index 9b500710c..95239caef 100644 --- a/config/dev/azure-clusterdeployment.yaml +++ b/config/dev/azure-clusterdeployment.yaml @@ -3,8 +3,6 @@ kind: ClusterDeployment metadata: name: azure-dev namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: template: azure-standalone-cp-0-0-5 credential: azure-cluster-identity-cred diff --git a/config/dev/azure-credentials.yaml b/config/dev/azure-credentials.yaml index 6ca798cbb..36f4c786b 100644 --- a/config/dev/azure-credentials.yaml +++ b/config/dev/azure-credentials.yaml @@ -4,7 +4,6 @@ kind: AzureClusterIdentity metadata: labels: clusterctl.cluster.x-k8s.io/move-hierarchy: "true" - k0rdent.mirantis.com/component: kcm name: azure-cluster-identity namespace: ${NAMESPACE} spec: @@ -21,8 +20,6 @@ kind: Secret metadata: name: azure-cluster-identity-secret namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm stringData: clientSecret: "${AZURE_CLIENT_SECRET}" type: Opaque @@ -32,8 +29,6 @@ kind: Credential metadata: name: azure-cluster-identity-cred namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: description: Azure credentials identityRef: diff --git a/config/dev/eks-clusterdeployment.yaml b/config/dev/eks-clusterdeployment.yaml index ea77f403d..e54435695 100644 --- a/config/dev/eks-clusterdeployment.yaml +++ b/config/dev/eks-clusterdeployment.yaml @@ -3,8 +3,6 @@ kind: ClusterDeployment metadata: name: eks-dev namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: template: aws-eks-0-0-4 credential: "aws-cluster-identity-cred" diff --git a/config/dev/vsphere-clusterdeployment.yaml b/config/dev/vsphere-clusterdeployment.yaml index 7b49ca71f..1795e28af 100644 --- a/config/dev/vsphere-clusterdeployment.yaml +++ b/config/dev/vsphere-clusterdeployment.yaml @@ -3,8 +3,6 @@ kind: ClusterDeployment metadata: name: vsphere-dev namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: template: vsphere-standalone-cp-0-0-5 credential: vsphere-cluster-identity-cred diff --git a/config/dev/vsphere-credentials.yaml b/config/dev/vsphere-credentials.yaml index d7c161f09..c338acb96 100644 --- a/config/dev/vsphere-credentials.yaml +++ b/config/dev/vsphere-credentials.yaml @@ -4,8 +4,6 @@ kind: VSphereClusterIdentity metadata: name: vsphere-cluster-identity namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: secretName: vsphere-cluster-identity-secret allowedNamespaces: @@ -17,8 +15,6 @@ kind: Secret metadata: name: vsphere-cluster-identity-secret namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm stringData: username: ${VSPHERE_USER} password: ${VSPHERE_PASSWORD} @@ -28,8 +24,6 @@ kind: Credential metadata: name: vsphere-cluster-identity-cred namespace: ${NAMESPACE} - labels: - k0rdent.mirantis.com/component: kcm spec: description: vSphere credentials identityRef: diff --git a/go.mod b/go.mod index 40d5d9a37..3ec2ed979 100644 --- a/go.mod +++ b/go.mod @@ -17,9 +17,10 @@ require ( github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 github.com/projectsveltos/addon-controller v0.45.0 github.com/projectsveltos/libsveltos v0.45.0 + github.com/robfig/cron/v3 v3.0.1 github.com/segmentio/analytics-go v3.1.0+incompatible github.com/stretchr/testify v1.10.0 - github.com/zerospiel/velero v0.0.0-20241213181215-1eaa894d12b8 + github.com/vmware-tanzu/velero v1.15.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.17.0 k8s.io/api v0.32.0 @@ -40,6 +41,7 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -108,10 +110,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.0 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -120,7 +119,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/kubernetes-csi/external-snapshotter/client/v7 v7.0.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -130,7 +128,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect @@ -141,7 +138,6 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest/blake3 v0.0.0-20240426182413-22b78e47854a // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect @@ -172,6 +168,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/go.sum b/go.sum index 99f963a4b..ab6a79a8b 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,6 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cert-manager/cert-manager v1.16.3 h1:seEF5eidFaeduaCuM85PFEuzH/1X/HOV5Y8zDQrHgpc= @@ -137,7 +135,6 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -259,8 +256,6 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= -github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -271,8 +266,6 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGN github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -291,8 +284,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -317,8 +308,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubernetes-csi/external-snapshotter/client/v7 v7.0.0 h1:j3YK74myEQRxR/srciTpOrm221SAvz6J5OVWbyfeXFo= -github.com/kubernetes-csi/external-snapshotter/client/v7 v7.0.0/go.mod h1:FlyYFe32mPxKEPaRXKNxfX576d1AoCzstYDoOOnyMA4= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -331,12 +320,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -349,8 +334,6 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -379,8 +362,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= @@ -431,11 +412,13 @@ github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fO github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= -github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= @@ -453,8 +436,6 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= @@ -476,11 +457,12 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vmware-tanzu/velero v1.15.1 h1:kKT4I6ZMQn+aiPYIalF+0Ui8VLbt9XK+3jwnCU09T0M= +github.com/vmware-tanzu/velero v1.15.1/go.mod h1:bZbnBC9OcwXfsovU0uCHwPlbm3ba8N9fwvBkwnU2vls= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -505,8 +487,6 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -github.com/zerospiel/velero v0.0.0-20241213181215-1eaa894d12b8 h1:26qmmcPx/o0wAxJY/q6u9atuJ8joHf523SeRi+24u/o= -github.com/zerospiel/velero v0.0.0-20241213181215-1eaa894d12b8/go.mod h1:k7TYpNEgFVIXGHwtK26LnGefF9r62pSams2/pPjhq/E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= @@ -597,15 +577,10 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -649,7 +624,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20241104194629-dd2ea8efbc28 h1:KJjNNclfpIkVqrZlTWcgOOaVQ00LdBnoEaRfkUx760s= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 h1:ChAdCYNQFDk5fYvFZMywKLIijG7TC2m1C2CMEu11G3o= google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484/go.mod h1:KRUmxRI4JmbpAm8gcZM4Jsffi859fo5LQjILwuqj9z8= google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= diff --git a/hack/templates.sh b/hack/templates.sh index ced1e8932..cbd0abff5 100755 --- a/hack/templates.sh +++ b/hack/templates.sh @@ -45,8 +45,6 @@ metadata: name: $template_name annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/internal/controller/backup/collect.go b/internal/controller/backup/collect.go new file mode 100644 index 000000000..e8e7d0d76 --- /dev/null +++ b/internal/controller/backup/collect.go @@ -0,0 +1,137 @@ +// Copyright 2024 +// +// 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 backup + +import ( + "context" + "fmt" + "maps" + "slices" + "strings" + "time" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" +) + +func getBackupTemplateSpec(ctx context.Context, cl client.Client) (*velerov1.BackupSpec, error) { + bs := &velerov1.BackupSpec{ + IncludedNamespaces: []string{"*"}, + ExcludedResources: []string{"clusters.cluster.x-k8s.io"}, + TTL: metav1.Duration{Duration: 30 * 24 * time.Hour}, // velero's default, set it for the sake of UX + } + + orSelectors := []*metav1.LabelSelector{ + // fixed ones + selector(kcmv1alpha1.GenericComponentNameLabel, kcmv1alpha1.GenericComponentLabelValueKCM), + selector(certmanagerv1.PartOfCertManagerControllerLabelKey, "true"), + selector(clusterapiv1beta1.ProviderNameLabel, "cluster-api"), + } + + clusterTemplates := new(kcmv1alpha1.ClusterTemplateList) + if err := cl.List(ctx, clusterTemplates); err != nil { + return nil, fmt.Errorf("failed to list ClusterTemplates: %w", err) + } + + if len(clusterTemplates.Items) == 0 { // just collect child clusters names + cldSelectors, err := getClusterDeploymentsSelectors(ctx, cl, "") + if err != nil { + return nil, fmt.Errorf("failed to get selectors for all clusterdeployments: %w", err) + } + + bs.OrLabelSelectors = sortDedup(append(orSelectors, cldSelectors...)) + + return bs, nil + } + + for _, cltpl := range clusterTemplates.Items { + cldSelectors, err := getClusterDeploymentsSelectors(ctx, cl, cltpl.Name) + if err != nil { + return nil, fmt.Errorf("failed to get selectors for clusterdeployments referencing %s clustertemplate: %w", client.ObjectKeyFromObject(&cltpl), err) + } + + // add only enabled providers + if len(cldSelectors) > 0 { + for _, provider := range cltpl.Status.Providers { + orSelectors = append(orSelectors, selector(clusterapiv1beta1.ProviderNameLabel, provider)) + } + } + + orSelectors = append(orSelectors, cldSelectors...) + } + + bs.OrLabelSelectors = sortDedup(orSelectors) + + return bs, nil +} + +func sortDedup(selectors []*metav1.LabelSelector) []*metav1.LabelSelector { + const nonKubeSep = "_" + + kvs := make([]string, len(selectors)) + for i, s := range selectors { + for k, v := range s.MatchLabels { // expect only one kv pair + kvs[i] = k + nonKubeSep + v + } + } + slices.Sort(kvs) + + for i, kv := range kvs { + sepIdx := strings.Index(kv, nonKubeSep) + if sepIdx < 0 { + continue // make compiler happy + } + k := kv[:sepIdx] + v := kv[sepIdx+len(nonKubeSep):] + selectors[i] = selector(k, v) + } + + return slices.Clip( + slices.CompactFunc(selectors, func(a, b *metav1.LabelSelector) bool { + return maps.Equal(a.MatchLabels, b.MatchLabels) + }), + ) +} + +func getClusterDeploymentsSelectors(ctx context.Context, cl client.Client, clusterTemplateRef string) ([]*metav1.LabelSelector, error) { + cldeploys := new(kcmv1alpha1.ClusterDeploymentList) + opts := []client.ListOption{} + if clusterTemplateRef != "" { + opts = append(opts, client.MatchingFields{kcmv1alpha1.ClusterDeploymentTemplateIndexKey: clusterTemplateRef}) + } + + if err := cl.List(ctx, cldeploys, opts...); err != nil { + return nil, fmt.Errorf("failed to list ClusterDeployments: %w", err) + } + + selectors := make([]*metav1.LabelSelector, len(cldeploys.Items)*2) + for i, cldeploy := range cldeploys.Items { + selectors[i*2] = selector(kcmv1alpha1.FluxHelmChartNameKey, cldeploy.Name) + selectors[i*2+1] = selector(clusterapiv1beta1.ClusterNameLabel, cldeploy.Name) + } + + return selectors, nil +} + +func selector(k, v string) *metav1.LabelSelector { + return &metav1.LabelSelector{ + MatchLabels: map[string]string{k: v}, + } +} diff --git a/internal/controller/backup/collect_test.go b/internal/controller/backup/collect_test.go new file mode 100644 index 000000000..c4403916e --- /dev/null +++ b/internal/controller/backup/collect_test.go @@ -0,0 +1,236 @@ +// Copyright 2024 +// +// 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 backup + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_sortDedup(t *testing.T) { + type testInput struct { + name string + selectors []*metav1.LabelSelector + expected []*metav1.LabelSelector + } + + tests := []testInput{ + { + name: "no dups unordered", + selectors: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "foo"}, + }, + { + MatchLabels: map[string]string{"app": "bar"}, + }, + }, + expected: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "bar"}, + }, + { + MatchLabels: map[string]string{"app": "foo"}, + }, + }, + }, + { + name: "some dups in keys", + selectors: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "foo"}, + }, + { + MatchLabels: map[string]string{"app": "foo"}, + }, + { + MatchLabels: map[string]string{"app": "bar"}, + }, + }, + expected: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "bar"}, + }, + { + MatchLabels: map[string]string{"app": "foo"}, + }, + }, + }, + { + name: "all dups", + selectors: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "foo"}, + }, + { + MatchLabels: map[string]string{"app": "foo"}, + }, + { + MatchLabels: map[string]string{"app": "foo"}, + }, + }, + expected: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"app": "foo"}, + }, + }, + }, + { + name: "huge dups unordered", + selectors: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"hmc.mirantis.com/component": "hmc"}, + }, + { + MatchLabels: map[string]string{"controller.cert-manager.io/fao": "true"}, + }, + { + MatchLabels: map[string]string{"helm.toolkit.fluxcd.io/name": "hmc"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "cluster-api"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-aws"}, + }, + { + MatchLabels: map[string]string{"helm.toolkit.fluxcd.io/name": "unusual-cluster-name"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/cluster-name": "unusual-cluster-name"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-azure"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-azure"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-vsphere"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-internal"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-aws"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-openstack"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-vsphere"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-aws"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-azure"}, + }, + }, + expected: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{"cluster.x-k8s.io/cluster-name": "unusual-cluster-name"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "bootstrap-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "cluster-api"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "control-plane-k0smotron"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-aws"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-azure"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-internal"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-openstack"}, + }, + { + MatchLabels: map[string]string{"cluster.x-k8s.io/provider": "infrastructure-vsphere"}, + }, + { + MatchLabels: map[string]string{"controller.cert-manager.io/fao": "true"}, + }, + { + MatchLabels: map[string]string{"helm.toolkit.fluxcd.io/name": "hmc"}, + }, + { + MatchLabels: map[string]string{"helm.toolkit.fluxcd.io/name": "unusual-cluster-name"}, + }, + { + MatchLabels: map[string]string{"hmc.mirantis.com/component": "hmc"}, + }, + }, + }, + } + + for _, test := range tests { + actual := sortDedup(test.selectors) + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("sortDedup(%s): \n\tactual:\n\t%v\n\n\twant:\n\t%v", test.name, actual, test.expected) + } + } +} diff --git a/internal/controller/backup/config.go b/internal/controller/backup/config.go new file mode 100644 index 000000000..9852a6f92 --- /dev/null +++ b/internal/controller/backup/config.go @@ -0,0 +1,60 @@ +// Copyright 2024 +// +// 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 backup + +import ( + "context" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" +) + +// Reconciler has logic to create and reconcile [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup] objects. +type Reconciler struct { + scheme *runtime.Scheme + cl client.Client + + systemNamespace string +} + +// NewReconciler creates instance of the [Reconciler]. +func NewReconciler(cl client.Client, scheme *runtime.Scheme, systemNamespace string) *Reconciler { + return &Reconciler{ + cl: cl, + scheme: scheme, + systemNamespace: systemNamespace, + } +} + +// ErrNoManagementExists is a sentinel error indicating no [github.com/K0rdent/kcm/api/v1alpha1.Management] object exists. +var ErrNoManagementExists = errors.New("no Management object exists") + +// GetManagement fetches a [github.com/K0rdent/kcm/api/v1alpha1.Management] object. +func (r *Reconciler) GetManagement(ctx context.Context) (*kcmv1alpha1.Management, error) { + mgmts := new(kcmv1alpha1.ManagementList) + if err := r.cl.List(ctx, mgmts, client.Limit(1)); err != nil { + return nil, fmt.Errorf("failed to list Management: %w", err) + } + + if len(mgmts.Items) == 0 { + return nil, ErrNoManagementExists + } + + return &mgmts.Items[0], nil +} diff --git a/internal/controller/backup/install.go b/internal/controller/backup/install.go deleted file mode 100644 index 69d89fc60..000000000 --- a/internal/controller/backup/install.go +++ /dev/null @@ -1,445 +0,0 @@ -// Copyright 2024 -// -// 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 backup - -import ( - "context" - "fmt" - "io" - "time" - - velerov1api "github.com/zerospiel/velero/pkg/apis/velero/v1" - veleroclient "github.com/zerospiel/velero/pkg/client" - veleroinstall "github.com/zerospiel/velero/pkg/install" - "github.com/zerospiel/velero/pkg/uploader" - "github.com/zerospiel/velero/pkg/util/kube" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -type Config struct { - kubeRestConfig *rest.Config - cl client.Client - - image string - systemNamespace string - features []string - - requeueAfter time.Duration -} - -const veleroName = "velero" - -type ConfigOpt func(*Config) - -func NewConfig(cl client.Client, kc *rest.Config, opts ...ConfigOpt) *Config { - c := newWithDefaults() - - for _, o := range opts { - o(c) - } - - c.cl = cl - c.kubeRestConfig = kc - - return c -} - -func WithRequeueAfter(d time.Duration) ConfigOpt { - return func(c *Config) { - if d == 0 { - return - } - c.requeueAfter = d - } -} - -func WithVeleroSystemNamespace(ns string) ConfigOpt { - return func(c *Config) { - if len(ns) == 0 { - return - } - c.systemNamespace = ns - } -} - -func WithVeleroImage(image string) ConfigOpt { - return func(c *Config) { - if len(image) == 0 { - return - } - c.image = image - } -} - -func WithFeatures(features ...string) ConfigOpt { - return func(c *Config) { - if len(features) == 0 { - return - } - c.features = features - } -} - -func newWithDefaults() *Config { - return &Config{ - requeueAfter: 5 * time.Second, - systemNamespace: veleroName, - image: fmt.Sprintf("%s/%s:%s", veleroName, veleroName, "v1.15.0"), // velero/velero:v1.15.0 - } -} - -// ReconcileVeleroInstallation reconciles installation of velero stack within a management cluster. -func (c *Config) ReconcileVeleroInstallation(ctx context.Context) (ctrl.Result, error) { - deployState, err := c.checkVeleroDeployIsInstalled(ctx) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to determine if velero is installed: %w", err) - } - - if deployState.needInstallation { - ctrl.LoggerFrom(ctx).Info("Installing velero stack") - if err := c.installVelero(ctx); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to perform velero stack installation: %w", err) - } - - return ctrl.Result{}, nil - } - - if deployState.needRequeue || deployState.needInstallation { - return ctrl.Result{Requeue: true, RequeueAfter: c.requeueAfter}, nil // either the installation has happened or direct requeue is required - } - - return ctrl.Result{}, nil -} - -// installVelero installs velero stack with all the required components. -func (c *Config) installVelero(ctx context.Context) error { - saName, err := c.ensureVeleroRBAC(ctx) - if err != nil { - return fmt.Errorf("failed to ensure velero RBAC: %w", err) - } - - options := &veleroinstall.VeleroOptions{ - Namespace: c.systemNamespace, - Image: c.image, - Features: c.features, - - ServiceAccountName: saName, - NoDefaultBackupLocation: true, // no need (explicit BSL) - - DefaultRepoMaintenanceFrequency: time.Hour, // default - GarbageCollectionFrequency: time.Hour, // default - PodVolumeOperationTimeout: 4 * time.Hour, // default - UploaderType: uploader.KopiaType, // the only supported - - // TODO: skip null params? - ProviderName: "", // no need, provided through the explicit BSL object - Bucket: "", // no need, provided through the explicit BSL object - - Prefix: "", // no need when out-of-tree - PodAnnotations: nil, // no need, default comes from velero - PodLabels: nil, // no need, default comes from velero - ServiceAccountAnnotations: nil, // customizable through the config? - - VeleroPodResources: corev1.ResourceRequirements{}, // unbounded - NodeAgentPodResources: corev1.ResourceRequirements{}, // not used - PodResources: kube.PodResources{}, // maintenance job resources, unlimited ok - - SecretData: nil, // no need, provided through the explicit BSL object - UseNodeAgent: false, // no need - RestoreOnly: false, // no need - PrivilegedNodeAgent: false, // no need - UseVolumeSnapshots: false, // no need - BSLConfig: nil, // backupstoragelocation - VSLConfig: nil, // volumesnapshotlocation - Plugins: nil, // should be installed on-demand (BSL object) - CACertData: nil, // no need (explicit BSL) - DefaultVolumesToFsBackup: false, // no volume backups, no need - DefaultSnapshotMoveData: false, // no snapshots, no need - DisableInformerCache: false, // dangerous - ScheduleSkipImmediately: false, // might be useful, but easy to customize directly through the deploy - KeepLatestMaintenanceJobs: 0, // optional - BackupRepoConfigMap: "", // no need, backup config through a CM - RepoMaintenanceJobConfigMap: "", // no need, main job config through a CM - NodeAgentConfigMap: "", // no need, node-agent config through a CM - } - - resources := veleroinstall.AllResources(options) - - dc, err := dynamic.NewForConfig(c.kubeRestConfig) - if err != nil { - return fmt.Errorf("failed to construct dynamic client: %w", err) - } - - return veleroinstall.Install(veleroclient.NewDynamicFactory(dc), c.cl, resources, io.Discard) -} - -// ensureVeleroRBAC creates required RBAC objects for velero to be functional -// with the minimal required set of permissions. -// Returns the name of created ServiceAccount referenced by created bindings. -func (c *Config) ensureVeleroRBAC(ctx context.Context) (string, error) { - crbName, clusterRoleName, rbName, roleName, saName := veleroName, veleroName, veleroName, veleroName, veleroName - if c.systemNamespace != veleroName { - vns := veleroName + "-" + c.systemNamespace - crbName, clusterRoleName, saName = vns+"-clusterrolebinding", vns+"-clusterrole", crbName+"-sa" - rbName, roleName = vns+"-rolebinding", vns+"-role" - } - - systemNS := new(corev1.Namespace) - if err := c.cl.Get(ctx, client.ObjectKey{Name: c.systemNamespace}, systemNS); apierrors.IsNotFound(err) { - systemNS.Name = c.systemNamespace - if err := c.cl.Create(ctx, systemNS); err != nil { - return "", fmt.Errorf("failed to create %s namespace for velero: %w", c.systemNamespace, err) - } - } - - sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: saName, Namespace: c.systemNamespace}} - if _, err := controllerutil.CreateOrUpdate(ctx, c.cl, sa, func() error { - sa.Labels = veleroinstall.Labels() - return nil - }); err != nil { - return "", fmt.Errorf("failed to create or update velero service account: %w", err) - } - - role := &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: roleName, Namespace: c.systemNamespace}} - if _, err := controllerutil.CreateOrUpdate(ctx, c.cl, role, func() error { - role.Labels = veleroinstall.Labels() - role.Rules = []rbacv1.PolicyRule{ - { - APIGroups: []string{velerov1api.SchemeGroupVersion.Group}, - Resources: []string{"*"}, - Verbs: []string{"*"}, - }, - { - APIGroups: []string{corev1.GroupName}, - Resources: []string{"secrets"}, - Verbs: []string{"create"}, - }, - } - return nil - }); err != nil { - return "", fmt.Errorf("failed to create or update velero role: %w", err) - } - - roleBinding := &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: rbName, Namespace: c.systemNamespace}} - if _, err := controllerutil.CreateOrUpdate(ctx, c.cl, roleBinding, func() error { - roleBinding.Labels = veleroinstall.Labels() - roleBinding.RoleRef = rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "Role", - Name: roleName, - } - roleBinding.Subjects = []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: saName, - Namespace: c.systemNamespace, - }, - } - return nil - }); err != nil { - return "", fmt.Errorf("failed to create or update velero role binding: %w", err) - } - - cr := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: clusterRoleName}} - if _, err := controllerutil.CreateOrUpdate(ctx, c.cl, cr, func() error { - cr.Labels = veleroinstall.Labels() - cr.Rules = []rbacv1.PolicyRule{ - { - APIGroups: []string{"*"}, - Resources: []string{"*"}, - Verbs: []string{"list", "get"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"namespaces"}, - Verbs: []string{"list", "get"}, - }, - { - APIGroups: []string{apiextv1.GroupName}, - Resources: []string{"customresourcedefinitions"}, - Verbs: []string{"get"}, - }, - } - return nil - }); err != nil { - return "", fmt.Errorf("failed to create or update velero cluster role: %w", err) - } - - crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: crbName}} - if _, err := controllerutil.CreateOrUpdate(ctx, c.cl, crb, func() error { - crb.Labels = veleroinstall.Labels() - crb.RoleRef = rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "ClusterRole", - Name: clusterRoleName, - } - crb.Subjects = []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: saName, - Namespace: c.systemNamespace, - }, - } - return nil - }); err != nil { - return "", fmt.Errorf("failed to create or update velero cluster role binding: %w", err) - } - - return saName, nil -} - -type deployState struct { - needRequeue bool - needInstallation bool -} - -// checkVeleroDeployIsInstalled check whether the velero deploy is already installed: -// - the deployment is presented; -// - is in ready state; -// - the only container has the expected image and replicas. -// -// If image or replica count are not expected, the deploy will be patched regardingly. -// If the deploy has unexpected container name, the deploy will be deleted. -func (c *Config) checkVeleroDeployIsInstalled(ctx context.Context) (deployState, error) { - l := ctrl.LoggerFrom(ctx).WithName("velero-deploy-checker") - - l.Info("Checking if Velero deployment is already installed") - - veleroDeploy := new(appsv1.Deployment) - err := c.cl.Get(ctx, client.ObjectKey{Namespace: c.systemNamespace, Name: veleroName}, veleroDeploy) - if err != nil && !apierrors.IsNotFound(err) { - return deployState{}, fmt.Errorf("failed to get velero deploy: %w", err) - } - - if apierrors.IsNotFound(err) { - l.Info("Deployment is not found, considering the stack has not been (yet) installed") - return deployState{needInstallation: true}, nil - } - - if len(veleroDeploy.Spec.Template.Spec.Containers) == 0 || - veleroDeploy.Spec.Template.Spec.Containers[0].Name != veleroName { - l.Info("Deployment has unexpected container name, considering to reinstall the deployment again") - // the deploy is "corrupted", remove only it and then reinstall - if err := c.cl.Delete(ctx, veleroDeploy); err != nil { - return deployState{}, fmt.Errorf("failed to delete velero deploy: %w", err) - } - - removalCtx, cancel := context.WithCancel(ctx) - var checkErr error - checkFn := func(ctx context.Context) { - key := client.ObjectKeyFromObject(veleroDeploy) - ll := l.V(1).WithValues("velero_deploy", key.String()) - ll.Info("Checking if the deployment has been removed") - if checkErr = c.cl.Get(ctx, client.ObjectKeyFromObject(veleroDeploy), veleroDeploy); checkErr != nil { - if apierrors.IsNotFound(checkErr) { - ll.Info("Removed successfully") - checkErr = nil - } - cancel() - return - } - ll.Info("Not removed yet") - } - - wait.UntilWithContext(removalCtx, checkFn, time.Millisecond*500) - if checkErr != nil { - return deployState{}, fmt.Errorf("failed to wait for velero deploy removal: %w", checkErr) - } - - return deployState{needInstallation: true}, nil - } - - isPatchRequired := false - // process 2 invariants beforehand - cont := veleroDeploy.Spec.Template.Spec.Containers[0] - if cont.Image != c.image { - l.Info("Deployment container has unexpected image", "current_image", cont.Image, "expected_image", c.image) - cont.Image = c.image - veleroDeploy.Spec.Template.Spec.Containers[0] = cont - isPatchRequired = true - } - - if veleroDeploy.Spec.Replicas == nil || *veleroDeploy.Spec.Replicas == 0 { - l.Info("Deployment is scaled to 0, scaling up to 1") - *veleroDeploy.Spec.Replicas = 1 - isPatchRequired = true - } - - if isPatchRequired { - l.Info("Patching the deployment") - if err := c.cl.Patch(ctx, veleroDeploy, client.Merge); err != nil { - return deployState{}, fmt.Errorf("failed to patch velero deploy: %w", err) - } - - l.Info("Need to requeue after the successful patch") - return deployState{needRequeue: true}, nil - } - - r := isDeploymentReady(veleroDeploy) // if no invariants then just check the readiness - if !r { - l.Info("Deployment is not ready yet, will requeue") - return deployState{needRequeue: true}, nil - } - - l.Info("Deployment is in the expected state") - return deployState{}, nil -} - -// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/rollout_status.go#L76-L89 - -// isDeploymentReady checks if the given Deployment instance is ready. -func isDeploymentReady(d *appsv1.Deployment) bool { - if d.Generation > d.Status.ObservedGeneration { - return false - } - - const timedOutReason = "ProgressDeadlineExceeded" // avoid dependency - var cond *appsv1.DeploymentCondition - for _, c := range d.Status.Conditions { - if c.Type == appsv1.DeploymentProgressing { - cond = &c - break - } - } - - if cond != nil && cond.Reason == timedOutReason { - return false - } - - if d.Spec.Replicas != nil && d.Status.UpdatedReplicas < *d.Spec.Replicas { - return false - } - - if d.Status.Replicas > d.Status.UpdatedReplicas { - return false - } - - if d.Status.AvailableReplicas < d.Status.UpdatedReplicas { - return false - } - - return true -} diff --git a/internal/controller/backup/oneshot.go b/internal/controller/backup/oneshot.go deleted file mode 100644 index edcc4a3a5..000000000 --- a/internal/controller/backup/oneshot.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 -// -// 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 backup - -import ( - "context" - - kcmv1 "github.com/K0rdent/kcm/api/v1alpha1" -) - -func (*Config) ReconcileBackup(ctx context.Context, backup *kcmv1.Backup) error { - if backup == nil { - return nil - } - - _ = ctx - return nil -} diff --git a/internal/controller/backup/periodic.go b/internal/controller/backup/periodic.go new file mode 100644 index 000000000..45a602248 --- /dev/null +++ b/internal/controller/backup/periodic.go @@ -0,0 +1,135 @@ +// Copyright 2024 +// +// 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 backup + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/manager" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" +) + +var _ manager.Runnable = (*Runner)(nil) + +// Runner is a periodic runner which enqueues [github.com/K0rdent/kcm/api/v1alpha1.ManagementBackup] for reconciliation on a schedule. +type Runner struct { + eventC chan event.GenericEvent + cl client.Client + started *atomic.Bool + interval time.Duration +} + +// RunnerOpt is a function which configures the [Runner]. +type RunnerOpt func(c *Runner) + +// NewRunner creates a new periodic [Runner] and configures it using the provided [RunnerOpt]. +func NewRunner(opts ...RunnerOpt) *Runner { + r := &Runner{ + eventC: make(chan event.GenericEvent), + started: new(atomic.Bool), + interval: 1 * time.Minute, + } + + for _, o := range opts { + o(r) + } + + return r +} + +// WithClient configures the [Runner] with the given client. +func WithClient(cl client.Client) RunnerOpt { + return func(c *Runner) { + c.cl = cl + } +} + +// WithInterval configures the [Runner] with the given interval. +func WithInterval(interval time.Duration) RunnerOpt { + return func(c *Runner) { + c.interval = interval + } +} + +// GetEventChannel returns the channel [sigs.k8s.io/controller-runtime/pkg/event.GenericEvent] typed. +func (r *Runner) GetEventChannel() <-chan event.GenericEvent { + return r.eventC +} + +// Start implements the [sigs.k8s.io/controller-runtime/pkg/manager.Runnable] interface. +func (r *Runner) Start(ctx context.Context) error { + if r.started.Load() { + return errors.New("the runner cannot be started twice") + } + if r.cl == nil { + return errors.New("cannot start runnter without the client") + } + + r.started.Store(true) + + defer close(r.eventC) + + l := ctrl.LoggerFrom(ctx).WithName("mgmtbackup_schedule_runner") + ctx = ctrl.LoggerInto(ctx, l) + + l.Info("Starting schedule runner") + + wait.Until(func() { + if err := r.enqueueScheduledBackups(ctx); err != nil { + if errors.Is(err, errEmptyList) { + l.V(1).Info("No management backups with schedule to enqueue") + return + } + + l.Error(err, "failed to enqueue management backups") + } + }, r.interval, ctx.Done()) + + return nil +} + +var errEmptyList = errors.New("no items available to enqueue") + +// enqueueScheduledBackups enqueues the [github.com/K0rdent/kcm/api/v1alpha1.ManagementBackup] objects which are properly annotated. +func (r *Runner) enqueueScheduledBackups(ctx context.Context) error { + schedules := new(kcmv1alpha1.ManagementBackupList) + if err := r.cl.List(ctx, schedules, client.MatchingFields{kcmv1alpha1.ManagementBackupScheduledIndexKey: "true"}); err != nil { + return fmt.Errorf("failed to list ManagementBackups in periodic runner: %w", err) + } + + if len(schedules.Items) == 0 { + return errEmptyList + } + + for _, item := range schedules.Items { + if item.Status.Paused { // no sense to enqueue paused schedules + continue + } + r.eventC <- event.GenericEvent{ + Object: &item, + } + } + + return nil +} diff --git a/internal/controller/backup/reconcile.go b/internal/controller/backup/reconcile.go new file mode 100644 index 000000000..76a6d282d --- /dev/null +++ b/internal/controller/backup/reconcile.go @@ -0,0 +1,242 @@ +// Copyright 2024 +// +// 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 backup + +import ( + "context" + "fmt" + "time" + + cron "github.com/robfig/cron/v3" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" +) + +// ScheduleMgmtNameLabel holds a reference to the [github.com/K0rdent/kcm/api/v1alpha1.ManagementBackup] object name. +const ScheduleMgmtNameLabel = "k0rdent.mirantis.com/management-backup" + +func (r *Reconciler) ReconcileBackup(ctx context.Context, mgmtBackup *kcmv1alpha1.ManagementBackup, mgmt *kcmv1alpha1.Management) (ctrl.Result, error) { + if mgmtBackup == nil || mgmt == nil { + return ctrl.Result{}, nil + } + + l := ctrl.LoggerFrom(ctx) + + if mgmtBackup.IsSchedule() && mgmtBackup.CreationTimestamp.IsZero() || mgmtBackup.UID == "" { + l.Info("Creating scheduled ManagementBackup") + mgmtBackup.Spec.StorageLocation = mgmt.Spec.Backup.StorageLocation + return r.createManagementBackup(ctx, mgmtBackup) + } + + mgmtBackup.Status.Paused = false + + // schedule-creation path + if mgmtBackup.IsSchedule() { + cronSchedule, err := cron.ParseStandard(mgmt.Spec.Backup.Schedule) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to parse cron schedule %s: %w", mgmt.Spec.Backup.Schedule, err) + } + + isDue, nextAttemptTime := getNextAttemptTime(mgmtBackup, cronSchedule) + + // here we can put as many conditions as we want, e.g. if upgrade is progressing + isOkayToCreateBackup := isDue && !r.isVeleroBackupProgressing(ctx, mgmtBackup) + + if isOkayToCreateBackup { + if mgmt.Spec.Backup.StorageLocation != "" && mgmtBackup.Spec.StorageLocation == "" { // sanity + mgmtBackup.Spec.StorageLocation = mgmt.Spec.Backup.StorageLocation // NOTE: the object's spec won't be updated + } + return r.createScheduleBackup(ctx, mgmtBackup, nextAttemptTime) + } + + newNextAttemptTime := &metav1.Time{Time: nextAttemptTime} + if !mgmtBackup.Status.NextAttempt.Equal(newNextAttemptTime) { + mgmtBackup.Status.NextAttempt = newNextAttemptTime + + if err := r.cl.Status().Update(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update ManagementBackup %s status with next attempt time: %w", mgmtBackup.Name, err) + } + } + + if mgmtBackup.Status.LastBackupName == "" { // is not due, nothing to do + return ctrl.Result{}, nil + } + } else if mgmtBackup.Status.LastBackupName == "" { // single mgmtbackup, velero backup has not been created yet + return r.createSingleBackup(ctx, mgmtBackup) + } + + l.V(1).Info("Collecting backup status") + + backupName := mgmtBackup.Name + if mgmtBackup.IsSchedule() { + backupName = mgmtBackup.Status.LastBackupName + } + veleroBackup := new(velerov1.Backup) + if err := r.cl.Get(ctx, client.ObjectKey{ + Name: backupName, + Namespace: r.systemNamespace, + }, veleroBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get velero Backup: %w", err) + } + + l.V(1).Info("Updating backup status") + mgmtBackup.Status.LastBackup = &veleroBackup.Status + if err := r.cl.Status().Update(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update ManagementBackup %s status: %w", mgmtBackup.Name, err) + } + + return ctrl.Result{}, nil +} + +func (r *Reconciler) createManagementBackup(ctx context.Context, mgmtBackup *kcmv1alpha1.ManagementBackup) (ctrl.Result, error) { + if mgmtBackup.Annotations == nil { + mgmtBackup.Annotations = make(map[string]string) + } + mgmtBackup.Annotations[kcmv1alpha1.ScheduleBackupAnnotation] = "true" + + if err := r.cl.Create(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create scheduled ManagementBackup: %w", err) + } + + return ctrl.Result{}, nil +} + +func (r *Reconciler) createScheduleBackup(ctx context.Context, mgmtBackup *kcmv1alpha1.ManagementBackup, nextAttemptTime time.Time) (ctrl.Result, error) { + now := time.Now() + backupName := mgmtBackup.TimestampedBackupName(now) + + if err := r.createNewVeleroBackup(ctx, backupName, withScheduleLabel(mgmtBackup.Name), withStorageLocation(mgmtBackup.Spec.StorageLocation)); err != nil { + return ctrl.Result{}, err + } + + mgmtBackup.Status.LastBackupName = backupName + mgmtBackup.Status.LastBackupTime = &metav1.Time{Time: now} + mgmtBackup.Status.NextAttempt = &metav1.Time{Time: nextAttemptTime} + + if err := r.cl.Status().Update(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update ManagementBackup %s status: %w", mgmtBackup.Name, err) + } + + return ctrl.Result{}, nil +} + +func (r *Reconciler) createSingleBackup(ctx context.Context, mgmtBackup *kcmv1alpha1.ManagementBackup) (ctrl.Result, error) { + if err := r.createNewVeleroBackup(ctx, mgmtBackup.Name, withStorageLocation(mgmtBackup.Spec.StorageLocation)); err != nil { + return ctrl.Result{}, err + } + + mgmtBackup.Status.LastBackupName = mgmtBackup.Name + mgmtBackup.Status.LastBackupTime = &metav1.Time{Time: time.Now()} + + if err := r.cl.Status().Update(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update ManagementBackup %s status: %w", mgmtBackup.Name, err) + } + + return ctrl.Result{}, nil +} + +type createOpt func(*velerov1.Backup) + +func withScheduleLabel(scheduleName string) createOpt { + return func(b *velerov1.Backup) { + if b.Labels == nil { + b.Labels = make(map[string]string) + } + b.Labels[ScheduleMgmtNameLabel] = scheduleName + } +} + +func withStorageLocation(loc string) createOpt { + return func(b *velerov1.Backup) { + b.Spec.StorageLocation = loc + } +} + +func (r *Reconciler) createNewVeleroBackup(ctx context.Context, backupName string, createOpts ...createOpt) error { + l := ctrl.LoggerFrom(ctx) + + veleroBackup, err := r.getNewVeleroBackup(ctx, backupName) + if err != nil { + return err + } + + for _, o := range createOpts { + o(veleroBackup) + } + + if err := r.cl.Create(ctx, veleroBackup); client.IgnoreAlreadyExists(err) != nil { // avoid err-loop on status update error + return fmt.Errorf("failed to create velero Backup: %w", err) + } + + l.V(1).Info("Initial backup has been created", "new_backup_name", client.ObjectKeyFromObject(veleroBackup)) + return nil +} + +func (r *Reconciler) getNewVeleroBackup(ctx context.Context, backupName string) (*velerov1.Backup, error) { + templateSpec, err := getBackupTemplateSpec(ctx, r.cl) + if err != nil { + return nil, fmt.Errorf("failed to construct velero backup spec: %w", err) + } + + veleroBackup := &velerov1.Backup{ + TypeMeta: metav1.TypeMeta{ + APIVersion: velerov1.SchemeGroupVersion.String(), + Kind: "Backup", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: backupName, + Namespace: r.systemNamespace, + }, + Spec: *templateSpec, + } + + return veleroBackup, nil +} + +func (r *Reconciler) isVeleroBackupProgressing(ctx context.Context, schedule *kcmv1alpha1.ManagementBackup) bool { + backups := &velerov1.BackupList{} + if err := r.cl.List(ctx, backups, client.InNamespace(r.systemNamespace), client.MatchingLabels{ScheduleMgmtNameLabel: schedule.Name}); err != nil { + return true + } + + for _, backup := range backups.Items { + if backup.Status.Phase == velerov1.BackupPhaseNew || + backup.Status.Phase == velerov1.BackupPhaseInProgress { + return true + } + } + + return false +} + +func getNextAttemptTime(schedule *kcmv1alpha1.ManagementBackup, cronSchedule cron.Schedule) (bool, time.Time) { + lastBackupTime := schedule.CreationTimestamp.Time + if schedule.Status.LastBackup != nil { + lastBackupTime = schedule.Status.LastBackupTime.Time + } + + nextAttemptTime := cronSchedule.Next(lastBackupTime) // might be in past so rely on now + now := time.Now() + isDue := now.After(nextAttemptTime) + if isDue { + nextAttemptTime = now + } + + return isDue, nextAttemptTime +} diff --git a/internal/controller/backup/schedule.go b/internal/controller/backup/schedule.go deleted file mode 100644 index 83305c8a4..000000000 --- a/internal/controller/backup/schedule.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 -// -// 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 backup - -import ( - "context" - - kcmv1 "github.com/K0rdent/kcm/api/v1alpha1" -) - -func (*Config) ReconcileScheduledBackup(ctx context.Context, schedule *kcmv1.Backup) error { - if schedule == nil { - return nil - } - _ = ctx - - return nil -} diff --git a/internal/controller/backup/type.go b/internal/controller/backup/type.go deleted file mode 100644 index c3d51bc88..000000000 --- a/internal/controller/backup/type.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2024 -// -// 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 backup - -import ( - "context" - "fmt" - - velerov1api "github.com/zerospiel/velero/pkg/apis/velero/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - kcmv1 "github.com/K0rdent/kcm/api/v1alpha1" -) - -type Typ uint - -const ( - TypeNone Typ = iota - TypeSchedule - TypeBackup -) - -func (c *Config) GetBackupType(ctx context.Context, instance *kcmv1.Backup, reqName string) (Typ, error) { - if instance.Status.Reference != nil { - gv := velerov1api.SchemeGroupVersion - switch instance.Status.Reference.GroupVersionKind() { - case gv.WithKind("Schedule"): - return TypeSchedule, nil - case gv.WithKind("Backup"): - return TypeBackup, nil - default: - return TypeNone, fmt.Errorf("unexpected kind %s in the backup reference", instance.Status.Reference.Kind) - } - } - - mgmts := new(kcmv1.ManagementList) - if err := c.cl.List(ctx, mgmts, client.Limit(1)); err != nil { - return TypeNone, fmt.Errorf("failed to list Management: %w", err) - } - - if len(mgmts.Items) == 0 { // nothing to do in such case for both scheduled/non-scheduled backups - return TypeNone, nil - } - - if reqName == mgmts.Items[0].Name { // mgmt name == scheduled-backup - return TypeSchedule, nil - } - - return TypeBackup, nil -} diff --git a/internal/controller/backup_controller.go b/internal/controller/backup_controller.go deleted file mode 100644 index ccb734205..000000000 --- a/internal/controller/backup_controller.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2024 -// -// 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 controller - -import ( - "context" - "fmt" - "os" - "strings" - "time" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - kcmv1 "github.com/K0rdent/kcm/api/v1alpha1" - "github.com/K0rdent/kcm/internal/controller/backup" -) - -// BackupReconciler reconciles a Backup object -type BackupReconciler struct { - client.Client - - kc *rest.Config - - image string - systemNamespace string - features []string - - requeueAfter time.Duration -} - -func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := ctrl.LoggerFrom(ctx) - - backupInstance := new(kcmv1.Backup) - err := r.Client.Get(ctx, req.NamespacedName, backupInstance) - if ierr := client.IgnoreNotFound(err); ierr != nil { - l.Error(ierr, "unable to fetch Backup") - return ctrl.Result{}, ierr - } - - bcfg := backup.NewConfig(r.Client, r.kc, - backup.WithFeatures(r.features...), - backup.WithRequeueAfter(r.requeueAfter), - backup.WithVeleroImage(r.image), - backup.WithVeleroSystemNamespace(r.systemNamespace), - ) - - if apierrors.IsNotFound(err) { - // if non-scheduled backup is not found(deleted), then just skip the error - // if scheduled backup is not found, then it either does not exist yet - // and we should create it, or it has been removed; - // if the latter is the case, we either should re-create it once again - // or do nothing if mgmt backup is disabled - mgmt := new(kcmv1.Management) - if err := r.Client.Get(ctx, req.NamespacedName, mgmt); err != nil { - l.Error(err, "unable to fetch Management") - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - if !mgmt.Spec.Backup.Enabled { - l.Info("Management backup is disabled, nothing to do") - return ctrl.Result{}, nil - } - - l.Info("Reconciling velero stack") - installRes, err := bcfg.ReconcileVeleroInstallation(ctx) - if err != nil { - l.Error(err, "velero installation") - return ctrl.Result{}, err - } - if installRes.Requeue || installRes.RequeueAfter > 0 { - return installRes, nil - } - - // required during creation - backupInstance.Name = req.Name - backupInstance.Namespace = req.Namespace - } - - btype, err := bcfg.GetBackupType(ctx, backupInstance, req.Name) - if err != nil { - l.Error(err, "failed to determine backup type") - return ctrl.Result{}, err - } - - switch btype { - case backup.TypeNone: - l.Info("There are nothing to reconcile, management does not exists") - // TODO: do we need to reconcile/delete/pause schedules in this case? - return ctrl.Result{}, nil - case backup.TypeBackup: - return ctrl.Result{}, bcfg.ReconcileBackup(ctx, backupInstance) - case backup.TypeSchedule: - return ctrl.Result{}, bcfg.ReconcileScheduledBackup(ctx, backupInstance) - } - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *BackupReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.kc = mgr.GetConfig() - - const reqDuration = "BACKUP_CTRL_REQUEUE_DURATION" - r.features = strings.Split(strings.ReplaceAll(os.Getenv("BACKUP_FEATURES"), ", ", ","), ",") - r.systemNamespace = os.Getenv("BACKUP_SYSTEM_NAMESPACE") - r.image = os.Getenv("BACKUP_BASIC_IMAGE") - d, err := time.ParseDuration(os.Getenv(reqDuration)) - if err != nil { - return fmt.Errorf("failed to parse env %s duration: %w", reqDuration, err) - } - r.requeueAfter = d - - return ctrl.NewControllerManagedBy(mgr). - For(&kcmv1.Backup{}). - Watches(&kcmv1.Management{}, handler.EnqueueRequestsFromMapFunc(func(_ context.Context, o client.Object) []ctrl.Request { - return []ctrl.Request{{NamespacedName: client.ObjectKeyFromObject(o)}} - }), builder.WithPredicates( // watch mgmt.spec.backup to manage the (only) scheduled Backup - predicate.Funcs{ - GenericFunc: func(event.TypedGenericEvent[client.Object]) bool { return false }, - DeleteFunc: func(event.TypedDeleteEvent[client.Object]) bool { return false }, - CreateFunc: func(tce event.TypedCreateEvent[client.Object]) bool { - mgmt, ok := tce.Object.(*kcmv1.Management) - if !ok { - return false - } - - return mgmt.Spec.Backup.Enabled - }, - UpdateFunc: func(tue event.TypedUpdateEvent[client.Object]) bool { - oldMgmt, ok := tue.ObjectOld.(*kcmv1.Management) - if !ok { - return false - } - - newMgmt, ok := tue.ObjectNew.(*kcmv1.Management) - if !ok { - return false - } - - return (newMgmt.Spec.Backup.Enabled != oldMgmt.Spec.Backup.Enabled || - newMgmt.Spec.Backup.Schedule != oldMgmt.Spec.Backup.Schedule) - }, - }, - )). - Complete(r) -} diff --git a/internal/controller/credential_controller.go b/internal/controller/credential_controller.go index 7faf56d9e..99027fae9 100644 --- a/internal/controller/credential_controller.go +++ b/internal/controller/credential_controller.go @@ -20,10 +20,13 @@ import ( "fmt" "time" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + capv "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -31,11 +34,11 @@ import ( "github.com/K0rdent/kcm/internal/utils" ) -const defaultSyncPeriod = 15 * time.Minute - // CredentialReconciler reconciles a Credential object type CredentialReconciler struct { client.Client + SystemNamespace string + syncPeriod time.Duration } func (r *CredentialReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, err error) { @@ -81,6 +84,16 @@ func (r *CredentialReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } + if err := utils.AddKCMComponentLabel(ctx, r.Client, clIdty); err != nil { + l.Error(err, "adding component label to the ClusterIdentity") + return ctrl.Result{}, err + } + + if err := r.updateIdentityReferencedSecret(ctx, cred.Spec.IdentityRef); err != nil { + l.Error(err, "adding component label to the Secret from the Identity Reference") + return ctrl.Result{}, err + } + apimeta.SetStatusCondition(cred.GetConditions(), metav1.Condition{ Type: kcm.CredentialReadyCondition, Status: metav1.ConditionTrue, @@ -88,9 +101,7 @@ func (r *CredentialReconciler) Reconcile(ctx context.Context, req ctrl.Request) Message: "Credential is ready", }) - return ctrl.Result{ - RequeueAfter: defaultSyncPeriod, - }, nil + return ctrl.Result{RequeueAfter: r.syncPeriod}, nil } func (r *CredentialReconciler) updateStatus(ctx context.Context, cred *kcm.Credential) error { @@ -109,8 +120,157 @@ func (r *CredentialReconciler) updateStatus(ctx context.Context, cred *kcm.Crede return nil } +// updateIdentityReferencedSecret updates referenced *Identity Secret with the component label. +// The component label on such Secret is required for a proper management backup. +func (r *CredentialReconciler) updateIdentityReferencedSecret(ctx context.Context, idRef *corev1.ObjectReference) error { + // avoid "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" dependency + const ( + awsClusterControllerIdentityKind = "AWSClusterControllerIdentity" + awsClusterStaticIdentityKind = "AWSClusterStaticIdentity" + awsClusterRoleIdentityKind = "AWSClusterRoleIdentity" + ) + + switch idRef.Kind { + case awsClusterControllerIdentityKind: // no secret refs + return nil + case awsClusterRoleIdentityKind: + return r.updateAWSSecretFromClusterRoleIdentity(ctx, idRef) + case awsClusterStaticIdentityKind: + return r.updateAWSSecretFromClusterStaticIdentity(ctx, idRef) + case capz.AzureClusterIdentityKind: + return r.updateAzureSecretFromClusterIdentity(ctx, idRef) + case string(capv.VSphereClusterIdentityKind): + return r.updateVsphereSecretFromClusterIdentity(ctx, idRef) + } + + return nil +} + +func (r *CredentialReconciler) updateAzureSecretFromClusterIdentity(ctx context.Context, reference *corev1.ObjectReference) error { + azureClusterIdentity := new(capz.AzureClusterIdentity) + if err := r.Client.Get(ctx, client.ObjectKey{ + Name: reference.Name, + Namespace: reference.Namespace, + }, azureClusterIdentity); err != nil { + return fmt.Errorf("failed to get AzureClusterIdentity %s/%s: %w", reference.Namespace, reference.Name, err) + } + + return updateSecret(ctx, r.Client, + client.ObjectKey{ + Name: azureClusterIdentity.Spec.ClientSecret.Name, + Namespace: azureClusterIdentity.Spec.ClientSecret.Namespace, + }, + azureClusterIdentity, + ) +} + +func (r *CredentialReconciler) updateVsphereSecretFromClusterIdentity(ctx context.Context, reference *corev1.ObjectReference) error { + vsphereClusterIdentity := new(capv.VSphereClusterIdentity) + if err := r.Client.Get(ctx, client.ObjectKey{Name: reference.Name}, vsphereClusterIdentity); err != nil { // cluster-scoped + return fmt.Errorf("failed to get VSphereClusterIdentity %s: %w", reference.Name, err) + } + + return updateSecret(ctx, r.Client, + client.ObjectKey{Name: vsphereClusterIdentity.Spec.SecretName, Namespace: r.SystemNamespace}, + vsphereClusterIdentity, + ) +} + +func (r *CredentialReconciler) updateAWSSecretFromClusterRoleIdentity(ctx context.Context, initialReference *corev1.ObjectReference) error { + const ( + awsClusterControllerIdentityKind = "AWSClusterControllerIdentity" + awsClusterStaticIdentityKind = "AWSClusterStaticIdentity" + ) + + kind, name := initialReference.Kind, initialReference.Name + + const limitRefs = 3 // consider an error if more nested refs + for range limitRefs { + clRoleIdentity := new(unstructured.Unstructured) + clRoleIdentity.SetAPIVersion(initialReference.APIVersion) + + clRoleIdentity.SetKind(kind) + clRoleIdentity.SetName(name) + + if err := r.Client.Get(ctx, client.ObjectKey{Name: name}, clRoleIdentity); err != nil { // cluster-scoped + return fmt.Errorf("failed to get %s identity reference: %w", kind, err) + } + + srcIdentityRefKind, ok, err := unstructured.NestedString(clRoleIdentity.Object, "spec", "sourceIdentityRef", "kind") + if err != nil { + return fmt.Errorf("failed to extract .spec.sourceIdentityRef.kind from %s %s: %w", clRoleIdentity.GetKind(), clRoleIdentity.GetName(), err) + } + if !ok { + return nil // sanity + } + + srcIdentityRefName, ok, err := unstructured.NestedString(clRoleIdentity.Object, "spec", "sourceIdentityRef", "name") + if err != nil { + return fmt.Errorf("failed to extract .spec.sourceIdentityRef.name from %s %s", clRoleIdentity.GetKind(), clRoleIdentity.GetName()) + } + if !ok { + return nil // sanity + } + + kind, name = srcIdentityRefKind, srcIdentityRefName + + switch srcIdentityRefKind { + case awsClusterControllerIdentityKind: // no secret refs + return nil + case awsClusterStaticIdentityKind: + newReference := &corev1.ObjectReference{ + APIVersion: initialReference.APIVersion, + Kind: kind, + Name: name, + } + return r.updateAWSSecretFromClusterStaticIdentity(ctx, newReference) + } + // nested refs case, continue + } + + return fmt.Errorf("failed to determine the secrets data from the %s %s identity reference", initialReference.Kind, initialReference.Name) +} + +func (r *CredentialReconciler) updateAWSSecretFromClusterStaticIdentity(ctx context.Context, reference *corev1.ObjectReference) error { + clStaticIdentity := new(unstructured.Unstructured) + clStaticIdentity.SetAPIVersion(reference.APIVersion) + clStaticIdentity.SetKind(reference.Kind) + clStaticIdentity.SetName(reference.Name) + + if err := r.Client.Get(ctx, client.ObjectKey{Name: reference.Name}, clStaticIdentity); err != nil { // cluster-scoped + return fmt.Errorf("failed to get %s identity reference: %w", reference.Kind, err) + } + + secretName, ok, err := unstructured.NestedString(clStaticIdentity.Object, "spec", "secretRef") + if err != nil { + return fmt.Errorf("failed to extract .spec.secretRef from %s %s", clStaticIdentity.GetKind(), clStaticIdentity.GetName()) + } + if !ok { + return nil // nothing to do + } + + return updateSecret(ctx, r.Client, + client.ObjectKey{Name: secretName, Namespace: r.SystemNamespace}, + clStaticIdentity, + ) +} + +func updateSecret(ctx context.Context, cl client.Client, secretKey client.ObjectKey, identity client.Object) error { + secret := new(corev1.Secret) + if err := cl.Get(ctx, secretKey, secret); err != nil { + return fmt.Errorf("failed to get Secret %s referenced in %s %s: %w", secretKey, identity.GetObjectKind().GroupVersionKind().Kind, client.ObjectKeyFromObject(identity), err) + } + + if err := utils.AddKCMComponentLabel(ctx, cl, secret); err != nil { + return fmt.Errorf("failed to add component label: %w", err) + } + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *CredentialReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.syncPeriod = 15 * time.Minute + return ctrl.NewControllerManagedBy(mgr). For(&kcm.Credential{}). Complete(r) diff --git a/internal/controller/management_backup_controller.go b/internal/controller/management_backup_controller.go new file mode 100644 index 000000000..32020f687 --- /dev/null +++ b/internal/controller/management_backup_controller.go @@ -0,0 +1,162 @@ +// Copyright 2024 +// +// 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 controller + +import ( + "context" + "errors" + "fmt" + "time" + + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" + "github.com/K0rdent/kcm/internal/controller/backup" +) + +// ManagementBackupReconciler reconciles a ManagementBackup object +type ManagementBackupReconciler struct { + client.Client + + internal *backup.Reconciler + + SystemNamespace string +} + +func (r *ManagementBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := ctrl.LoggerFrom(ctx) + + mgmtBackup := new(kcmv1alpha1.ManagementBackup) + if err := r.Client.Get(ctx, req.NamespacedName, mgmtBackup); client.IgnoreNotFound(err) != nil { + l.Error(err, "unable to fetch ManagementBackup") + return ctrl.Result{}, err + } + + mgmt, err := r.internal.GetManagement(ctx) + if err != nil && !errors.Is(err, backup.ErrNoManagementExists) { // error during list + return ctrl.Result{}, err + } + + // TODO: NOTE: should we allow to continue with onetime backups if backup feature is disabled? + if errors.Is(err, backup.ErrNoManagementExists) || !mgmt.Spec.Backup.Enabled { + if mgmtBackup.IsSchedule() && !mgmtBackup.Status.Paused { + mgmtBackup.Status.Paused = true + if err := r.Client.Status().Update(ctx, mgmtBackup); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to mark ManagementBackup status as paused: %w", err) + } + } + + l.V(1).Info("No Management object exists or backup is disabled, nothing to do") + return ctrl.Result{}, nil + } + + if mgmtBackup.CreationTimestamp.IsZero() || mgmtBackup.UID == "" { // mgmt exists at this point + requestEqualsMgmt := mgmt != nil && req.Name == mgmt.Name + if !requestEqualsMgmt { // single backup + l.V(1).Info("ManagementBackup object has not been found, nothing to do") + return ctrl.Result{}, nil + } + + // required during creation + mgmtBackup.Name = req.Name + } + + return r.internal.ReconcileBackup(ctx, mgmtBackup, mgmt) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ManagementBackupReconciler) SetupWithManager(mgr ctrl.Manager) error { + const scheduleSyncTime = 1 * time.Minute + runner := backup.NewRunner( + backup.WithClient(mgr.GetClient()), + backup.WithInterval(scheduleSyncTime), + ) + if err := mgr.Add(runner); err != nil { + return fmt.Errorf("unable to add periodic runner: %w", err) + } + + r.internal = backup.NewReconciler(r.Client, mgr.GetScheme(), r.SystemNamespace) + + return ctrl.NewControllerManagedBy(mgr). + For(&kcmv1alpha1.ManagementBackup{}). + Named("mgmtbackup_controller"). + WatchesRawSource(source.Channel(runner.GetEventChannel(), &handler.EnqueueRequestForObject{})). + Watches(&velerov1.Backup{}, handler.Funcs{ + GenericFunc: nil, + DeleteFunc: nil, + CreateFunc: nil, + UpdateFunc: func(_ context.Context, tue event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { + oldO, ok := tue.ObjectOld.(*velerov1.Backup) + if !ok { + return + } + + newO, ok := tue.ObjectNew.(*velerov1.Backup) + if !ok { + return + } + + if oldO.Status.Phase == "" || newO.Status.Phase == oldO.Status.Phase { + return + } + + name := newO.Labels[backup.ScheduleMgmtNameLabel] + if name == "" { + name = newO.Name + } + + q.Add(ctrl.Request{NamespacedName: client.ObjectKey{Name: name}}) + }, + }). + Watches(&kcmv1alpha1.Management{}, handler.Funcs{ + GenericFunc: nil, + DeleteFunc: func(_ context.Context, tde event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { + q.Add(ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tde.Object)}) // disable schedule on mgmt absence + }, + CreateFunc: func(_ context.Context, tce event.TypedCreateEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { + mgmt, ok := tce.Object.(*kcmv1alpha1.Management) + if !ok || !mgmt.Spec.Backup.Enabled { + return + } + + q.Add(ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tce.Object)}) + }, + UpdateFunc: func(_ context.Context, tue event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[ctrl.Request]) { + oldMgmt, ok := tue.ObjectOld.(*kcmv1alpha1.Management) + if !ok { + return + } + + newMgmt, ok := tue.ObjectNew.(*kcmv1alpha1.Management) + if !ok { + return + } + + if newMgmt.Spec.Backup.Enabled == oldMgmt.Spec.Backup.Enabled && + newMgmt.Spec.Backup.Schedule == oldMgmt.Spec.Backup.Schedule { + return + } + + q.Add(ctrl.Request{NamespacedName: client.ObjectKeyFromObject(newMgmt)}) + }, + }). + Complete(r) +} diff --git a/internal/controller/backup_controller_test.go b/internal/controller/management_backup_controller_test.go similarity index 87% rename from internal/controller/backup_controller_test.go rename to internal/controller/management_backup_controller_test.go index f224ada89..7eb14495f 100644 --- a/internal/controller/backup_controller_test.go +++ b/internal/controller/management_backup_controller_test.go @@ -15,34 +15,30 @@ package controller import ( - "context" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - kcmv1 "github.com/K0rdent/kcm/api/v1alpha1" + hmcmirantiscomv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" ) var _ = Describe("Backup Controller", func() { Context("When reconciling a resource", func() { const resourceName = "test-resource" - ctx := context.Background() - typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: metav1.NamespaceAll, } - backup := &kcmv1.Backup{} + backup := &hmcmirantiscomv1alpha1.ManagementBackup{} BeforeEach(func() { By("creating the custom resource for the Kind Backup") err := k8sClient.Get(ctx, typeNamespacedName, backup) if err != nil && errors.IsNotFound(err) { - resource := &kcmv1.Backup{ + resource := &hmcmirantiscomv1alpha1.ManagementBackup{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: metav1.NamespaceAll, @@ -53,7 +49,7 @@ var _ = Describe("Backup Controller", func() { }) AfterEach(func() { - resource := &kcmv1.Backup{} + resource := &hmcmirantiscomv1alpha1.ManagementBackup{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -63,7 +59,7 @@ var _ = Describe("Backup Controller", func() { It("should successfully reconcile the resource", func() { By("Reconciling the created resource") - controllerReconciler := &BackupReconciler{ + controllerReconciler := &ManagementBackupReconciler{ Client: k8sClient, } _ = controllerReconciler diff --git a/internal/controller/management_controller.go b/internal/controller/management_controller.go index 8875cfd42..e83be6ca6 100644 --- a/internal/controller/management_controller.go +++ b/internal/controller/management_controller.go @@ -603,6 +603,15 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m capiOperatorValues = v } + if config["velero"] != nil { + v, ok := config["velero"].(map[string]any) + if !ok { + return fmt.Errorf("failed to cast 'velero' (type %T) to map[string]any", config["velero"]) + } + + config["velero"] = v + } + if r.Config != nil { if err := certmanager.VerifyAPI(ctx, r.Config, r.SystemNamespace); err != nil { return fmt.Errorf("failed to check in the cert-manager API is installed: %w", err) diff --git a/internal/controller/multiclusterservice_controller.go b/internal/controller/multiclusterservice_controller.go index d87d45f94..f84d8b687 100644 --- a/internal/controller/multiclusterservice_controller.go +++ b/internal/controller/multiclusterservice_controller.go @@ -71,7 +71,7 @@ func (r *MultiClusterServiceReconciler) Reconcile(ctx context.Context, req ctrl. } func (r *MultiClusterServiceReconciler) reconcileUpdate(ctx context.Context, mcs *kcm.MultiClusterService) (_ ctrl.Result, err error) { - if utils.AddLabel(mcs, kcm.GenericComponentLabelName, kcm.GenericComponentLabelValueKCM) { + if utils.AddLabel(mcs, kcm.GenericComponentNameLabel, kcm.GenericComponentLabelValueKCM) { if err := r.Client.Update(ctx, mcs); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update labels: %w", err) } diff --git a/internal/controller/multiclusterservice_controller_test.go b/internal/controller/multiclusterservice_controller_test.go index 84120a6d9..377c4464a 100644 --- a/internal/controller/multiclusterservice_controller_test.go +++ b/internal/controller/multiclusterservice_controller_test.go @@ -135,7 +135,7 @@ var _ = Describe("MultiClusterService Controller", func() { Namespace: testSystemNamespace, Labels: map[string]string{ kcm.KCMManagedLabelKey: "true", - kcm.GenericComponentLabelName: kcm.GenericComponentLabelValueKCM, + kcm.GenericComponentNameLabel: kcm.GenericComponentLabelValueKCM, }, }, Spec: kcm.ServiceTemplateSpec{ @@ -158,7 +158,7 @@ var _ = Describe("MultiClusterService Controller", func() { ObjectMeta: metav1.ObjectMeta{ Name: serviceTemplate2Name, Namespace: testSystemNamespace, - Labels: map[string]string{kcm.GenericComponentLabelName: kcm.GenericComponentLabelValueKCM}, + Labels: map[string]string{kcm.GenericComponentNameLabel: kcm.GenericComponentLabelValueKCM}, }, Spec: kcm.ServiceTemplateSpec{ Helm: kcm.HelmSpec{ @@ -207,7 +207,7 @@ var _ = Describe("MultiClusterService Controller", func() { multiClusterService = &kcm.MultiClusterService{ ObjectMeta: metav1.ObjectMeta{ Name: multiClusterServiceName, - Labels: map[string]string{kcm.GenericComponentLabelName: kcm.GenericComponentLabelValueKCM}, + Labels: map[string]string{kcm.GenericComponentNameLabel: kcm.GenericComponentLabelValueKCM}, Finalizers: []string{ // Reconcile attempts to add this finalizer and returns immediately // if successful. So adding this finalizer here manually in order diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 95ada5436..7376f0446 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -80,7 +80,7 @@ func (r *ClusterTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - if utils.AddLabel(clusterTemplate, kcm.GenericComponentLabelName, kcm.GenericComponentLabelValueKCM) { + if utils.AddLabel(clusterTemplate, kcm.GenericComponentNameLabel, kcm.GenericComponentLabelValueKCM) { if err := r.Update(ctx, clusterTemplate); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update labels: %w", err) } @@ -97,7 +97,7 @@ func (r *ClusterTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err := r.validateCompatibilityAttrs(ctx, clusterTemplate); err != nil { if apierrors.IsNotFound(err) { l.Info("Validation cannot be performed until Management cluster appears", "requeue in", defaultRequeueTime) - return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil // generation has not changed, need explicit requeue } l.Error(err, "failed to validate compatibility attributes") @@ -121,7 +121,7 @@ func (r *ServiceTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - if utils.AddLabel(serviceTemplate, kcm.GenericComponentLabelName, kcm.GenericComponentLabelValueKCM) { + if utils.AddLabel(serviceTemplate, kcm.GenericComponentNameLabel, kcm.GenericComponentLabelValueKCM) { if err := r.Update(ctx, serviceTemplate); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update labels: %w", err) } @@ -146,7 +146,7 @@ func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, err } - if utils.AddLabel(providerTemplate, kcm.GenericComponentLabelName, kcm.GenericComponentLabelValueKCM) { + if utils.AddLabel(providerTemplate, kcm.GenericComponentNameLabel, kcm.GenericComponentLabelValueKCM) { if err := r.Update(ctx, providerTemplate); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update labels: %w", err) } @@ -160,7 +160,7 @@ func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Req } if changed { l.Info("Updating OwnerReferences with associated Releases") - return ctrl.Result{}, r.Update(ctx, providerTemplate) + return ctrl.Result{Requeue: true}, r.Update(ctx, providerTemplate) } return r.ReconcileTemplate(ctx, providerTemplate) diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go index 06549e688..dd5a486b4 100644 --- a/internal/controller/template_controller_test.go +++ b/internal/controller/template_controller_test.go @@ -218,7 +218,7 @@ var _ = Describe("Template Controller", func() { ObjectMeta: metav1.ObjectMeta{ Name: clusterTemplateName, Namespace: metav1.NamespaceDefault, - Labels: map[string]string{kcmv1.GenericComponentLabelName: kcmv1.GenericComponentLabelValueKCM}, + Labels: map[string]string{kcmv1.GenericComponentNameLabel: kcmv1.GenericComponentLabelValueKCM}, }, Spec: kcmv1.ClusterTemplateSpec{ Helm: helmSpec, diff --git a/internal/controller/templatechain_controller_test.go b/internal/controller/templatechain_controller_test.go index 864d0eae7..ef963ae0d 100644 --- a/internal/controller/templatechain_controller_test.go +++ b/internal/controller/templatechain_controller_test.go @@ -170,7 +170,7 @@ var _ = Describe("Template Chain Controller", func() { ObjectMeta: metav1.ObjectMeta{ Name: chain.Name, Namespace: chain.Namespace, - Labels: map[string]string{kcmv1.GenericComponentLabelName: kcmv1.GenericComponentLabelValueKCM}, + Labels: map[string]string{kcmv1.GenericComponentNameLabel: kcmv1.GenericComponentLabelValueKCM}, }, Spec: kcmv1.TemplateChainSpec{SupportedTemplates: supportedClusterTemplates[chain.Name]}, } @@ -186,7 +186,7 @@ var _ = Describe("Template Chain Controller", func() { ObjectMeta: metav1.ObjectMeta{ Name: chain.Name, Namespace: chain.Namespace, - Labels: map[string]string{kcmv1.GenericComponentLabelName: kcmv1.GenericComponentLabelValueKCM}, + Labels: map[string]string{kcmv1.GenericComponentNameLabel: kcmv1.GenericComponentLabelValueKCM}, }, Spec: kcmv1.TemplateChainSpec{SupportedTemplates: supportedServiceTemplates[chain.Name]}, } @@ -201,7 +201,7 @@ var _ = Describe("Template Chain Controller", func() { if template.Labels == nil { template.Labels = make(map[string]string) } - template.Labels[kcmv1.GenericComponentLabelName] = kcmv1.GenericComponentLabelValueKCM + template.Labels[kcmv1.GenericComponentNameLabel] = kcmv1.GenericComponentLabelValueKCM template.Spec.Providers = ctProviders Expect(k8sClient.Create(ctx, template)).To(Succeed()) } @@ -216,7 +216,7 @@ var _ = Describe("Template Chain Controller", func() { if template.Labels == nil { template.Labels = make(map[string]string) } - template.Labels[kcmv1.GenericComponentLabelName] = kcmv1.GenericComponentLabelValueKCM + template.Labels[kcmv1.GenericComponentNameLabel] = kcmv1.GenericComponentLabelValueKCM template.Spec.Providers = stProviders Expect(k8sClient.Create(ctx, template)).To(Succeed()) } diff --git a/internal/credspropagation/common.go b/internal/credspropagation/common.go index c61273c15..f7edc9780 100644 --- a/internal/credspropagation/common.go +++ b/internal/credspropagation/common.go @@ -59,6 +59,9 @@ func makeSecret(name string, data map[string][]byte) *corev1.Secret { ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: metav1.NamespaceSystem, + Labels: map[string]string{ + kcm.GenericComponentNameLabel: kcm.GenericComponentLabelValueKCM, + }, }, Data: data, } @@ -71,6 +74,9 @@ func makeConfigMap(name string, data map[string]string) *corev1.ConfigMap { ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: metav1.NamespaceSystem, + Labels: map[string]string{ + kcm.GenericComponentNameLabel: kcm.GenericComponentLabelValueKCM, + }, }, Data: data, } diff --git a/internal/utils/label.go b/internal/utils/label.go index 8faca2c2d..fa62ab170 100644 --- a/internal/utils/label.go +++ b/internal/utils/label.go @@ -43,7 +43,7 @@ func AddLabel(o client.Object, labelKey, labelValue string) (labelsUpdated bool) // AddKCMComponentLabel adds the common KCM component label with the kcm value to the given object // and updates if it is required. func AddKCMComponentLabel(ctx context.Context, cl client.Client, o client.Object) error { - if !AddLabel(o, kcmv1.GenericComponentLabelName, kcmv1.GenericComponentLabelValueKCM) { + if !AddLabel(o, kcmv1.GenericComponentNameLabel, kcmv1.GenericComponentLabelValueKCM) { return nil } if err := cl.Update(ctx, o); err != nil { diff --git a/internal/utils/status/status.go b/internal/utils/status/status.go index 28ca98e51..71b3c164d 100644 --- a/internal/utils/status/status.go +++ b/internal/utils/status/status.go @@ -36,12 +36,12 @@ func ConditionsFromUnstructured(unstrObj *unstructured.Unstructured) ([]metav1.C // Iterate the status conditions and ensure each condition reports a "Ready" // status. unstrConditions, found, err := unstructured.NestedSlice(unstrObj.Object, "status", "conditions") - if !found { - return nil, fmt.Errorf("no status conditions found for %s: %s", objKind, objName) - } if err != nil { return nil, fmt.Errorf("failed to get status conditions for %s: %s: %w", objKind, objName, err) } + if !found { + return nil, fmt.Errorf("no status conditions found for %s: %s", objKind, objName) + } conditions := make([]metav1.Condition, 0, len(unstrConditions)) diff --git a/internal/webhook/clusterdeployment_webhook.go b/internal/webhook/clusterdeployment_webhook.go index 601fd43f9..66fefd052 100644 --- a/internal/webhook/clusterdeployment_webhook.go +++ b/internal/webhook/clusterdeployment_webhook.go @@ -25,6 +25,8 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + capv "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -268,6 +270,8 @@ func isCredMatchTemplate(cred *kcmv1.Credential, template *kcmv1.ClusterTemplate return fmt.Errorf("wrong kind of the ClusterIdentity %q for provider %q", idtyKind, provider) } + const secretKind = "Secret" + for _, provider := range template.Status.Providers { switch provider { case "infrastructure-aws": @@ -277,16 +281,16 @@ func isCredMatchTemplate(cred *kcmv1.Credential, template *kcmv1.ClusterTemplate return errMsg(provider) } case "infrastructure-azure": - if idtyKind != "AzureClusterIdentity" && - idtyKind != "Secret" { + if idtyKind != capz.AzureClusterIdentityKind && + idtyKind != secretKind { return errMsg(provider) } case "infrastructure-vsphere": - if idtyKind != "VSphereClusterIdentity" { + if idtyKind != string(capv.VSphereClusterIdentityKind) { return errMsg(provider) } case "infrastructure-openstack", "infrastructure-internal": - if idtyKind != "Secret" { + if idtyKind != secretKind { return errMsg(provider) } default: diff --git a/internal/webhook/management_webhook.go b/internal/webhook/management_webhook.go index 82f4a1ac5..393579997 100644 --- a/internal/webhook/management_webhook.go +++ b/internal/webhook/management_webhook.go @@ -272,7 +272,7 @@ func (*ManagementValidator) Default(_ context.Context, obj runtime.Object) error } if mgmt.Spec.Backup.Enabled && mgmt.Spec.Backup.Schedule == "" { - mgmt.Spec.Backup.Schedule = "0 */6 * * *" // every 6h + mgmt.Spec.Backup.Schedule = "0 */6 * * *" // At minute 0 past every 6th hour. } return nil diff --git a/internal/webhook/management_webhook_test.go b/internal/webhook/management_webhook_test.go index dd46eb4b1..f23455bf1 100644 --- a/internal/webhook/management_webhook_test.go +++ b/internal/webhook/management_webhook_test.go @@ -446,13 +446,13 @@ func TestManagementDefault(t *testing.T) { }{ { name: "should not set default backup schedule if already set", - input: management.NewManagement(management.WithBackup(v1alpha1.ManagementBackup{Enabled: true, Schedule: "0"})), - expected: management.NewManagement(management.WithBackup(v1alpha1.ManagementBackup{Enabled: true, Schedule: "0"})), + input: management.NewManagement(management.WithBackup(v1alpha1.Backup{Enabled: true, Schedule: "0"})), + expected: management.NewManagement(management.WithBackup(v1alpha1.Backup{Enabled: true, Schedule: "0"})), }, { name: "should set every six hours default backup schedule if backup is enabled but not set", - input: management.NewManagement(management.WithBackup(v1alpha1.ManagementBackup{Enabled: true})), - expected: management.NewManagement(management.WithBackup(v1alpha1.ManagementBackup{Enabled: true, Schedule: "0 */6 * * *"})), + input: management.NewManagement(management.WithBackup(v1alpha1.Backup{Enabled: true})), + expected: management.NewManagement(management.WithBackup(v1alpha1.Backup{Enabled: true, Schedule: "0 */6 * * *"})), }, { name: "should not set schedule if backup is disabled", diff --git a/internal/webhook/managementbackup_webhook.go b/internal/webhook/managementbackup_webhook.go new file mode 100644 index 000000000..5d71b3a2d --- /dev/null +++ b/internal/webhook/managementbackup_webhook.go @@ -0,0 +1,72 @@ +// Copyright 2024 +// +// 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 webhook + +import ( + "context" + "fmt" + + 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/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + kcmv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" +) + +type ManagementBackupValidator struct { + client.Client +} + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (v *ManagementBackupValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { + v.Client = mgr.GetClient() + return ctrl.NewWebhookManagedBy(mgr). + For(&kcmv1alpha1.ManagementBackup{}). + WithValidator(v). + Complete() +} + +var _ webhook.CustomValidator = &ManagementBackupValidator{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (v *ManagementBackupValidator) ValidateCreate(ctx context.Context, _ runtime.Object) (admission.Warnings, error) { + return v.validateBackupEnabled(ctx) +} + +func (v *ManagementBackupValidator) validateBackupEnabled(ctx context.Context) (admission.Warnings, error) { + mgmt, err := getManagement(ctx, v.Client) + if err != nil { + return nil, fmt.Errorf("failed to get Management: %w", err) + } + + if !mgmt.Spec.Backup.Enabled { + return admission.Warnings{"Management backup feature is disabled"}, apierrors.NewBadRequest("management backup is disabled, create of ManagementBackup objects disabled") + } + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (*ManagementBackupValidator) ValidateUpdate(context.Context, runtime.Object, runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (*ManagementBackupValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/managementbackup_webhook_test.go b/internal/webhook/managementbackup_webhook_test.go new file mode 100644 index 000000000..e939c9b37 --- /dev/null +++ b/internal/webhook/managementbackup_webhook_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 +// +// 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 webhook + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + hmcv1alpha1 "github.com/K0rdent/kcm/api/v1alpha1" + "github.com/K0rdent/kcm/test/objects/management" + "github.com/K0rdent/kcm/test/scheme" +) + +func TestManagementBackup_validateBackupEnabled(t *testing.T) { + g := NewWithT(t) + + tests := []struct { + name string + existingObjects []runtime.Object + err string + }{ + { + name: "should fail if > 1 Management", + existingObjects: []runtime.Object{management.NewManagement(), management.NewManagement(management.WithName("second"))}, + err: "failed to get Management: expected 1 Management object, got 2", + }, + { + name: "should fail if no Management", + err: "failed to get Management: " + errManagementIsNotFound.Error(), + }, + { + name: "should fail if backup is disabled", + existingObjects: []runtime.Object{management.NewManagement()}, + err: "management backup is disabled, create of ManagementBackup objects disabled", + }, + { + name: "should succeed if backup is enabled", + existingObjects: []runtime.Object{management.NewManagement(management.WithBackup(hmcv1alpha1.Backup{Enabled: true}))}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build() + validator := &ManagementBackupValidator{Client: c} + + ctx := context.Background() + + _, err := validator.validateBackupEnabled(ctx) + if tt.err != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tt.err)) + } else { + g.Expect(err).To(Succeed()) + } + }) + } +} diff --git a/templates/cluster/aws-hosted-cp/Chart.yaml b/templates/cluster/aws-hosted-cp/Chart.yaml index e0c81494d..82629980d 100644 --- a/templates/cluster/aws-hosted-cp/Chart.yaml +++ b/templates/cluster/aws-hosted-cp/Chart.yaml @@ -14,7 +14,7 @@ version: 0.0.4 # It is recommended to use it with quotes. appVersion: "v1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-aws, control-plane-k0smotron, bootstrap-k0smotron - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/provider: infrastructure-aws, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-aws: v1beta2 diff --git a/templates/cluster/aws-standalone-cp/Chart.yaml b/templates/cluster/aws-standalone-cp/Chart.yaml index 1d8e9342e..2d0670299 100644 --- a/templates/cluster/aws-standalone-cp/Chart.yaml +++ b/templates/cluster/aws-standalone-cp/Chart.yaml @@ -13,7 +13,7 @@ version: 0.0.5 # It is recommended to use it with quotes. appVersion: "v1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-aws, control-plane-k0smotron, bootstrap-k0smotron - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/provider: infrastructure-aws, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-aws: v1beta2 diff --git a/templates/cluster/azure-hosted-cp/Chart.yaml b/templates/cluster/azure-hosted-cp/Chart.yaml index c716e4a95..71416bcbe 100644 --- a/templates/cluster/azure-hosted-cp/Chart.yaml +++ b/templates/cluster/azure-hosted-cp/Chart.yaml @@ -14,7 +14,7 @@ version: 0.0.4 # It is recommended to use it with quotes. appVersion: "v1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-azure, control-plane-k0smotron, bootstrap-k0smotron - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/provider: infrastructure-azure, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-azure: v1beta1 diff --git a/templates/cluster/azure-standalone-cp/Chart.yaml b/templates/cluster/azure-standalone-cp/Chart.yaml index 1071f954a..b91d40cc4 100644 --- a/templates/cluster/azure-standalone-cp/Chart.yaml +++ b/templates/cluster/azure-standalone-cp/Chart.yaml @@ -13,7 +13,7 @@ version: 0.0.5 # It is recommended to use it with quotes. appVersion: "1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-azure, control-plane-k0smotron, bootstrap-k0smotron - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/provider: infrastructure-azure, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-azure: v1beta1 diff --git a/templates/cluster/openstack-standalone-cp/Chart.yaml b/templates/cluster/openstack-standalone-cp/Chart.yaml index 3e857f554..88e3b90be 100644 --- a/templates/cluster/openstack-standalone-cp/Chart.yaml +++ b/templates/cluster/openstack-standalone-cp/Chart.yaml @@ -13,7 +13,7 @@ version: 0.0.2 # It is recommended to use it with quotes. appVersion: "1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-openstack, control-plane-k0smotron, bootstrap-k0smotron - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/provider: infrastructure-openstack, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-openstack: v1beta1 diff --git a/templates/cluster/vsphere-hosted-cp/Chart.yaml b/templates/cluster/vsphere-hosted-cp/Chart.yaml index c73e25efb..0056449fd 100644 --- a/templates/cluster/vsphere-hosted-cp/Chart.yaml +++ b/templates/cluster/vsphere-hosted-cp/Chart.yaml @@ -14,8 +14,8 @@ version: 0.0.5 # It is recommended to use it with quotes. appVersion: "v1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-vsphere, control-plane-k0smotron, bootstrap-k0smotron + cluster.x-k8s.io/provider: infrastructure-vsphere, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron k0rdent.mirantis.com/type: deployment - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-vsphere: v1beta1 diff --git a/templates/cluster/vsphere-standalone-cp/Chart.yaml b/templates/cluster/vsphere-standalone-cp/Chart.yaml index e803d4097..8fa9645c5 100644 --- a/templates/cluster/vsphere-standalone-cp/Chart.yaml +++ b/templates/cluster/vsphere-standalone-cp/Chart.yaml @@ -13,8 +13,8 @@ version: 0.0.5 # It is recommended to use it with quotes. appVersion: "v1.31.1+k0s.1" annotations: - cluster.x-k8s.io/provider: infrastructure-vsphere, control-plane-k0smotron, bootstrap-k0smotron + cluster.x-k8s.io/provider: infrastructure-vsphere, control-plane-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron k0rdent.mirantis.com/type: deployment - cluster.x-k8s.io/bootstrap-k0smotron: v1beta1 - cluster.x-k8s.io/control-plane-k0smotron: v1beta1 + cluster.x-k8s.io/bootstrap-k0sproject-k0smotron: v1beta1 + cluster.x-k8s.io/control-plane-k0sproject-k0smotron: v1beta1 cluster.x-k8s.io/infrastructure-vsphere: v1beta1 diff --git a/templates/provider/k0smotron/Chart.yaml b/templates/provider/k0smotron/Chart.yaml index 1624a0694..662a30b68 100644 --- a/templates/provider/k0smotron/Chart.yaml +++ b/templates/provider/k0smotron/Chart.yaml @@ -20,5 +20,5 @@ version: 0.0.6 # It is recommended to use it with quotes. appVersion: "1.3.0" annotations: - cluster.x-k8s.io/provider: infrastructure-k0smotron, bootstrap-k0smotron, control-plane-k0smotron + cluster.x-k8s.io/provider: infrastructure-k0sproject-k0smotron, bootstrap-k0sproject-k0smotron, control-plane-k0sproject-k0smotron cluster.x-k8s.io/v1beta1: v1beta1 diff --git a/templates/provider/kcm-templates/files/release.yaml b/templates/provider/kcm-templates/files/release.yaml index e65f37b31..4d8ca4254 100644 --- a/templates/provider/kcm-templates/files/release.yaml +++ b/templates/provider/kcm-templates/files/release.yaml @@ -4,8 +4,6 @@ metadata: name: kcm-0-0-7 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: version: 0.0.7 kcm: diff --git a/templates/provider/kcm-templates/files/templates/adopted-cluster-0-0-2.yaml b/templates/provider/kcm-templates/files/templates/adopted-cluster-0-0-2.yaml index 2baa558df..0172b4fdf 100644 --- a/templates/provider/kcm-templates/files/templates/adopted-cluster-0-0-2.yaml +++ b/templates/provider/kcm-templates/files/templates/adopted-cluster-0-0-2.yaml @@ -4,8 +4,6 @@ metadata: name: adopted-cluster-0-0-2 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/aws-eks-0-0-4.yaml b/templates/provider/kcm-templates/files/templates/aws-eks-0-0-4.yaml index e1bda6099..46a6871c4 100644 --- a/templates/provider/kcm-templates/files/templates/aws-eks-0-0-4.yaml +++ b/templates/provider/kcm-templates/files/templates/aws-eks-0-0-4.yaml @@ -4,8 +4,6 @@ metadata: name: aws-eks-0-0-4 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/aws-hosted-cp-0-0-4.yaml b/templates/provider/kcm-templates/files/templates/aws-hosted-cp-0-0-4.yaml index 48696d9a7..9ed64c548 100644 --- a/templates/provider/kcm-templates/files/templates/aws-hosted-cp-0-0-4.yaml +++ b/templates/provider/kcm-templates/files/templates/aws-hosted-cp-0-0-4.yaml @@ -4,8 +4,6 @@ metadata: name: aws-hosted-cp-0-0-4 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/aws-standalone-cp-0-0-5.yaml b/templates/provider/kcm-templates/files/templates/aws-standalone-cp-0-0-5.yaml index 377ea219b..671bf0b57 100644 --- a/templates/provider/kcm-templates/files/templates/aws-standalone-cp-0-0-5.yaml +++ b/templates/provider/kcm-templates/files/templates/aws-standalone-cp-0-0-5.yaml @@ -4,8 +4,6 @@ metadata: name: aws-standalone-cp-0-0-5 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/azure-aks-0-0-2.yaml b/templates/provider/kcm-templates/files/templates/azure-aks-0-0-2.yaml index d7a36e0f4..5959a60e6 100644 --- a/templates/provider/kcm-templates/files/templates/azure-aks-0-0-2.yaml +++ b/templates/provider/kcm-templates/files/templates/azure-aks-0-0-2.yaml @@ -4,8 +4,6 @@ metadata: name: azure-aks-0-0-2 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/azure-hosted-cp-0-0-4.yaml b/templates/provider/kcm-templates/files/templates/azure-hosted-cp-0-0-4.yaml index 0a16c935f..1ff1bdd50 100644 --- a/templates/provider/kcm-templates/files/templates/azure-hosted-cp-0-0-4.yaml +++ b/templates/provider/kcm-templates/files/templates/azure-hosted-cp-0-0-4.yaml @@ -4,8 +4,6 @@ metadata: name: azure-hosted-cp-0-0-4 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/azure-standalone-cp-0-0-5.yaml b/templates/provider/kcm-templates/files/templates/azure-standalone-cp-0-0-5.yaml index 8d7977625..8e703f691 100644 --- a/templates/provider/kcm-templates/files/templates/azure-standalone-cp-0-0-5.yaml +++ b/templates/provider/kcm-templates/files/templates/azure-standalone-cp-0-0-5.yaml @@ -4,8 +4,6 @@ metadata: name: azure-standalone-cp-0-0-5 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cert-manager-1-16-2.yaml b/templates/provider/kcm-templates/files/templates/cert-manager-1-16-2.yaml index 0aed1e3d2..7c1954556 100644 --- a/templates/provider/kcm-templates/files/templates/cert-manager-1-16-2.yaml +++ b/templates/provider/kcm-templates/files/templates/cert-manager-1-16-2.yaml @@ -4,8 +4,6 @@ metadata: name: cert-manager-1-16-2 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cluster-api-provider-aws.yaml b/templates/provider/kcm-templates/files/templates/cluster-api-provider-aws.yaml index c2b9a3c26..987106f87 100644 --- a/templates/provider/kcm-templates/files/templates/cluster-api-provider-aws.yaml +++ b/templates/provider/kcm-templates/files/templates/cluster-api-provider-aws.yaml @@ -4,8 +4,6 @@ metadata: name: cluster-api-provider-aws-0-0-4 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cluster-api-provider-azure.yaml b/templates/provider/kcm-templates/files/templates/cluster-api-provider-azure.yaml index 310df433e..10a57e7fa 100644 --- a/templates/provider/kcm-templates/files/templates/cluster-api-provider-azure.yaml +++ b/templates/provider/kcm-templates/files/templates/cluster-api-provider-azure.yaml @@ -4,8 +4,6 @@ metadata: name: cluster-api-provider-azure-0-0-4 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cluster-api-provider-openstack.yaml b/templates/provider/kcm-templates/files/templates/cluster-api-provider-openstack.yaml index e665d4b63..675c9d837 100644 --- a/templates/provider/kcm-templates/files/templates/cluster-api-provider-openstack.yaml +++ b/templates/provider/kcm-templates/files/templates/cluster-api-provider-openstack.yaml @@ -4,8 +4,6 @@ metadata: name: cluster-api-provider-openstack-0-0-1 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cluster-api-provider-vsphere.yaml b/templates/provider/kcm-templates/files/templates/cluster-api-provider-vsphere.yaml index f97131803..137a1a51e 100644 --- a/templates/provider/kcm-templates/files/templates/cluster-api-provider-vsphere.yaml +++ b/templates/provider/kcm-templates/files/templates/cluster-api-provider-vsphere.yaml @@ -4,8 +4,6 @@ metadata: name: cluster-api-provider-vsphere-0-0-5 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/cluster-api.yaml b/templates/provider/kcm-templates/files/templates/cluster-api.yaml index a2674fa72..7f9e7b696 100644 --- a/templates/provider/kcm-templates/files/templates/cluster-api.yaml +++ b/templates/provider/kcm-templates/files/templates/cluster-api.yaml @@ -4,8 +4,6 @@ metadata: name: cluster-api-0-0-6 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/dex-0-19-1.yaml b/templates/provider/kcm-templates/files/templates/dex-0-19-1.yaml index 3f5189886..a22ec9d91 100644 --- a/templates/provider/kcm-templates/files/templates/dex-0-19-1.yaml +++ b/templates/provider/kcm-templates/files/templates/dex-0-19-1.yaml @@ -4,8 +4,6 @@ metadata: name: dex-0-19-1 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/external-secrets-0-11-0.yaml b/templates/provider/kcm-templates/files/templates/external-secrets-0-11-0.yaml index 992cccb08..4bfe2638d 100644 --- a/templates/provider/kcm-templates/files/templates/external-secrets-0-11-0.yaml +++ b/templates/provider/kcm-templates/files/templates/external-secrets-0-11-0.yaml @@ -4,8 +4,6 @@ metadata: name: external-secrets-0-11-0 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-0.yaml b/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-0.yaml index a12f9e945..98b093354 100644 --- a/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-0.yaml +++ b/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-0.yaml @@ -4,8 +4,6 @@ metadata: name: ingress-nginx-4-11-0 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-3.yaml b/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-3.yaml index 7e2149825..39c6c5ea6 100644 --- a/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-3.yaml +++ b/templates/provider/kcm-templates/files/templates/ingress-nginx-4-11-3.yaml @@ -4,8 +4,6 @@ metadata: name: ingress-nginx-4-11-3 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/k0smotron.yaml b/templates/provider/kcm-templates/files/templates/k0smotron.yaml index 8d8aa2499..f17e95f8b 100644 --- a/templates/provider/kcm-templates/files/templates/k0smotron.yaml +++ b/templates/provider/kcm-templates/files/templates/k0smotron.yaml @@ -4,8 +4,6 @@ metadata: name: k0smotron-0-0-6 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/kcm.yaml b/templates/provider/kcm-templates/files/templates/kcm.yaml index 2e6d496cc..d40192377 100644 --- a/templates/provider/kcm-templates/files/templates/kcm.yaml +++ b/templates/provider/kcm-templates/files/templates/kcm.yaml @@ -4,8 +4,6 @@ metadata: name: kcm-0-0-7 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/kyverno-3-2-6.yaml b/templates/provider/kcm-templates/files/templates/kyverno-3-2-6.yaml index 3a54aee25..bc9d5b441 100644 --- a/templates/provider/kcm-templates/files/templates/kyverno-3-2-6.yaml +++ b/templates/provider/kcm-templates/files/templates/kyverno-3-2-6.yaml @@ -4,8 +4,6 @@ metadata: name: kyverno-3-2-6 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/openstack-standalone-cp-0-0-2.yaml b/templates/provider/kcm-templates/files/templates/openstack-standalone-cp-0-0-2.yaml index 9837a5cfb..d5015703b 100644 --- a/templates/provider/kcm-templates/files/templates/openstack-standalone-cp-0-0-2.yaml +++ b/templates/provider/kcm-templates/files/templates/openstack-standalone-cp-0-0-2.yaml @@ -4,8 +4,6 @@ metadata: name: openstack-standalone-cp-0-0-2 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/projectsveltos.yaml b/templates/provider/kcm-templates/files/templates/projectsveltos.yaml index 0238d1150..8432bf14a 100644 --- a/templates/provider/kcm-templates/files/templates/projectsveltos.yaml +++ b/templates/provider/kcm-templates/files/templates/projectsveltos.yaml @@ -4,8 +4,6 @@ metadata: name: projectsveltos-0-45-0 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/velero-8-1-0.yaml b/templates/provider/kcm-templates/files/templates/velero-8-1-0.yaml index 0148a310e..7f031ec98 100644 --- a/templates/provider/kcm-templates/files/templates/velero-8-1-0.yaml +++ b/templates/provider/kcm-templates/files/templates/velero-8-1-0.yaml @@ -4,8 +4,6 @@ metadata: name: velero-8-1-0 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/vsphere-hosted-cp-0-0-5.yaml b/templates/provider/kcm-templates/files/templates/vsphere-hosted-cp-0-0-5.yaml index 504e68ccc..31bd034d0 100644 --- a/templates/provider/kcm-templates/files/templates/vsphere-hosted-cp-0-0-5.yaml +++ b/templates/provider/kcm-templates/files/templates/vsphere-hosted-cp-0-0-5.yaml @@ -4,8 +4,6 @@ metadata: name: vsphere-hosted-cp-0-0-5 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm-templates/files/templates/vsphere-standalone-cp-0-0-5.yaml b/templates/provider/kcm-templates/files/templates/vsphere-standalone-cp-0-0-5.yaml index 981e37e25..8cf21bf2e 100644 --- a/templates/provider/kcm-templates/files/templates/vsphere-standalone-cp-0-0-5.yaml +++ b/templates/provider/kcm-templates/files/templates/vsphere-standalone-cp-0-0-5.yaml @@ -4,8 +4,6 @@ metadata: name: vsphere-standalone-cp-0-0-5 annotations: helm.sh/resource-policy: keep - labels: - k0rdent.mirantis.com/component: kcm spec: helm: chartSpec: diff --git a/templates/provider/kcm/Chart.lock b/templates/provider/kcm/Chart.lock index 2c78f5041..b743c1081 100644 --- a/templates/provider/kcm/Chart.lock +++ b/templates/provider/kcm/Chart.lock @@ -8,5 +8,8 @@ dependencies: - name: cluster-api-operator repository: https://kubernetes-sigs.github.io/cluster-api-operator version: 0.15.1 -digest: sha256:f068e94c55bea2f75210f42f5fd2b8641e6cf0c7dc611ca9ea779d05387a321d -generated: "2025-01-14T17:01:06.682429+07:00" +- name: velero + repository: https://vmware-tanzu.github.io/helm-charts + version: 8.3.0 +digest: sha256:4809d9819fa4b5911dc1b1576ae7d56ca0c585121740158daf7e88dbd2fbffa2 +generated: "2025-01-16T13:12:13.858492+01:00" diff --git a/templates/provider/kcm/Chart.yaml b/templates/provider/kcm/Chart.yaml index 8e61479bb..51242f6af 100644 --- a/templates/provider/kcm/Chart.yaml +++ b/templates/provider/kcm/Chart.yaml @@ -28,3 +28,7 @@ dependencies: version: 0.15.1 repository: https://kubernetes-sigs.github.io/cluster-api-operator condition: cluster-api-operator.enabled + - name: velero + version: 8.3.0 + repository: https://vmware-tanzu.github.io/helm-charts + condition: velero.enabled diff --git a/templates/provider/kcm/templates/_helpers.tpl b/templates/provider/kcm/templates/_helpers.tpl index 4a6be0d9e..e771596db 100644 --- a/templates/provider/kcm/templates/_helpers.tpl +++ b/templates/provider/kcm/templates/_helpers.tpl @@ -109,11 +109,3 @@ kcm-webhook - list - watch {{- end -}} - -{{- define "backup.imageName" -}} -{{- if (.Values.controller.backup.image.fullName) -}} -{{- .Values.controller.backup.image.fullName -}} -{{- else -}} -{{- printf "%s/%s:%s" .Values.controller.backup.image.repository .Values.controller.backup.image.name .Values.controller.backup.image.tag -}} -{{- end -}} -{{- end -}} diff --git a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_backups.yaml b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managementbackups.yaml similarity index 65% rename from templates/provider/kcm/templates/crds/k0rdent.mirantis.com_backups.yaml rename to templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managementbackups.yaml index cdd191a28..200acc8d0 100644 --- a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_backups.yaml +++ b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managementbackups.yaml @@ -4,20 +4,46 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.3 - name: backups.k0rdent.mirantis.com + name: managementbackups.k0rdent.mirantis.com spec: group: k0rdent.mirantis.com names: - kind: Backup - listKind: BackupList - plural: backups - singular: backup + kind: ManagementBackup + listKind: ManagementBackupList + plural: managementbackups + shortNames: + - kcmbackup + - mgmtbackup + singular: managementbackup scope: Cluster versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Status of last backup run + jsonPath: .status.lastBackup.phase + name: LastBackupStatus + type: string + - description: Next scheduled attempt to back up + jsonPath: .status.nextAttempt + name: NextBackup + type: string + - description: Time elapsed since last backup run + jsonPath: .status.lastBackupTime + name: SinceLastBackup + priority: 1 + type: date + - description: Time elapsed since object creation + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Schedule is on pause + jsonPath: .status.paused + name: Paused + priority: 1 + type: boolean + name: v1alpha1 schema: openAPIV3Schema: - description: Backup is the Schema for the backups API + description: ManagementBackup is the Schema for the managementbackups API properties: apiVersion: description: |- @@ -37,19 +63,20 @@ spec: metadata: type: object spec: - description: BackupSpec defines the desired state of Backup + description: ManagementBackupSpec defines the desired state of ManagementBackup properties: - oneshot: + storageLocation: description: |- - Oneshot indicates whether the Backup should not be scheduled - and rather created immediately and only once. - type: boolean + StorageLocation is the name of a [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.StorageLocation] + where the backup should be stored. + type: string type: object status: - description: BackupStatus defines the observed state of Backup + description: ManagementBackupStatus defines the observed state of ManagementBackup properties: lastBackup: - description: Last Velero Backup that has been created. + description: Most recently [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup] + that has been created. properties: backupItemOperationsAttempted: description: |- @@ -194,89 +221,22 @@ spec: file in object storage. type: integer type: object + lastBackupName: + description: Name of most recently created [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup]. + type: string + lastBackupTime: + description: Time of the most recently created [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.Backup]. + format: date-time + type: string nextAttempt: description: |- - NextAttempt indicates the time when the next scheduled backup will be performed. - Always absent for the Backups with the .spec.oneshot set to true. + NextAttempt indicates the time when the next backup will be created. + Always absent for a single [ManagementBackup]. format: date-time type: string - reference: - description: |- - Reference to the underlying Velero object being managed. - Might be either Velero Backup or Schedule. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - schedule: - description: |- - Status of the Velero Schedule for the Management scheduled backups. - Always absent for the Backups with the .spec.oneshot set to true. - properties: - lastBackup: - description: |- - LastBackup is the last time a Backup was run for this - Schedule schedule - format: date-time - nullable: true - type: string - lastSkipped: - description: LastSkipped is the last time a Schedule was skipped - format: date-time - nullable: true - type: string - phase: - description: Phase is the current phase of the Schedule - enum: - - New - - Enabled - - FailedValidation - type: string - validationErrors: - description: |- - ValidationErrors is a slice of all validation errors (if - applicable) - items: - type: string - type: array - type: object + paused: + description: Paused indicates if the schedule is currently paused. + type: boolean type: object type: object served: true diff --git a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml index 4dcb51e3c..78c71fba1 100644 --- a/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml +++ b/templates/provider/kcm/templates/crds/k0rdent.mirantis.com_managements.yaml @@ -43,21 +43,26 @@ spec: description: ManagementSpec defines the desired state of Management properties: backup: - description: ManagementBackup enables a feature to backup KCM objects - into a cloud. + description: Backup enables a feature to backup KCM objects into a + cloud. properties: enabled: description: |- - Flag to indicate whether the backup feature is enabled. - If set to true, [Velero] platform will be installed. - If set to false, creation or modification of Backups/Restores will be blocked. + Flag to indicate whether the management cluster backup feature is enabled. + The backup is done using [Velero]. [Velero]: https://velero.io type: boolean schedule: description: |- - Schedule is a Cron expression defining when to run the scheduled Backup. - Default value is to backup every 6 hours. + Schedule is a Cron expression defining when to run the scheduled [ManagementBackup]. + Default value is to backup at minute 0 past every 6th hour (0 */6 * * *). + type: string + storageLocation: + description: |- + StorageLocation is the name of a [github.com/vmware-tanzu/velero/pkg/apis/velero/v1.StorageLocation] + where the backup should be stored. It is propagated to the corresponding scheduled [ManagementBackup] + unless set in the object. type: string type: object core: diff --git a/templates/provider/kcm/templates/deployment.yaml b/templates/provider/kcm/templates/deployment.yaml index 2d92dc77b..e7a7c7aef 100644 --- a/templates/provider/kcm/templates/deployment.yaml +++ b/templates/provider/kcm/templates/deployment.yaml @@ -32,6 +32,7 @@ spec: - --create-templates={{ .Values.controller.createTemplates }} - --enable-telemetry={{ .Values.controller.enableTelemetry }} - --enable-webhook={{ .Values.admissionWebhook.enabled }} + - --enable-velero={{ .Values.velero.enabled }} - --webhook-port={{ .Values.admissionWebhook.port }} - --webhook-cert-dir={{ .Values.admissionWebhook.certDir }} command: @@ -39,14 +40,6 @@ spec: env: - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} - - name: BACKUP_BASIC_IMAGE - value: {{ template "backup.imageName" . }} - - name: BACKUP_FEATURES - value: {{ .Values.controller.backup.features }} - - name: BACKUP_SYSTEM_NAMESPACE - value: {{ .Values.controller.backup.namespace }} - - name: BACKUP_CTRL_REQUEUE_DURATION - value: {{ .Values.controller.backup.requeue }} image: {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} imagePullPolicy: {{ .Values.image.pullPolicy }} diff --git a/templates/provider/kcm/templates/rbac/controller/roles.yaml b/templates/provider/kcm/templates/rbac/controller/roles.yaml index 9786ace17..41e3c9058 100644 --- a/templates/provider/kcm/templates/rbac/controller/roles.yaml +++ b/templates/provider/kcm/templates/rbac/controller/roles.yaml @@ -184,7 +184,8 @@ rules: - awsclusterroleidentities - azureclusteridentities - vsphereclusteridentities - verbs: {{ include "rbac.viewerVerbs" . | nindent 4 }} + verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} + - update # required for the managementbackups - apiGroups: - config.projectsveltos.io resources: @@ -216,71 +217,34 @@ rules: resources: - secrets verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} - - create -# backup-ctrl + - update # required for the managementbackups +# managementbackups-ctrl - apiGroups: - k0rdent.mirantis.com resources: - - backups + - managementbackups verbs: {{ include "rbac.editorVerbs" . | nindent 4 }} - apiGroups: - k0rdent.mirantis.com resources: - - backups/finalizers + - managementbackups/finalizers verbs: - update - apiGroups: - k0rdent.mirantis.com resources: - - backups/status + - managementbackups/status verbs: - get - patch - update -- apiGroups: - - "" - resources: - - serviceaccounts - - namespaces - verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} - - create -- apiGroups: - - apps - resources: - - deployments - verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} - - create - - delete - - patch -- apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterrolebindings - - clusterroles - - rolebindings - - roles - verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} - - create -- apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - verbs: {{ include "rbac.viewerVerbs" . | nindent 2 }} - - create - apiGroups: - velero.io resources: - '*' verbs: - '*' -- apiGroups: - - '*' - resources: - - '*' - verbs: - - list - - get -# backup-ctrl +# managementbackups-ctrl --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role diff --git a/templates/provider/kcm/templates/rbac/user-facing/backup-editor.yaml b/templates/provider/kcm/templates/rbac/user-facing/managementbackup-editor.yaml similarity index 79% rename from templates/provider/kcm/templates/rbac/user-facing/backup-editor.yaml rename to templates/provider/kcm/templates/rbac/user-facing/managementbackup-editor.yaml index ef787a669..892147a20 100644 --- a/templates/provider/kcm/templates/rbac/user-facing/backup-editor.yaml +++ b/templates/provider/kcm/templates/rbac/user-facing/managementbackup-editor.yaml @@ -1,4 +1,4 @@ -# permissions for end users to edit backups. +# permissions for end users to edit managementbackups. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -9,8 +9,8 @@ rules: - apiGroups: - k0rdent.mirantis.com resources: - - backups - - backups/status + - managementbackups + - managementbackups/status verbs: {{ include "rbac.editorVerbs" . | nindent 6 }} - apiGroups: - velero.io diff --git a/templates/provider/kcm/templates/rbac/user-facing/backup-viewer.yaml b/templates/provider/kcm/templates/rbac/user-facing/managementbackup-viewer.yaml similarity index 79% rename from templates/provider/kcm/templates/rbac/user-facing/backup-viewer.yaml rename to templates/provider/kcm/templates/rbac/user-facing/managementbackup-viewer.yaml index 9783440bb..e03945645 100644 --- a/templates/provider/kcm/templates/rbac/user-facing/backup-viewer.yaml +++ b/templates/provider/kcm/templates/rbac/user-facing/managementbackup-viewer.yaml @@ -1,4 +1,4 @@ -# permissions for end users to view backups. +# permissions for end users to view managementbackups. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -9,8 +9,8 @@ rules: - apiGroups: - k0rdent.mirantis.com resources: - - backups - - backups/status + - managementbackups + - managementbackups/status verbs: {{ include "rbac.viewerVerbs" . | nindent 6 }} - apiGroups: - velero.io diff --git a/templates/provider/kcm/templates/webhooks.yaml b/templates/provider/kcm/templates/webhooks.yaml index 4668ac293..b3627e05a 100644 --- a/templates/provider/kcm/templates/webhooks.yaml +++ b/templates/provider/kcm/templates/webhooks.yaml @@ -341,4 +341,25 @@ webhooks: resources: - releases sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "kcm.webhook.serviceName" . }} + namespace: {{ include "kcm.webhook.serviceNamespace" . }} + path: /validate-k0rdent-mirantis-com-v1alpha1-managementbackup + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.managementbackup.k0rdent.mirantis.com + rules: + - apiGroups: + - k0rdent.mirantis.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - managementbackups + sideEffects: None {{- end }} diff --git a/templates/provider/kcm/values.yaml b/templates/provider/kcm/values.yaml index a009cc1dd..61e20118e 100644 --- a/templates/provider/kcm/values.yaml +++ b/templates/provider/kcm/values.yaml @@ -15,14 +15,6 @@ controller: createRelease: true createTemplates: true enableTelemetry: true - backup: - namespace: velero - features: "" - image: - repository: velero - name: velero - tag: v1.15.0 - requeue: 5s containerSecurityContext: allowPrivilegeEscalation: false @@ -95,3 +87,34 @@ cluster-api-operator: requests: cpu: 100m memory: 150Mi + +velero: + enabled: true + initContainers: + - name: velero-plugin-for-aws + image: velero/velero-plugin-for-aws:v1.11.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /target + name: plugins + - name: velero-plugin-for-microsoft-azure + image: velero/velero-plugin-for-microsoft-azure:v1.11.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /target + name: plugins + - name: velero-plugin-for-gcp + image: velero/velero-plugin-for-gcp:v1.11.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /target + name: plugins + metrics: + enabled: false + upgradeCRDs: false + cleanUpCRDs: false + credentials: + useSecret: false + snapshotsEnabled: false + backupsEnabled: false + deployNodeAgent: false diff --git a/test/objects/management/management.go b/test/objects/management/management.go index c103bc981..03c295250 100644 --- a/test/objects/management/management.go +++ b/test/objects/management/management.go @@ -90,7 +90,7 @@ func WithRelease(v string) Opt { } } -func WithBackup(v v1alpha1.ManagementBackup) Opt { +func WithBackup(v v1alpha1.Backup) Opt { return func(management *v1alpha1.Management) { management.Spec.Backup = v }