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/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/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/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/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/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 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/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 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)