From f35f29a04d6647d458468a7cca2fce97ebaaa414 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Tue, 31 Mar 2026 18:57:10 +0200 Subject: [PATCH 1/3] [b/r] Add OpenStackBackupConfig controller Add the BackupConfig CRD, API types, controller, RBAC, samples, and envtests for the backup/restore labeling feature. The controller watches CRD instances across operators and labels resources (secrets, configmaps, NADs, cert-manager issuers) with backup.openstack.org labels for backup/restore integration. Supports annotation overrides on individual resources to customize restore ordering or exclude from backup. Jira: OSPRH-22912 Jira: OSPRH-22913 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Schuppert --- PROJECT | 9 + api/backup/v1beta1/conditions.go | 23 + api/backup/v1beta1/groupversion_info.go | 36 + .../v1beta1/openstackbackupconfig_types.go | 158 +++ api/backup/v1beta1/zz_generated.deepcopy.go | 179 +++ ....openstack.org_openstackbackupconfigs.yaml | 314 +++++ api/go.mod | 14 +- api/go.sum | 24 +- bindata/crds/crds.yaml | 330 ++++++ .../instanceha.openstack.org_instancehas.yaml | 4 + .../mariadb.openstack.org_galerabackups.yaml | 4 + ...twork.openstack.org_bgpconfigurations.yaml | 4 + .../crds/network.openstack.org_dnsdata.yaml | 4 + .../crds/network.openstack.org_ipsets.yaml | 4 + .../network.openstack.org_netconfigs.yaml | 4 + .../network.openstack.org_reservations.yaml | 4 + ...bbitmq.openstack.org_rabbitmqpolicies.yaml | 4 + .../rabbitmq.openstack.org_rabbitmqusers.yaml | 4 + ...rabbitmq.openstack.org_rabbitmqvhosts.yaml | 4 + .../topology.openstack.org_topologies.yaml | 4 + bindata/rbac/designate-operator-rbac.yaml | 10 + bindata/rbac/rbac.yaml | 144 +++ bindata/rbac/swift-operator-rbac.yaml | 9 + cmd/main.go | 26 +- ....openstack.org_openstackbackupconfigs.yaml | 314 +++++ config/crd/kustomization.yaml | 1 + ...nstack-operator.clusterserviceversion.yaml | 15 + config/operator/manager_operator_images.yaml | 10 +- ...ckup_openstackbackupconfig_admin_role.yaml | 27 + ...kup_openstackbackupconfig_editor_role.yaml | 33 + ...kup_openstackbackupconfig_viewer_role.yaml | 29 + config/rbac/kustomization.yaml | 3 + config/rbac/role.yaml | 73 ++ .../backup_v1beta1_openstackbackupconfig.yaml | 32 + config/samples/kustomization.yaml | 1 + go.mod | 18 +- go.sum | 28 +- hack/export_operator_related_images.sh | 12 +- .../openstackbackupconfig_controller.go | 664 +++++++++++ .../openstackbackupconfig_controller_test.go | 1031 +++++++++++++++++ test/functional/ctlplane/suite_test.go | 25 + 41 files changed, 3592 insertions(+), 44 deletions(-) create mode 100644 api/backup/v1beta1/conditions.go create mode 100644 api/backup/v1beta1/groupversion_info.go create mode 100644 api/backup/v1beta1/openstackbackupconfig_types.go create mode 100644 api/backup/v1beta1/zz_generated.deepcopy.go create mode 100644 api/bases/backup.openstack.org_openstackbackupconfigs.yaml create mode 100644 config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_admin_role.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_editor_role.yaml create mode 100644 config/rbac/backup_openstackbackupconfig_viewer_role.yaml create mode 100644 config/samples/backup_v1beta1_openstackbackupconfig.yaml create mode 100644 internal/controller/backup/openstackbackupconfig_controller.go create mode 100644 test/functional/ctlplane/openstackbackupconfig_controller_test.go diff --git a/PROJECT b/PROJECT index 1f2ad9ab4..8ea2c3253 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 000000000..07590d0c5 --- /dev/null +++ b/api/backup/v1beta1/conditions.go @@ -0,0 +1,23 @@ +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" + + // OpenStackBackupConfigIssuersReadyCondition - cert-manager Issuers labeling status + OpenStackBackupConfigIssuersReadyCondition condition.Type = "IssuersReady" + + // 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 000000000..be184fe9f --- /dev/null +++ b/api/backup/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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. +*/ + +// 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 000000000..713a5a7f7 --- /dev/null +++ b/api/backup/v1beta1/openstackbackupconfig_types.go @@ -0,0 +1,158 @@ +/* +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. +*/ + +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"` + + // Issuers configuration for backup labeling of cert-manager Issuers. + // Only custom (user-provided) Issuers without ownerReferences are labeled. + // Operator-created Issuers (rootca-*, selfsigned-issuer) have ownerRefs + // and are recreated by the operator during reconciliation. + // Custom Issuers default to restore order 20 (after secrets at order 10, + // since Issuers reference CA secrets). + // +kubebuilder:validation:Optional + // +kubebuilder:default={labeling:enabled,restoreOrder:"20"} + Issuers ResourceBackupConfig `json:"issuers"` +} + +// 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,omitempty"` + + // ConfigMaps is the number of configmaps labeled for backup + // +kubebuilder:validation:Optional + ConfigMaps int `json:"configMaps,omitempty"` + + // NetworkAttachmentDefinitions is the number of NADs labeled for backup + // +kubebuilder:validation:Optional + NetworkAttachmentDefinitions int `json:"networkAttachmentDefinitions,omitempty"` + + // Issuers is the number of cert-manager Issuers labeled for backup + // +kubebuilder:validation:Optional + Issuers int `json:"issuers,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=osbkpcfg +// +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="Custom Issuers",type="integer",JSONPath=".status.labeledResources.issuers",description="Labeled custom cert-manager Issuers (without ownerReferences)" +// +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 000000000..d0fb6450e --- /dev/null +++ b/api/backup/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,179 @@ +//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) + in.Issuers.DeepCopyInto(&out.Issuers) +} + +// 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 000000000..a196938be --- /dev/null +++ b/api/bases/backup.openstack.org_openstackbackupconfigs.yaml @@ -0,0 +1,314 @@ +--- +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 + 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 custom cert-manager Issuers (without ownerReferences) + jsonPath: .status.labeledResources.issuers + name: Custom Issuers + 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 + issuers: + default: + labeling: enabled + restoreOrder: "20" + description: |- + Issuers configuration for backup labeling of cert-manager Issuers. + Only custom (user-provided) Issuers without ownerReferences are labeled. + Operator-created Issuers (rootca-*, selfsigned-issuer) have ownerRefs + and are recreated by the operator during reconciliation. + Custom Issuers default to restore order 20 (after secrets at order 10, + since Issuers reference CA secrets). + 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 + 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 + issuers: + description: Issuers is the number of cert-manager Issuers 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/go.mod b/api/go.mod index 6606eb431..c87ff53f9 100644 --- a/api/go.mod +++ b/api/go.mod @@ -16,7 +16,7 @@ require ( github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260316100655-863ae03d41af github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260321081258-5c806856eeb6 github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260326092926-8a2950f0575b github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260324115114-e3be8a47a45e github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7 github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260321081546-85bcf5293c70 @@ -143,3 +143,15 @@ replace k8s.io/code-generator => k8s.io/code-generator v0.31.14 //allow-merging replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging replace github.com/cert-manager/cmctl/v2 => github.com/cert-manager/cmctl/v2 v2.1.2-0.20241127223932-88edb96860cf //allow-merging + +replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79 + +replace github.com/openstack-k8s-operators/mariadb-operator/api => github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136 + +replace github.com/openstack-k8s-operators/glance-operator/api => github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17 + +replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9 + +replace github.com/openstack-k8s-operators/swift-operator/api => github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b + +replace github.com/openstack-k8s-operators/designate-operator/api => github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2 diff --git a/api/go.sum b/api/go.sum index a275fae2a..5325eea2c 100644 --- a/api/go.sum +++ b/api/go.sum @@ -118,30 +118,20 @@ github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20260321080732 github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20260321080732-c31d77fca95d/go.mod h1:CsJPeetdAsW1tWwjgeS/BTtASLrkG8WfzZnCggl1OVg= github.com/openstack-k8s-operators/cinder-operator/api v0.6.1-0.20260323152123-4cc81903f791 h1:E/izQYgQJZsBOlrlnaXQHxbHDkYPTE+9p7lnRC8/3Eo= github.com/openstack-k8s-operators/cinder-operator/api v0.6.1-0.20260323152123-4cc81903f791/go.mod h1:82/md756vpv6AQhtRUkeL923qaX6Uu2sfnHdgfgJkFA= -github.com/openstack-k8s-operators/designate-operator/api v0.6.1-0.20260321080424-30da87862de0 h1:KRQ8YQeA6HegAeoS6qoIkxJqbGcumvH4FUyj4L1Q19g= -github.com/openstack-k8s-operators/designate-operator/api v0.6.1-0.20260321080424-30da87862de0/go.mod h1:eYiZSSr4liFHK3ycScT2V0egI6JXx3ffxh6kGZsP0bk= -github.com/openstack-k8s-operators/glance-operator/api v0.6.1-0.20260321081011-835a7b2f1753 h1:liEY4nDerLxsXvtmgFhSmxRtDQwXjpJY5RBIEJRKqIM= -github.com/openstack-k8s-operators/glance-operator/api v0.6.1-0.20260321081011-835a7b2f1753/go.mod h1:DLiEQFdAeeqAWDzsm19iKnnO+aLQeYeA7edIS9qlj2E= github.com/openstack-k8s-operators/heat-operator/api v0.6.1-0.20260321081011-0ad5f7292fb5 h1:x0MjtALo7BqY3PD7ZmYYHVpcxpBki60f/CYpdImHt+s= github.com/openstack-k8s-operators/heat-operator/api v0.6.1-0.20260321081011-0ad5f7292fb5/go.mod h1:D/clVT1Pf25PC/N2SEQiB2QQO/TF2IBCOPT5VkNkhHQ= github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260321080731-03e8ffbd65e3 h1:t80wfV1UAjxA0ey3LHRsiJgoL6OEuBqOjqU6yvn7nOg= github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260321080731-03e8ffbd65e3/go.mod h1:qQ7Ie2fWv5PnlffsAWlv6Y7jUgFbG0WQVZHCOgS0d/Y= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260316100655-863ae03d41af h1:Ow12j/PVbEtul1bZ7s/ZenVnKPIHK2q+0VgTp+j/wro= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260316100655-863ae03d41af/go.mod h1:nC/Jf3OYJRML8UEzJ/mn/TQcSCv/nhqO6x6LGkdDt60= github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260321081258-5c806856eeb6 h1:8jpYazj7pGgzomNtQFL+BW5VxtDjRMfNJ7pTd53+5fw= github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260321081258-5c806856eeb6/go.mod h1:y5Er36n6rjQA2Gi3dtwamhyeqWTGBIszN+ZexsvJCIo= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f h1:60I2YLHRznTY2BQXqXWc+ByJ3ipdQgKgW52t9J8C5DY= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f/go.mod h1:8o6LSPt1VAvvB2ngS2QObGS6HEikSdVpHoKIgmb78KI= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e h1:mHKwo8Cg9xHRRShBtJfcPYdE7FaivrgRBegEMDgv7fY= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:XUUV+h1nZC4kra5oF+cXPkviWYJ3ELhccHxnVO7CvQQ= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260320125710-3a5f82ff0f18 h1:eJDwc8LPJg+H4bHMLh/pDJBk+OezQ+wkjUNpExUFhbM= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260320125710-3a5f82ff0f18/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260324115114-e3be8a47a45e h1:KpBZFtg8RnE9F6uINOEH7c9Zgp8HIl5+haHLv3IFEGk= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:3loLaPUDQyvbPekylZd9OCLF+EXH2klRI9IeeQhuMcs= github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7 h1:0iQ9FFFI1/y6m2qi2O9NLyj90KmajuZOr1FTSsPdrPw= github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7/go.mod h1:O6PjMV49R7rfZyCmufUdVwiKBc5XW/dNJYSLSut/PRI= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260321081546-85bcf5293c70 h1:4REWM4l6kTOH14dsBSp/hhNdULbq3LDoCvfMWofPx4k= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260321081546-85bcf5293c70/go.mod h1:cyeexUkEIgzQ3c1vVVv/DQ3AbnECfDwKdZteKC+sZKY= github.com/openstack-k8s-operators/neutron-operator/api v0.6.1-0.20260314103518-fe1a1eae182d h1:oKRiIKhr1dm1wudWqBMvLViMPlXqi8B+PQT2Mv7rsj4= github.com/openstack-k8s-operators/neutron-operator/api v0.6.1-0.20260314103518-fe1a1eae182d/go.mod h1:ljfpLBr2EyNAS7W7c+CQy61UhkwALTVLWl2Mc9YTSNA= github.com/openstack-k8s-operators/nova-operator/api v0.6.1-0.20260324185405-5701277f8fe2 h1:q5dK7GggmutgL8Rrfb7JNg1oKwLpe0uppW3Cm/hdupo= @@ -156,8 +146,6 @@ github.com/openstack-k8s-operators/placement-operator/api v0.6.1-0.2026032114385 github.com/openstack-k8s-operators/placement-operator/api v0.6.1-0.20260321143858-aaffa49d81f5/go.mod h1:IuKiktN8yyVTD6T57XZN9Cbx0ZFIU+gwO4OLFa8nxu8= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec/go.mod h1:Nh2NEePLjovUQof2krTAg4JaAoLacqtPTZQXK6izNfg= -github.com/openstack-k8s-operators/swift-operator/api v0.6.1-0.20260321143859-b86b733f44bb h1:uEZZguEl/iVOlXkGiWkVK+29k+S++w83FxGICUSKlaw= -github.com/openstack-k8s-operators/swift-operator/api v0.6.1-0.20260321143859-b86b733f44bb/go.mod h1:81OMzM3nKYvmpYrQ4egBzwiMjsh6To+eY8bFzW0zyoA= github.com/openstack-k8s-operators/telemetry-operator/api v0.6.1-0.20260324101924-e6b1d6cc59cd h1:BSn3UxztfqM/Z0X9pgMG+NPh0JV9KtBqpqP0eFaWhOw= github.com/openstack-k8s-operators/telemetry-operator/api v0.6.1-0.20260324101924-e6b1d6cc59cd/go.mod h1:htVoPbguZfrRyEs4NNK6WpSpofHagOx5oNtJbyt8SVY= github.com/openstack-k8s-operators/watcher-operator/api v0.6.1-0.20260323205620-1d7c183eebeb h1:etP2QwTs6SXpQn//jn2xH25tHFg+/yb2XdQp5Gk0GTs= @@ -189,6 +177,18 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2 h1:gvVe1CaZt6FhzEOHSASM9Iyjp5p+ZVu8d42yJ2k+6+Q= +github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2/go.mod h1:eYiZSSr4liFHK3ycScT2V0egI6JXx3ffxh6kGZsP0bk= +github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17 h1:L0w5xRP/fcZk+G/ddfy15P+R4EceF8hLdr2MQOFxauw= +github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17/go.mod h1:DLiEQFdAeeqAWDzsm19iKnnO+aLQeYeA7edIS9qlj2E= +github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9 h1:BOrJTRpWg7R3C4ZGc1ZZWiQy0RCpbJXMRzxYDrMW87I= +github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9/go.mod h1:beHi9rkte1U3mJyzRLWxv8zzLK5l5BYGg95guE2HQIk= +github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79 h1:GvxShswn6tdYd9oYyaeCfREmCSlHy8lmesTcteXBeH0= +github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= +github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136 h1:BT5CBFct9EfwblCKZyjMVE5vWITpHX9OFjKxTyuMk0I= +github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136/go.mod h1:cyeexUkEIgzQ3c1vVVv/DQ3AbnECfDwKdZteKC+sZKY= +github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b h1:+3dvtbudA/AfUkHh3uhz6Mb28y/vPy2hzTm3mQU0QSg= +github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b/go.mod h1:81OMzM3nKYvmpYrQ4egBzwiMjsh6To+eY8bFzW0zyoA= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/bindata/crds/crds.yaml b/bindata/crds/crds.yaml index 6ac453e18..b3fb6903e 100644 --- a/bindata/crds/crds.yaml +++ b/bindata/crds/crds.yaml @@ -1,5 +1,319 @@ 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 + 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 custom cert-manager Issuers (without ownerReferences) + jsonPath: .status.labeledResources.issuers + name: Custom Issuers + 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 + issuers: + default: + labeling: enabled + restoreOrder: "20" + description: |- + Issuers configuration for backup labeling of cert-manager Issuers. + Only custom (user-provided) Issuers without ownerReferences are labeled. + Operator-created Issuers (rootca-*, selfsigned-issuer) have ownerRefs + and are recreated by the operator during reconciliation. + Custom Issuers default to restore order 20 (after secrets at order 10, + since Issuers reference CA secrets). + 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 + 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 + issuers: + description: Issuers is the number of cert-manager Issuers 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 +583,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 @@ -18929,6 +19247,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 @@ -20925,6 +21247,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 @@ -21222,6 +21548,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/crds/instanceha.openstack.org_instancehas.yaml b/bindata/crds/instanceha.openstack.org_instancehas.yaml index 96039dfaf..caabf6c1e 100644 --- a/bindata/crds/instanceha.openstack.org_instancehas.yaml +++ b/bindata/crds/instanceha.openstack.org_instancehas.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: instancehas.instanceha.openstack.org spec: group: instanceha.openstack.org diff --git a/bindata/crds/mariadb.openstack.org_galerabackups.yaml b/bindata/crds/mariadb.openstack.org_galerabackups.yaml index 71546fe7b..dfa2c7cb9 100644 --- a/bindata/crds/mariadb.openstack.org_galerabackups.yaml +++ b/bindata/crds/mariadb.openstack.org_galerabackups.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: galerabackups.mariadb.openstack.org spec: group: mariadb.openstack.org diff --git a/bindata/crds/network.openstack.org_bgpconfigurations.yaml b/bindata/crds/network.openstack.org_bgpconfigurations.yaml index e76e5a260..dd39a5dea 100644 --- a/bindata/crds/network.openstack.org_bgpconfigurations.yaml +++ b/bindata/crds/network.openstack.org_bgpconfigurations.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: bgpconfigurations.network.openstack.org spec: group: network.openstack.org diff --git a/bindata/crds/network.openstack.org_dnsdata.yaml b/bindata/crds/network.openstack.org_dnsdata.yaml index c6757b07d..b344ac01b 100644 --- a/bindata/crds/network.openstack.org_dnsdata.yaml +++ b/bindata/crds/network.openstack.org_dnsdata.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: dnsdata.network.openstack.org spec: group: network.openstack.org diff --git a/bindata/crds/network.openstack.org_ipsets.yaml b/bindata/crds/network.openstack.org_ipsets.yaml index a304faa2f..d03cb4f11 100644 --- a/bindata/crds/network.openstack.org_ipsets.yaml +++ b/bindata/crds/network.openstack.org_ipsets.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: ipsets.network.openstack.org spec: group: network.openstack.org diff --git a/bindata/crds/network.openstack.org_netconfigs.yaml b/bindata/crds/network.openstack.org_netconfigs.yaml index 154bc5e63..654278dde 100644 --- a/bindata/crds/network.openstack.org_netconfigs.yaml +++ b/bindata/crds/network.openstack.org_netconfigs.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: netconfigs.network.openstack.org spec: group: network.openstack.org diff --git a/bindata/crds/network.openstack.org_reservations.yaml b/bindata/crds/network.openstack.org_reservations.yaml index 359590a41..75590ac84 100644 --- a/bindata/crds/network.openstack.org_reservations.yaml +++ b/bindata/crds/network.openstack.org_reservations.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "30" name: reservations.network.openstack.org spec: group: network.openstack.org diff --git a/bindata/crds/rabbitmq.openstack.org_rabbitmqpolicies.yaml b/bindata/crds/rabbitmq.openstack.org_rabbitmqpolicies.yaml index 5d74271f8..09692beb5 100644 --- a/bindata/crds/rabbitmq.openstack.org_rabbitmqpolicies.yaml +++ b/bindata/crds/rabbitmq.openstack.org_rabbitmqpolicies.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: rabbitmqpolicies.rabbitmq.openstack.org spec: group: rabbitmq.openstack.org diff --git a/bindata/crds/rabbitmq.openstack.org_rabbitmqusers.yaml b/bindata/crds/rabbitmq.openstack.org_rabbitmqusers.yaml index 558ef8477..211db3305 100644 --- a/bindata/crds/rabbitmq.openstack.org_rabbitmqusers.yaml +++ b/bindata/crds/rabbitmq.openstack.org_rabbitmqusers.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: rabbitmqusers.rabbitmq.openstack.org spec: group: rabbitmq.openstack.org diff --git a/bindata/crds/rabbitmq.openstack.org_rabbitmqvhosts.yaml b/bindata/crds/rabbitmq.openstack.org_rabbitmqvhosts.yaml index b3a0ce7f4..55a11cbbb 100644 --- a/bindata/crds/rabbitmq.openstack.org_rabbitmqvhosts.yaml +++ b/bindata/crds/rabbitmq.openstack.org_rabbitmqvhosts.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: controlplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "40" name: rabbitmqvhosts.rabbitmq.openstack.org spec: group: rabbitmq.openstack.org diff --git a/bindata/crds/topology.openstack.org_topologies.yaml b/bindata/crds/topology.openstack.org_topologies.yaml index 3f32a9182..8c4388c88 100644 --- a/bindata/crds/topology.openstack.org_topologies.yaml +++ b/bindata/crds/topology.openstack.org_topologies.yaml @@ -4,6 +4,10 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null + labels: + backup.openstack.org/category: dataplane + backup.openstack.org/restore: "true" + backup.openstack.org/restore-order: "20" name: topologies.topology.openstack.org spec: group: topology.openstack.org diff --git a/bindata/rbac/designate-operator-rbac.yaml b/bindata/rbac/designate-operator-rbac.yaml index 97e77f7af..0c1fe0106 100644 --- a/bindata/rbac/designate-operator-rbac.yaml +++ b/bindata/rbac/designate-operator-rbac.yaml @@ -81,6 +81,16 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: diff --git a/bindata/rbac/rbac.yaml b/bindata/rbac/rbac.yaml index 20a134624..1ff442f2e 100644 --- a/bindata/rbac/rbac.yaml +++ b/bindata/rbac/rbac.yaml @@ -50,6 +50,77 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: openstack-operator + name: openstack-operator-backup-openstackbackupconfig-admin-role +rules: +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - '*' +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: openstack-operator + name: openstack-operator-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 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: openstack-operator + name: openstack-operator-backup-openstackbackupconfig-viewer-role +rules: +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs + verbs: + - get + - list + - watch +- apiGroups: + - backup.openstack.org + resources: + - openstackbackupconfigs/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: name: openstack-operator-manager-role rules: @@ -118,6 +189,14 @@ rules: - '*' verbs: - '*' +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch - apiGroups: - apps resources: @@ -130,6 +209,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 +247,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 +551,8 @@ rules: verbs: - get - list + - patch + - update - watch - apiGroups: - keystone.openstack.org diff --git a/bindata/rbac/swift-operator-rbac.yaml b/bindata/rbac/swift-operator-rbac.yaml index f1e7d6e5e..eb00e35d6 100644 --- a/bindata/rbac/swift-operator-rbac.yaml +++ b/bindata/rbac/swift-operator-rbac.yaml @@ -91,6 +91,15 @@ rules: - "" resources: - persistentvolumeclaims + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: - pods verbs: - get diff --git a/cmd/main.go b/cmd/main.go index 6df1bb0ae..9fff7cea9 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 000000000..a196938be --- /dev/null +++ b/config/crd/bases/backup.openstack.org_openstackbackupconfigs.yaml @@ -0,0 +1,314 @@ +--- +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 + 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 custom cert-manager Issuers (without ownerReferences) + jsonPath: .status.labeledResources.issuers + name: Custom Issuers + 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 + issuers: + default: + labeling: enabled + restoreOrder: "20" + description: |- + Issuers configuration for backup labeling of cert-manager Issuers. + Only custom (user-provided) Issuers without ownerReferences are labeled. + Operator-created Issuers (rootca-*, selfsigned-issuer) have ownerRefs + and are recreated by the operator during reconciliation. + Custom Issuers default to restore order 20 (after secrets at order 10, + since Issuers reference CA secrets). + 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 + 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 + issuers: + description: Issuers is the number of cert-manager Issuers labeled + for backup + type: integer + networkAttachmentDefinitions: + description: NetworkAttachmentDefinitions is the number of NADs + labeled for backup + type: integer + secrets: + description: Secrets is the number of secrets labeled for backup + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 40534231a..e7cceda24 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 0d8ae4b2a..19cc73f6f 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 @@ -376,6 +388,9 @@ spec: Resource displayName: Template path: openstackclient.template + - description: List of environment variables to set in the container. + displayName: Env + path: openstackclient.template.env - description: Ovn - Overrides to use when creating the OVN Services displayName: Ovn path: ovn diff --git a/config/operator/manager_operator_images.yaml b/config/operator/manager_operator_images.yaml index 7d57d95b3..63b487f6c 100644 --- a/config/operator/manager_operator_images.yaml +++ b/config/operator/manager_operator_images.yaml @@ -18,15 +18,15 @@ spec: - name: RELATED_IMAGE_CINDER_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/cinder-operator@sha256:0c226cbc35bf93181b36bb7d0e5e1cd65d370d1f53c66895fdc73f9f84d5a06b - name: RELATED_IMAGE_DESIGNATE_OPERATOR_MANAGER_IMAGE_URL - value: quay.io/openstack-k8s-operators/designate-operator@sha256:4c7f06f3d9676d55c6f6f726df750fe370f71756048184106e06713923612267 + value: quay.io/mschuppe/designate-operator@sha256:06883bc94e99fea56d7230df05928fdb9f3cae72ba65c1fb7f3738d7cac79f51 - name: RELATED_IMAGE_GLANCE_OPERATOR_MANAGER_IMAGE_URL - value: quay.io/openstack-k8s-operators/glance-operator@sha256:f649f31aca138e78f72963c98bafaeb0da514133f1d731019552c76dad08394c + value: quay.io/mschuppe/glance-operator@sha256:d1aecaf675cf02a4f17d99a9847a1016ae5f57b9c7f78ead4d26c22283a5f8c5 - name: RELATED_IMAGE_HEAT_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/heat-operator@sha256:875a5a57ca5820e2b8b01463d5efbe0644afc288fcbb78898795d34637c1b7e2 - name: RELATED_IMAGE_HORIZON_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/horizon-operator@sha256:67c5689bf3ea12b55f2c76e8dbefad03a980a6545d46f16493004cdcff4bfee4 - name: RELATED_IMAGE_INFRA_OPERATOR_MANAGER_IMAGE_URL - value: quay.io/openstack-k8s-operators/infra-operator@sha256:d5a153d4e0fd948855eb2f0d4d9e48b0a1f6cb4ee290490ec22a8d8a631b913b + value: quay.io/mschuppe/infra-operator@sha256:7fafd4df6c0ebc5bdc10fa11406975d9654a72780dfd770217aedca7b5f6f7d9 - name: RELATED_IMAGE_IRONIC_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/ironic-operator@sha256:741c1bc38c0d9430995a0d0aae6adae5c1f490b23d620564595cdd40683df68b - name: RELATED_IMAGE_KEYSTONE_OPERATOR_MANAGER_IMAGE_URL @@ -34,7 +34,7 @@ spec: - name: RELATED_IMAGE_MANILA_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/manila-operator@sha256:e5f1303497321c083933cd8ab46e0c95c3f7f3f4101e0c2c3df79eb089abab9e - name: RELATED_IMAGE_MARIADB_OPERATOR_MANAGER_IMAGE_URL - value: quay.io/openstack-k8s-operators/mariadb-operator@sha256:16b276e3f22dc79232c2150ae53becd10ebcf2d9b883f7df4ff98a929eefac91 + value: quay.io/mschuppe/mariadb-operator@sha256:8347867b11e5ff96d8979b15f055f415077132460e20c2506c95b349b981e3be - name: RELATED_IMAGE_NEUTRON_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/neutron-operator@sha256:526f9d4965431e1a5e4f8c3224bcee3f636a3108a5e0767296a994c2a517404a - name: RELATED_IMAGE_NOVA_OPERATOR_MANAGER_IMAGE_URL @@ -50,7 +50,7 @@ spec: - name: RELATED_IMAGE_RABBITMQ_CLUSTER_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/rabbitmq-cluster-operator@sha256:893e66303c1b0bc1d00a299a3f0380bad55c8dc813c8a1c6a4aab379f5aa12a2 - name: RELATED_IMAGE_SWIFT_OPERATOR_MANAGER_IMAGE_URL - value: quay.io/openstack-k8s-operators/swift-operator@sha256:cbc03ca8837c64974a4670506a8df688c44432c4aab095f3fa7f1330e72bd3bd + value: quay.io/mschuppe/swift-operator@sha256:5231db233a72f97ea81d0bc6c13559450a3876875fec5685f347cb7afdd4b303 - name: RELATED_IMAGE_TELEMETRY_OPERATOR_MANAGER_IMAGE_URL value: quay.io/openstack-k8s-operators/telemetry-operator@sha256:566b1f4d3f3d50e9620b845e12ef72bf3a27e07233a9c7424c1102045a4e74a2 - name: RELATED_IMAGE_TEST_OPERATOR_MANAGER_IMAGE_URL diff --git a/config/rbac/backup_openstackbackupconfig_admin_role.yaml b/config/rbac/backup_openstackbackupconfig_admin_role.yaml new file mode 100644 index 000000000..76127c4ab --- /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 000000000..e875d3229 --- /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 000000000..0988092d7 --- /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 fc721b406..961f33370 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 92ee8f170..1c68d8089 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 000000000..4cedc3d61 --- /dev/null +++ b/config/samples/backup_v1beta1_openstackbackupconfig.yaml @@ -0,0 +1,32 @@ +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: + # Target namespace to watch for resources + targetNamespace: openstack + + # 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 138d15b6b..687ef6853 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 5abd2b2f2..9e1e7144a 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20260324115114-e3be8a47a45e github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260324115114-e3be8a47a45e - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260326092926-8a2950f0575b github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260324115114-e3be8a47a45e github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260324115114-e3be8a47a45e github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7 @@ -45,6 +45,7 @@ require ( go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.14 + k8s.io/apiextensions-apiserver v0.33.2 k8s.io/apimachinery v0.31.14 k8s.io/client-go v0.31.14 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d @@ -139,7 +140,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.33.2 // indirect k8s.io/apiserver v0.33.2 // indirect k8s.io/component-base v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect @@ -181,3 +181,17 @@ replace k8s.io/code-generator => k8s.io/code-generator v0.31.14 //allow-merging replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging replace github.com/cert-manager/cmctl/v2 => github.com/cert-manager/cmctl/v2 v2.1.2-0.20241127223932-88edb96860cf //allow-merging + +replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79 + +replace github.com/openstack-k8s-operators/lib-common/modules/certmanager => github.com/stuggi/lib-common/modules/certmanager v0.0.0-20260331130034-f04bcb447a79 + +replace github.com/openstack-k8s-operators/mariadb-operator/api => github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136 + +replace github.com/openstack-k8s-operators/glance-operator/api => github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17 + +replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9 + +replace github.com/openstack-k8s-operators/swift-operator/api => github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b + +replace github.com/openstack-k8s-operators/designate-operator/api => github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2 diff --git a/go.sum b/go.sum index 8261fdb2a..ae9c009ce 100644 --- a/go.sum +++ b/go.sum @@ -142,26 +142,16 @@ github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20260321080732 github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20260321080732-c31d77fca95d/go.mod h1:CsJPeetdAsW1tWwjgeS/BTtASLrkG8WfzZnCggl1OVg= github.com/openstack-k8s-operators/cinder-operator/api v0.6.1-0.20260323152123-4cc81903f791 h1:E/izQYgQJZsBOlrlnaXQHxbHDkYPTE+9p7lnRC8/3Eo= github.com/openstack-k8s-operators/cinder-operator/api v0.6.1-0.20260323152123-4cc81903f791/go.mod h1:82/md756vpv6AQhtRUkeL923qaX6Uu2sfnHdgfgJkFA= -github.com/openstack-k8s-operators/designate-operator/api v0.6.1-0.20260321080424-30da87862de0 h1:KRQ8YQeA6HegAeoS6qoIkxJqbGcumvH4FUyj4L1Q19g= -github.com/openstack-k8s-operators/designate-operator/api v0.6.1-0.20260321080424-30da87862de0/go.mod h1:eYiZSSr4liFHK3ycScT2V0egI6JXx3ffxh6kGZsP0bk= -github.com/openstack-k8s-operators/glance-operator/api v0.6.1-0.20260321081011-835a7b2f1753 h1:liEY4nDerLxsXvtmgFhSmxRtDQwXjpJY5RBIEJRKqIM= -github.com/openstack-k8s-operators/glance-operator/api v0.6.1-0.20260321081011-835a7b2f1753/go.mod h1:DLiEQFdAeeqAWDzsm19iKnnO+aLQeYeA7edIS9qlj2E= github.com/openstack-k8s-operators/heat-operator/api v0.6.1-0.20260321081011-0ad5f7292fb5 h1:x0MjtALo7BqY3PD7ZmYYHVpcxpBki60f/CYpdImHt+s= github.com/openstack-k8s-operators/heat-operator/api v0.6.1-0.20260321081011-0ad5f7292fb5/go.mod h1:D/clVT1Pf25PC/N2SEQiB2QQO/TF2IBCOPT5VkNkhHQ= github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260321080731-03e8ffbd65e3 h1:t80wfV1UAjxA0ey3LHRsiJgoL6OEuBqOjqU6yvn7nOg= github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260321080731-03e8ffbd65e3/go.mod h1:qQ7Ie2fWv5PnlffsAWlv6Y7jUgFbG0WQVZHCOgS0d/Y= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260316100655-863ae03d41af h1:Ow12j/PVbEtul1bZ7s/ZenVnKPIHK2q+0VgTp+j/wro= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260316100655-863ae03d41af/go.mod h1:nC/Jf3OYJRML8UEzJ/mn/TQcSCv/nhqO6x6LGkdDt60= github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260321081258-5c806856eeb6 h1:8jpYazj7pGgzomNtQFL+BW5VxtDjRMfNJ7pTd53+5fw= github.com/openstack-k8s-operators/ironic-operator/api v0.6.1-0.20260321081258-5c806856eeb6/go.mod h1:y5Er36n6rjQA2Gi3dtwamhyeqWTGBIszN+ZexsvJCIo= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f h1:60I2YLHRznTY2BQXqXWc+ByJ3ipdQgKgW52t9J8C5DY= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260321081256-de45f3b1de4f/go.mod h1:8o6LSPt1VAvvB2ngS2QObGS6HEikSdVpHoKIgmb78KI= github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20260324115114-e3be8a47a45e h1:myPJ0FD7Ky1z6KqBuAYWhlgV3NscL+FMwKAnYepLXC0= github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:tXxVkkk8HlATwTmDA5RTP3b+c8apfuMM15mZ2wW5iNs= -github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260324115114-e3be8a47a45e h1:i41AUMiLWiT1/fFBCOChHYWYMDeyoM6YDpnIvXzm6rg= -github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:GzD7Jc5o98ptJ97DSjhC0CQ6OiTP0PB/2qJqxYGcOH8= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e h1:mHKwo8Cg9xHRRShBtJfcPYdE7FaivrgRBegEMDgv7fY= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:XUUV+h1nZC4kra5oF+cXPkviWYJ3ELhccHxnVO7CvQQ= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260320125710-3a5f82ff0f18 h1:eJDwc8LPJg+H4bHMLh/pDJBk+OezQ+wkjUNpExUFhbM= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260320125710-3a5f82ff0f18/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260324115114-e3be8a47a45e h1:KpBZFtg8RnE9F6uINOEH7c9Zgp8HIl5+haHLv3IFEGk= @@ -170,8 +160,6 @@ github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.202603241151 github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260324115114-e3be8a47a45e/go.mod h1:dEjz8zHRIlP3vnMmWdHytlLeSZ6BHcIiSTPM7xTQxFg= github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7 h1:0iQ9FFFI1/y6m2qi2O9NLyj90KmajuZOr1FTSsPdrPw= github.com/openstack-k8s-operators/manila-operator/api v0.6.1-0.20260321081547-64d64a0c02c7/go.mod h1:O6PjMV49R7rfZyCmufUdVwiKBc5XW/dNJYSLSut/PRI= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260321081546-85bcf5293c70 h1:4REWM4l6kTOH14dsBSp/hhNdULbq3LDoCvfMWofPx4k= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260321081546-85bcf5293c70/go.mod h1:cyeexUkEIgzQ3c1vVVv/DQ3AbnECfDwKdZteKC+sZKY= github.com/openstack-k8s-operators/neutron-operator/api v0.6.1-0.20260314103518-fe1a1eae182d h1:oKRiIKhr1dm1wudWqBMvLViMPlXqi8B+PQT2Mv7rsj4= github.com/openstack-k8s-operators/neutron-operator/api v0.6.1-0.20260314103518-fe1a1eae182d/go.mod h1:ljfpLBr2EyNAS7W7c+CQy61UhkwALTVLWl2Mc9YTSNA= github.com/openstack-k8s-operators/nova-operator/api v0.6.1-0.20260324185405-5701277f8fe2 h1:q5dK7GggmutgL8Rrfb7JNg1oKwLpe0uppW3Cm/hdupo= @@ -186,8 +174,6 @@ github.com/openstack-k8s-operators/placement-operator/api v0.6.1-0.2026032114385 github.com/openstack-k8s-operators/placement-operator/api v0.6.1-0.20260321143858-aaffa49d81f5/go.mod h1:IuKiktN8yyVTD6T57XZN9Cbx0ZFIU+gwO4OLFa8nxu8= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec/go.mod h1:Nh2NEePLjovUQof2krTAg4JaAoLacqtPTZQXK6izNfg= -github.com/openstack-k8s-operators/swift-operator/api v0.6.1-0.20260321143859-b86b733f44bb h1:uEZZguEl/iVOlXkGiWkVK+29k+S++w83FxGICUSKlaw= -github.com/openstack-k8s-operators/swift-operator/api v0.6.1-0.20260321143859-b86b733f44bb/go.mod h1:81OMzM3nKYvmpYrQ4egBzwiMjsh6To+eY8bFzW0zyoA= github.com/openstack-k8s-operators/telemetry-operator/api v0.6.1-0.20260324101924-e6b1d6cc59cd h1:BSn3UxztfqM/Z0X9pgMG+NPh0JV9KtBqpqP0eFaWhOw= github.com/openstack-k8s-operators/telemetry-operator/api v0.6.1-0.20260324101924-e6b1d6cc59cd/go.mod h1:htVoPbguZfrRyEs4NNK6WpSpofHagOx5oNtJbyt8SVY= github.com/openstack-k8s-operators/test-operator/api v0.6.1-0.20260321224840-abe628e8088e h1:mt33L4pRztvWVx76ojzCK6YB7CVeF4vL+1PV70N0prQ= @@ -232,6 +218,20 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2 h1:gvVe1CaZt6FhzEOHSASM9Iyjp5p+ZVu8d42yJ2k+6+Q= +github.com/stuggi/designate-operator/api v0.0.0-20260331140431-da1a258454e2/go.mod h1:eYiZSSr4liFHK3ycScT2V0egI6JXx3ffxh6kGZsP0bk= +github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17 h1:L0w5xRP/fcZk+G/ddfy15P+R4EceF8hLdr2MQOFxauw= +github.com/stuggi/glance-operator/api v0.0.0-20260331140204-06f4c758ae17/go.mod h1:DLiEQFdAeeqAWDzsm19iKnnO+aLQeYeA7edIS9qlj2E= +github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9 h1:BOrJTRpWg7R3C4ZGc1ZZWiQy0RCpbJXMRzxYDrMW87I= +github.com/stuggi/infra-operator/apis v0.0.0-20260331140545-6350ea9574d9/go.mod h1:beHi9rkte1U3mJyzRLWxv8zzLK5l5BYGg95guE2HQIk= +github.com/stuggi/lib-common/modules/certmanager v0.0.0-20260331130034-f04bcb447a79 h1:m3GEvNs412tNVUK93kIoJFC8HT0/Pi2TZi6qXbuB8lE= +github.com/stuggi/lib-common/modules/certmanager v0.0.0-20260331130034-f04bcb447a79/go.mod h1:GzD7Jc5o98ptJ97DSjhC0CQ6OiTP0PB/2qJqxYGcOH8= +github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79 h1:GvxShswn6tdYd9oYyaeCfREmCSlHy8lmesTcteXBeH0= +github.com/stuggi/lib-common/modules/common v0.0.0-20260331130034-f04bcb447a79/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= +github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136 h1:BT5CBFct9EfwblCKZyjMVE5vWITpHX9OFjKxTyuMk0I= +github.com/stuggi/mariadb-operator/api v0.0.0-20260331140344-c186516b2136/go.mod h1:cyeexUkEIgzQ3c1vVVv/DQ3AbnECfDwKdZteKC+sZKY= +github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b h1:+3dvtbudA/AfUkHh3uhz6Mb28y/vPy2hzTm3mQU0QSg= +github.com/stuggi/swift-operator/api v0.0.0-20260331140240-2d47591ad16b/go.mod h1:81OMzM3nKYvmpYrQ4egBzwiMjsh6To+eY8bFzW0zyoA= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/hack/export_operator_related_images.sh b/hack/export_operator_related_images.sh index 64fd5d657..0c1019061 100644 --- a/hack/export_operator_related_images.sh +++ b/hack/export_operator_related_images.sh @@ -2,23 +2,23 @@ export RELATED_IMAGE_BARBICAN_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/barbican-operator@sha256:8a7004a3835cbb93cdda6d3006cfe098b3333a6010344099a4dfbd2927280cc5 export RELATED_IMAGE_CINDER_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/cinder-operator@sha256:0c226cbc35bf93181b36bb7d0e5e1cd65d370d1f53c66895fdc73f9f84d5a06b -export RELATED_IMAGE_DESIGNATE_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/designate-operator@sha256:4c7f06f3d9676d55c6f6f726df750fe370f71756048184106e06713923612267 -export RELATED_IMAGE_GLANCE_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/glance-operator@sha256:f649f31aca138e78f72963c98bafaeb0da514133f1d731019552c76dad08394c +export RELATED_IMAGE_DESIGNATE_OPERATOR_MANAGER_IMAGE_URL=quay.io/mschuppe/designate-operator@sha256:06883bc94e99fea56d7230df05928fdb9f3cae72ba65c1fb7f3738d7cac79f51 +export RELATED_IMAGE_GLANCE_OPERATOR_MANAGER_IMAGE_URL=quay.io/mschuppe/glance-operator@sha256:d1aecaf675cf02a4f17d99a9847a1016ae5f57b9c7f78ead4d26c22283a5f8c5 export RELATED_IMAGE_HEAT_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/heat-operator@sha256:875a5a57ca5820e2b8b01463d5efbe0644afc288fcbb78898795d34637c1b7e2 export RELATED_IMAGE_HORIZON_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/horizon-operator@sha256:67c5689bf3ea12b55f2c76e8dbefad03a980a6545d46f16493004cdcff4bfee4 -export RELATED_IMAGE_INFRA_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/infra-operator@sha256:d5a153d4e0fd948855eb2f0d4d9e48b0a1f6cb4ee290490ec22a8d8a631b913b +export RELATED_IMAGE_INFRA_OPERATOR_MANAGER_IMAGE_URL=quay.io/mschuppe/infra-operator@sha256:7fafd4df6c0ebc5bdc10fa11406975d9654a72780dfd770217aedca7b5f6f7d9 export RELATED_IMAGE_IRONIC_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/ironic-operator@sha256:741c1bc38c0d9430995a0d0aae6adae5c1f490b23d620564595cdd40683df68b export RELATED_IMAGE_KEYSTONE_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/keystone-operator@sha256:9fd3681c6c8549a78b12dc5e83676bc0956558b01327b95598aa424d62acb189 export RELATED_IMAGE_MANILA_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/manila-operator@sha256:e5f1303497321c083933cd8ab46e0c95c3f7f3f4101e0c2c3df79eb089abab9e -export RELATED_IMAGE_MARIADB_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/mariadb-operator@sha256:16b276e3f22dc79232c2150ae53becd10ebcf2d9b883f7df4ff98a929eefac91 +export RELATED_IMAGE_MARIADB_OPERATOR_MANAGER_IMAGE_URL=quay.io/mschuppe/mariadb-operator@sha256:8347867b11e5ff96d8979b15f055f415077132460e20c2506c95b349b981e3be export RELATED_IMAGE_NEUTRON_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/neutron-operator@sha256:526f9d4965431e1a5e4f8c3224bcee3f636a3108a5e0767296a994c2a517404a export RELATED_IMAGE_NOVA_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/nova-operator@sha256:388c06cd947e4eaf823e3d64de2d3ba7660dbd9d4c01729d92bd628e5e73bc5f export RELATED_IMAGE_OCTAVIA_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/octavia-operator@sha256:b6d44a28b047f402b17b4cc07584f04cd6f1168d8742a9a8b17a9ce7c8550c5a export RELATED_IMAGE_OPENSTACK_BAREMETAL_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/openstack-baremetal-operator@sha256:3d084b1d36a44eee6d2412f49662a478752bdd7d930eda9ec4cb5a8169965d91 export RELATED_IMAGE_OVN_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/ovn-operator@sha256:4b983bc9e9cebbde8a781fdaaf774b8dd13bb30f66f323d94c2187707f6552d9 -export RELATED_IMAGE_PLACEMENT_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/placement-operator@sha256:b27cbefac3c9b9ecaf392314feff2c0065ebf7f835d225167a846e2f2224c352 +export RELATED_IMAGE_PLACEMENT_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/placement-operator@sha256:96eade4f229c073e64fb9ff9c5a8479c93078b1007469ac1ea7d8135e1d29946 export RELATED_IMAGE_RABBITMQ_CLUSTER_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/rabbitmq-cluster-operator@sha256:893e66303c1b0bc1d00a299a3f0380bad55c8dc813c8a1c6a4aab379f5aa12a2 -export RELATED_IMAGE_SWIFT_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/swift-operator@sha256:cbc03ca8837c64974a4670506a8df688c44432c4aab095f3fa7f1330e72bd3bd +export RELATED_IMAGE_SWIFT_OPERATOR_MANAGER_IMAGE_URL=quay.io/mschuppe/swift-operator@sha256:5231db233a72f97ea81d0bc6c13559450a3876875fec5685f347cb7afdd4b303 export RELATED_IMAGE_TELEMETRY_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/telemetry-operator@sha256:566b1f4d3f3d50e9620b845e12ef72bf3a27e07233a9c7424c1102045a4e74a2 export RELATED_IMAGE_TEST_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/test-operator@sha256:2c1ef8575d74ef938c900e7ea7e622afeb589db6b4dcf30da544cc5689775296 export RELATED_IMAGE_WATCHER_OPERATOR_MANAGER_IMAGE_URL=quay.io/openstack-k8s-operators/watcher-operator@sha256:cbfb984a8e275ea0a5f80f343d6650d7e9ac0aface0b4f7aa38a2de3b115153c diff --git a/internal/controller/backup/openstackbackupconfig_controller.go b/internal/controller/backup/openstackbackupconfig_controller.go new file mode 100644 index 000000000..86f7d19b3 --- /dev/null +++ b/internal/controller/backup/openstackbackupconfig_controller.go @@ -0,0 +1,664 @@ +/* +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. +*/ + +// 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" + + certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + + "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 +} + +// 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 + } + + // Find the served version (prefer storage version, fall back to first served) + 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, + }, 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 unless they have annotation overrides. +// Resources that already have a restore label (set by operators at creation time, +// e.g. cert-manager secrets) are skipped unless they have annotation overrides. +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) +} + +// labelIssuers labels cert-manager Issuers in the target namespace +func (r *OpenStackBackupConfigReconciler) labelIssuers(ctx context.Context, log logr.Logger, instance *backupv1beta1.OpenStackBackupConfig) (int, error) { + list := &certmgrv1.IssuerList{} + 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.Issuers, instance.Spec.DefaultRestoreOrder), "") + return r.labelResourceItems(ctx, log, items, instance.Spec.Issuers, 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 not populated at setup time + 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)) + } + + 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) + 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) + 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()) + continue + } + count++ + } + } + + return count, nil +} + +// +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=cert-manager.io,resources=issuers,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch +// +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.OpenStackBackupConfigIssuersReadyCondition, 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, + "Labeled %d secrets", 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, + "Labeled %d configmaps", 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, + "Labeled %d NADs", nadCount)) + } + + issuerCount, err := r.labelIssuers(ctx, log, instance) + if err != nil { + log.Error(err, "Failed to label issuers") + instance.Status.Conditions.Set(condition.FalseCondition( + backupv1beta1.OpenStackBackupConfigIssuersReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + "Failed to label issuers: %v", err)) + reconcileErrs = append(reconcileErrs, err) + } else { + instance.Status.Conditions.Set(condition.TrueCondition( + backupv1beta1.OpenStackBackupConfigIssuersReadyCondition, + "Labeled %d issuers", issuerCount)) + } + + // 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, + "Labeled %d CRs", crCount)) + } + + // Update status counts + instance.Status.LabeledResources.Secrets = secretCount + instance.Status.LabeledResources.ConfigMaps = configMapCount + instance.Status.LabeledResources.NetworkAttachmentDefinitions = nadCount + instance.Status.LabeledResources.Issuers = issuerCount + + if len(reconcileErrs) > 0 { + return ctrl.Result{}, stderrors.Join(reconcileErrs...) + } + + log.Info("Successfully labeled resources", "secrets", secretCount, "configmaps", configMapCount, "nads", nadCount, "issuers", issuerCount, "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 + }, +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenStackBackupConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + Log := ctrl.Log.WithName("backup").WithName("setup") + + // findBackupConfigForSrc maps a resource back to the BackupConfig that should process it + findBackupConfigForSrc := func(ctx context.Context, obj client.Object) []reconcile.Request { + configList := &backupv1beta1.OpenStackBackupConfigList{} + if err := mgr.GetClient().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 + } + + bldr := ctrl.NewControllerManagedBy(mgr). + For(&backupv1beta1.OpenStackBackupConfig{}). + Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)). + Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)). + Watches(&k8s_networkingv1.NetworkAttachmentDefinition{}, handler.EnqueueRequestsFromMapFunc(findBackupConfigForSrc), builder.WithPredicates(backupResourcePredicate)). + Watches(&certmgrv1.Issuer{}, handler.EnqueueRequestsFromMapFunc(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(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 + } + + 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, + }, nil +} diff --git a/test/functional/ctlplane/openstackbackupconfig_controller_test.go b/test/functional/ctlplane/openstackbackupconfig_controller_test.go new file mode 100644 index 000000000..a23e79dbf --- /dev/null +++ b/test/functional/ctlplane/openstackbackupconfig_controller_test.go @@ -0,0 +1,1031 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package 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" + + certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + 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"}, + }, + Issuers: backupv1.ResourceBackupConfig{ + Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled), + RestoreOrder: "20", + }, + 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.OpenStackBackupConfigIssuersReadyCondition, + 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("A custom cert-manager Issuer without ownerRef exists", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-issuers", + Namespace: namespace, + } + + // Create a custom Issuer (no ownerRef - user-provided) + issuer := &certmgrv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-custom-issuer", + Namespace: namespace, + }, + Spec: certmgrv1.IssuerSpec{ + IssuerConfig: certmgrv1.IssuerConfig{ + SelfSigned: &certmgrv1.SelfSignedIssuer{}, + }, + }, + } + Expect(k8sClient.Create(ctx, issuer)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, issuer) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should label the custom issuer for backup", func() { + Eventually(func(g Gomega) { + issuer := &certmgrv1.Issuer{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "my-custom-issuer", Namespace: namespace, + }, issuer)).Should(Succeed()) + + labels := issuer.GetLabels() + g.Expect(labels).NotTo(BeNil()) + g.Expect(labels[commonbackup.BackupRestoreLabel]).To(Equal("true")) + }, timeout, interval).Should(Succeed()) + }) + + It("Should set IssuersReady condition to True", func() { + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + backupv1.OpenStackBackupConfigIssuersReadyCondition, + k8s_corev1.ConditionTrue, + ) + }) + + It("Should update issuer count in status", func() { + Eventually(func(g Gomega) { + backupConfig := GetOpenStackBackupConfig(backupConfigName) + g.Expect(backupConfig.Status.LabeledResources.Issuers).To(BeNumerically(">=", 1)) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("An operator-created Issuer with ownerRef exists", func() { + BeforeEach(func() { + backupConfigName = types.NamespacedName{ + Name: "test-backup-issuer-ownerref", + Namespace: namespace, + } + + // Create an Issuer with ownerRef (simulating operator-created) + issuer := &certmgrv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rootca-internal", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "core.openstack.org/v1beta1", + Kind: "OpenStackControlPlane", + Name: "controlplane", + UID: "fake-uid", + }, + }, + }, + Spec: certmgrv1.IssuerSpec{ + IssuerConfig: certmgrv1.IssuerConfig{ + CA: &certmgrv1.CAIssuer{ + SecretName: "rootca-internal", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, issuer)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, issuer) + + backupConfig := CreateBackupConfig(backupConfigName) + DeferCleanup(th.DeleteInstance, backupConfig) + }) + + It("Should not label the operator-created issuer", func() { + // Wait for reconciliation + th.ExpectCondition( + backupConfigName, + ConditionGetterFunc(OpenStackBackupConfigConditionGetter), + condition.ReadyCondition, + k8s_corev1.ConditionTrue, + ) + + issuer := &certmgrv1.Issuer{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "rootca-internal", Namespace: namespace, + }, issuer)).Should(Succeed()) + + labels := issuer.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) + + // Create a custom issuer + issuer := &certmgrv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-test-issuer", + Namespace: namespace, + }, + Spec: certmgrv1.IssuerSpec{ + IssuerConfig: certmgrv1.IssuerConfig{ + SelfSigned: &certmgrv1.SelfSignedIssuer{}, + }, + }, + } + Expect(k8sClient.Create(ctx, issuer)).Should(Succeed()) + DeferCleanup(th.DeleteInstance, issuer) + + 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.OpenStackBackupConfigIssuersReadyCondition, + 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")) + + issuer := &certmgrv1.Issuer{} + g.Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: "multi-test-issuer", Namespace: namespace, + }, issuer)).Should(Succeed()) + g.Expect(issuer.GetLabels()[commonbackup.BackupRestoreLabel]).To(Equal("true")) + }, timeout, interval).Should(Succeed()) + }) + }) +}) diff --git a/test/functional/ctlplane/suite_test.go b/test/functional/ctlplane/suite_test.go index bfaab9671..01b637582 100644 --- a/test/functional/ctlplane/suite_test.go +++ b/test/functional/ctlplane/suite_test.go @@ -15,6 +15,7 @@ import ( . "github.com/onsi/gomega" //revive:disable:dot-imports "go.uber.org/zap/zapcore" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" @@ -29,6 +30,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + k8s_networkingv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" routev1 "github.com/openshift/api/route/v1" rabbitmqv2 "github.com/rabbitmq/cluster-operator/v2/api/v1beta1" @@ -48,6 +50,7 @@ import ( neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" novav1 "github.com/openstack-k8s-operators/nova-operator/api/nova/v1beta1" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" + backupv1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1" openstackclientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1" corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" dataplanev1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" @@ -58,6 +61,7 @@ import ( telemetryv1 "github.com/openstack-k8s-operators/telemetry-operator/api/v1beta1" watcherv1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + backup_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/backup" client_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/client" core_ctrl "github.com/openstack-k8s-operators/openstack-operator/internal/controller/core" @@ -187,6 +191,9 @@ var _ = BeforeSuite(func() { watcherCRDs, err := test.GetCRDDirFromModule( "github.com/openstack-k8s-operators/watcher-operator/api", gomod, "bases") Expect(err).ShouldNot(HaveOccurred()) + nadCRDs, err := test.GetCRDDirFromModule( + "github.com/k8snetworkplumbingwg/network-attachment-definition-client", gomod, "artifacts/networks-crd.yaml") + Expect(err).ShouldNot(HaveOccurred()) By("bootstrapping test environment") testEnv = &envtest.Environment{ @@ -220,6 +227,9 @@ var _ = BeforeSuite(func() { ControlPlaneStartTimeout: 2 * time.Minute, ControlPlaneStopTimeout: 2 * time.Minute, CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + nadCRDs, + }, MaxTime: 5 * time.Minute, }, ControlPlane: envtest.ControlPlane{ @@ -312,6 +322,12 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = watcherv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = backupv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = apiextensionsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = k8s_networkingv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -389,6 +405,15 @@ var _ = BeforeSuite(func() { }).SetupWithManager(ctx, k8sManager) Expect(err).ToNot(HaveOccurred()) + // Setup OpenStackBackupConfig controller + // CRD label cache is built lazily on first reconcile + err = (&backup_ctrl.OpenStackBackupConfigReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) From 4eafd75a856b4a59fac7ee57fcc69ccd57625532 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Tue, 31 Mar 2026 18:57:34 +0200 Subject: [PATCH 2/3] [b/r] Add backup/restore labels to ControlPlane controller Wire the BackupConfig reconciliation into the ControlPlane controller. Add backup/restore labels to CA cert secrets via SecretTemplate, and restore=false labels to internal service cert requests. Add the ReconcileBackupConfig call, secret watch with annotation change predicate, and RBAC for openstackbackupconfigs. Set BackupConfig spec defaults in the CreateOrPatch mutate function. Jira: OSPRH-22912 Jira: OSPRH-22913 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Schuppert --- ....openstack.org_openstackcontrolplanes.yaml | 4 + .../core.openstack.org_openstackversions.yaml | 4 + ...nstack.org_openstackdataplanenodesets.yaml | 4 + ...nstack.org_openstackdataplaneservices.yaml | 4 + .../v1beta1/openstackcontrolplane_types.go | 3 + api/core/v1beta1/openstackversion_types.go | 3 + .../openstackdataplanenodeset_types.go | 5 +- .../openstackdataplaneservice_types.go | 3 + ....openstack.org_openstackcontrolplanes.yaml | 4 + .../core.openstack.org_openstackversions.yaml | 4 + ...nstack.org_openstackdataplanenodesets.yaml | 4 + ...nstack.org_openstackdataplaneservices.yaml | 4 + .../core/openstackcontrolplane_controller.go | 36 +++++++ internal/dataplane/cert.go | 10 +- internal/openstack/backup.go | 101 ++++++++++++++++++ internal/openstack/ca.go | 9 +- internal/openstack/common.go | 34 ++++-- internal/openstack/galera.go | 7 +- internal/openstack/memcached.go | 13 ++- internal/openstack/neutron.go | 7 +- internal/openstack/nova.go | 13 ++- internal/openstack/octavia.go | 7 +- internal/openstack/ovn.go | 25 +++-- internal/openstack/rabbitmq.go | 7 +- internal/openstack/redis.go | 6 +- 25 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 internal/openstack/backup.go diff --git a/api/bases/core.openstack.org_openstackcontrolplanes.yaml b/api/bases/core.openstack.org_openstackcontrolplanes.yaml index 57e1db0a0..6c3236246 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 ae79d6c77..fe0f9bd4b 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 5eead8763..53be12829 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 d594951dd..917bae442 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/openstackcontrolplane_types.go b/api/core/v1beta1/openstackcontrolplane_types.go index f49e6b47e..3477c6de6 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 { diff --git a/api/core/v1beta1/openstackversion_types.go b/api/core/v1beta1/openstackversion_types.go index 1f138156d..122a3164e 100644 --- a/api/core/v1beta1/openstackversion_types.go +++ b/api/core/v1beta1/openstackversion_types.go @@ -215,6 +215,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 c4ab4c4c9..256e7462d 100644 --- a/api/dataplane/v1beta1/openstackdataplanenodeset_types.go +++ b/api/dataplane/v1beta1/openstackdataplanenodeset_types.go @@ -94,10 +94,13 @@ 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 -type OpenStackDataPlaneNodeSet struct { +type OpenStackDataPlaneNodeSet struct{ metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/dataplane/v1beta1/openstackdataplaneservice_types.go b/api/dataplane/v1beta1/openstackdataplaneservice_types.go index b1205a9ba..d613f4fab 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/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml index 57e1db0a0..6c3236246 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 ae79d6c77..fe0f9bd4b 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 5eead8763..53be12829 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 d594951dd..917bae442 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/internal/controller/core/openstackcontrolplane_controller.go b/internal/controller/core/openstackcontrolplane_controller.go index 8a96dff22..49806482b 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 @@ -248,6 +250,15 @@ func (r *OpenStackControlPlaneReconciler) Reconcile(ctx context.Context, req ctr return ctrlResult, nil } + // Automatically create OpenStackBackupConfig CR for this controlplane + Log.Info("Reconciling OpenStackBackupConfig") + _, _, err = openstack.ReconcileBackupConfig(ctx, instance, helper) + if err != nil { + Log.Error(err, "Failed to reconcile OpenStackBackupConfig") + // Don't fail the reconcile, just log the error - backup config is not critical for controlplane + // The user can manually create it if needed + } + versionHelper, err := common_helper.NewHelper( version, r.Client, @@ -870,6 +881,11 @@ func (r *OpenStackControlPlaneReconciler) SetupWithManager( handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.findControlPlaneForSrc), + builder.WithPredicates(backup.AnnotationChangedPredicate(openstack.ServiceCertSelector)), + ). Complete(r) } @@ -907,6 +923,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/dataplane/cert.go b/internal/dataplane/cert.go index 28d9a3ca9..dd0555b10 100644 --- a/internal/dataplane/cert.go +++ b/internal/dataplane/cert.go @@ -37,6 +37,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" "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/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" "github.com/openstack-k8s-operators/lib-common/modules/common/util" @@ -114,10 +115,11 @@ func EnsureTLSCerts(ctx context.Context, helper *helper.Helper, // For now we just add the hostname so we can select all the certs on one node hostName := node.HostName labels := map[string]string{ - HostnameLabel: hostName, - ServiceLabel: service.Name, - ServiceKeyLabel: certKey, - NodeSetLabel: instance.Name, + HostnameLabel: hostName, + ServiceLabel: service.Name, + ServiceKeyLabel: certKey, + NodeSetLabel: instance.Name, + backup.BackupRestoreLabel: "false", } certName = service.Name + "-" + certKey + "-" + hostName diff --git a/internal/openstack/backup.go b/internal/openstack/backup.go new file mode 100644 index 000000000..0049ff8e0 --- /dev/null +++ b/internal/openstack/backup.go @@ -0,0 +1,101 @@ +/* +Copyright 2025 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" + + helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + 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) + + // return if OpenStackBackupConfig CR already exists + if err := helper.GetClient().Get(ctx, types.NamespacedName{ + Name: instance.Name, + Namespace: instance.Namespace, + }, + backupConfig); err == nil { + Log.Info(fmt.Sprintf("OpenStackBackupConfig found. Name: %s", backupConfig.Name)) + } else { + Log.Info(fmt.Sprintf("OpenStackBackupConfig does not exist. Creating: %s", backupConfig.Name)) + } + + 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. 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. + if backupConfig.Spec.DefaultRestoreOrder == "" { + backupConfig.Spec.DefaultRestoreOrder = "10" + } + 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 + } + if backupConfig.Spec.Issuers.Labeling == nil { + backupConfig.Spec.Issuers.Labeling = &defaultLabeling + } + if backupConfig.Spec.Issuers.RestoreOrder == "" { + backupConfig.Spec.Issuers.RestoreOrder = "20" + } + + 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 f70598fa1..23c1e2c1c 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) diff --git a/internal/openstack/common.go b/internal/openstack/common.go index 07d0f6ecd..b08ae275f 100644 --- a/internal/openstack/common.go +++ b/internal/openstack/common.go @@ -21,6 +21,7 @@ import ( 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/certmanager" + "github.com/openstack-k8s-operators/lib-common/modules/common/backup" "github.com/openstack-k8s-operators/lib-common/modules/common/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -64,8 +65,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" @@ -81,6 +82,18 @@ func GetLogger(ctx context.Context) logr.Logger { return log.FromContext(ctx).WithName("Controllers").WithName("OpenstackControlPlane") } +// getCertSecretBackupLabels returns backup labels for a cert secret, respecting +// annotation overrides. Delegates to backup.GetCertSecretBackupLabels in lib-common. +func getCertSecretBackupLabels( + ctx context.Context, + c client.Client, + certName string, + namespace string, + defaultLabels map[string]string, +) map[string]string { + return backup.GetCertSecretBackupLabels(ctx, c, certName, namespace, defaultLabels) +} + // EnsureDeleted - Delete the object which in turn will clean the sub resources func EnsureDeleted(ctx context.Context, helper *helper.Helper, obj client.Object) (ctrl.Result, error) { key := client.ObjectKeyFromObject(obj) @@ -331,8 +344,9 @@ func EnsureEndpointConfig( }, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), - Usages: nil, + Labels: util.MergeMaps(ed.Labels, getCertSecretBackupLabels(ctx, helper.GetClient(), ed.Service.TLS.CertName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"})), + Usages: nil, } addSubjNames := util.GetStringListFromMap(svc.Annotations, tls.AdditionalSubjectNamesKey) @@ -381,8 +395,9 @@ func EnsureEndpointConfig( }, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), - Usages: nil, + Labels: util.MergeMaps(ed.Labels, getCertSecretBackupLabels(ctx, helper.GetClient(), ed.Service.TLS.CertName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"})), + Usages: nil, } addSubjNames := util.GetStringListFromMap(svc.Annotations, tls.AdditionalSubjectNamesKey) @@ -638,8 +653,9 @@ func (ed *EndpointDetail) CreateRoute( Hostnames: []string{*ed.Hostname}, Ips: nil, Annotations: ed.Annotations, - Labels: util.MergeMaps(ed.Labels, map[string]string{serviceCertSelector: ""}), - Usages: nil, + Labels: util.MergeMaps(ed.Labels, getCertSecretBackupLabels(ctx, helper.GetClient(), ed.Route.TLS.CertName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"})), + Usages: nil, } if instance.Spec.TLS.Ingress.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.Ingress.Cert.Duration.Duration @@ -945,7 +961,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 d4cf2dce1..60ff9365d 100644 --- a/internal/openstack/galera.go +++ b/internal/openstack/galera.go @@ -10,6 +10,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -117,9 +118,10 @@ func ReconcileGaleras( // Galera gets always configured to support TLS connections. // If TLS can/must be used is a per user configuration. + certName := fmt.Sprintf("galera-%s-svc", name) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("galera-%s-svc", name), + CertName: certName, Hostnames: []string{ hostname, fmt.Sprintf("%s.%s", hostname, clusterDomain), @@ -142,7 +144,8 @@ func ReconcileGaleras( "server auth", "client auth", }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } 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 cccdee0e1..589096f6e 100644 --- a/internal/openstack/memcached.go +++ b/internal/openstack/memcached.go @@ -10,6 +10,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" "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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -204,16 +205,18 @@ func reconcileMemcached( if instance.Spec.TLS.PodLevel.Enabled { Log.Info("Reconciling Memcached TLS", "Memcached.Namespace", instance.Namespace, "Memcached.Name", name) clusterDomain := clusterdns.GetDNSClusterDomain() + certName := fmt.Sprintf("%s-svc", memcached.Name) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("%s-svc", memcached.Name), + CertName: certName, Hostnames: []string{ fmt.Sprintf("%s.%s.svc", name, instance.Namespace), fmt.Sprintf("*.%s.%s.svc", name, instance.Namespace), 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: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration @@ -238,14 +241,16 @@ func reconcileMemcached( if spec.TLS.MTLS.SslVerifyMode == "Request" || spec.TLS.MTLS.SslVerifyMode == "Require" { Log.Info("Reconciling Memcached mTLS", "Memcached.Namespace", instance.Namespace, "Memcached.Name", name) clusterDomain = clusterdns.GetDNSClusterDomain() + mtlsCertName := fmt.Sprintf("%s-mtls", memcached.Name) certRequest = certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("%s-mtls", memcached.Name), + CertName: mtlsCertName, Hostnames: []string{ fmt.Sprintf("*.%s.svc", instance.Namespace), fmt.Sprintf("*.%s.svc.%s", instance.Namespace, clusterDomain), }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), mtlsCertName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), Usages: []certmgrv1.KeyUsage{ certmgrv1.UsageKeyEncipherment, certmgrv1.UsageDigitalSignature, diff --git a/internal/openstack/neutron.go b/internal/openstack/neutron.go index 45479df0e..ca7afdb7f 100644 --- a/internal/openstack/neutron.go +++ b/internal/openstack/neutron.go @@ -6,6 +6,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -78,9 +79,10 @@ func ReconcileNeutron(ctx context.Context, instance *corev1beta1.OpenStackContro serviceName := "neutron" clusterDomain := clusterdns.GetDNSClusterDomain() // create ovndb client certificate for neutron + certName := fmt.Sprintf("%s-ovndbs", serviceName) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: fmt.Sprintf("%s-ovndbs", serviceName), + CertName: certName, Hostnames: []string{ fmt.Sprintf("%s.%s.svc", serviceName, instance.Namespace), fmt.Sprintf("%s.%s.svc.%s", serviceName, instance.Namespace, clusterDomain), @@ -91,7 +93,8 @@ func ReconcileNeutron(ctx context.Context, instance *corev1beta1.OpenStackContro certmgrv1.UsageDigitalSignature, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } 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 59ee3e04f..c7117592a 100644 --- a/internal/openstack/nova.go +++ b/internal/openstack/nova.go @@ -21,6 +21,7 @@ import ( "fmt" "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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -273,7 +274,8 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl nova.Namespace, instance.Spec.Nova.Template.MetadataServiceTemplate.Override.Service.Labels, instance.GetInternalIssuer(), - nil) + nil, + map[string]string{backup.BackupRestoreLabel: "false"}) if err != nil && !k8s_errors.IsNotFound(err) { return ctrlResult, err } else if (ctrlResult != ctrl.Result{}) { @@ -296,7 +298,8 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl nova.Namespace, cellTemplate.MetadataServiceTemplate.Override.Service.Labels, instance.GetInternalIssuer(), - nil) + nil, + map[string]string{backup.BackupRestoreLabel: "false"}) if err != nil && !k8s_errors.IsNotFound(err) { return ctrlResult, err } else if (ctrlResult != ctrl.Result{}) { @@ -361,9 +364,10 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl clusterDomain := clusterdns.GetDNSClusterDomain() serviceName := endpointDetails.EndpointDetails[service.EndpointPublic].Service.Spec.Name hostname := fmt.Sprintf("%s.%s.svc", serviceName, instance.Namespace) + certName := nova.Name + "-novncproxy-" + cellName + "-vencrypt" certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetLibvirtIssuer(), - CertName: nova.Name + "-novncproxy-" + cellName + "-vencrypt", + CertName: certName, CommonName: ptr.To(serviceName), // common name has a max length of 64bytes, therefore just set the short name Hostnames: []string{ hostname, @@ -378,7 +382,8 @@ func ReconcileNova(ctx context.Context, instance *corev1beta1.OpenStackControlPl certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } 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 825cdd9da..5375092b3 100644 --- a/internal/openstack/octavia.go +++ b/internal/openstack/octavia.go @@ -22,6 +22,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -119,9 +120,10 @@ func ReconcileOctavia(ctx context.Context, instance *corev1beta1.OpenStackContro serviceName := "octavia" // create ovndb client certificate for octavia + certName := fmt.Sprintf("%s-ovndbs", serviceName) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: fmt.Sprintf("%s-ovndbs", serviceName), + CertName: certName, Hostnames: []string{ fmt.Sprintf("%s.%s.svc", serviceName, instance.Namespace), fmt.Sprintf("%s.%s.svc.%s", serviceName, instance.Namespace, clusterDomain), @@ -132,7 +134,8 @@ func ReconcileOctavia(ctx context.Context, instance *corev1beta1.OpenStackContro certmgrv1.UsageDigitalSignature, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } 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 7a13dd1f2..21e7120db 100644 --- a/internal/openstack/ovn.go +++ b/internal/openstack/ovn.go @@ -6,6 +6,7 @@ import ( "reflect" "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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -165,9 +166,10 @@ func ReconcileOVNDbClusters(ctx context.Context, instance *corev1beta1.OpenStack if instance.Spec.TLS.PodLevel.Enabled { // create certificate for ovndbclusters + certName := fmt.Sprintf("%s-ovndbs", name) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: fmt.Sprintf("%s-ovndbs", name), + CertName: certName, // Cert needs to be valid for the individual pods in the statefulset so make this a wildcard cert Hostnames: []string{ fmt.Sprintf("*.%s.svc", instance.Namespace), @@ -180,7 +182,8 @@ func ReconcileOVNDbClusters(ctx context.Context, instance *corev1beta1.OpenStack certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -306,9 +309,10 @@ func ReconcileOVNNorthd(ctx context.Context, instance *corev1beta1.OpenStackCont serviceName := ovnv1.ServiceNameOvnNorthd // create certificate for ovnnorthd + certName := fmt.Sprintf("%s-ovndbs", "ovnnorthd") certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: fmt.Sprintf("%s-ovndbs", "ovnnorthd"), + CertName: certName, Hostnames: []string{ fmt.Sprintf("%s.%s.svc", serviceName, instance.Namespace), fmt.Sprintf("%s.%s.svc.%s", serviceName, instance.Namespace, dnsSuffix), @@ -320,7 +324,8 @@ func ReconcileOVNNorthd(ctx context.Context, instance *corev1beta1.OpenStackCont certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -450,9 +455,10 @@ func ReconcileOVNController(ctx context.Context, instance *corev1beta1.OpenStack serviceName := ovnv1.ServiceNameOvnController // create certificate for ovncontroller + certName := fmt.Sprintf("%s-ovndbs", "ovncontroller") certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: fmt.Sprintf("%s-ovndbs", "ovncontroller"), + CertName: certName, Hostnames: []string{ fmt.Sprintf("%s.%s.svc", serviceName, instance.Namespace), fmt.Sprintf("%s.%s.svc.%s", serviceName, instance.Namespace, dnsSuffix), @@ -464,7 +470,8 @@ func ReconcileOVNController(ctx context.Context, instance *corev1beta1.OpenStack certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } if instance.Spec.TLS.PodLevel.Ovn.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Ovn.Cert.Duration.Duration @@ -592,9 +599,10 @@ func EnsureOVNMetricsCert(ctx context.Context, instance *corev1beta1.OpenStackCo dnsSuffix := clusterdns.GetDNSClusterDomain() + certName := "ovn-metrics" certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetOvnIssuer(), - CertName: "ovn-metrics", + CertName: certName, Hostnames: []string{ // Cert needs to be valid for the individual pods services so make this a wildcard cert fmt.Sprintf("*.%s.svc", instance.Namespace), @@ -607,7 +615,8 @@ func EnsureOVNMetricsCert(ctx context.Context, instance *corev1beta1.OpenStackCo certmgrv1.UsageServerAuth, certmgrv1.UsageClientAuth, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } // Apply certificate duration settings if configured diff --git a/internal/openstack/rabbitmq.go b/internal/openstack/rabbitmq.go index 742b92ac8..994d614b2 100644 --- a/internal/openstack/rabbitmq.go +++ b/internal/openstack/rabbitmq.go @@ -10,6 +10,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" "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/clusterdns" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -207,9 +208,10 @@ func reconcileRabbitMQ( tlsCert := "" if instance.Spec.TLS.PodLevel.Enabled { + certName := fmt.Sprintf("%s-svc", rabbitmq.Name) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("%s-svc", rabbitmq.Name), + CertName: certName, Hostnames: hostnames, Subject: &certmgrv1.X509Subject{ Organizations: []string{fmt.Sprintf("%s.%s", rabbitmq.Namespace, clusterDomain)}, @@ -222,7 +224,8 @@ func reconcileRabbitMQ( certmgrv1.UsageClientAuth, certmgrv1.UsageContentCommitment, }, - Labels: map[string]string{serviceCertSelector: ""}, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } 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 6d9154c50..428fc49c1 100644 --- a/internal/openstack/redis.go +++ b/internal/openstack/redis.go @@ -10,6 +10,7 @@ import ( certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" redisv1 "github.com/openstack-k8s-operators/infra-operator/apis/redis/v1beta1" "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/clusterdns" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -215,9 +216,10 @@ func reconcileRedis( tlsCert := "" if instance.Spec.TLS.PodLevel.Enabled { clusterDomain := clusterdns.GetDNSClusterDomain() + certName := fmt.Sprintf("%s-svc", redis.Name) certRequest := certmanager.CertificateRequest{ IssuerName: instance.GetInternalIssuer(), - CertName: fmt.Sprintf("%s-svc", redis.Name), + CertName: certName, Hostnames: []string{ fmt.Sprintf("redis-%s.%s.svc", name, instance.Namespace), fmt.Sprintf("*.redis-%s.%s.svc", name, instance.Namespace), @@ -233,6 +235,8 @@ func reconcileRedis( "server auth", "client auth", }, + Labels: getCertSecretBackupLabels(ctx, helper.GetClient(), certName, instance.Namespace, + map[string]string{ServiceCertSelector: "", backup.BackupRestoreLabel: "false"}), } if instance.Spec.TLS.PodLevel.Internal.Cert.Duration != nil { certRequest.Duration = &instance.Spec.TLS.PodLevel.Internal.Cert.Duration.Duration From 2e3227a4bd81d408c9259948bccee2453c783333 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Fri, 6 Mar 2026 16:40:58 +0100 Subject: [PATCH 3/3] Fix pin-bundle-images.sh IMAGENAMESPACE behavior Revert IMAGENAMESPACE behavior to pre-507cdb80 logic where it only affects replaced operators (non-openstack-k8s-operators) and operators matching IMAGEBASE. Standard openstack-k8s-operators bundles now always use quay.io/openstack-k8s-operators regardless of IMAGENAMESPACE. This fixes the issue where setting IMAGENAMESPACE to a custom value would try to find all bundles (barbican, cinder, etc.) in the custom namespace instead of quay.io/openstack-k8s-operators, causing failures when those bundles don't exist there. The previous behavior is restored: - IMAGENAMESPACE only affects replaced operators (e.g., custom forks) - IMAGEBASE explicitly targets a specific operator for custom namespace - Standard operators always use openstack-k8s-operators namespace All bug fixes from 507cdb80 are retained (better error handling, jq safety checks, proper query string syntax). Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Martin Schuppert --- hack/pin-bundle-images.sh | 63 +++++++++++---------------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/hack/pin-bundle-images.sh b/hack/pin-bundle-images.sh index b85726c5b..05075341f 100755 --- a/hack/pin-bundle-images.sh +++ b/hack/pin-bundle-images.sh @@ -43,62 +43,33 @@ for MOD_PATH in ${MOD_PATHS}; do CURL_REGISTRY="quay.io" REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/openstack-k8s-operators" REPO_URL="${CURL_REGISTRY}/openstack-k8s-operators" - - # IMAGEBASE takes precedence - if this operator matches IMAGEBASE, use custom registry - if [[ "$BASE" == "$IMAGEBASE" ]]; then - REPO_URL="${IMAGEREGISTRY}/${IMAGENAMESPACE}" - CURL_REGISTRY="${IMAGEREGISTRY}" - if [[ ${LOCAL_REGISTRY} -eq 1 ]]; then - REPO_CURL_URL="http://${CURL_REGISTRY}/v2/${IMAGENAMESPACE}" - elif [[ "${CURL_REGISTRY}" == "docker.io" ]]; then - REPO_CURL_URL="https://hub.docker.com/v2/repositories/${IMAGENAMESPACE}" - else - REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/${IMAGENAMESPACE}" - fi - # For operators with replace directives (non-openstack-k8s-operators users), - # bundle images are only on quay.io, not mirrored to local registry - elif [[ "$GITHUB_USER" != "openstack-k8s-operators" ]]; then - # Force quay.io for replaced operators, use the GitHub user's namespace - CURL_REGISTRY="quay.io" - REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/${GITHUB_USER}" - REPO_URL="${CURL_REGISTRY}/${GITHUB_USER}" - # For standard operators with custom registry settings - elif [[ "$IMAGENAMESPACE" != "openstack-k8s-operators" || "${IMAGEREGISTRY}" != "quay.io" ]]; then - REPO_URL="${IMAGEREGISTRY}/${IMAGENAMESPACE}" - CURL_REGISTRY="${IMAGEREGISTRY}" - # Quay registry v2 api does not return all the tags that's why keeping v1 for quay and v2 - # for local registry - if [[ ${LOCAL_REGISTRY} -eq 1 ]]; then - REPO_CURL_URL="http://${CURL_REGISTRY}/v2/${IMAGENAMESPACE}" - elif [[ "${CURL_REGISTRY}" == "docker.io" ]]; then - # replace docker.io by hub.docker.com to read tags - REPO_CURL_URL="https://hub.docker.com/v2/repositories/${IMAGENAMESPACE}" + if [[ "$GITHUB_USER" != "openstack-k8s-operators" || "$BASE" == "$IMAGEBASE" ]]; then + if [[ "$IMAGENAMESPACE" != "openstack-k8s-operators" || "${IMAGEREGISTRY}" != "quay.io" ]]; then + REPO_URL="${IMAGEREGISTRY}/${IMAGENAMESPACE}" + CURL_REGISTRY="${IMAGEREGISTRY}" + # Quay registry v2 api does not return all the tags that's why keeping v1 for quay and v2 + # for local registry + if [[ ${LOCAL_REGISTRY} -eq 1 ]]; then + REPO_CURL_URL="http://${CURL_REGISTRY}/v2/${IMAGENAMESPACE}" + elif [[ "${CURL_REGISTRY}" == "docker.io" ]]; then + # replace docker.io by hub.docker.com to read tags + REPO_CURL_URL="https://hub.docker.com/v2/repositories/${IMAGENAMESPACE}" + else + REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/${IMAGENAMESPACE}" + fi else - REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/${IMAGENAMESPACE}" + REPO_CURL_URL="https://${CURL_REGISTRY}/api/v1/repository/${GITHUB_USER}" + REPO_URL="${CURL_REGISTRY}/${GITHUB_USER}" fi fi - # Query local registry only for standard operators (openstack-k8s-operators) or custom IMAGEBASE - # Replaced operators (e.g., lmiccini/*) always query quay.io since bundles aren't mirrored locally - if [[ ${LOCAL_REGISTRY} -eq 1 && ( "$GITHUB_USER" == "openstack-k8s-operators" || "$BASE" == "$IMAGEBASE" ) ]]; then + if [[ ${LOCAL_REGISTRY} -eq 1 && ( "$GITHUB_USER" != "openstack-k8s-operators" || "$BASE" == "$IMAGEBASE" ) ]]; then SHA=$(curl -s ${REPO_CURL_URL}/$BASE-operator-bundle/tags/list | jq -r '.tags // [] | .[]' | sort -u | { grep $REF || true; }) - # If local registry doesn't have the bundle, fall back to quay.io - if [ -z "$SHA" ]; then - SHA=$(curl -s https://quay.io/api/v1/repository/openstack-k8s-operators/$BASE-operator-bundle/tag/?onlyActiveTags=true\&filter_tag_name=like:$REF | jq -r '.tags // [] | .[].name') - # Update REPO_URL to use quay.io since we're falling back - REPO_URL="quay.io/openstack-k8s-operators" - fi elif [[ "${CURL_REGISTRY}" == "docker.io" ]]; then SHA=$(curl -s ${REPO_CURL_URL}/$BASE-operator-bundle/tags/?page_size=100 | jq -r '.results // [] | .[].name' | sort -u | { grep $REF || true; }) elif [[ "${CURL_REGISTRY}" != "quay.io" ]]; then # quay.rdoproject.io doesn't support filter_tag_name, so increase limit to 100 SHA=$(curl -s ${REPO_CURL_URL}/$BASE-operator-bundle/tag/?onlyActiveTags=true\&limit=100 | jq -r '.tags // [] | .[].name' | sort -u | { grep $REF || true; }) - # If non-quay.io registry doesn't have the bundle for openstack-k8s-operators, fall back to quay.io - if [[ -z "$SHA" && "$GITHUB_USER" == "openstack-k8s-operators" ]]; then - SHA=$(curl -s https://quay.io/api/v1/repository/openstack-k8s-operators/$BASE-operator-bundle/tag/?onlyActiveTags=true\&filter_tag_name=like:$REF | jq -r '.tags // [] | .[].name') - # Update REPO_URL to use quay.io since we're falling back - REPO_URL="quay.io/openstack-k8s-operators" - fi else SHA=$(curl -s ${REPO_CURL_URL}/$BASE-operator-bundle/tag/?onlyActiveTags=true\&filter_tag_name=like:$REF | jq -r '.tags // [] | .[].name') fi