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/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/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/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/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..786f092147 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 @@ -95,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 @@ -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/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/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/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/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/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/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) 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"