From 6dc5d420b9e81b6474afb9496f0b3253021a881b Mon Sep 17 00:00:00 2001 From: Alexander Dejanovski Date: Tue, 23 Jan 2024 20:15:53 +0100 Subject: [PATCH] Create the MedusaConfiguration API (#1180) --- .github/workflows/kind_e2e_tests.yaml | 1 + CHANGELOG/CHANGELOG-1.12.md | 2 + PROJECT | 13 ++ .../v1alpha1/medusaconfiguration_types.go | 118 ++++++++++ apis/medusa/v1alpha1/zz_generated.deepcopy.go | 98 ++++++++ ...usa.k8ssandra.io_medusaconfigurations.yaml | 220 ++++++++++++++++++ config/crd/kustomization.yaml | 3 + ...ection_in_medusa_medusaconfigurations.yaml | 7 + ...ebhook_in_medusa_medusaconfigurations.yaml | 16 ++ ...edusa_medusaconfiguration_editor_role.yaml | 31 +++ ...edusa_medusaconfiguration_viewer_role.yaml | 27 +++ config/rbac/role.yaml | 26 +++ config/samples/kustomization.yaml | 1 + .../medusa_v1alpha1_medusaconfiguration.yaml | 20 ++ controllers/medusa/controllers_test.go | 76 ++++++ .../medusa/medusaconfiguration_controller.go | 121 ++++++++++ .../medusaconfiguration_controller_test.go | 169 ++++++++++++++ .../content/en/tasks/backup-restore/_index.md | 23 ++ go.mod | 5 + go.sum | 8 +- main.go | 7 + test/e2e/medusa_test.go | 23 ++ test/e2e/suite_test.go | 30 ++- .../medusa-configuration/kustomization.yaml | 4 + .../medusa-configuration.yaml | 18 ++ 25 files changed, 1053 insertions(+), 14 deletions(-) create mode 100644 apis/medusa/v1alpha1/medusaconfiguration_types.go create mode 100644 config/crd/bases/medusa.k8ssandra.io_medusaconfigurations.yaml create mode 100644 config/crd/patches/cainjection_in_medusa_medusaconfigurations.yaml create mode 100644 config/crd/patches/webhook_in_medusa_medusaconfigurations.yaml create mode 100644 config/rbac/medusa_medusaconfiguration_editor_role.yaml create mode 100644 config/rbac/medusa_medusaconfiguration_viewer_role.yaml create mode 100644 config/samples/medusa_v1alpha1_medusaconfiguration.yaml create mode 100644 controllers/medusa/medusaconfiguration_controller.go create mode 100644 controllers/medusa/medusaconfiguration_controller_test.go create mode 100644 test/testdata/fixtures/medusa-configuration/kustomization.yaml create mode 100644 test/testdata/fixtures/medusa-configuration/medusa-configuration.yaml diff --git a/.github/workflows/kind_e2e_tests.yaml b/.github/workflows/kind_e2e_tests.yaml index 28f2c405c..62d4289cd 100644 --- a/.github/workflows/kind_e2e_tests.yaml +++ b/.github/workflows/kind_e2e_tests.yaml @@ -103,6 +103,7 @@ jobs: - PerNodeConfig/UserDefined - RemoveLocalDcFromCluster - AddDcToClusterSameDataplane + - CreateMedusaConfiguration fail-fast: false name: ${{ matrix.e2e_test }} env: diff --git a/CHANGELOG/CHANGELOG-1.12.md b/CHANGELOG/CHANGELOG-1.12.md index 411c45663..db805daaf 100644 --- a/CHANGELOG/CHANGELOG-1.12.md +++ b/CHANGELOG/CHANGELOG-1.12.md @@ -14,8 +14,10 @@ Changelog for the K8ssandra Operator, new PRs should update the `unreleased` sec When cutting a new release, update the `unreleased` heading to the tag being generated and date, like `## vX.Y.Z - YYYY-MM-DD` and create a new placeholder section for `unreleased` entries. ## unreleased + * [CHANGE] [#1050](https://github.com/k8ssandra/k8ssandra-operator/issues/1050) Remove unnecessary requeues in the Medusa controllers * [CHANGE] [#1165](https://github.com/k8ssandra/k8ssandra-operator/issues/1165) Upgrade to Medusa v0.17.1 +* [FEATURE] [#1157](https://github.com/k8ssandra/k8ssandra-operator/issues/1157) Add the MedusaConfiguration API * [FEATURE] [#1165](https://github.com/k8ssandra/k8ssandra-operator/issues/1165) Expose Medusa ssl_verify option to allow disabling cert verification for some on prem S3 compatible systems * [ENHANCEMENT] [#1094](https://github.com/k8ssandra/k8ssandra-operator/issues/1094) Expose AdditionalAnnotations field for cassDC. * [ENHANCEMENT] [#1160](https://github.com/k8ssandra/k8ssandra-operator/issues/1160) Allow disabling Reaper front-end auth. diff --git a/PROJECT b/PROJECT index 6f0569619..52bc34cee 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: k8ssandra.io layout: - go.kubebuilder.io/v3 @@ -130,4 +134,13 @@ resources: kind: K8ssandraTask path: github.com/k8ssandra/k8ssandra-operator/apis/control/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: k8ssandra.io + group: medusa + kind: MedusaConfiguration + path: github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1 + version: v1alpha1 version: "3" diff --git a/apis/medusa/v1alpha1/medusaconfiguration_types.go b/apis/medusa/v1alpha1/medusaconfiguration_types.go new file mode 100644 index 000000000..94a72c1f5 --- /dev/null +++ b/apis/medusa/v1alpha1/medusaconfiguration_types.go @@ -0,0 +1,118 @@ +/* +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MedusaConfigurationSpec defines the desired state of MedusaConfiguration +type MedusaConfigurationSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // StorageProperties defines the storage backend settings to use for the backups. + StorageProperties Storage `json:"storageProperties,omitempty"` +} + +type MedusaConfigurationConditionType string + +const ( + ControlStatusSecretAvailable MedusaConfigurationConditionType = "SecretAvailable" + ControlStatusReady MedusaConfigurationConditionType = "Ready" +) + +// MedusaConfigurationStatus defines the observed state of MedusaConfiguration +type MedusaConfigurationStatus struct { + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// SetCondition sets the condition with the given type to the given status. +// Returns true if the condition was changed. +func (m *MedusaConfigurationStatus) SetCondition(msg MedusaConfigurationConditionType, status metav1.ConditionStatus) bool { + condition := metav1.Condition{ + Type: string(msg), + Status: status, + Reason: string(msg), + Message: string(msg), + LastTransitionTime: metav1.Now(), + } + found := false + updated := false + for i, c := range m.Conditions { + if c.Type == string(msg) { + found = true + if c.Status == status { + continue + } + m.Conditions[i] = condition + updated = true + } + } + if !found { + m.Conditions = append(m.Conditions, condition) + updated = true + } + return updated +} + +func (m *MedusaConfigurationStatus) SetConditionMessage(msg MedusaConfigurationConditionType, message string) { + for i, c := range m.Conditions { + if c.Type == string(msg) { + m.Conditions[i].Message = message + return + } + } +} + +func (m *MedusaConfigurationStatus) GetCondition(msg MedusaConfigurationConditionType) *metav1.Condition { + for _, c := range m.Conditions { + if c.Type == string(msg) { + return &c + } + } + return nil +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// MedusaConfiguration is the Schema for the medusaconfigurations API +type MedusaConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MedusaConfigurationSpec `json:"spec,omitempty"` + Status MedusaConfigurationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MedusaConfigurationList contains a list of MedusaConfiguration +type MedusaConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MedusaConfiguration `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MedusaConfiguration{}, &MedusaConfigurationList{}) +} diff --git a/apis/medusa/v1alpha1/zz_generated.deepcopy.go b/apis/medusa/v1alpha1/zz_generated.deepcopy.go index 20d88551c..918071f1a 100644 --- a/apis/medusa/v1alpha1/zz_generated.deepcopy.go +++ b/apis/medusa/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,7 @@ package v1alpha1 import ( "github.com/k8ssandra/k8ssandra-operator/pkg/images" "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -415,6 +416,103 @@ func (in *MedusaClusterTemplate) DeepCopy() *MedusaClusterTemplate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MedusaConfiguration) DeepCopyInto(out *MedusaConfiguration) { + *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 MedusaConfiguration. +func (in *MedusaConfiguration) DeepCopy() *MedusaConfiguration { + if in == nil { + return nil + } + out := new(MedusaConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MedusaConfiguration) 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 *MedusaConfigurationList) DeepCopyInto(out *MedusaConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MedusaConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MedusaConfigurationList. +func (in *MedusaConfigurationList) DeepCopy() *MedusaConfigurationList { + if in == nil { + return nil + } + out := new(MedusaConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MedusaConfigurationList) 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 *MedusaConfigurationSpec) DeepCopyInto(out *MedusaConfigurationSpec) { + *out = *in + in.StorageProperties.DeepCopyInto(&out.StorageProperties) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MedusaConfigurationSpec. +func (in *MedusaConfigurationSpec) DeepCopy() *MedusaConfigurationSpec { + if in == nil { + return nil + } + out := new(MedusaConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MedusaConfigurationStatus) DeepCopyInto(out *MedusaConfigurationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MedusaConfigurationStatus. +func (in *MedusaConfigurationStatus) DeepCopy() *MedusaConfigurationStatus { + if in == nil { + return nil + } + out := new(MedusaConfigurationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MedusaRestoreJob) DeepCopyInto(out *MedusaRestoreJob) { *out = *in diff --git a/config/crd/bases/medusa.k8ssandra.io_medusaconfigurations.yaml b/config/crd/bases/medusa.k8ssandra.io_medusaconfigurations.yaml new file mode 100644 index 000000000..ee2f56845 --- /dev/null +++ b/config/crd/bases/medusa.k8ssandra.io_medusaconfigurations.yaml @@ -0,0 +1,220 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: medusaconfigurations.medusa.k8ssandra.io +spec: + group: medusa.k8ssandra.io + names: + kind: MedusaConfiguration + listKind: MedusaConfigurationList + plural: medusaconfigurations + singular: medusaconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MedusaConfiguration is the Schema for the medusaconfigurations + API + 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: MedusaConfigurationSpec defines the desired state of MedusaConfiguration + properties: + storageProperties: + description: StorageProperties defines the storage backend settings + to use for the backups. + properties: + apiProfile: + description: AWS Profile to use for authentication. + type: string + backupGracePeriodInDays: + description: Age after which orphan sstables can be deleted from + the storage backend. Protects from race conditions between purge + and ongoing backups. Defaults to 10 days. + type: integer + bucketName: + description: The name of the bucket to use for the backups. + type: string + concurrentTransfers: + default: 1 + description: Number of concurrent uploads. Helps maximizing the + speed of uploads but puts more pressure on the network. Defaults + to 1. + type: integer + host: + description: Host to connect to for the storage backend. + type: string + maxBackupAge: + default: 0 + description: Maximum backup age that the purge process should + observe. + type: integer + maxBackupCount: + default: 0 + description: Maximum number of backups to keep (used by the purge + process). Default is unlimited. + type: integer + multiPartUploadThreshold: + default: 104857600 + description: File size over which cloud specific cli tools are + used for transfer. Defaults to 100 MB. + type: integer + podStorage: + description: Pod storage settings for the local storage provider + properties: + accessModes: + description: Pod local storage access modes + items: + type: string + type: array + size: + anyOf: + - type: integer + - type: string + default: 10Gi + description: Size of the pod's storage in bytes. Defaults + to 10 GB. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + storageClassName: + description: Storage class name to use for the pod's storage. + type: string + type: object + port: + description: Port to connect to for the storage backend. + type: integer + prefix: + description: Name of the top level folder in the backup bucket. + If empty, the cluster name will be used. + type: string + region: + description: Region of the storage bucket. Defaults to "default". + type: string + secure: + description: Whether to use SSL for the storage backend. + type: boolean + sslVerify: + description: When using SSL, whether to also verify the certificate. + type: boolean + storageProvider: + description: The storage backend to use for the backups. + enum: + - google_storage + - azure_blobs + - s3 + - s3_compatible + - s3_rgw + - ibm_storage + type: string + storageSecretRef: + description: Kubernetes Secret that stores the key file for the + storage provider's API. If using 'local' storage, this value + is ignored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + x-kubernetes-map-type: atomic + transferMaxBandwidth: + default: 50MB/s + description: Max upload bandwidth in MB/s. Defaults to 50 MB/s. + type: string + type: object + type: object + status: + description: MedusaConfigurationStatus defines the observed state of MedusaConfiguration + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the 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: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 55de319a3..9a5c98825 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -13,6 +13,7 @@ resources: - bases/medusa.k8ssandra.io_medusarestorejobs.yaml - bases/medusa.k8ssandra.io_medusabackupschedules.yaml - bases/control.k8ssandra.io_k8ssandratasks.yaml +- bases/medusa.k8ssandra.io_medusaconfigurations.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -28,6 +29,7 @@ patchesStrategicMerge: #- patches/webhook_in_medusarestorejobs.yaml #- patches/webhook_in_medusabackupschedules.yaml #- patches/webhook_in_k8ssandratasks.yaml +#- patches/webhook_in_medusaconfigurations.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -42,6 +44,7 @@ patchesStrategicMerge: #- patches/cainjection_in_medusarestorejobs.yaml #- patches/cainjection_in_medusabackupschedules.yaml #- patches/cainjection_in_k8ssandratasks.yaml +#- patches/cainjection_in_medusaconfigurations.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_medusa_medusaconfigurations.yaml b/config/crd/patches/cainjection_in_medusa_medusaconfigurations.yaml new file mode 100644 index 000000000..cb5065982 --- /dev/null +++ b/config/crd/patches/cainjection_in_medusa_medusaconfigurations.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: medusaconfigurations.medusa.k8ssandra.io diff --git a/config/crd/patches/webhook_in_medusa_medusaconfigurations.yaml b/config/crd/patches/webhook_in_medusa_medusaconfigurations.yaml new file mode 100644 index 000000000..ad7dff23d --- /dev/null +++ b/config/crd/patches/webhook_in_medusa_medusaconfigurations.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: medusaconfigurations.medusa.k8ssandra.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/medusa_medusaconfiguration_editor_role.yaml b/config/rbac/medusa_medusaconfiguration_editor_role.yaml new file mode 100644 index 000000000..67ccc9ae5 --- /dev/null +++ b/config/rbac/medusa_medusaconfiguration_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit medusaconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: medusaconfiguration-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8ssandra-operator + app.kubernetes.io/part-of: k8ssandra-operator + app.kubernetes.io/managed-by: kustomize + name: medusaconfiguration-editor-role +rules: +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations/status + verbs: + - get diff --git a/config/rbac/medusa_medusaconfiguration_viewer_role.yaml b/config/rbac/medusa_medusaconfiguration_viewer_role.yaml new file mode 100644 index 000000000..65a45f116 --- /dev/null +++ b/config/rbac/medusa_medusaconfiguration_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view medusaconfigurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: medusaconfiguration-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: k8ssandra-operator + app.kubernetes.io/part-of: k8ssandra-operator + app.kubernetes.io/managed-by: kustomize + name: medusaconfiguration-viewer-role +rules: +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations + verbs: + - get + - list + - watch +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 916de604c..712924196 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -288,6 +288,32 @@ rules: - get - patch - update +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations/finalizers + verbs: + - update +- apiGroups: + - medusa.k8ssandra.io + resources: + - medusaconfigurations/status + verbs: + - get + - patch + - update - apiGroups: - medusa.k8ssandra.io resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 0c434d47a..8b463b38b 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -9,4 +9,5 @@ resources: - medusa_v1alpha1_medusarestorejob.yaml - medusa_v1alpha1_medusabackupschedule.yaml - control_v1alpha1_k8ssandratask.yaml +- medusa_v1alpha1_medusaconfiguration.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/medusa_v1alpha1_medusaconfiguration.yaml b/config/samples/medusa_v1alpha1_medusaconfiguration.yaml new file mode 100644 index 000000000..1d6f13209 --- /dev/null +++ b/config/samples/medusa_v1alpha1_medusaconfiguration.yaml @@ -0,0 +1,20 @@ +apiVersion: medusa.k8ssandra.io/v1alpha1 +kind: MedusaConfiguration +metadata: + labels: + app.kubernetes.io/name: medusaconfiguration + app.kubernetes.io/instance: medusaconfiguration-sample + app.kubernetes.io/part-of: k8ssandra-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: k8ssandra-operator + name: medusaconfiguration-sample +spec: + storageProperties: + storageProvider: s3_compatible + bucketName: k8ssandra-medusa + prefix: test + storageSecretRef: + name: medusa-bucket-key + host: minio-service.minio.svc.cluster.local + port: 9000 + secure: false diff --git a/controllers/medusa/controllers_test.go b/controllers/medusa/controllers_test.go index e431590a8..a5de2436a 100644 --- a/controllers/medusa/controllers_test.go +++ b/controllers/medusa/controllers_test.go @@ -49,6 +49,12 @@ func TestCassandraBackupRestore(t *testing.T) { t.Run("TestMedusaRestoreDatacenter", testEnv3.ControllerTest(ctx, testMedusaRestoreDatacenter)) t.Run("TestValidationErrorStopsRestore", testEnv3.ControllerTest(ctx, testValidationErrorStopsRestore)) + + testEnv4 := setupMedusaConfigurationTestEnv(t, ctx) + defer testEnv4.Stop(t) + defer cancel() + t.Run("TestMedusaConfiguration", testEnv4.ControllerTest(ctx, testMedusaConfiguration)) + } func setupMedusaBackupTestEnv(t *testing.T, ctx context.Context) *testutils.MultiClusterTestEnv { @@ -265,6 +271,76 @@ func setupMedusaTaskTestEnv(t *testing.T, ctx context.Context) *testutils.MultiC return testEnv } +func setupMedusaConfigurationTestEnv(t *testing.T, ctx context.Context) *testutils.MultiClusterTestEnv { + testEnv := &testutils.MultiClusterTestEnv{ + NumDataPlanes: 1, + BeforeTest: func(t *testing.T) { + managementApi.SetT(t) + managementApi.UseDefaultAdapter() + }, + } + seedsResolver.callback = func(dc *cassdcapi.CassandraDatacenter) ([]string, error) { + return []string{}, nil + } + + reconcilerConfig := config.InitConfig() + + reconcilerConfig.DefaultDelay = 100 * time.Millisecond + reconcilerConfig.LongDelay = 300 * time.Millisecond + + medusaClientFactory = NewMedusaClientFactory() + + err := testEnv.Start(ctx, t, func(controlPlaneMgr manager.Manager, clientCache *clientcache.ClientCache, clusters []cluster.Cluster) error { + err := (&MedusaConfigurationReconciler{ + ReconcilerConfig: reconcilerConfig, + Client: controlPlaneMgr.GetClient(), + Scheme: scheme.Scheme, + }).SetupWithManager(controlPlaneMgr) + if err != nil { + return err + } + for _, env := range testEnv.GetDataPlaneEnvTests() { + dataPlaneMgr, err := ctrl.NewManager( + env.Config, + ctrl.Options{ + Scheme: scheme.Scheme, + Host: env.WebhookInstallOptions.LocalServingHost, + Port: env.WebhookInstallOptions.LocalServingPort, + CertDir: env.WebhookInstallOptions.LocalServingCertDir, + }, + ) + if err != nil { + return err + } + err = (&MedusaConfigurationReconciler{ + ReconcilerConfig: reconcilerConfig, + Client: dataPlaneMgr.GetClient(), + Scheme: scheme.Scheme, + }).SetupWithManager(dataPlaneMgr) + if err != nil { + return err + } + + secretswebhook.SetupSecretsInjectorWebhook(dataPlaneMgr) + + if err != nil { + return err + } + go func() { + err := dataPlaneMgr.Start(ctx) + if err != nil { + t.Errorf("failed to start manager: %s", err) + } + }() + } + return nil + }) + if err != nil { + t.Fatalf("failed to start test environment: %s", err) + } + return testEnv +} + type fakeSeedsResolver struct { callback func(dc *cassdcapi.CassandraDatacenter) ([]string, error) } diff --git a/controllers/medusa/medusaconfiguration_controller.go b/controllers/medusa/medusaconfiguration_controller.go new file mode 100644 index 000000000..21e4934f4 --- /dev/null +++ b/controllers/medusa/medusaconfiguration_controller.go @@ -0,0 +1,121 @@ +/* +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 medusa + +import ( + "context" + "github.com/go-logr/logr" + "github.com/k8ssandra/k8ssandra-operator/pkg/config" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + medusav1alpha1 "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1" +) + +// MedusaConfigurationReconciler reconciles a MedusaConfiguration object +type MedusaConfigurationReconciler struct { + *config.ReconcilerConfig + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=medusa.k8ssandra.io,namespace="k8ssandra",resources=medusaconfigurations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=medusa.k8ssandra.io,namespace="k8ssandra",resources=medusaconfigurations/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=medusa.k8ssandra.io,namespace="k8ssandra",resources=medusaconfigurations/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the MedusaConfiguration object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *MedusaConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithValues("medusabackupjob", req.NamespacedName) + + logger.Info("Starting reconciliation") + + // Fetch the MedusaConfiguration instance + instance := &medusav1alpha1.MedusaConfiguration{} + err := r.Get(ctx, req.NamespacedName, instance) + if err != nil { + logger.Error(err, "Failed to get MedusaConfiguration") + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + configuration := instance.DeepCopy() + patch := client.MergeFrom(configuration.DeepCopy()) + + // Check if the referenced secret exists + if configuration.Spec.StorageProperties.StorageSecretRef.Name != "" { + err = r.CheckSecretPresence(ctx, r.Client, req, configuration.Spec.StorageProperties.StorageSecretRef.Name) + if err != nil { + logger.Error(err, "Failed to get MedusaConfiguration referenced secret") + configuration.Status.SetCondition(medusav1alpha1.ControlStatusSecretAvailable, metav1.ConditionFalse) + configuration.Status.SetConditionMessage(medusav1alpha1.ControlStatusSecretAvailable, err.Error()) + r.patchStatus(ctx, configuration, patch, logger) + return ctrl.Result{}, err + } else { + configuration.Status.SetCondition(medusav1alpha1.ControlStatusSecretAvailable, metav1.ConditionTrue) + } + } + + configuration.Status.SetCondition(medusav1alpha1.ControlStatusReady, metav1.ConditionTrue) + r.patchStatus(ctx, configuration, patch, logger) + logger.Info("MedusaConfiguration Reconciliation complete", "MedusaConfiguration", req.NamespacedName) + + return ctrl.Result{}, nil +} + +func (r *MedusaConfigurationReconciler) patchStatus(ctx context.Context, configuration *medusav1alpha1.MedusaConfiguration, patch client.Patch, logger logr.Logger) { + if patchErr := r.Status().Patch(ctx, configuration, patch); patchErr != nil { + logger.Error(patchErr, "failed to update MedusaConfiguration status") + } else { + logger.Info("updated MedusaConfiguration status") + } +} + +func (r *MedusaConfigurationReconciler) CheckSecretPresence(ctx context.Context, client client.Client, req ctrl.Request, secretName string) error { + // Get the referenced secret to check if it exists + secret := &corev1.Secret{} + secretNamespacedName := types.NamespacedName{Namespace: req.Namespace, Name: secretName} + err := client.Get(ctx, secretNamespacedName, secret) + if err != nil { + return err + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MedusaConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&medusav1alpha1.MedusaConfiguration{}). + Complete(r) +} diff --git a/controllers/medusa/medusaconfiguration_controller_test.go b/controllers/medusa/medusaconfiguration_controller_test.go new file mode 100644 index 000000000..77f4fe5c5 --- /dev/null +++ b/controllers/medusa/medusaconfiguration_controller_test.go @@ -0,0 +1,169 @@ +package medusa + +import ( + "context" + "testing" + + api "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1" + "github.com/k8ssandra/k8ssandra-operator/test/framework" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func testMedusaConfiguration(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + t.Run("testMedusaConfigurationOk", func(t *testing.T) { + testMedusaConfigurationOk(t, ctx, f, namespace) + }) + t.Run("testMedusaConfigurationKo", func(t *testing.T) { + testMedusaConfigurationKo(t, ctx, f, namespace) + }) + t.Run("testMedusaConfigurationNoSecret", func(t *testing.T) { + testMedusaConfigurationNoSecret(t, ctx, f, namespace) + }) +} + +func testMedusaConfigurationOk(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + require := require.New(t) + + bucketKeySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "medusa-bucket-key", + Namespace: namespace, + }, + Data: map[string][]byte{ + "credentials": []byte("test"), + }, + } + + t.Log("Creating medusa bucket key secret") + err := f.Client.Create(ctx, bucketKeySecret) + require.NoError(err, "failed to create medusa bucket key secret") + + medusaConfig := &api.MedusaConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "medusa-config", + Namespace: namespace, + }, + Spec: api.MedusaConfigurationSpec{ + StorageProperties: api.Storage{ + StorageSecretRef: corev1.LocalObjectReference{ + Name: "medusa-bucket-key", + }, + BucketName: "test", + StorageProvider: "s3", + }, + }, + } + err = f.Client.Create(ctx, medusaConfig) + require.NoError(err, "failed to create medusa configuration") + require.Eventually(func() bool { + updated := &api.MedusaConfiguration{} + err := f.Client.Get(ctx, types.NamespacedName{Name: "medusa-config", Namespace: namespace}, updated) + if err != nil { + t.Logf("failed to get medusa configuration: %v", err) + return false + } + for _, condition := range updated.Status.Conditions { + t.Logf("medusa configuration condition: %v", condition) + if condition.Type == string(api.ControlStatusReady) { + return condition.Status == metav1.ConditionTrue + } + } + t.Logf("medusa configuration not ready yet") + return false + }, timeout, interval) +} + +func testMedusaConfigurationKo(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + require := require.New(t) + + medusaConfig := &api.MedusaConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "medusa-config-ko", + Namespace: namespace, + }, + Spec: api.MedusaConfigurationSpec{ + StorageProperties: api.Storage{ + StorageSecretRef: corev1.LocalObjectReference{ + Name: "medusa-bucket-key-ko", + }, + BucketName: "test", + StorageProvider: "s3", + }, + }, + } + err := f.Client.Create(ctx, medusaConfig) + require.NoError(err, "failed to create medusa configuration") + require.Eventually(func() bool { + updated := &api.MedusaConfiguration{} + err := f.Client.Get(ctx, types.NamespacedName{Name: "medusa-config-ko", Namespace: namespace}, updated) + if err != nil { + t.Logf("failed to get medusa configuration: %v", err) + return false + } + for _, condition := range updated.Status.Conditions { + t.Logf("medusa configuration condition: %v", condition) + if condition.Type == string(api.ControlStatusSecretAvailable) { + return condition.Status == metav1.ConditionFalse + } + } + t.Logf("medusa configuration not ready yet") + return false + }, timeout, interval) + + require.Never(func() bool { + updated := &api.MedusaConfiguration{} + err := f.Client.Get(ctx, types.NamespacedName{Name: "medusa-config-ko", Namespace: namespace}, updated) + if err != nil { + t.Logf("failed to get medusa configuration: %v", err) + return false + } + for _, condition := range updated.Status.Conditions { + t.Logf("medusa configuration condition: %v", condition) + if condition.Type == string(api.ControlStatusReady) { + return condition.Status == metav1.ConditionTrue + } + } + t.Logf("medusa configuration not ready yet") + return false + }, timeout, interval) +} + +// Testing that the medusa configuration is ready even if no secret is provided. +// The secret can be defined in the K8ssandraCluster object directly without being referenced by the medusa configuration. +func testMedusaConfigurationNoSecret(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + require := require.New(t) + + medusaConfig := &api.MedusaConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "medusa-config-no-secret", + Namespace: namespace, + }, + Spec: api.MedusaConfigurationSpec{ + StorageProperties: api.Storage{ + BucketName: "test", + StorageProvider: "s3", + }, + }, + } + err := f.Client.Create(ctx, medusaConfig) + require.NoError(err, "failed to create medusa configuration") + require.Eventually(func() bool { + updated := &api.MedusaConfiguration{} + err := f.Client.Get(ctx, types.NamespacedName{Name: "medusa-config-no-secret", Namespace: namespace}, updated) + if err != nil { + t.Logf("failed to get medusa configuration: %v", err) + return false + } + for _, condition := range updated.Status.Conditions { + t.Logf("medusa configuration condition: %v", condition) + if condition.Type == string(api.ControlStatusReady) { + return condition.Status == metav1.ConditionTrue + } + } + t.Logf("medusa configuration not ready yet") + return false + }, timeout, interval) +} diff --git a/docs/content/en/tasks/backup-restore/_index.md b/docs/content/en/tasks/backup-restore/_index.md index ab61256d4..9dc801bc6 100644 --- a/docs/content/en/tasks/backup-restore/_index.md +++ b/docs/content/en/tasks/backup-restore/_index.md @@ -108,6 +108,29 @@ The file should always specify `credentials` as shown in the example above; in t A successful deployment should inject a new init container named `medusa-restore` and a new container named `medusa` in the Cassandra StatefulSet pods. +## Using shared medusa configuration properties + +Medusa configuration properties can be shared across multiple K8ssandraClusters by creating a `MedusaConfiguration` custom resource in the Control Plane K8ssandra cluster. +Example: + +```yaml +apiVersion: medusa.k8ssandra.io/v1alpha1 +kind: MedusaConfiguration +metadata: + name: medusaconfiguration-s3 +spec: + storageProperties: + storageProvider: s3 + region: us-west-2 + bucketName: k8ssandra-medusa + storageSecretRef: + name: medusa-bucket-key +``` + +This allows creating bucket configurations that are easy to share across multiple clusters, without repeating their storage properties in each `K8ssandraCluster` definition. + +The referenced secret must exist in the same namespace as the `MedusaConfiguration` object, and must contain the credentials file for the storage backend, as described in the previous section. + ## Creating a Backup To perform a backup of a Cassandra datacenter, create the following custom resource in the same namespace and Kubernetes cluster as the CassandraDatacenter resource, `cassandradatacenter/dc1` in this case : diff --git a/go.mod b/go.mod index 99a38de6c..6d7b690ed 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( github.com/gruntwork-io/terratest v0.37.7 github.com/k8ssandra/cass-operator v1.18.1-0.20240109145046-4215a6003303 github.com/k8ssandra/reaper-client-go v0.3.1-0.20220114183114-6923e077c4f5 + github.com/onsi/ginkgo/v2 v2.9.4 + github.com/onsi/gomega v1.27.6 github.com/pkg/errors v0.9.1 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.52.1 github.com/robfig/cron/v3 v3.0.1 @@ -52,6 +54,7 @@ require ( github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -60,6 +63,7 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -89,6 +93,7 @@ require ( golang.org/x/term v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.8.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect diff --git a/go.sum b/go.sum index 7239d6923..4e24621dd 100644 --- a/go.sum +++ b/go.sum @@ -449,6 +449,7 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -557,6 +558,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de h1:6bMcLOeKoNo0+mTOb1ee3McF6CCKGixjLR3EDQY1Jik= +github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -655,8 +657,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k8ssandra/cass-operator v1.18.1-0.20240109145046-4215a6003303 h1:NwcRMl97EFzya4Rmt2DuEE6pinzd9JRLbGaY+djWEjE= github.com/k8ssandra/cass-operator v1.18.1-0.20240109145046-4215a6003303/go.mod h1:8gYoASfrQYiDmvfMAeYSe31dO1qVe8uIISJeVXGFSiI= -github.com/k8ssandra/cass-operator v1.18.1 h1:5EzHuIIdoka92ysId0bgpM0KSn532k6+XS0Srvvv0bE= -github.com/k8ssandra/cass-operator v1.18.1/go.mod h1:8gYoASfrQYiDmvfMAeYSe31dO1qVe8uIISJeVXGFSiI= github.com/k8ssandra/reaper-client-go v0.3.1-0.20220114183114-6923e077c4f5 h1:Dq0VdM960G3AbhYwFuaebmsE08IzOYHYhngUfDmWaAc= github.com/k8ssandra/reaper-client-go v0.3.1-0.20220114183114-6923e077c4f5/go.mod h1:WsQymIaVT39xbcstZhdqynUS13AGzP2p6U9Hsk1oy5M= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -765,7 +765,6 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= @@ -773,6 +772,7 @@ github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7 github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= +github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -787,6 +787,7 @@ github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8lu github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1435,6 +1436,7 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 0fdd08cec..cd2557523 100644 --- a/main.go +++ b/main.go @@ -275,6 +275,13 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "MedusaBackupSchedule") os.Exit(1) } + if err = (&medusactrl.MedusaConfigurationReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MedusaConfiguration") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/test/e2e/medusa_test.go b/test/e2e/medusa_test.go index 58210b350..512f49443 100644 --- a/test/e2e/medusa_test.go +++ b/test/e2e/medusa_test.go @@ -297,3 +297,26 @@ func checkMedusaStandaloneServiceExists(t *testing.T, ctx context.Context, dcKey return err == nil }, polling.medusaReady.timeout, polling.medusaReady.interval, "Medusa standalone service doesn't exist") } + +func createMedusaConfiguration(t *testing.T, ctx context.Context, namespace string, f *framework.E2eFramework) { + require := require.New(t) + medusaConfig := &medusa.MedusaConfiguration{} + medusaConfigKey := framework.ClusterKey{K8sContext: "kind-k8ssandra-0", NamespacedName: types.NamespacedName{Namespace: namespace, Name: "config1"}} + err := f.Get(ctx, medusaConfigKey, medusaConfig) + require.NoError(err, "Error getting the MedusaConfiguration") + + require.Eventually(func() bool { + updated := &medusa.MedusaConfiguration{} + err := f.Get(ctx, medusaConfigKey, updated) + if err != nil { + t.Logf("failed to get medusa configuration: %v", err) + return false + } + for _, condition := range updated.Status.Conditions { + if condition.Type == string(medusa.ControlStatusReady) { + return condition.Status == metav1.ConditionTrue + } + } + return false + }, polling.medusaConfigurationReady.timeout, polling.medusaConfigurationReady.interval) +} diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 267cd2b24..53e9bd206 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -64,17 +64,18 @@ type ingressConfig struct { var ( polling struct { - nodetoolStatus pollingConfig - datacenterReady pollingConfig - operatorDeploymentReady pollingConfig - k8ssandraClusterStatus pollingConfig - stargateReady pollingConfig - reaperReady pollingConfig - medusaReady pollingConfig - medusaBackupDone pollingConfig - medusaRestoreDone pollingConfig - datacenterUpdating pollingConfig - cassandraTaskCreated pollingConfig + nodetoolStatus pollingConfig + datacenterReady pollingConfig + operatorDeploymentReady pollingConfig + k8ssandraClusterStatus pollingConfig + stargateReady pollingConfig + reaperReady pollingConfig + medusaReady pollingConfig + medusaBackupDone pollingConfig + medusaRestoreDone pollingConfig + datacenterUpdating pollingConfig + cassandraTaskCreated pollingConfig + medusaConfigurationReady pollingConfig } ) @@ -379,6 +380,10 @@ func TestOperator(t *testing.T) { testFunc: createMultiDatacenterTask, fixture: framework.NewTestFixture("multi-dc", controlPlane), })) + t.Run("CreateMedusaConfiguration", e2eTest(ctx, &e2eTestOpts{ + testFunc: createMedusaConfiguration, + fixture: framework.NewTestFixture("medusa-configuration", controlPlane), + })) } func beforeSuite(t *testing.T) { @@ -717,6 +722,9 @@ func applyPollingDefaults() { polling.medusaReady.timeout = 5 * time.Minute polling.medusaReady.interval = 5 * time.Second + + polling.medusaConfigurationReady.timeout = 1 * time.Minute + polling.medusaConfigurationReady.interval = 5 * time.Second } func afterTest(t *testing.T, f *framework.E2eFramework, opts *e2eTestOpts) { diff --git a/test/testdata/fixtures/medusa-configuration/kustomization.yaml b/test/testdata/fixtures/medusa-configuration/kustomization.yaml new file mode 100644 index 000000000..94c663cdc --- /dev/null +++ b/test/testdata/fixtures/medusa-configuration/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - medusa-configuration.yaml diff --git a/test/testdata/fixtures/medusa-configuration/medusa-configuration.yaml b/test/testdata/fixtures/medusa-configuration/medusa-configuration.yaml new file mode 100644 index 000000000..6ac4558bc --- /dev/null +++ b/test/testdata/fixtures/medusa-configuration/medusa-configuration.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-secret +type: kubernetes.io/basic-auth +stringData: + username: admin + password: t0p-Secret +--- +apiVersion: medusa.k8ssandra.io/v1alpha1 +kind: MedusaConfiguration +metadata: + name: config1 +spec: + storageProperties: + storageSecretRef: + name: test-secret + bucketName: test