From 465c956a157581e952b1bf4305bb0b2069bbe161 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Thu, 16 Apr 2026 09:15:43 +0200 Subject: [PATCH 1/2] [b/r] Add OpenStackBackupConfig controller Add the BackupConfig CRD, API types, controller, RBAC, samples, and envtests for the backup/restore labeling feature. The controller watches CRD instances across operators and labels resources (secrets, configmaps, NADs) with backup.openstack.org labels for backup/restore integration. Supports annotation overrides on individual resources to customize restore ordering or exclude from backup. Custom Issuer labeling is handled by the ControlPlane controller in ca.go, not by the BackupConfig controller. Jira: OSPRH-22912 Jira: OSPRH-22913 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Schuppert --- PROJECT | 9 + api/backup/v1beta1/conditions.go | 36 + api/backup/v1beta1/groupversion_info.go | 36 + .../v1beta1/openstackbackupconfig_types.go | 149 +++ api/backup/v1beta1/zz_generated.deepcopy.go | 178 ++++ ....openstack.org_openstackbackupconfigs.yaml | 269 ++++++ bindata/crds/crds.yaml | 285 ++++++ bindata/rbac/rbac.yaml | 73 ++ cmd/main.go | 26 +- ....openstack.org_openstackbackupconfigs.yaml | 269 ++++++ config/crd/kustomization.yaml | 1 + ...nstack-operator.clusterserviceversion.yaml | 12 + ...ckup_openstackbackupconfig_admin_role.yaml | 27 + ...kup_openstackbackupconfig_editor_role.yaml | 33 + ...kup_openstackbackupconfig_viewer_role.yaml | 29 + config/rbac/kustomization.yaml | 3 + config/rbac/role.yaml | 73 ++ .../backup_v1beta1_openstackbackupconfig.yaml | 29 + config/samples/kustomization.yaml | 1 + go.mod | 2 +- .../openstackbackupconfig_controller.go | 622 ++++++++++++ .../openstackbackupconfig_controller_test.go | 882 ++++++++++++++++++ test/functional/ctlplane/suite_test.go | 25 + 23 files changed, 3064 insertions(+), 5 deletions(-) create mode 100644 api/backup/v1beta1/conditions.go create mode 100644 api/backup/v1beta1/groupversion_info.go create mode 100644 api/backup/v1beta1/openstackbackupconfig_types.go create mode 100644 api/backup/v1beta1/zz_generated.deepcopy.go create mode 100644 api/bases/backup.openstack.org_openstackbackupconfigs.yaml create mode 100644 config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_admin_role.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_editor_role.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_viewer_role.yaml create mode 100644 config/samples/backup_v1beta1_openstackbackupconfig.yaml create mode 100644 internal/controller/backup/openstackbackupconfig_controller.go create mode 100644 test/functional/ctlplane/openstackbackupconfig_controller_test.go diff --git a/PROJECT b/PROJECT index 1f2ad9ab47..8ea2c3253b 100644 --- a/PROJECT +++ b/PROJECT @@ -99,4 +99,13 @@ resources: kind: OpenStack path: github.com/openstack-k8s-operators/openstack-operator/api/operator/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openstack.org + group: backup + kind: OpenStackBackupConfig + path: github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1 + version: v1beta1 version: "3" diff --git a/api/backup/v1beta1/conditions.go b/api/backup/v1beta1/conditions.go new file mode 100644 index 0000000000..87e142965b --- /dev/null +++ b/api/backup/v1beta1/conditions.go @@ -0,0 +1,36 @@ +/* +Copyright 2026. + +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 v1beta1 + +import ( + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" +) + +// Condition types for OpenStackBackupConfig +const ( + // OpenStackBackupConfigSecretsReadyCondition - Secrets labeling status + OpenStackBackupConfigSecretsReadyCondition condition.Type = "SecretsReady" + + // OpenStackBackupConfigConfigMapsReadyCondition - ConfigMaps labeling status + OpenStackBackupConfigConfigMapsReadyCondition condition.Type = "ConfigMapsReady" + + // OpenStackBackupConfigNADsReadyCondition - NetworkAttachmentDefinitions labeling status + OpenStackBackupConfigNADsReadyCondition condition.Type = "NADsReady" + + // OpenStackBackupConfigCRsReadyCondition - CR instances labeling status + OpenStackBackupConfigCRsReadyCondition condition.Type = "CRsReady" +) diff --git a/api/backup/v1beta1/groupversion_info.go b/api/backup/v1beta1/groupversion_info.go new file mode 100644 index 0000000000..ce41c201e0 --- /dev/null +++ b/api/backup/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2026. + +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 v1beta1 contains API Schema definitions for the backup v1beta1 API group. +// +kubebuilder:object:generate=true +// +groupName=backup.openstack.org +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "backup.openstack.org", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/backup/v1beta1/openstackbackupconfig_types.go b/api/backup/v1beta1/openstackbackupconfig_types.go new file mode 100644 index 0000000000..21edb08fbd --- /dev/null +++ b/api/backup/v1beta1/openstackbackupconfig_types.go @@ -0,0 +1,149 @@ +/* +Copyright 2026. + +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 v1beta1 + +import ( + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BackupLabelingPolicy controls whether backup labeling is active for a resource type +// +kubebuilder:validation:Enum=enabled;disabled +type BackupLabelingPolicy string + +const ( + // BackupLabelingEnabled enables backup labeling for the resource type + BackupLabelingEnabled BackupLabelingPolicy = "enabled" + // BackupLabelingDisabled disables backup labeling for the resource type + BackupLabelingDisabled BackupLabelingPolicy = "disabled" +) + +// OpenStackBackupConfigSpec defines the desired state of OpenStackBackupConfig. +type OpenStackBackupConfigSpec struct { + // DefaultRestoreOrder is the restore order assigned to user-provided resources + // +kubebuilder:validation:Optional + // +kubebuilder:default="10" + DefaultRestoreOrder string `json:"defaultRestoreOrder"` + + // Secrets configuration for backup labeling + // +kubebuilder:validation:Optional + // +kubebuilder:default={labeling:enabled} + Secrets ResourceBackupConfig `json:"secrets"` + + // ConfigMaps configuration for backup labeling + // Defaults: Excludes kube-root-ca.crt and openshift-service-ca.crt + // +kubebuilder:validation:Optional + // +kubebuilder:default={labeling:enabled,excludeNames:{"kube-root-ca.crt","openshift-service-ca.crt"}} + ConfigMaps ResourceBackupConfig `json:"configMaps"` + + // NetworkAttachmentDefinitions configuration for backup labeling + // +kubebuilder:validation:Optional + // +kubebuilder:default={labeling:enabled} + NetworkAttachmentDefinitions ResourceBackupConfig `json:"networkAttachmentDefinitions"` + +} + +// ResourceBackupConfig defines backup labeling rules for a resource type +type ResourceBackupConfig struct { + // Labeling controls whether to label this resource type for backup + // +kubebuilder:validation:Optional + Labeling *BackupLabelingPolicy `json:"labeling,omitempty"` + + // RestoreOrder overrides the default restore order for this resource type. + // If empty, the global DefaultRestoreOrder is used. + // +kubebuilder:validation:Optional + RestoreOrder string `json:"restoreOrder,omitempty"` + + // ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + // Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + // +kubebuilder:validation:Optional + ExcludeLabelKeys []string `json:"excludeLabelKeys,omitempty"` + + // ExcludeNames is a list of resource names to exclude from backup labeling + // Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + // +kubebuilder:validation:Optional + ExcludeNames []string `json:"excludeNames,omitempty"` + + // IncludeLabelSelector allows filtering resources by label selector + // Only resources matching this selector will be labeled (in addition to ownerRef check) + // +kubebuilder:validation:Optional + IncludeLabelSelector map[string]string `json:"includeLabelSelector,omitempty"` +} + +// OpenStackBackupConfigStatus defines the observed state of OpenStackBackupConfig. +type OpenStackBackupConfigStatus struct { + // LabeledResources tracks how many resources of each type were labeled + // +kubebuilder:validation:Optional + LabeledResources ResourceCounts `json:"labeledResources,omitempty"` + + // Conditions represents the latest available observations of the resource's current state + // +operator-sdk:csv:customresourcedefinitions:type=status + Conditions condition.Conditions `json:"conditions,omitempty"` +} + +// ResourceCounts tracks labeled resource counts by type +type ResourceCounts struct { + // Secrets is the number of secrets labeled for backup + // +kubebuilder:validation:Optional + Secrets int `json:"secrets"` + + // ConfigMaps is the number of configmaps labeled for backup + // +kubebuilder:validation:Optional + ConfigMaps int `json:"configMaps"` + + // NetworkAttachmentDefinitions is the number of NADs labeled for backup + // +kubebuilder:validation:Optional + NetworkAttachmentDefinitions int `json:"networkAttachmentDefinitions"` + + // CRs is the number of CR instances labeled for backup + // +kubebuilder:validation:Optional + CRs int `json:"crs"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=osbkpcfg;osbackupcfg;osbackupconfig +// +kubebuilder:printcolumn:name="Secrets",type="integer",JSONPath=".status.labeledResources.secrets",description="Labeled Secrets" +// +kubebuilder:printcolumn:name="ConfigMaps",type="integer",JSONPath=".status.labeledResources.configMaps",description="Labeled ConfigMaps" +// +kubebuilder:printcolumn:name="NADs",type="integer",JSONPath=".status.labeledResources.networkAttachmentDefinitions",description="Labeled NADs" +// +kubebuilder:printcolumn:name="CRs",type="integer",JSONPath=".status.labeledResources.crs",description="Labeled CR instances" +// +kubebuilder:metadata:labels=backup.openstack.org/restore=true +// +kubebuilder:metadata:labels=backup.openstack.org/category=controlplane +// +kubebuilder:metadata:labels=backup.openstack.org/restore-order=20 + +// OpenStackBackupConfig is the Schema for the openstackbackupconfigs API. +// It configures automatic backup labeling for user-provided resources (without ownerReferences). +type OpenStackBackupConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenStackBackupConfigSpec `json:"spec,omitempty"` + Status OpenStackBackupConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// OpenStackBackupConfigList contains a list of OpenStackBackupConfig. +type OpenStackBackupConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenStackBackupConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OpenStackBackupConfig{}, &OpenStackBackupConfigList{}) +} diff --git a/api/backup/v1beta1/zz_generated.deepcopy.go b/api/backup/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..4d0c593483 --- /dev/null +++ b/api/backup/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,178 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2022. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackBackupConfig) DeepCopyInto(out *OpenStackBackupConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackBackupConfig. +func (in *OpenStackBackupConfig) DeepCopy() *OpenStackBackupConfig { + if in == nil { + return nil + } + out := new(OpenStackBackupConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackBackupConfig) 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 *OpenStackBackupConfigList) DeepCopyInto(out *OpenStackBackupConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenStackBackupConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackBackupConfigList. +func (in *OpenStackBackupConfigList) DeepCopy() *OpenStackBackupConfigList { + if in == nil { + return nil + } + out := new(OpenStackBackupConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackBackupConfigList) 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 *OpenStackBackupConfigSpec) DeepCopyInto(out *OpenStackBackupConfigSpec) { + *out = *in + in.Secrets.DeepCopyInto(&out.Secrets) + in.ConfigMaps.DeepCopyInto(&out.ConfigMaps) + in.NetworkAttachmentDefinitions.DeepCopyInto(&out.NetworkAttachmentDefinitions) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackBackupConfigSpec. +func (in *OpenStackBackupConfigSpec) DeepCopy() *OpenStackBackupConfigSpec { + if in == nil { + return nil + } + out := new(OpenStackBackupConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackBackupConfigStatus) DeepCopyInto(out *OpenStackBackupConfigStatus) { + *out = *in + out.LabeledResources = in.LabeledResources + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(condition.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackBackupConfigStatus. +func (in *OpenStackBackupConfigStatus) DeepCopy() *OpenStackBackupConfigStatus { + if in == nil { + return nil + } + out := new(OpenStackBackupConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceBackupConfig) DeepCopyInto(out *ResourceBackupConfig) { + *out = *in + if in.Labeling != nil { + in, out := &in.Labeling, &out.Labeling + *out = new(BackupLabelingPolicy) + **out = **in + } + if in.ExcludeLabelKeys != nil { + in, out := &in.ExcludeLabelKeys, &out.ExcludeLabelKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeNames != nil { + in, out := &in.ExcludeNames, &out.ExcludeNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IncludeLabelSelector != nil { + in, out := &in.IncludeLabelSelector, &out.IncludeLabelSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceBackupConfig. +func (in *ResourceBackupConfig) DeepCopy() *ResourceBackupConfig { + if in == nil { + return nil + } + out := new(ResourceBackupConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceCounts) DeepCopyInto(out *ResourceCounts) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceCounts. +func (in *ResourceCounts) DeepCopy() *ResourceCounts { + if in == nil { + return nil + } + out := new(ResourceCounts) + in.DeepCopyInto(out) + return out +} diff --git a/api/bases/backup.openstack.org_openstackbackupconfigs.yaml b/api/bases/backup.openstack.org_openstackbackupconfigs.yaml new file mode 100644 index 0000000000..3a0d5ee1e3 --- /dev/null +++ b/api/bases/backup.openstack.org_openstackbackupconfigs.yaml @@ -0,0 +1,269 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" + name: openstackbackupconfigs.backup.openstack.org +spec: + group: backup.openstack.org + names: + kind: OpenStackBackupConfig + listKind: OpenStackBackupConfigList + plural: openstackbackupconfigs + shortNames: + - osbkpcfg + - osbackupcfg + - osbackupconfig + singular: openstackbackupconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Labeled Secrets + jsonPath: .status.labeledResources.secrets + name: Secrets + type: integer + - description: Labeled ConfigMaps + jsonPath: .status.labeledResources.configMaps + name: ConfigMaps + type: integer + - description: Labeled NADs + jsonPath: .status.labeledResources.networkAttachmentDefinitions + name: NADs + type: integer + - description: Labeled CR instances + jsonPath: .status.labeledResources.crs + name: CRs + type: integer + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + OpenStackBackupConfig is the Schema for the openstackbackupconfigs API. + It configures automatic backup labeling for user-provided resources (without ownerReferences). + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpenStackBackupConfigSpec defines the desired state of OpenStackBackupConfig. + properties: + configMaps: + default: + excludeNames: + - kube-root-ca.crt + - openshift-service-ca.crt + labeling: enabled + description: |- + ConfigMaps configuration for backup labeling + Defaults: Excludes kube-root-ca.crt and openshift-service-ca.crt + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + defaultRestoreOrder: + default: "10" + description: DefaultRestoreOrder is the restore order assigned to + user-provided resources + type: string + networkAttachmentDefinitions: + default: + labeling: enabled + description: NetworkAttachmentDefinitions configuration for backup + labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + secrets: + default: + labeling: enabled + description: Secrets configuration for backup labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + type: object + status: + description: OpenStackBackupConfigStatus defines the observed state of + OpenStackBackupConfig. + properties: + conditions: + description: Conditions represents the latest available observations + of the resource's current state + items: + description: Condition defines an observation of a API resource + operational state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. + type: string + severity: + description: |- + Severity provides a classification of Reason code, so the current situation is immediately + understandable and could act accordingly. + It is meant for situations where Status=False and it should be indicated if it is just + informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue + and no actions to automatically resolve the issue can/should be done). + For conditions where Status=Unknown or Status=True the Severity should be SeverityNone. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + labeledResources: + description: LabeledResources tracks how many resources of each type + were labeled + properties: + configMaps: + description: ConfigMaps is the number of configmaps labeled for + backup + type: integer + crs: + description: CRs is the number of CR instances labeled for backup + type: integer + networkAttachmentDefinitions: + description: NetworkAttachmentDefinitions is the number of NADs + labeled for backup + type: integer + secrets: + description: Secrets is the number of secrets labeled for backup + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/bindata/crds/crds.yaml b/bindata/crds/crds.yaml index 64c83dd53d..38aed21500 100644 --- a/bindata/crds/crds.yaml +++ b/bindata/crds/crds.yaml @@ -1,5 +1,274 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" + name: openstackbackupconfigs.backup.openstack.org +spec: + group: backup.openstack.org + names: + kind: OpenStackBackupConfig + listKind: OpenStackBackupConfigList + plural: openstackbackupconfigs + shortNames: + - osbkpcfg + - osbackupcfg + - osbackupconfig + singular: openstackbackupconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Labeled Secrets + jsonPath: .status.labeledResources.secrets + name: Secrets + type: integer + - description: Labeled ConfigMaps + jsonPath: .status.labeledResources.configMaps + name: ConfigMaps + type: integer + - description: Labeled NADs + jsonPath: .status.labeledResources.networkAttachmentDefinitions + name: NADs + type: integer + - description: Labeled CR instances + jsonPath: .status.labeledResources.crs + name: CRs + type: integer + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + OpenStackBackupConfig is the Schema for the openstackbackupconfigs API. + It configures automatic backup labeling for user-provided resources (without ownerReferences). + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpenStackBackupConfigSpec defines the desired state of OpenStackBackupConfig. + properties: + configMaps: + default: + excludeNames: + - kube-root-ca.crt + - openshift-service-ca.crt + labeling: enabled + description: |- + ConfigMaps configuration for backup labeling + Defaults: Excludes kube-root-ca.crt and openshift-service-ca.crt + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + defaultRestoreOrder: + default: "10" + description: DefaultRestoreOrder is the restore order assigned to + user-provided resources + type: string + networkAttachmentDefinitions: + default: + labeling: enabled + description: NetworkAttachmentDefinitions configuration for backup + labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + secrets: + default: + labeling: enabled + description: Secrets configuration for backup labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + type: object + status: + description: OpenStackBackupConfigStatus defines the observed state of + OpenStackBackupConfig. + properties: + conditions: + description: Conditions represents the latest available observations + of the resource's current state + items: + description: Condition defines an observation of a API resource + operational state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. + type: string + severity: + description: |- + Severity provides a classification of Reason code, so the current situation is immediately + understandable and could act accordingly. + It is meant for situations where Status=False and it should be indicated if it is just + informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue + and no actions to automatically resolve the issue can/should be done). + For conditions where Status=Unknown or Status=True the Severity should be SeverityNone. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + labeledResources: + description: LabeledResources tracks how many resources of each type + were labeled + properties: + configMaps: + description: ConfigMaps is the number of configmaps labeled for + backup + type: integer + crs: + description: CRs is the number of CR instances labeled for backup + type: integer + networkAttachmentDefinitions: + description: NetworkAttachmentDefinitions is the number of NADs + labeled for backup + type: integer + secrets: + description: Secrets is the number of secrets labeled for backup + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 @@ -269,6 +538,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "30" name: openstackcontrolplanes.core.openstack.org spec: group: core.openstack.org @@ -18937,6 +19210,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "60" name: openstackdataplanenodesets.dataplane.openstack.org spec: group: dataplane.openstack.org @@ -20933,6 +21210,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: openstackdataplaneservices.dataplane.openstack.org spec: group: dataplane.openstack.org @@ -21230,6 +21511,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: openstackversions.core.openstack.org spec: group: core.openstack.org diff --git a/bindata/rbac/rbac.yaml b/bindata/rbac/rbac.yaml index 20a1346240..35260f1f09 100644 --- a/bindata/rbac/rbac.yaml +++ b/bindata/rbac/rbac.yaml @@ -118,6 +118,14 @@ rules: - '*' verbs: - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch - apiGroups: - apps resources: @@ -130,6 +138,32 @@ rules: - patch - update - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/finalizers + verbs: + - update +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get + - patch + - update - apiGroups: - barbican.openstack.org resources: @@ -142,6 +176,43 @@ rules: - patch - update - watch +- apiGroups: + - barbican.openstack.org + - baremetal.openstack.org + - cinder.openstack.org + - client.openstack.org + - core.openstack.org + - dataplane.openstack.org + - designate.openstack.org + - glance.openstack.org + - heat.openstack.org + - horizon.openstack.org + - instanceha.openstack.org + - ironic.openstack.org + - keystone.openstack.org + - manila.openstack.org + - mariadb.openstack.org + - memcached.openstack.org + - network.openstack.org + - neutron.openstack.org + - nova.openstack.org + - octavia.openstack.org + - ovn.openstack.org + - placement.openstack.org + - rabbitmq.openstack.org + - redis.openstack.org + - swift.openstack.org + - telemetry.openstack.org + - topology.openstack.org + - watcher.openstack.org + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - baremetal.openstack.org resources: @@ -409,6 +480,8 @@ rules: verbs: - get - list + - patch + - update - watch - apiGroups: - keystone.openstack.org diff --git a/cmd/main.go b/cmd/main.go index 6df1bb0aed..9fff7cea97 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,6 +27,7 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -46,6 +47,9 @@ import ( webhookcorev1beta1 "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/core/v1beta1" webhookdataplanev1beta1 "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/dataplane/v1beta1" + backupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" + backupcontroller "github.com/openstack-k8s-operators/openstack-operator/internal/controller/backup" + // +kubebuilder:scaffold:imports certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" @@ -73,10 +77,6 @@ import ( novav1 "github.com/openstack-k8s-operators/nova-operator/api/nova/v1beta1" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1" - clientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1" - corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" - dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" - "github.com/openstack-k8s-operators/openstack-operator/internal/openstack" ovnv1 "github.com/openstack-k8s-operators/ovn-operator/api/v1beta1" placementv1 "github.com/openstack-k8s-operators/placement-operator/api/v1beta1" swiftv1 "github.com/openstack-k8s-operators/swift-operator/api/v1beta1" @@ -86,6 +86,11 @@ import ( rabbitmqclusterv2 "github.com/rabbitmq/cluster-operator/v2/api/v1beta1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client/config" + + clientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1" + corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" + dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" + "github.com/openstack-k8s-operators/openstack-operator/internal/openstack" ) var ( @@ -95,6 +100,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(dataplanev1.AddToScheme(scheme)) utilruntime.Must(keystonev1.AddToScheme(scheme)) @@ -130,6 +136,7 @@ func init() { utilruntime.Must(operatorv1beta1.AddToScheme(scheme)) utilruntime.Must(topologyv1.AddToScheme(scheme)) utilruntime.Must(watcherv1.AddToScheme(scheme)) + utilruntime.Must(backupv1beta1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -360,6 +367,17 @@ func main() { os.Exit(1) } + // Setup OpenStackBackupConfig controller + backupReconciler := &backupcontroller.OpenStackBackupConfigReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Kclient: kclient, + } + if err := backupReconciler.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenStackBackupConfig") + os.Exit(1) + } + corecontroller.SetupVersionDefaults() // Defaults for service operators diff --git a/config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml b/config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml new file mode 100644 index 0000000000..3a0d5ee1e3 --- /dev/null +++ b/config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml @@ -0,0 +1,269 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" + name: openstackbackupconfigs.backup.openstack.org +spec: + group: backup.openstack.org + names: + kind: OpenStackBackupConfig + listKind: OpenStackBackupConfigList + plural: openstackbackupconfigs + shortNames: + - osbkpcfg + - osbackupcfg + - osbackupconfig + singular: openstackbackupconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Labeled Secrets + jsonPath: .status.labeledResources.secrets + name: Secrets + type: integer + - description: Labeled ConfigMaps + jsonPath: .status.labeledResources.configMaps + name: ConfigMaps + type: integer + - description: Labeled NADs + jsonPath: .status.labeledResources.networkAttachmentDefinitions + name: NADs + type: integer + - description: Labeled CR instances + jsonPath: .status.labeledResources.crs + name: CRs + type: integer + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + OpenStackBackupConfig is the Schema for the openstackbackupconfigs API. + It configures automatic backup labeling for user-provided resources (without ownerReferences). + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OpenStackBackupConfigSpec defines the desired state of OpenStackBackupConfig. + properties: + configMaps: + default: + excludeNames: + - kube-root-ca.crt + - openshift-service-ca.crt + labeling: enabled + description: |- + ConfigMaps configuration for backup labeling + Defaults: Excludes kube-root-ca.crt and openshift-service-ca.crt + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + defaultRestoreOrder: + default: "10" + description: DefaultRestoreOrder is the restore order assigned to + user-provided resources + type: string + networkAttachmentDefinitions: + default: + labeling: enabled + description: NetworkAttachmentDefinitions configuration for backup + labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + secrets: + default: + labeling: enabled + description: Secrets configuration for backup labeling + properties: + excludeLabelKeys: + description: |- + ExcludeLabelKeys is a list of label keys - resources with any of these labels are excluded + Example: ["service-cert", "osdp-service"] excludes service-cert and dataplane service secrets + items: + type: string + type: array + excludeNames: + description: |- + ExcludeNames is a list of resource names to exclude from backup labeling + Example: ["kube-root-ca.crt", "openshift-service-ca.crt"] for system ConfigMaps + items: + type: string + type: array + includeLabelSelector: + additionalProperties: + type: string + description: |- + IncludeLabelSelector allows filtering resources by label selector + Only resources matching this selector will be labeled (in addition to ownerRef check) + type: object + labeling: + description: Labeling controls whether to label this resource + type for backup + enum: + - enabled + - disabled + type: string + restoreOrder: + description: |- + RestoreOrder overrides the default restore order for this resource type. + If empty, the global DefaultRestoreOrder is used. + type: string + type: object + type: object + status: + description: OpenStackBackupConfigStatus defines the observed state of + OpenStackBackupConfig. + properties: + conditions: + description: Conditions represents the latest available observations + of the resource's current state + items: + description: Condition defines an observation of a API resource + operational state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. + type: string + severity: + description: |- + Severity provides a classification of Reason code, so the current situation is immediately + understandable and could act accordingly. + It is meant for situations where Status=False and it should be indicated if it is just + informational, warning (next reconciliation might fix it) or an error (e.g. DB create issue + and no actions to automatically resolve the issue can/should be done). + For conditions where Status=Unknown or Status=True the Severity should be SeverityNone. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + labeledResources: + description: LabeledResources tracks how many resources of each type + were labeled + properties: + configMaps: + description: ConfigMaps is the number of configmaps labeled for + backup + type: integer + crs: + description: CRs is the number of CR instances labeled for backup + type: integer + networkAttachmentDefinitions: + description: NetworkAttachmentDefinitions is the number of NADs + labeled for backup + type: integer + secrets: + description: Secrets is the number of secrets labeled for backup + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 40534231a5..e7cceda24d 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/dataplane.openstack.org_openstackdataplaneservices.yaml - bases/dataplane.openstack.org_openstackdataplanedeployments.yaml #- bases/operator.openstack.org_openstacks.yaml +- bases/backup.openstack.org_openstackbackupconfigs.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml index ca98482c6a..19cc73f6fa 100644 --- a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml @@ -23,6 +23,18 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: |- + OpenStackBackupConfig is the Schema for the openstackbackupconfigs API. + It configures automatic backup labeling for user-provided resources (without ownerReferences). + displayName: Open Stack Backup Config + kind: OpenStackBackupConfig + name: openstackbackupconfigs.backup.openstack.org + statusDescriptors: + - description: Conditions represents the latest available observations of the + resource's current state + displayName: Conditions + path: conditions + version: v1beta1 - description: OpenStackClient is the Schema for the openstackclients API displayName: OpenStack Client kind: OpenStackClient diff --git a/config/rbac/backup_openstackbackupconfig_admin_role.yaml b/config/rbac/backup_openstackbackupconfig_admin_role.yaml new file mode 100644 index 0000000000..76127c4ab2 --- /dev/null +++ b/config/rbac/backup_openstackbackupconfig_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project openstack-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over backup.openstack.org. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: backup-openstackbackupconfig-admin-role +rules: +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - '*' +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get diff --git a/config/rbac/backup_openstackbackupconfig_editor_role.yaml b/config/rbac/backup_openstackbackupconfig_editor_role.yaml new file mode 100644 index 0000000000..e875d32291 --- /dev/null +++ b/config/rbac/backup_openstackbackupconfig_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project openstack-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the backup.openstack.org. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: backup-openstackbackupconfig-editor-role +rules: +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get diff --git a/config/rbac/backup_openstackbackupconfig_viewer_role.yaml b/config/rbac/backup_openstackbackupconfig_viewer_role.yaml new file mode 100644 index 0000000000..0988092d76 --- /dev/null +++ b/config/rbac/backup_openstackbackupconfig_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project openstack-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to backup.openstack.org resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: backup-openstackbackupconfig-viewer-role +rules: +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - get + - list + - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index fc721b4061..5908081ae3 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -32,6 +32,9 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the openstack-operator itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +#- backup_openstackbackupconfig_admin_role.yaml +#- backup_openstackbackupconfig_editor_role.yaml +#- backup_openstackbackupconfig_viewer_role.yaml #- operator_openstack_admin_role.yaml #- operator_openstack_editor_role.yaml #- operator_openstack_viewer_role.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 92ee8f1708..1c68d8089b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -69,6 +69,14 @@ rules: - '*' verbs: - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch - apiGroups: - apps resources: @@ -81,6 +89,32 @@ rules: - patch - update - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/finalizers + verbs: + - update +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get + - patch + - update - apiGroups: - barbican.openstack.org resources: @@ -93,6 +127,43 @@ rules: - patch - update - watch +- apiGroups: + - barbican.openstack.org + - baremetal.openstack.org + - cinder.openstack.org + - client.openstack.org + - core.openstack.org + - dataplane.openstack.org + - designate.openstack.org + - glance.openstack.org + - heat.openstack.org + - horizon.openstack.org + - instanceha.openstack.org + - ironic.openstack.org + - keystone.openstack.org + - manila.openstack.org + - mariadb.openstack.org + - memcached.openstack.org + - network.openstack.org + - neutron.openstack.org + - nova.openstack.org + - octavia.openstack.org + - ovn.openstack.org + - placement.openstack.org + - rabbitmq.openstack.org + - redis.openstack.org + - swift.openstack.org + - telemetry.openstack.org + - topology.openstack.org + - watcher.openstack.org + resources: + - '*' + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - baremetal.openstack.org resources: @@ -360,6 +431,8 @@ rules: verbs: - get - list + - patch + - update - watch - apiGroups: - keystone.openstack.org diff --git a/config/samples/backup_v1beta1_openstackbackupconfig.yaml b/config/samples/backup_v1beta1_openstackbackupconfig.yaml new file mode 100644 index 0000000000..191898d6f6 --- /dev/null +++ b/config/samples/backup_v1beta1_openstackbackupconfig.yaml @@ -0,0 +1,29 @@ +apiVersion: backup.openstack.org/v1beta1 +kind: OpenStackBackupConfig +metadata: + labels: + app.kubernetes.io/name: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: openstackbackupconfig-sample +spec: + # Default restore order for user-provided resources (foundation resources) + defaultRestoreOrder: "10" + + # Secrets configuration - defaults shown for reference + # These defaults are applied automatically, no need to specify unless overriding + # secrets: + # enabled: true + # excludeLabelKeys: + # - service-cert # Service-cert managed secrets (auto-recreated) + # - osdp-service # Dataplane service certs (recreated on deployment) + + # ConfigMaps configuration - defaults shown for reference + # configMaps: + # enabled: true + # excludeNames: + # - kube-root-ca.crt # Kubernetes system CA (auto-created) + # - openshift-service-ca.crt # OpenShift service CA (auto-created) + + # NetworkAttachmentDefinitions configuration - defaults shown for reference + # networkAttachmentDefinitions: + # enabled: true diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 138d15b6b0..687ef6853e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -10,4 +10,5 @@ resources: #- dataplane_v1beta1_openstackdataplaneservice_empty.yaml #- dataplane_v1beta1_openstackdataplanedeployment_empty.yaml - operator_v1beta1_openstack.yaml +- backup_v1beta1_openstackbackupconfig.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index aa5cf3f659..ea1e1974c1 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.14 + k8s.io/apiextensions-apiserver v0.33.2 k8s.io/apimachinery v0.31.14 k8s.io/client-go v0.31.14 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d @@ -139,7 +140,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.33.2 // indirect k8s.io/apiserver v0.33.2 // indirect k8s.io/component-base v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/internal/controller/backup/openstackbackupconfig_controller.go b/internal/controller/backup/openstackbackupconfig_controller.go new file mode 100644 index 0000000000..d4f3936736 --- /dev/null +++ b/internal/controller/backup/openstackbackupconfig_controller.go @@ -0,0 +1,622 @@ +/* +Copyright 2026. + +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 contains the controller for OpenStackBackupConfig resources. +package backup + +import ( + "context" + stderrors "errors" + "fmt" + + "github.com/go-logr/logr" + backupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" + + "github.com/openstack-k8s-operators/lib-common/modules/common/backup" + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + + "k8s.io/client-go/kubernetes" + + k8s_networkingv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + 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" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// OpenStackBackupConfigReconciler reconciles a OpenStackBackupConfig object +type OpenStackBackupConfigReconciler struct { + client.Client + Kclient kubernetes.Interface + Scheme *runtime.Scheme + CRDLabelCache backup.CRDLabelCache +} + +// gvkFromCRD extracts the GVK from a CRD, preferring the storage version. +func gvkFromCRD(crd *apiextensionsv1.CustomResourceDefinition) schema.GroupVersionKind { + var version string + for _, v := range crd.Spec.Versions { + if v.Storage { + version = v.Name + break + } + if v.Served && version == "" { + version = v.Name + } + } + return schema.GroupVersionKind{ + Group: crd.Spec.Group, + Version: version, + Kind: crd.Spec.Names.Kind, + } +} + +// getGVKFromCRD looks up a CRD by name and returns its GVK +func (r *OpenStackBackupConfigReconciler) getGVKFromCRD(ctx context.Context, crdName string) (schema.GroupVersionKind, error) { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := r.Get(ctx, types.NamespacedName{Name: crdName}, crd); err != nil { + return schema.GroupVersionKind{}, err + } + return gvkFromCRD(crd), nil +} + +// shouldLabelResource checks if a resource should be labeled based on ownerReferences and config +func shouldLabelResource(obj client.Object, config backupv1beta1.ResourceBackupConfig) bool { + // Check if labeling is enabled (nil treated as enabled for backward compatibility) + if config.Labeling != nil && *config.Labeling == backupv1beta1.BackupLabelingDisabled { + return false + } + + // Only label resources without ownerReferences (user-provided) + if len(obj.GetOwnerReferences()) > 0 { + return false + } + + labels := obj.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + + // Check exclude label keys + for _, excludeKey := range config.ExcludeLabelKeys { + if _, exists := labels[excludeKey]; exists { + return false + } + } + + // Check exclude names + for _, excludeName := range config.ExcludeNames { + if obj.GetName() == excludeName { + return false + } + } + + // Check include label selector (if specified, resource must match) + if len(config.IncludeLabelSelector) > 0 { + for key, value := range config.IncludeLabelSelector { + if labels[key] != value { + return false + } + } + } + + return true +} + +// getRestoreOrder returns the per-type restore order if set, otherwise the global default +func getRestoreOrder(config backupv1beta1.ResourceBackupConfig, defaultOrder string) string { + if config.RestoreOrder != "" { + return config.RestoreOrder + } + return defaultOrder +} + +// hasBackupAnnotations returns true if the resource has any backup-related annotations +func hasBackupAnnotations(obj client.Object) bool { + annotations := obj.GetAnnotations() + if annotations == nil { + return false + } + for _, key := range backup.LabelKeys() { + if _, has := annotations[key]; has { + return true + } + } + return false +} + +// labelResourceItems labels a list of resources with backup labels. +// Resources with ownerReferences are skipped (operator-managed). +// Resources that already have a restore label (set by operators at creation time) +// are skipped to preserve operator-set values. +// Annotation overrides on a resource bypass both checks, and changed annotation +// values are always re-synced to labels via EnsureBackupLabels. +func (r *OpenStackBackupConfigReconciler) labelResourceItems( + ctx context.Context, + log logr.Logger, + items []client.Object, + config backupv1beta1.ResourceBackupConfig, + defaultLabels map[string]string, +) (int, error) { + var errs []error + count := 0 + for _, obj := range items { + // Annotation overrides bypass all filtering + if !hasBackupAnnotations(obj) { + // Skip resources that already have a restore label (set by operators or previous reconcile) + if restoreVal, hasRestoreLabel := obj.GetLabels()[backup.BackupRestoreLabel]; hasRestoreLabel { + if restoreVal == "true" { + count++ + } + continue + } + if !shouldLabelResource(obj, config) { + continue + } + } + + if _, err := backup.EnsureBackupLabels(ctx, r.Client, obj, defaultLabels); err != nil { + log.Error(err, "Failed to label resource", "name", obj.GetName()) + errs = append(errs, fmt.Errorf("%s: %w", obj.GetName(), err)) + continue + } + count++ + } + return count, stderrors.Join(errs...) +} + +// labelSecrets labels secrets in the target namespace +func (r *OpenStackBackupConfigReconciler) labelSecrets(ctx context.Context, log logr.Logger, instance *backupv1beta1.OpenStackBackupConfig) (int, error) { + list := &corev1.SecretList{} + if err := r.List(ctx, list, client.InNamespace(instance.Namespace)); err != nil { + return 0, err + } + items := make([]client.Object, len(list.Items)) + for i := range list.Items { + items[i] = &list.Items[i] + } + defaultLabels := backup.GetRestoreLabels(getRestoreOrder(instance.Spec.Secrets, instance.Spec.DefaultRestoreOrder), "") + return r.labelResourceItems(ctx, log, items, instance.Spec.Secrets, defaultLabels) +} + +// labelConfigMaps labels configmaps in the target namespace +func (r *OpenStackBackupConfigReconciler) labelConfigMaps(ctx context.Context, log logr.Logger, instance *backupv1beta1.OpenStackBackupConfig) (int, error) { + list := &corev1.ConfigMapList{} + if err := r.List(ctx, list, client.InNamespace(instance.Namespace)); err != nil { + return 0, err + } + items := make([]client.Object, len(list.Items)) + for i := range list.Items { + items[i] = &list.Items[i] + } + defaultLabels := backup.GetRestoreLabels(getRestoreOrder(instance.Spec.ConfigMaps, instance.Spec.DefaultRestoreOrder), "") + return r.labelResourceItems(ctx, log, items, instance.Spec.ConfigMaps, defaultLabels) +} + +// labelNetworkAttachmentDefinitions labels NADs in the target namespace +func (r *OpenStackBackupConfigReconciler) labelNetworkAttachmentDefinitions(ctx context.Context, log logr.Logger, instance *backupv1beta1.OpenStackBackupConfig) (int, error) { + list := &k8s_networkingv1.NetworkAttachmentDefinitionList{} + if err := r.List(ctx, list, client.InNamespace(instance.Namespace)); err != nil { + return 0, err + } + items := make([]client.Object, len(list.Items)) + for i := range list.Items { + items[i] = &list.Items[i] + } + defaultLabels := backup.GetRestoreLabels(getRestoreOrder(instance.Spec.NetworkAttachmentDefinitions, instance.Spec.DefaultRestoreOrder), "") + return r.labelResourceItems(ctx, log, items, instance.Spec.NetworkAttachmentDefinitions, defaultLabels) +} + +// labelCRInstances labels CR instances based on CRD backup-restore labels +// This labels CRs like OpenStackControlPlane, OpenStackVersion, NetConfig, etc. +// based on their CRD's backup/restore configuration. +func (r *OpenStackBackupConfigReconciler) labelCRInstances(ctx context.Context, log logr.Logger, instance *backupv1beta1.OpenStackBackupConfig) (int, error) { + // Fallback: build cache if SetupWithManager failed to populate it. + // Note: watches for CRD instance types are only registered at setup time, + // so CR instance changes won't trigger reconciliation in this case. + if len(r.CRDLabelCache) == 0 { + cache, err := backup.BuildCRDLabelCache(ctx, r.Client) + if err != nil { + return 0, fmt.Errorf("failed to build CRD label cache: %w", err) + } + r.CRDLabelCache = cache + log.Info("Built CRD label cache", "entries", len(cache)) + } + + var errs []error + count := 0 + + // Iterate through all CRDs that have backup-restore enabled + for crdName, backupConfig := range r.CRDLabelCache { + if !backupConfig.Enabled { + continue + } + + // Look up the CRD to get proper group, version, and kind + gvk, err := r.getGVKFromCRD(ctx, crdName) + if err != nil { + log.Error(err, "Failed to get CRD", "name", crdName) + errs = append(errs, fmt.Errorf("CRD %s: %w", crdName, err)) + continue + } + + // Create a metadata-only list for this CRD type + list := &metav1.PartialObjectMetadataList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + }) + + if err := r.List(ctx, list, client.InNamespace(instance.Namespace)); err != nil { + log.Error(err, "Failed to list CR instances", "crd", crdName) + errs = append(errs, fmt.Errorf("list %s: %w", crdName, err)) + continue + } + + // Label each CR instance + defaultLabels := backup.GetRestoreLabels(backupConfig.RestoreOrder, backupConfig.Category) + for i := range list.Items { + obj := &list.Items[i] + + if _, err := backup.EnsureBackupLabels(ctx, r.Client, obj, defaultLabels); err != nil { + log.Error(err, "Failed to label CR instance", "kind", gvk.Kind, "name", obj.GetName()) + errs = append(errs, fmt.Errorf("%s/%s: %w", gvk.Kind, obj.GetName(), err)) + continue + } + count++ + } + } + + return count, stderrors.Join(errs...) +} + +// +kubebuilder:rbac:groups=backup.openstack.org,resources=openstackbackupconfigs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=backup.openstack.org,resources=openstackbackupconfigs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=backup.openstack.org,resources=openstackbackupconfigs/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=k8s.cni.cncf.io,resources=network-attachment-definitions,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch +// RBAC for labeling CR instances across all openstack.org API groups. +// Kubernetes RBAC does not support wildcard group patterns (*.openstack.org), +// so each group must be listed explicitly. +// +kubebuilder:rbac:groups=barbican.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=baremetal.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=cinder.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=client.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=core.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=dataplane.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=designate.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=glance.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=heat.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=horizon.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=instanceha.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=ironic.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=keystone.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=manila.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=mariadb.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=memcached.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=network.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=neutron.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=nova.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=octavia.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=ovn.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=placement.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=rabbitmq.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=redis.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=swift.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=telemetry.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=topology.openstack.org,resources=*,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=watcher.openstack.org,resources=*,verbs=get;list;watch;update;patch + +// Reconcile labels user-provided resources (without ownerReferences) for backup/restore. +func (r *OpenStackBackupConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) { + log := ctrl.LoggerFrom(ctx) + + // Fetch the OpenStackBackupConfig instance + instance := &backupv1beta1.OpenStackBackupConfig{} + err := r.Get(ctx, req.NamespacedName, instance) + if err != nil { + if errors.IsNotFound(err) { + log.Info("OpenStackBackupConfig resource not found, ignoring") + return ctrl.Result{}, nil + } + log.Error(err, "Failed to get OpenStackBackupConfig") + return ctrl.Result{}, err + } + + h, err := helper.NewHelper(instance, r.Client, r.Kclient, r.Scheme, log) + if err != nil { + log.Error(err, "Failed to create helper") + return ctrl.Result{}, err + } + + // + // initialize Conditions + // + if instance.Status.Conditions == nil { + instance.Status.Conditions = condition.Conditions{} + } + + cl := condition.CreateList( + condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage), + condition.UnknownCondition(backupv1beta1.OpenStackBackupConfigSecretsReadyCondition, condition.InitReason, condition.InitReason), + condition.UnknownCondition(backupv1beta1.OpenStackBackupConfigConfigMapsReadyCondition, condition.InitReason, condition.InitReason), + condition.UnknownCondition(backupv1beta1.OpenStackBackupConfigNADsReadyCondition, condition.InitReason, condition.InitReason), + condition.UnknownCondition(backupv1beta1.OpenStackBackupConfigCRsReadyCondition, condition.InitReason, condition.InitReason), + ) + instance.Status.Conditions.Init(&cl) + + // Save a copy of the conditions for LastTransitionTime restore + savedConditions := instance.Status.Conditions.DeepCopy() + + // Always patch the instance status when exiting this function + defer func() { + // update the Ready condition based on the sub conditions + if instance.Status.Conditions.AllSubConditionIsTrue() { + instance.Status.Conditions.MarkTrue( + condition.ReadyCondition, condition.ReadyMessage) + } else { + // something is not ready so reset the Ready condition + instance.Status.Conditions.MarkUnknown( + condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage) + // and recalculate it based on the state of the rest of the conditions + instance.Status.Conditions.Set( + instance.Status.Conditions.Mirror(condition.ReadyCondition)) + } + + condition.RestoreLastTransitionTimes(&instance.Status.Conditions, savedConditions) + if err := h.PatchInstance(ctx, instance); err != nil { + _err = err + return + } + }() + + // Label resources in target namespace — process all types and collect errors + var reconcileErrs []error + + secretCount, err := r.labelSecrets(ctx, log, instance) + if err != nil { + log.Error(err, "Failed to label secrets") + instance.Status.Conditions.Set(condition.FalseCondition( + backupv1beta1.OpenStackBackupConfigSecretsReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + "Failed to label secrets: %v", err)) + reconcileErrs = append(reconcileErrs, err) + } else { + instance.Status.Conditions.Set(condition.TrueCondition( + backupv1beta1.OpenStackBackupConfigSecretsReadyCondition, + "%d secrets have backup labels", secretCount)) + } + + configMapCount, err := r.labelConfigMaps(ctx, log, instance) + if err != nil { + log.Error(err, "Failed to label configmaps") + instance.Status.Conditions.Set(condition.FalseCondition( + backupv1beta1.OpenStackBackupConfigConfigMapsReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + "Failed to label configmaps: %v", err)) + reconcileErrs = append(reconcileErrs, err) + } else { + instance.Status.Conditions.Set(condition.TrueCondition( + backupv1beta1.OpenStackBackupConfigConfigMapsReadyCondition, + "%d configmaps have backup labels", configMapCount)) + } + + nadCount, err := r.labelNetworkAttachmentDefinitions(ctx, log, instance) + if err != nil { + log.Error(err, "Failed to label network-attachment-definitions") + instance.Status.Conditions.Set(condition.FalseCondition( + backupv1beta1.OpenStackBackupConfigNADsReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + "Failed to label network-attachment-definitions: %v", err)) + reconcileErrs = append(reconcileErrs, err) + } else { + instance.Status.Conditions.Set(condition.TrueCondition( + backupv1beta1.OpenStackBackupConfigNADsReadyCondition, + "%d NADs have backup labels", nadCount)) + } + + // Label CR instances based on CRD backup-restore labels + crCount, err := r.labelCRInstances(ctx, log, instance) + if err != nil { + log.Error(err, "Failed to label CR instances") + instance.Status.Conditions.Set(condition.FalseCondition( + backupv1beta1.OpenStackBackupConfigCRsReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + "Failed to label CR instances: %v", err)) + reconcileErrs = append(reconcileErrs, err) + } else { + instance.Status.Conditions.Set(condition.TrueCondition( + backupv1beta1.OpenStackBackupConfigCRsReadyCondition, + "%d CRs have backup labels", crCount)) + } + + // Update status counts + instance.Status.LabeledResources.Secrets = secretCount + instance.Status.LabeledResources.ConfigMaps = configMapCount + instance.Status.LabeledResources.NetworkAttachmentDefinitions = nadCount + instance.Status.LabeledResources.CRs = crCount + if len(reconcileErrs) > 0 { + return ctrl.Result{}, stderrors.Join(reconcileErrs...) + } + + log.Info("Successfully labeled resources", "secrets", secretCount, "configmaps", configMapCount, "nads", nadCount, "crs", crCount) + return ctrl.Result{}, nil +} + +// backupLabelKeys are the label keys managed by this controller. +var backupLabelKeys = []string{ + backup.BackupLabel, + backup.BackupRestoreLabel, + backup.BackupRestoreOrderLabel, + backup.BackupCategoryLabel, +} + +// needsBackupLabeling returns true if a resource does not yet have backup labels. +func needsBackupLabeling(labels map[string]string) bool { + _, hasBackup := labels[backup.BackupLabel] + _, hasRestore := labels[backup.BackupRestoreLabel] + return !hasBackup && !hasRestore +} + +// backupAnnotationsChanged returns true if backup-related annotations differ between old and new. +func backupAnnotationsChanged(oldAnnotations, newAnnotations map[string]string) bool { + for _, key := range backup.LabelKeys() { + if oldAnnotations[key] != newAnnotations[key] { + return true + } + } + return false +} + +// backupLabelsRemoved returns true if any backup labels were present on old but removed from new. +func backupLabelsRemoved(oldLabels, newLabels map[string]string) bool { + for _, key := range backupLabelKeys { + if _, hadIt := oldLabels[key]; hadIt { + if _, hasIt := newLabels[key]; !hasIt { + return true + } + } + } + return false +} + +// backupResourcePredicate filters events to only reconcile when backup labeling is needed. +// Triggers on: +// - Create: resource has no backup labels yet +// - Update: backup annotations changed OR backup labels were removed +// +// Ignores deletes and generic events entirely. +var backupResourcePredicate = predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return needsBackupLabeling(e.Object.GetLabels()) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return backupAnnotationsChanged(e.ObjectOld.GetAnnotations(), e.ObjectNew.GetAnnotations()) || + backupLabelsRemoved(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) + }, + DeleteFunc: func(_ event.DeleteEvent) bool { + return false + }, + GenericFunc: func(_ event.GenericEvent) bool { + return false + }, +} + +// findBackupConfigForSrc maps a resource back to the BackupConfig that should process it +func (r *OpenStackBackupConfigReconciler) findBackupConfigForSrc(ctx context.Context, obj client.Object) []reconcile.Request { + configList := &backupv1beta1.OpenStackBackupConfigList{} + if err := r.List(ctx, configList, client.InNamespace(obj.GetNamespace())); err != nil { + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, len(configList.Items)) + for i, config := range configList.Items { + requests[i] = reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: config.GetName(), + Namespace: config.GetNamespace(), + }, + } + } + return requests +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenStackBackupConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + Log := ctrl.Log.WithName("backup").WithName("setup") + + bldr := ctrl.NewControllerManagedBy(mgr). + For(&backupv1beta1.OpenStackBackupConfig{}). + Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)). + Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(r.findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)). + Watches(&k8s_networkingv1.NetworkAttachmentDefinition{}, handler.EnqueueRequestsFromMapFunc(r.findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)) + + // Build CRD label cache and add watches for CRD instance types. + // Uses the API reader since the manager's cache is not started yet. + apiReader := mgr.GetAPIReader() + cache, err := buildCRDLabelCacheFromReader(apiReader) + if err != nil { + Log.Error(err, "Failed to build CRD label cache, CR instances will not be watched") + } else { + r.CRDLabelCache = cache + for crdName := range cache { + gvk, err := getGVKFromCRDUsingReader(apiReader, crdName) + if err != nil { + Log.Error(err, "Failed to get GVK for CRD, skipping watch", "crd", crdName) + continue + } + obj := &metav1.PartialObjectMetadata{} + obj.SetGroupVersionKind(gvk) + bldr = bldr.Watches(obj, handler.EnqueueRequestsFromMapFunc(r.findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)) + Log.Info("Added watch for CRD instances", "crd", crdName, "gvk", gvk) + } + } + + return bldr.Named("openstackbackupconfig").Complete(r) +} + +// buildCRDLabelCacheFromReader builds the CRD label cache using a client.Reader. +// Used at setup time when the manager's cache is not started. +func buildCRDLabelCacheFromReader(reader client.Reader) (backup.CRDLabelCache, error) { + cache := make(backup.CRDLabelCache) + + crdList := &apiextensionsv1.CustomResourceDefinitionList{} + if err := reader.List(context.Background(), crdList); err != nil { + return nil, err + } + + for _, crd := range crdList.Items { + labels := crd.GetLabels() + if labels == nil || labels[backup.BackupRestoreLabel] != "true" { + continue + } + cache[crd.Name] = backup.Config{ + Enabled: true, + RestoreOrder: labels[backup.BackupRestoreOrderLabel], + Category: labels[backup.BackupCategoryLabel], + } + } + + return cache, nil +} + +// getGVKFromCRDUsingReader looks up a CRD by name using a reader and returns its GVK. +// Used at setup time when the manager's cache is not started. +func getGVKFromCRDUsingReader(reader client.Reader, crdName string) (schema.GroupVersionKind, error) { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := reader.Get(context.Background(), types.NamespacedName{Name: crdName}, crd); err != nil { + return schema.GroupVersionKind{}, err + } + return gvkFromCRD(crd), nil +} diff --git a/test/functional/ctlplane/openstackbackupconfig_controller_test.go b/test/functional/ctlplane/openstackbackupconfig_controller_test.go new file mode 100644 index 0000000000..d9f9493a3e --- /dev/null +++ b/test/functional/ctlplane/openstackbackupconfig_controller_test.go @@ -0,0 +1,882 @@ +/* +Copyright 2026. + +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 functional_test + +import ( + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + + k8s_corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + commonbackup "github.com/openstack-k8s-operators/lib-common/modules/common/backup" + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + //revive:disable-next-line:dot-imports + . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + backupv1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" + corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" +) + +func GetOpenStackBackupConfig(name types.NamespacedName) *backupv1.OpenStackBackupConfig { + instance := &backupv1.OpenStackBackupConfig{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + +func OpenStackBackupConfigConditionGetter(name types.NamespacedName) condition.Conditions { + instance := GetOpenStackBackupConfig(name) + return instance.Status.Conditions +} + +func backupLabelingPtr(p backupv1.BackupLabelingPolicy) *backupv1.BackupLabelingPolicy { + return &p +} + +func CreateBackupConfig(name types.NamespacedName) *backupv1.OpenStackBackupConfig { + backupConfig := &backupv1.OpenStackBackupConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + Spec: backupv1.OpenStackBackupConfigSpec{ + // Kubebuilder defaults are only applied via webhooks. + // Set them explicitly for envtest. + DefaultRestoreOrder: "10", + Secrets: backupv1.ResourceBackupConfig{ + Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled), + }, + ConfigMaps: backupv1.ResourceBackupConfig{ + Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled), + ExcludeNames: []string{"kube-root-ca.crt", "openshift-service-ca.crt"}, + }, + NetworkAttachmentDefinitions: backupv1.ResourceBackupConfig{ + Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled), + }, + }, + } + Expect(k8sClient.Create(ctx, backupConfig)).Should(Succeed()) + return backupConfig +} + +var _ = Describe("OpenStackBackupConfig controller", func() { + var backupConfigName types.NamespacedName + + When("A OpenStackBackupConfig is created", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-config", + Namespace: namespace, + } + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should exist and be retrievable", func() { + backupConfig := &backupv1.OpenStackBackupConfig{} + Expect(k8sClient.Get(ctx, backupConfigName, backupConfig)).Should(Succeed()) + Expect(backupConfig.Namespace).To(Equal(namespace)) + }) + + It("Should initialize all conditions", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigSecretsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigConfigMapsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigNADsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigCRsReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + + It("Should become Ready when all sub-conditions are True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + }) + + When("A secret without ownerRef exists in the namespace", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-secrets", + Namespace: namespace, + } + + // Create a user-provided secret (no ownerRef) + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-secret", + Namespace: namespace, + }, + Data: map[string][]byte{ + "key": []byte("value"), + }, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should label the secret for backup", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "user-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true")) + }, timeout, interval).Should(Succeed()) + }) + + It("Should set SecretsReady condition to True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigSecretsReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + + It("Should update status counts", func() { + Eventually(func(g Gomega) { + backupConfig := GetOpenStackBackupConfig(backupConfigName) + g.Expect(backupConfig.Status.LabeledResources.Secrets).To(BeNumerically(">=", 1)) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret already has a restore label", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-existing-restore-label", + Namespace: namespace, + } + + // Create a secret with restore=false (as set by controlplane controller for leaf certs) + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-secret", + Namespace: namespace, + Labels: map[string]string{ + commonbackup.BackupRestoreLabel: "false", + }, + }, + Data: map[string][]byte{ + "tls.crt": []byte("cert"), + }, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should not overwrite the existing restore label", func() { + // Wait for reconciliation to complete + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + + // Verify the restore label was preserved as "false" + secret := &k8s_corev1.Secret{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "cert-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("false")) + // Should NOT have backup or restore-order labels + Expect(labels[commonbackup.BackupLabel]).To(BeEmpty()) + }) + }) + + When("A configmap without ownerRef exists in the namespace", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-configmaps", + Namespace: namespace, + } + + // Create a user-provided configmap (no ownerRef) + cm := &k8s_corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-configmap", + Namespace: namespace, + }, + Data: map[string]string{ + "key": "value", + }, + } + Expect(k8sClient.Create(ctx, cm)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, cm) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should label the configmap for backup", func() { + Eventually(func(g Gomega) { + cm := &k8s_corev1.ConfigMap{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "user-configmap", Namespace: namespace, + }, cm)).Should(Succeed()) + + labels := cm.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true")) + }, timeout, interval).Should(Succeed()) + }) + + It("Should set ConfigMapsReady condition to True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigConfigMapsReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + }) + + When("An excluded configmap exists in the namespace", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-exclude-cm", + Namespace: namespace, + } + + // Create a system configmap (excluded by default) + cm := &k8s_corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-root-ca.crt", + Namespace: namespace, + }, + Data: map[string]string{ + "ca.crt": "system-ca", + }, + } + Expect(k8sClient.Create(ctx, cm)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, cm) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should not label the excluded configmap", func() { + // Wait for reconciliation + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + + cm := &k8s_corev1.ConfigMap{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "kube-root-ca.crt", Namespace: namespace, + }, cm)).Should(Succeed()) + + labels := cm.GetLabels() + Expect(labels[commonbackup.BackupRestoreLabel]).To(BeEmpty()) + }) + }) + + When("OpenStackBackupConfig reconciles with CRs in namespace", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-with-crs", + Namespace: namespace, + } + + // Create OpenStackControlPlane (CRD has backup-restore labels) + controlPlaneName := types.NamespacedName{ + Name: "test-controlplane", + Namespace: namespace, + } + spec := GetDefaultOpenStackControlPlaneSpec() + CreateOpenStackControlPlane(controlPlaneName, spec) + DeferCleanup(th.DeleteInstance, GetOpenStackControlPlane(controlPlaneName)) + + // Create OpenStackBackupConfig after CRs exist + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should label CR instances with backup labels", func() { + controlPlaneName := types.NamespacedName{ + Name: "test-controlplane", + Namespace: namespace, + } + + Eventually(func(g Gomega) { + controlPlane := &corev1.OpenStackControlPlane{} + g.Expect(k8sClient.Get(ctx, controlPlaneName, controlPlane)).Should(Succeed()) + + labels := controlPlane.GetLabels() + g.Expect(labels).NotTo(BeNil(), "ControlPlane should have labels") + g.Expect(labels[commonbackup.BackupRestoreLabel]).To( + Equal("true"), + "ControlPlane should have backup label", + ) + g.Expect(labels[commonbackup.BackupRestoreOrderLabel]).To( + Equal("30"), + "ControlPlane should have restore-order label from CRD", + ) + }, timeout, interval).Should(Succeed()) + }) + + It("Should set CRsReady condition to True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigCRsReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + + It("Should set ReadyCondition to True when all resources are labeled", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + }) + + When("A CA cert secret already labeled for restore exists", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-ca-cert-secret", + Namespace: namespace, + } + + // Create a CA cert secret with restore labels (as set by controlplane controller) + caSecret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rootca-internal", + Namespace: namespace, + Labels: map[string]string{ + commonbackup.BackupRestoreLabel: "true", + commonbackup.BackupRestoreOrderLabel: "10", + commonbackup.BackupLabel: "true", + }, + }, + Data: map[string][]byte{ + "tls.crt": []byte("ca-cert"), + "tls.key": []byte("ca-key"), + }, + } + Expect(k8sClient.Create(ctx, caSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, caSecret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should preserve the existing restore labels set by the controlplane controller", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + + secret := &k8s_corev1.Secret{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "rootca-internal", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "CA cert secret restore label must be preserved") + Expect(labels[commonbackup.BackupRestoreOrderLabel]).To(Equal("10"), + "CA cert secret restore order must be preserved") + }) + }) + + When("A leaf cert secret already labeled restore=false exists", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-leaf-cert-secret", + Namespace: namespace, + } + + // Create a leaf cert secret with restore=false (as set by controlplane controller) + leafSecret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-keystone-internal-svc", + Namespace: namespace, + Labels: map[string]string{ + commonbackup.BackupRestoreLabel: "false", + }, + }, + Data: map[string][]byte{ + "tls.crt": []byte("leaf-cert"), + "tls.key": []byte("leaf-key"), + }, + } + Expect(k8sClient.Create(ctx, leafSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, leafSecret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should not overwrite the restore=false label set by the controlplane controller", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + + secret := &k8s_corev1.Secret{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "cert-keystone-internal-svc", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("false"), + "Leaf cert secret should keep restore=false") + Expect(labels).NotTo(HaveKey(commonbackup.BackupRestoreOrderLabel), + "restore-order should not be set when restore=false") + }) + }) + + When("A user-provided secret without a restore label exists", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-user-cert-secret", + Namespace: namespace, + } + + // Create a user-provided secret (no restore label) + userSecret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-custom-cert-tls", + Namespace: namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("user-cert"), + "tls.key": []byte("user-key"), + }, + } + Expect(k8sClient.Create(ctx, userSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, userSecret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should label the user-provided secret for restore", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "my-custom-cert-tls", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "User-provided secret should be labeled for restore") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret has a restore annotation override set to false", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-annotation-false", + Namespace: namespace, + } + + // Create a secret with annotation override restore=false + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "override-skip-secret", + Namespace: namespace, + Annotations: map[string]string{ + commonbackup.BackupRestoreLabel: "false", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should sync the annotation to label restore=false", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "override-skip-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("false"), + "Annotation override restore=false should be synced to label") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret has a restore annotation override set to true", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-annotation-true", + Namespace: namespace, + } + + // Create a secret that would normally be excluded (has ownerRef) + // but has annotation override restore=true + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "override-restore-secret", + Namespace: namespace, + Annotations: map[string]string{ + commonbackup.BackupRestoreLabel: "true", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "core.openstack.org/v1beta1", + Kind: "OpenStackControlPlane", + Name: "controlplane", + UID: "fake-uid", + }, + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should sync the annotation to label restore=true with default restore-order even with ownerRef", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "override-restore-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "Annotation override restore=true should be synced to label, overriding ownerRef exclusion") + g.Expect(labels[commonbackup.BackupRestoreOrderLabel]).NotTo(BeEmpty(), + "restore-order should be set to default when restore=true via annotation") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret has a restore-order annotation override", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-annotation-order", + Namespace: namespace, + } + + // Create a secret with annotation override for restore order + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-order-secret", + Namespace: namespace, + Annotations: map[string]string{ + commonbackup.BackupRestoreLabel: "true", + commonbackup.BackupRestoreOrderLabel: "05", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should sync both restore and restore-order annotations to labels", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "custom-order-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true")) + g.Expect(labels[commonbackup.BackupRestoreOrderLabel]).To(Equal("05"), + "Annotation override restore-order=05 should be synced to label") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret with restore=false label has annotation override restore=true", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-cert-override", + Namespace: namespace, + } + + // Create a secret with restore=false label but annotation override to force restore + leafSecret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert-keystone-override-svc", + Namespace: namespace, + Labels: map[string]string{ + commonbackup.BackupRestoreLabel: "false", + }, + Annotations: map[string]string{ + commonbackup.BackupRestoreLabel: "true", + }, + }, + Data: map[string][]byte{ + "tls.crt": []byte("leaf-cert"), + "tls.key": []byte("leaf-key"), + }, + } + Expect(k8sClient.Create(ctx, leafSecret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, leafSecret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should honor annotation override and set restore=true with default restore-order", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "cert-keystone-override-svc", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "Annotation override should take precedence over operator-set label") + g.Expect(labels[commonbackup.BackupRestoreOrderLabel]).NotTo(BeEmpty(), + "restore-order should be set to default when restore=true via annotation") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret has only a restore-order annotation override", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-annotation-order-only", + Namespace: namespace, + } + + // Create a secret with only restore-order annotation (no restore annotation) + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "order-only-secret", + Namespace: namespace, + Annotations: map[string]string{ + commonbackup.BackupRestoreOrderLabel: "05", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should imply restore=true and use the specified restore-order", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "order-only-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "restore-order annotation should imply restore=true") + g.Expect(labels[commonbackup.BackupRestoreOrderLabel]).To(Equal("05"), + "restore-order should use the annotation value") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A secret has annotation override with mixed case value", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-annotation-case", + Namespace: namespace, + } + + // Create a secret with mixed-case annotation value + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-case-secret", + Namespace: namespace, + Annotations: map[string]string{ + commonbackup.BackupRestoreLabel: "True", + }, + }, + Data: map[string][]byte{"key": []byte("value")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should normalize the annotation value to lowercase in the label", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "mixed-case-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + + labels := secret.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true"), + "Mixed case 'True' annotation should be normalized to 'true' label") + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Multiple resource types exist in the namespace", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-multi", + Namespace: namespace, + } + + // Create a user secret + secret := &k8s_corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-test-secret", + Namespace: namespace, + }, + Data: map[string][]byte{"key": []byte("val")}, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, secret) + + // Create a user configmap + cm := &k8s_corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-test-cm", + Namespace: namespace, + }, + Data: map[string]string{"key": "val"}, + } + Expect(k8sClient.Create(ctx, cm)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, cm) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should set all sub-conditions to True and ReadyCondition to True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigSecretsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigConfigMapsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigNADsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigCRsReadyCondition, + k8s_corev1.ConditionTrue, + ) + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + + It("Should label all resource types", func() { + Eventually(func(g Gomega) { + secret := &k8s_corev1.Secret{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "multi-test-secret", Namespace: namespace, + }, secret)).Should(Succeed()) + g.Expect(secret.GetLabels()[commonbackup.BackupRestoreLabel]).To(Equal("true")) + + cm := &k8s_corev1.ConfigMap{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "multi-test-cm", Namespace: namespace, + }, cm)).Should(Succeed()) + g.Expect(cm.GetLabels()[commonbackup.BackupRestoreLabel]).To(Equal("true")) + }, timeout, interval).Should(Succeed()) + }) + }) +}) diff --git a/test/functional/ctlplane/suite_test.go b/test/functional/ctlplane/suite_test.go index bfaab96716..01b6375825 100644 --- a/test/functional/ctlplane/suite_test.go +++ b/test/functional/ctlplane/suite_test.go @@ -15,6 +15,7 @@ import ( . "github.com/onsi/gomega" //revive:disable:dot-imports "go.uber.org/zap/zapcore" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" @@ -29,6 +30,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + k8s_networkingv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" routev1 "github.com/openshift/api/route/v1" rabbitmqv2 "github.com/rabbitmq/cluster-operator/v2/api/v1beta1" @@ -48,6 +50,7 @@ import ( neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" novav1 "github.com/openstack-k8s-operators/nova-operator/api/nova/v1beta1" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" + backupv1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" openstackclientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1" corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" dataplanev1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" @@ -58,6 +61,7 @@ import ( telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1" watcherv1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + backup_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/backup" client_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/client" core_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/core" @@ -187,6 +191,9 @@ var _ = BeforeSuite(func() { watcherCRDs, err := test.GetCRDDirFromModule( "github.com/openstack-k8s-operators/watcher-operator/api", gomod, "bases") Expect(err).ShouldNot(HaveOccurred()) + nadCRDs, err := test.GetCRDDirFromModule( + "github.com/k8snetworkplumbingwg/network-attachment-definition-client", gomod, "artifacts/networks-crd.yaml") + Expect(err).ShouldNot(HaveOccurred()) By("bootstrapping test environment") testEnv = &envtest.Environment{ @@ -220,6 +227,9 @@ var _ = BeforeSuite(func() { ControlPlaneStartTimeout: 2 * time.Minute, ControlPlaneStopTimeout: 2 * time.Minute, CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + nadCRDs, + }, MaxTime: 5 * time.Minute, }, ControlPlane: envtest.ControlPlane{ @@ -312,6 +322,12 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = watcherv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = backupv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = apiextensionsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = k8s_networkingv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -389,6 +405,15 @@ var _ = BeforeSuite(func() { }).SetupWithManager(ctx, k8sManager) Expect(err).ToNot(HaveOccurred()) + // Setup OpenStackBackupConfig controller + // CRD label cache is built lazily on first reconcile + err = (&backup_ctrl.OpenStackBackupConfigReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) From 1fad42fc32a85670f5c64689535342dc67b69ee5 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Thu, 16 Apr 2026 09:17:04 +0200 Subject: [PATCH 2/2] [b/r] Add backup/restore labels to ControlPlane controller Wire the BackupConfig reconciliation into the ControlPlane controller with proper condition handling (OpenStackControlPlaneBackupConfigReady). Add backup/restore labels to CA cert secrets via SecretTemplate, and restore=false labels to internal service cert requests. Add the ReconcileBackupConfig call, secret watch with annotation change predicate, and RBAC for openstackbackupconfigs. Set BackupConfig spec defaults in the CreateOrPatch mutate function. Label custom Issuers for backup/restore in addIssuerLabelAnnotation after removeIssuerLabel so the MatchingLabels query only uses CA selector labels. Remove getCertSecretBackupLabels wrapper, call backup.GetCertSecretBackupLabels directly. Return error from GetCertSecretBackupLabels for non-NotFound errors. Rename GetConfig parameter from gvk to crdName. Jira: OSPRH-22912 Jira: OSPRH-22913 --- ....openstack.org_openstackcontrolplanes.yaml | 4 + .../core.openstack.org_openstackversions.yaml | 4 + ...nstack.org_openstackdataplanenodesets.yaml | 4 + ...nstack.org_openstackdataplaneservices.yaml | 4 + api/core/v1beta1/conditions.go | 12 +++ .../v1beta1/openstackcontrolplane_types.go | 4 + api/core/v1beta1/openstackversion_types.go | 3 + .../openstackdataplanenodeset_types.go | 3 + .../openstackdataplaneservice_types.go | 3 + api/go.mod | 2 +- api/go.sum | 4 +- ....openstack.org_openstackcontrolplanes.yaml | 4 + .../core.openstack.org_openstackversions.yaml | 4 + ...nstack.org_openstackdataplanenodesets.yaml | 4 + ...nstack.org_openstackdataplaneservices.yaml | 4 + go.mod | 2 +- go.sum | 4 +- .../core/openstackcontrolplane_controller.go | 45 ++++++++++ internal/openstack/backup.go | 86 +++++++++++++++++++ internal/openstack/ca.go | 14 +-- internal/openstack/common.go | 12 +-- internal/openstack/galera.go | 10 ++- internal/openstack/memcached.go | 4 +- internal/openstack/neutron.go | 2 +- internal/openstack/nova.go | 2 +- internal/openstack/octavia.go | 2 +- internal/openstack/ovn.go | 8 +- internal/openstack/rabbitmq.go | 2 +- internal/openstack/redis.go | 1 + test/functional/ctlplane/base_test.go | 9 ++ .../openstackoperator_controller_test.go | 17 ++++ .../common/assert-sample-deployment.yaml | 4 + .../03-assert-deploy-custom-cacert.yaml | 4 + .../01-assert-collapsed-cell.yaml | 4 + .../01-assert-galera-3replicas.yaml | 4 + ...01-assert-infrastructure-ready-paused.yaml | 4 + .../00-assert-deploy-openstack.yaml | 4 + .../03-assert-new-certs.yaml | 4 + .../01-assert-deploy-openstack.yaml | 4 + .../09-assert-deploy-openstack.yaml | 4 + .../03-assert-deploy-openstack.yaml | 4 + 41 files changed, 293 insertions(+), 30 deletions(-) create mode 100644 internal/openstack/backup.go diff --git a/api/bases/core.openstack.org_openstackcontrolplanes.yaml b/api/bases/core.openstack.org_openstackcontrolplanes.yaml index d806844107..81a216d318 100644 --- a/api/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/api/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "30" name: openstackcontrolplanes.core.openstack.org spec: group: core.openstack.org diff --git a/api/bases/core.openstack.org_openstackversions.yaml b/api/bases/core.openstack.org_openstackversions.yaml index 1ae078e398..beb521d207 100644 --- a/api/bases/core.openstack.org_openstackversions.yaml +++ b/api/bases/core.openstack.org_openstackversions.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: openstackversions.core.openstack.org spec: group: core.openstack.org diff --git a/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml b/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml index 5eead87632..53be12829a 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "60" name: openstackdataplanenodesets.dataplane.openstack.org spec: group: dataplane.openstack.org diff --git a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index d594951dd7..917bae442f 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: openstackdataplaneservices.dataplane.openstack.org spec: group: dataplane.openstack.org diff --git a/api/core/v1beta1/conditions.go b/api/core/v1beta1/conditions.go index 0c67ba534d..a53968c7e7 100644 --- a/api/core/v1beta1/conditions.go +++ b/api/core/v1beta1/conditions.go @@ -166,6 +166,9 @@ const ( // Infrastructure includes: CAs, DNSMasq, RabbitMQ, Galera (MariaDB), Memcached, and OVN databases // This condition is set to True when deployment-stage annotation is "infrastructure-only" and all infrastructure is ready OpenStackControlPlaneInfrastructureReadyCondition condition.Type = "OpenStackControlPlaneInfrastructureReady" + + // OpenStackControlPlaneBackupConfigReadyCondition Status=True condition which indicates if OpenStackBackupConfig is reconciled + OpenStackControlPlaneBackupConfigReadyCondition condition.Type = "OpenStackControlPlaneBackupConfigReady" ) // Common Messages used by API objects. @@ -501,6 +504,15 @@ const ( // OpenStackControlPlaneOpenStackVersionInitializationReadyErrorMessage OpenStackControlPlaneOpenStackVersionInitializationReadyErrorMessage = "OpenStackControlPlane OpenStackVersion initialization error occured %s" + // OpenStackControlPlaneBackupConfigReadyInitMessage + OpenStackControlPlaneBackupConfigReadyInitMessage = "OpenStackControlPlane BackupConfig not started" + + // OpenStackControlPlaneBackupConfigReadyMessage + OpenStackControlPlaneBackupConfigReadyMessage = "OpenStackControlPlane BackupConfig ready" + + // OpenStackControlPlaneBackupConfigReadyErrorMessage + OpenStackControlPlaneBackupConfigReadyErrorMessage = "OpenStackControlPlane BackupConfig error occured %s" + // OpenStackControlPlaneWatcherReadyInitMessage OpenStackControlPlaneWatcherReadyInitMessage = "OpenStackControlPlane Watcher not started" diff --git a/api/core/v1beta1/openstackcontrolplane_types.go b/api/core/v1beta1/openstackcontrolplane_types.go index f49e6b47e8..1c4721e5a7 100644 --- a/api/core/v1beta1/openstackcontrolplane_types.go +++ b/api/core/v1beta1/openstackcontrolplane_types.go @@ -1118,6 +1118,9 @@ type TLSCAStatus struct { // +kubebuilder:resource:shortName=osctlplane;osctlplanes;oscp;oscps // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status" // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message" +// +kubebuilder:metadata:labels=backup.openstack.org/restore=true +// +kubebuilder:metadata:labels=backup.openstack.org/category=controlplane +// +kubebuilder:metadata:labels=backup.openstack.org/restore-order=30 // OpenStackControlPlane is the Schema for the openstackcontrolplanes API type OpenStackControlPlane struct { @@ -1192,6 +1195,7 @@ func (instance *OpenStackControlPlane) InitConditions() { condition.UnknownCondition(OpenStackControlPlaneOpenStackVersionInitializationReadyCondition, condition.InitReason, OpenStackControlPlaneOpenStackVersionInitializationReadyInitMessage), condition.UnknownCondition(OpenStackControlPlaneWatcherReadyCondition, condition.InitReason, OpenStackControlPlaneWatcherReadyInitMessage), condition.UnknownCondition(OpenStackControlPlaneInfrastructureReadyCondition, condition.InitReason, OpenStackControlPlaneInfrastructureReadyInitMessage), + condition.UnknownCondition(OpenStackControlPlaneBackupConfigReadyCondition, condition.InitReason, OpenStackControlPlaneBackupConfigReadyInitMessage), // Also add the overall status condition as Unknown condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage), diff --git a/api/core/v1beta1/openstackversion_types.go b/api/core/v1beta1/openstackversion_types.go index c01747a4bf..9bc685abbe 100644 --- a/api/core/v1beta1/openstackversion_types.go +++ b/api/core/v1beta1/openstackversion_types.go @@ -218,6 +218,9 @@ type OpenStackVersionStatus struct { // +kubebuilder:printcolumn:name="Target Version",type=string,JSONPath=`.spec.targetVersion` // +kubebuilder:printcolumn:name="Available Version",type=string,JSONPath=`.status.availableVersion` // +kubebuilder:printcolumn:name="Deployed Version",type=string,JSONPath=`.status.deployedVersion` +// +kubebuilder:metadata:labels=backup.openstack.org/restore=true +// +kubebuilder:metadata:labels=backup.openstack.org/category=controlplane +// +kubebuilder:metadata:labels=backup.openstack.org/restore-order=20 // OpenStackVersion defines the Schema for the openstackversionupdates API type OpenStackVersion struct { diff --git a/api/dataplane/v1beta1/openstackdataplanenodeset_types.go b/api/dataplane/v1beta1/openstackdataplanenodeset_types.go index 4b81bff8e7..521dc5dda1 100644 --- a/api/dataplane/v1beta1/openstackdataplanenodeset_types.go +++ b/api/dataplane/v1beta1/openstackdataplanenodeset_types.go @@ -94,6 +94,9 @@ type OpenStackDataPlaneNodeSetSpec struct { // +kubebuilder:resource:shortName=osdpns;osdpnodeset;osdpnodesets // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status" // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message" +// +kubebuilder:metadata:labels=backup.openstack.org/restore=true +// +kubebuilder:metadata:labels=backup.openstack.org/category=dataplane +// +kubebuilder:metadata:labels=backup.openstack.org/restore-order=60 // OpenStackDataPlaneNodeSet is the Schema for the openstackdataplanenodesets API // OpenStackDataPlaneNodeSet name must be a valid RFC1123 as it is used in labels diff --git a/api/dataplane/v1beta1/openstackdataplaneservice_types.go b/api/dataplane/v1beta1/openstackdataplaneservice_types.go index b1205a9ba2..d613f4fab6 100644 --- a/api/dataplane/v1beta1/openstackdataplaneservice_types.go +++ b/api/dataplane/v1beta1/openstackdataplaneservice_types.go @@ -130,6 +130,9 @@ type OpenStackDataPlaneServiceStatus struct { // +kubebuilder:subresource:status // +kubebuilder:resource:shortName=osdps;osdpservice;osdpservices // +operator-sdk:csv:customresourcedefinitions:displayName="OpenStack Data Plane Service" +// +kubebuilder:metadata:labels=backup.openstack.org/restore=true +// +kubebuilder:metadata:labels=backup.openstack.org/category=dataplane +// +kubebuilder:metadata:labels=backup.openstack.org/restore-order=40 // OpenStackDataPlaneService defines the Schema for the openstackdataplaneservices API. // OpenStackDataPlaneService name must be a valid RFC1123 as it is used in labels diff --git a/api/go.mod b/api/go.mod index 44ba6f6138..ccd2375493 100644 --- a/api/go.mod +++ b/api/go.mod @@ -80,7 +80,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openshift/api v3.9.0+incompatible // indirect - github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d // indirect + github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/api/go.sum b/api/go.sum index f001e40e66..dcc67a1b85 100644 --- a/api/go.sum +++ b/api/go.sum @@ -134,8 +134,8 @@ github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260414133946 github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260414133946-c146da0da0d6/go.mod h1:x67aZ50eUt/qS175aNzi8OMZ4ihtrhuo789E9GR+TYk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d h1:T0U7XbP2JOJ05lEn7FhI3l05qPMP9UZJMuoURNhPsQ8= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 h1:jN3Kvt+RYUTaL9EXeeeIqRXVjqeNF74SuLTDXmi4X2Y= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260417092244-81c71b39e981 h1:X3/Gc+i0ZxaROExrpLXonz9EPhftlubFnOK4aSkRLvo= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:3loLaPUDQyvbPekylZd9OCLF+EXH2klRI9IeeQhuMcs= github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260415214016-523bbc811363 h1:Z8oqhNixI/vOJd9G25g/+rK2bJPVupBeninrMG9wTts= diff --git a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml index d806844107..81a216d318 100644 --- a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "30" name: openstackcontrolplanes.core.openstack.org spec: group: core.openstack.org diff --git a/config/crd/bases/core.openstack.org_openstackversions.yaml b/config/crd/bases/core.openstack.org_openstackversions.yaml index 1ae078e398..beb521d207 100644 --- a/config/crd/bases/core.openstack.org_openstackversions.yaml +++ b/config/crd/bases/core.openstack.org_openstackversions.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: openstackversions.core.openstack.org spec: group: core.openstack.org diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml index 5eead87632..53be12829a 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "60" name: openstackdataplanenodesets.dataplane.openstack.org spec: group: dataplane.openstack.org diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index d594951dd7..917bae442f 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -4,6 +4,10 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: openstackdataplaneservices.dataplane.openstack.org spec: group: dataplane.openstack.org diff --git a/go.mod b/go.mod index ea1e1974c1..786f092147 100644 --- a/go.mod +++ b/go.mod @@ -96,7 +96,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d // indirect + github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/go.sum b/go.sum index 2809a15e7d..28cbf731f0 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,8 @@ github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260 github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:GzD7Jc5o98ptJ97DSjhC0CQ6OiTP0PB/2qJqxYGcOH8= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d h1:T0U7XbP2JOJ05lEn7FhI3l05qPMP9UZJMuoURNhPsQ8= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260331122750-ecff41ebb61d/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 h1:jN3Kvt+RYUTaL9EXeeeIqRXVjqeNF74SuLTDXmi4X2Y= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260417092244-81c71b39e981 h1:X3/Gc+i0ZxaROExrpLXonz9EPhftlubFnOK4aSkRLvo= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:3loLaPUDQyvbPekylZd9OCLF+EXH2klRI9IeeQhuMcs= github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260417092244-81c71b39e981 h1:KAQ8T+Ri3JWgsyK1D6QybScMh6fpkYUUA+0ntnOiAl4= diff --git a/internal/controller/core/openstackcontrolplane_controller.go b/internal/controller/core/openstackcontrolplane_controller.go index 8a96dff22b..f9a5a55f70 100644 --- a/internal/controller/core/openstackcontrolplane_controller.go +++ b/internal/controller/core/openstackcontrolplane_controller.go @@ -35,6 +35,7 @@ import ( redisv1 "github.com/openstack-k8s-operators/infra-operator/apis/redis/v1beta1" ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/common/backup" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/webhook" @@ -91,6 +92,7 @@ func (r *OpenStackControlPlaneReconciler) GetLogger(ctx context.Context) logr.Lo // +kubebuilder:rbac:groups=core.openstack.org,resources=openstackcontrolplanes/status,verbs=get;update;patch // +kubebuilder:rbac:groups=core.openstack.org,resources=openstackcontrolplanes/finalizers,verbs=update;patch // +kubebuilder:rbac:groups=core.openstack.org,resources=openstackversions,verbs=get;list;create +// +kubebuilder:rbac:groups=backup.openstack.org,resources=openstackbackupconfigs,verbs=get;list;create;update;patch // +kubebuilder:rbac:groups=ironic.openstack.org,resources=ironics,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=client.openstack.org,resources=openstackclients,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=horizon.openstack.org,resources=horizons,verbs=get;list;watch;create;update;patch;delete @@ -283,6 +285,19 @@ func (r *OpenStackControlPlaneReconciler) Reconcile(ctx context.Context, req ctr instance.Status.Conditions.MarkTrue(corev1beta1.OpenStackControlPlaneOpenStackVersionInitializationReadyCondition, corev1beta1.OpenStackControlPlaneOpenStackVersionInitializationReadyMessage) + // Automatically create OpenStackBackupConfig CR for this controlplane + _, _, err = openstack.ReconcileBackupConfig(ctx, instance, helper) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + corev1beta1.OpenStackControlPlaneBackupConfigReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + corev1beta1.OpenStackControlPlaneBackupConfigReadyErrorMessage, + err.Error())) + return ctrl.Result{}, err + } + instance.Status.Conditions.MarkTrue(corev1beta1.OpenStackControlPlaneBackupConfigReadyCondition, corev1beta1.OpenStackControlPlaneBackupConfigReadyMessage) + if instance.Status.DeployedVersion == nil || version.Spec.TargetVersion == *instance.Status.DeployedVersion { //revive:disable:indent-error-flow // green field deployment or no minor update in progress ctrlResult, err := r.reconcileNormal(ctx, instance, version, helper) @@ -870,6 +885,16 @@ func (r *OpenStackControlPlaneReconciler) SetupWithManager( handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + // Watch cert secrets for backup annotation changes. When a user adds + // annotation overrides (e.g., backup.openstack.org/restore=true) to a + // cert secret, the controller reconciles so EnsureCert can read the + // annotation and update the Certificate CR's SecretTemplate labels, + // preventing cert-manager from overwriting the user's override. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.findControlPlaneForSrc), + builder.WithPredicates(backup.AnnotationChangedPredicate(openstack.ServiceCertSelector)), + ). Complete(r) } @@ -907,6 +932,26 @@ func (r *OpenStackControlPlaneReconciler) findObjectsForSrc(ctx context.Context, return requests } +// findControlPlaneForSrc maps a source object to the OpenStackControlPlane +// instances in the same namespace for reconciliation. +func (r *OpenStackControlPlaneReconciler) findControlPlaneForSrc(ctx context.Context, src client.Object) []reconcile.Request { + crList := &corev1beta1.OpenStackControlPlaneList{} + if err := r.List(ctx, crList, client.InNamespace(src.GetNamespace())); err != nil { + return nil + } + + requests := make([]reconcile.Request, 0, len(crList.Items)) + for _, item := range crList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: item.GetName(), + Namespace: item.GetNamespace(), + }, + }) + } + return requests +} + // needsServiceLevelMigration checks if any service-level rabbitMqClusterName fields need migration func (r *OpenStackControlPlaneReconciler) needsServiceLevelMigration(instance *corev1beta1.OpenStackControlPlane) bool { // Helper function to check if a service needs migration diff --git a/internal/openstack/backup.go b/internal/openstack/backup.go new file mode 100644 index 0000000000..332326d67b --- /dev/null +++ b/internal/openstack/backup.go @@ -0,0 +1,86 @@ +/* +Copyright 2026 Red Hat + +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 openstack + +import ( + "context" + "fmt" + + backupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" + corev1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" + + "github.com/openstack-k8s-operators/lib-common/modules/common/backup" + helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ReconcileBackupConfig - reconciles OpenStackBackupConfig CR +// Automatically creates an OpenStackBackupConfig CR when OpenStackControlPlane is created +// Similar pattern to ReconcileVersion +func ReconcileBackupConfig(ctx context.Context, instance *corev1beta1.OpenStackControlPlane, helper *helper.Helper) (ctrl.Result, *backupv1beta1.OpenStackBackupConfig, error) { + backupConfig := &backupv1beta1.OpenStackBackupConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: instance.Name, + Namespace: instance.Namespace, + }, + } + + Log := GetLogger(ctx) + + defaultLabeling := backupv1beta1.BackupLabelingEnabled + + op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), backupConfig, func() error { + // Note: We do NOT set ownerReference here. OpenStackBackupConfig is a configuration + // resource that users may customize. It should persist even if the ControlPlane is + // deleted, and should be backed up/restored with user customizations intact. + + // Set spec defaults only on create. CRD schema defaults only apply when + // fields are absent from the request, but Go serializes zero-value structs + // as empty objects which bypasses CRD defaulting. On update we must not + // override user customizations (e.g. clearing ExcludeNames to []). + if backupConfig.CreationTimestamp.IsZero() { + if backupConfig.Spec.DefaultRestoreOrder == "" { + backupConfig.Spec.DefaultRestoreOrder = backup.RestoreOrder10 + } + if backupConfig.Spec.Secrets.Labeling == nil { + backupConfig.Spec.Secrets.Labeling = &defaultLabeling + } + if backupConfig.Spec.ConfigMaps.Labeling == nil { + backupConfig.Spec.ConfigMaps.Labeling = &defaultLabeling + } + if len(backupConfig.Spec.ConfigMaps.ExcludeNames) == 0 { + backupConfig.Spec.ConfigMaps.ExcludeNames = []string{"kube-root-ca.crt", "openshift-service-ca.crt"} + } + if backupConfig.Spec.NetworkAttachmentDefinitions.Labeling == nil { + backupConfig.Spec.NetworkAttachmentDefinitions.Labeling = &defaultLabeling + } + } + return nil + }) + + if err != nil { + return ctrl.Result{}, nil, err + } + if op != controllerutil.OperationResultNone { + Log.Info(fmt.Sprintf("OpenStackBackupConfig %s - %s", backupConfig.Name, op)) + } + + return ctrl.Result{}, backupConfig, nil +} diff --git a/internal/openstack/ca.go b/internal/openstack/ca.go index f70598fa16..8791622455 100644 --- a/internal/openstack/ca.go +++ b/internal/openstack/ca.go @@ -17,6 +17,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" certmgrmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" "github.com/openstack-k8s-operators/lib-common/modules/certmanager" + "github.com/openstack-k8s-operators/lib-common/modules/common/backup" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" @@ -654,9 +655,11 @@ func createRootCACertAndIssuer( Duration: caCfg.Duration, RenewBefore: caCfg.RenewBefore, SecretTemplate: &certmgrv1.CertificateSecretTemplate{ - Labels: map[string]string{ - caCertSelector: "", - }, + Labels: util.MergeMaps( + map[string]string{caCertSelector: ""}, + backup.GetBackupLabels(backup.CategoryControlPlane), + backup.GetRestoreLabels(backup.RestoreOrder10, backup.CategoryControlPlane), + ), }, }) cert := certmanager.NewCertificate(caCertReq, 5) @@ -904,8 +907,9 @@ func addIssuerLabelAnnotation( caCertSecretName = issuer.Spec.CA.SecretName beforeIssuer := issuer.DeepCopyObject().(client.Object) - // merge labels - issuer.Labels = util.MergeMaps(issuer.Labels, labels) + // merge labels (CA selector + backup/restore labels for custom issuers) + issuer.Labels = util.MergeMaps(issuer.Labels, labels, + backup.GetRestoreLabels(backup.RestoreOrder20, backup.CategoryControlPlane)) // merge annotations issuer.Annotations = util.MergeMaps(issuer.Annotations, annotations) diff --git a/internal/openstack/common.go b/internal/openstack/common.go index 07d0f6ecd6..3b9e914bee 100644 --- a/internal/openstack/common.go +++ b/internal/openstack/common.go @@ -64,8 +64,8 @@ const ( // overrides ooAppSelector = "osctlplane-service" - // serviceCertSelector selector passed to cert-manager to set on the service cert secret - serviceCertSelector = "service-cert" + // ServiceCertSelector selector passed to cert-manager to set on the service cert secret + ServiceCertSelector = "service-cert" // caCertSelector selector passed to cert-manager to set on the ca cert secret caCertSelector = "ca-cert" @@ -331,7 +331,7 @@ func EnsureEndpointConfig( }, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), + Labels: util.MergeMaps(ed.Labels, map[string]string{ServiceCertSelector: ""}), Usages: nil, } @@ -381,7 +381,7 @@ func EnsureEndpointConfig( }, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), + Labels: util.MergeMaps(ed.Labels, map[string]string{ServiceCertSelector: ""}), Usages: nil, } @@ -638,7 +638,7 @@ func (ed *EndpointDetail) CreateRoute( Hostnames: []string{*ed.Hostname}, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), + Labels: util.MergeMaps(ed.Labels, map[string]string{ServiceCertSelector: ""}), Usages: nil, } if instance.Spec.TLS.Ingress.Cert.Duration != nil { @@ -945,7 +945,7 @@ func DeleteCertsAndRoutes( // Delete certs by service and route-name for _, cert := range certs.Items { - if _, ok := cert.Labels[serviceCertSelector]; ok && strings.Contains(cert.Name, route.Name) { + if _, ok := cert.Labels[ServiceCertSelector]; ok && strings.Contains(cert.Name, route.Name) { if object.CheckOwnerRefExist(instance.GetUID(), cert.OwnerReferences) { log.Info("Deleting certificate", ":", cert.Name) err := DeleteCertificate(ctx, helper, instance.Namespace, cert.Name) diff --git a/internal/openstack/galera.go b/internal/openstack/galera.go index d4cf2dce16..1b9175d2fe 100644 --- a/internal/openstack/galera.go +++ b/internal/openstack/galera.go @@ -33,6 +33,10 @@ const ( galeraReady galeraStatus = iota ) +func galeraCertName(name string) string { + return fmt.Sprintf("galera-%s-svc", name) +} + func deleteUndefinedGaleras( ctx context.Context, instance *corev1beta1.OpenStackControlPlane, @@ -57,7 +61,7 @@ func deleteUndefinedGaleras( if object.CheckOwnerRefExist(instance.GetUID(), galeraObj.OwnerReferences) { log.Info("Deleting Galera", "", galeraObj.Name) - certName := fmt.Sprintf("galera-%s-svc", galeraObj.Name) + certName := galeraCertName(galeraObj.Name) err = DeleteCertificate(ctx, helper, instance.Namespace, certName) if err != nil { delErrs = append(delErrs, fmt.Errorf("galera cert deletion for '%s' failed, because: %w", certName, err)) @@ -119,7 +123,7 @@ func ReconcileGaleras( // If TLS can/must be used is a per user configuration. certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("galera-%s-svc", name), + CertName: galeraCertName(name), Hostnames: []string{ hostname, fmt.Sprintf("%s.%s", hostname, clusterDomain), @@ -142,7 +146,7 @@ func ReconcileGaleras( "server auth", "client auth", }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration diff --git a/internal/openstack/memcached.go b/internal/openstack/memcached.go index cccdee0e1c..5223819e9e 100644 --- a/internal/openstack/memcached.go +++ b/internal/openstack/memcached.go @@ -213,7 +213,7 @@ func reconcileMemcached( fmt.Sprintf("%s.%s.svc.%s", name, instance.Namespace, clusterDomain), fmt.Sprintf("*.%s.%s.svc.%s", name, instance.Namespace, clusterDomain), }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration @@ -245,12 +245,12 @@ func reconcileMemcached( fmt.Sprintf("*.%s.svc", instance.Namespace), fmt.Sprintf("*.%s.svc.%s", instance.Namespace, clusterDomain), }, - Labels: map[string]string{serviceCertSelector: ""}, Usages: []certmgrv1.KeyUsage{ certmgrv1.UsageKeyEncipherment, certmgrv1.UsageDigitalSignature, certmgrv1.UsageClientAuth, }, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration diff --git a/internal/openstack/neutron.go b/internal/openstack/neutron.go index 45479df0ee..29418e4271 100644 --- a/internal/openstack/neutron.go +++ b/internal/openstack/neutron.go @@ -91,7 +91,7 @@ func ReconcileNeutron(ctx context.Context, instance *corev1beta1.OpenStackContro certmgrv1.UsageDigitalSignature, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration diff --git a/internal/openstack/nova.go b/internal/openstack/nova.go index 59ee3e04f1..7a5bbf2f3e 100644 --- a/internal/openstack/nova.go +++ b/internal/openstack/nova.go @@ -378,7 +378,7 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Libvirt.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Libvirt.Cert.Duration.Duration diff --git a/internal/openstack/octavia.go b/internal/openstack/octavia.go index 825cdd9dab..9d98b89f88 100644 --- a/internal/openstack/octavia.go +++ b/internal/openstack/octavia.go @@ -132,7 +132,7 @@ func ReconcileOctavia(ctx context.Context, instance *corev1beta1.OpenStackContro certmgrv1.UsageDigitalSignature, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration diff --git a/internal/openstack/ovn.go b/internal/openstack/ovn.go index 7a13dd1f2e..e18f8c6f6c 100644 --- a/internal/openstack/ovn.go +++ b/internal/openstack/ovn.go @@ -180,7 +180,7 @@ func ReconcileOVNDbClusters(ctx context.Context, instance *corev1beta1.OpenStack certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -320,7 +320,7 @@ func ReconcileOVNNorthd(ctx context.Context, instance *corev1beta1.OpenStackCont certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -464,7 +464,7 @@ func ReconcileOVNController(ctx context.Context, instance *corev1beta1.OpenStack certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -607,7 +607,7 @@ func EnsureOVNMetricsCert(ctx context.Context, instance *corev1beta1.OpenStackCo certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } // Apply certificate duration settings if configured diff --git a/internal/openstack/rabbitmq.go b/internal/openstack/rabbitmq.go index 1886c774a7..c19b5cb6dd 100644 --- a/internal/openstack/rabbitmq.go +++ b/internal/openstack/rabbitmq.go @@ -223,7 +223,7 @@ func reconcileRabbitMQ( certmgrv1.UsageClientAuth, certmgrv1.UsageContentCommitment, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration diff --git a/internal/openstack/redis.go b/internal/openstack/redis.go index 6d9154c50c..a291f75a85 100644 --- a/internal/openstack/redis.go +++ b/internal/openstack/redis.go @@ -233,6 +233,7 @@ func reconcileRedis( "server auth", "client auth", }, + Labels: map[string]string{ServiceCertSelector: ""}, } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration diff --git a/test/functional/ctlplane/base_test.go b/test/functional/ctlplane/base_test.go index 3d5cd9e36f..6806c3bfc2 100644 --- a/test/functional/ctlplane/base_test.go +++ b/test/functional/ctlplane/base_test.go @@ -40,6 +40,7 @@ import ( neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" novav1 "github.com/openstack-k8s-operators/nova-operator/api/nova/v1beta1" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" + backupv1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" openstackclientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1" corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" @@ -704,6 +705,14 @@ func GetOpenStackControlPlane(name types.NamespacedName) *corev1.OpenStackContro return instance } +func GetOpenStackBackupConfigList(namespace string) *backupv1.OpenStackBackupConfigList { + instance := &backupv1.OpenStackBackupConfigList{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.List(ctx, instance, client.InNamespace(namespace))).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} + func OpenStackControlPlaneConditionGetter(name types.NamespacedName) condition.Conditions { instance := GetOpenStackControlPlane(name) return instance.Status.Conditions diff --git a/test/functional/ctlplane/openstackoperator_controller_test.go b/test/functional/ctlplane/openstackoperator_controller_test.go index 6ac8e0baa6..d65e71ea14 100644 --- a/test/functional/ctlplane/openstackoperator_controller_test.go +++ b/test/functional/ctlplane/openstackoperator_controller_test.go @@ -758,6 +758,23 @@ var _ = Describe("OpenStackOperator controller", func() { //Expect(OSCtlplane.Spec.Placement.APIOverride.Route.Annotations).Should(HaveKeyWithValue("api.placement.openstack.org/timeout", "60s")) }) + It("should create OpenStackBackupConfig and set condition to True", func() { + Eventually(func(g Gomega) { + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + g.Expect(OSCtlplane.Status.Conditions.Get( + corev1.OpenStackControlPlaneBackupConfigReadyCondition)).ToNot(BeNil()) + g.Expect(OSCtlplane.Status.Conditions.Get( + corev1.OpenStackControlPlaneBackupConfigReadyCondition).Status).To( + Equal(k8s_corev1.ConditionTrue)) + }, timeout, interval).Should(Succeed()) + + // Verify BackupConfig CR was created + Eventually(func(g Gomega) { + backupConfigList := GetOpenStackBackupConfigList(names.OpenStackControlplaneName.Namespace) + g.Expect(backupConfigList.Items).ToNot(BeEmpty()) + }, timeout, interval).Should(Succeed()) + }) + It("should create selfsigned issuer and public+internal CA and issuer", func() { OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) diff --git a/test/kuttl/common/assert-sample-deployment.yaml b/test/kuttl/common/assert-sample-deployment.yaml index 5574d57639..c5ed7debd5 100644 --- a/test/kuttl/common/assert-sample-deployment.yaml +++ b/test/kuttl/common/assert-sample-deployment.yaml @@ -202,6 +202,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-basic-deployment/03-assert-deploy-custom-cacert.yaml b/test/kuttl/tests/ctlplane-basic-deployment/03-assert-deploy-custom-cacert.yaml index cd5464cc01..67cf1fd3d5 100644 --- a/test/kuttl/tests/ctlplane-basic-deployment/03-assert-deploy-custom-cacert.yaml +++ b/test/kuttl/tests/ctlplane-basic-deployment/03-assert-deploy-custom-cacert.yaml @@ -11,6 +11,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-collapsed/01-assert-collapsed-cell.yaml b/test/kuttl/tests/ctlplane-collapsed/01-assert-collapsed-cell.yaml index c944cfa333..1cf85f5962 100644 --- a/test/kuttl/tests/ctlplane-collapsed/01-assert-collapsed-cell.yaml +++ b/test/kuttl/tests/ctlplane-collapsed/01-assert-collapsed-cell.yaml @@ -176,6 +176,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-galera-3replicas/01-assert-galera-3replicas.yaml b/test/kuttl/tests/ctlplane-galera-3replicas/01-assert-galera-3replicas.yaml index e28d527835..6269ab2193 100644 --- a/test/kuttl/tests/ctlplane-galera-3replicas/01-assert-galera-3replicas.yaml +++ b/test/kuttl/tests/ctlplane-galera-3replicas/01-assert-galera-3replicas.yaml @@ -167,6 +167,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-staged-deployment/01-assert-infrastructure-ready-paused.yaml b/test/kuttl/tests/ctlplane-staged-deployment/01-assert-infrastructure-ready-paused.yaml index 1f77fbde94..5e46165e9f 100644 --- a/test/kuttl/tests/ctlplane-staged-deployment/01-assert-infrastructure-ready-paused.yaml +++ b/test/kuttl/tests/ctlplane-staged-deployment/01-assert-infrastructure-ready-paused.yaml @@ -17,6 +17,10 @@ status: status: "False" type: Ready # Following conditions are alphabetically sorted by type + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - reason: Init status: Unknown type: OpenStackControlPlaneBarbicanReady diff --git a/test/kuttl/tests/ctlplane-tls-cert-rotation/00-assert-deploy-openstack.yaml b/test/kuttl/tests/ctlplane-tls-cert-rotation/00-assert-deploy-openstack.yaml index bdc67fa8a6..2f679ba1b6 100644 --- a/test/kuttl/tests/ctlplane-tls-cert-rotation/00-assert-deploy-openstack.yaml +++ b/test/kuttl/tests/ctlplane-tls-cert-rotation/00-assert-deploy-openstack.yaml @@ -187,6 +187,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-tls-cert-rotation/03-assert-new-certs.yaml b/test/kuttl/tests/ctlplane-tls-cert-rotation/03-assert-new-certs.yaml index 144981300a..c57c35a495 100644 --- a/test/kuttl/tests/ctlplane-tls-cert-rotation/03-assert-new-certs.yaml +++ b/test/kuttl/tests/ctlplane-tls-cert-rotation/03-assert-new-certs.yaml @@ -223,6 +223,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-tls-custom-issuers/01-assert-deploy-openstack.yaml b/test/kuttl/tests/ctlplane-tls-custom-issuers/01-assert-deploy-openstack.yaml index b6012a58fa..071a9393bd 100644 --- a/test/kuttl/tests/ctlplane-tls-custom-issuers/01-assert-deploy-openstack.yaml +++ b/test/kuttl/tests/ctlplane-tls-custom-issuers/01-assert-deploy-openstack.yaml @@ -188,6 +188,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-tls-custom-issuers/09-assert-deploy-openstack.yaml b/test/kuttl/tests/ctlplane-tls-custom-issuers/09-assert-deploy-openstack.yaml index b6012a58fa..071a9393bd 100644 --- a/test/kuttl/tests/ctlplane-tls-custom-issuers/09-assert-deploy-openstack.yaml +++ b/test/kuttl/tests/ctlplane-tls-custom-issuers/09-assert-deploy-openstack.yaml @@ -188,6 +188,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True" diff --git a/test/kuttl/tests/ctlplane-tls-custom-route/03-assert-deploy-openstack.yaml b/test/kuttl/tests/ctlplane-tls-custom-route/03-assert-deploy-openstack.yaml index c929877457..7d222c8d66 100644 --- a/test/kuttl/tests/ctlplane-tls-custom-route/03-assert-deploy-openstack.yaml +++ b/test/kuttl/tests/ctlplane-tls-custom-route/03-assert-deploy-openstack.yaml @@ -210,6 +210,10 @@ status: reason: Ready status: "True" type: Ready + - message: OpenStackControlPlane BackupConfig ready + reason: Ready + status: "True" + type: OpenStackControlPlaneBackupConfigReady - message: OpenStackControlPlane Barbican completed reason: Ready status: "True"