diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a787bca..3a5b4d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD +- Add `ClickhouseDatabase` kind - Replace `Database` kind validations and default values with CRD validation rules - Perform upgrade tasks to check if PG service can be upgraded before updating the service - Expose project CA certificate to service secrets: `REDIS_CA_CERT`, `MYSQL_CA_CERT`, etc. diff --git a/PROJECT b/PROJECT index 96fd248b..c64121b2 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: aiven.io layout: - go.kubebuilder.io/v3 @@ -247,4 +251,16 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: aiven.io + kind: ClickhouseDatabase + path: github.com/aiven/aiven-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/clickhousedatabase_types.go b/api/v1alpha1/clickhousedatabase_types.go new file mode 100644 index 00000000..bbe140d6 --- /dev/null +++ b/api/v1alpha1/clickhousedatabase_types.go @@ -0,0 +1,76 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClickhouseDatabaseSpec defines the desired state of ClickhouseDatabase +type ClickhouseDatabaseSpec struct { + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Format="^[a-zA-Z0-9_-]*$" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // Project to link the database to + Project string `json:"project"` + + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // Clickhouse service to link the database to + ServiceName string `json:"serviceName"` + + // It is a Kubernetes side deletion protections, which prevents the database + // from being deleted by Kubernetes. It is recommended to enable this for any production + // databases containing critical data. + TerminationProtection *bool `json:"terminationProtection,omitempty"` + + // Authentication reference to Aiven token in a secret + AuthSecretRef *AuthSecretReference `json:"authSecretRef,omitempty"` +} + +// ClickhouseDatabaseStatus defines the observed state of ClickhouseDatabase +type ClickhouseDatabaseStatus struct { + // Conditions represent the latest available observations of an ClickhouseDatabase state + Conditions []metav1.Condition `json:"conditions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ClickhouseDatabase is the Schema for the databases API +// +kubebuilder:printcolumn:name="Service Name",type="string",JSONPath=".spec.serviceName" +// +kubebuilder:printcolumn:name="Project",type="string",JSONPath=".spec.project" +type ClickhouseDatabase struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClickhouseDatabaseSpec `json:"spec,omitempty"` + Status ClickhouseDatabaseStatus `json:"status,omitempty"` +} + +var _ AivenManagedObject = &ClickhouseDatabase{} + +func (*ClickhouseDatabase) NoSecret() bool { + return false +} + +func (in *ClickhouseDatabase) AuthSecretRef() *AuthSecretReference { + return in.Spec.AuthSecretRef +} + +func (in *ClickhouseDatabase) Conditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +// +kubebuilder:object:root=true + +// ClickhouseDatabaseList contains a list of ClickhouseDatabase +type ClickhouseDatabaseList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClickhouseDatabase `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClickhouseDatabase{}, &ClickhouseDatabaseList{}) +} diff --git a/api/v1alpha1/clickhousedatabase_webhook.go b/api/v1alpha1/clickhousedatabase_webhook.go new file mode 100644 index 00000000..b98775a3 --- /dev/null +++ b/api/v1alpha1/clickhousedatabase_webhook.go @@ -0,0 +1,58 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package v1alpha1 + +import ( + "errors" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var clickhousedatabaselog = logf.Log.WithName("clickhousedatabase-resource") + +func (in *ClickhouseDatabase) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(in). + Complete() +} + +//+kubebuilder:webhook:path=/mutate-aiven-io-v1alpha1-clickhousedatabase,mutating=true,failurePolicy=fail,groups=aiven.io,resources=clickhousedatabases,verbs=create;update,versions=v1alpha1,name=mclickhousedatabase.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &ClickhouseDatabase{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (in *ClickhouseDatabase) Default() { + clickhousedatabaselog.Info("default", "name", in.Name) +} + +//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-aiven-io-v1alpha1-clickhousedatabase,mutating=false,failurePolicy=fail,groups=aiven.io,resources=clickhousedatabases,versions=v1alpha1,name=vclickhousedatabase.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Validator = &ClickhouseDatabase{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (in *ClickhouseDatabase) ValidateCreate() error { + clickhousedatabaselog.Info("validate create", "name", in.Name) + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (in *ClickhouseDatabase) ValidateUpdate(old runtime.Object) error { + clickhousedatabaselog.Info("validate update", "name", in.Name) + + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (in *ClickhouseDatabase) ValidateDelete() error { + clickhousedatabaselog.Info("validate delete", "name", in.Name) + + if in.Spec.TerminationProtection != nil && *in.Spec.TerminationProtection { + return errors.New("cannot delete ClickhouseDatabase, termination protection is on") + } + return nil +} diff --git a/api/v1alpha1/setup_webhooks.go b/api/v1alpha1/setup_webhooks.go index 9bdaf547..fa83efe0 100644 --- a/api/v1alpha1/setup_webhooks.go +++ b/api/v1alpha1/setup_webhooks.go @@ -55,6 +55,9 @@ func SetupWebhooks(mgr ctrl.Manager) error { if err := (&ClickhouseUser{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("webhook ClickhouseUser: %w", err) } + if err := (&ClickhouseDatabase{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("webhook ClickhouseDatabase: %w", err) + } if err := (&MySQL{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("webhook MySQL: %w", err) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 09448660..ad9c3e22 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -164,6 +164,112 @@ func (in *Clickhouse) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickhouseDatabase) DeepCopyInto(out *ClickhouseDatabase) { + *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 ClickhouseDatabase. +func (in *ClickhouseDatabase) DeepCopy() *ClickhouseDatabase { + if in == nil { + return nil + } + out := new(ClickhouseDatabase) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickhouseDatabase) 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 *ClickhouseDatabaseList) DeepCopyInto(out *ClickhouseDatabaseList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickhouseDatabase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseDatabaseList. +func (in *ClickhouseDatabaseList) DeepCopy() *ClickhouseDatabaseList { + if in == nil { + return nil + } + out := new(ClickhouseDatabaseList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickhouseDatabaseList) 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 *ClickhouseDatabaseSpec) DeepCopyInto(out *ClickhouseDatabaseSpec) { + *out = *in + if in.TerminationProtection != nil { + in, out := &in.TerminationProtection, &out.TerminationProtection + *out = new(bool) + **out = **in + } + if in.AuthSecretRef != nil { + in, out := &in.AuthSecretRef, &out.AuthSecretRef + *out = new(AuthSecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseDatabaseSpec. +func (in *ClickhouseDatabaseSpec) DeepCopy() *ClickhouseDatabaseSpec { + if in == nil { + return nil + } + out := new(ClickhouseDatabaseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickhouseDatabaseStatus) DeepCopyInto(out *ClickhouseDatabaseStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickhouseDatabaseStatus. +func (in *ClickhouseDatabaseStatus) DeepCopy() *ClickhouseDatabaseStatus { + if in == nil { + return nil + } + out := new(ClickhouseDatabaseStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClickhouseList) DeepCopyInto(out *ClickhouseList) { *out = *in diff --git a/charts/aiven-operator-crds/templates/aiven.io_clickhousedatabases.yaml b/charts/aiven-operator-crds/templates/aiven.io_clickhousedatabases.yaml new file mode 100644 index 00000000..63af2f79 --- /dev/null +++ b/charts/aiven-operator-crds/templates/aiven.io_clickhousedatabases.yaml @@ -0,0 +1,173 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: clickhousedatabases.aiven.io +spec: + group: aiven.io + names: + kind: ClickhouseDatabase + listKind: ClickhouseDatabaseList + plural: clickhousedatabases + singular: clickhousedatabase + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.serviceName + name: Service Name + type: string + - jsonPath: .spec.project + name: Project + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClickhouseDatabase is the Schema for the databases 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: ClickhouseDatabaseSpec defines the desired state of ClickhouseDatabase + properties: + authSecretRef: + description: Authentication reference to Aiven token in a secret + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object + project: + description: Project to link the database to + format: ^[a-zA-Z0-9_-]*$ + maxLength: 63 + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + serviceName: + description: Clickhouse service to link the database to + maxLength: 63 + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + terminationProtection: + description: + It is a Kubernetes side deletion protections, which prevents + the database from being deleted by Kubernetes. It is recommended + to enable this for any production databases containing critical + data. + type: boolean + required: + - project + - serviceName + type: object + status: + description: ClickhouseDatabaseStatus defines the observed state of ClickhouseDatabase + properties: + conditions: + description: + Conditions represent the latest available observations + of an ClickhouseDatabase state + 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 + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/aiven-operator/templates/cluster_role.yaml b/charts/aiven-operator/templates/cluster_role.yaml index 9161738e..1d29f8ff 100644 --- a/charts/aiven-operator/templates/cluster_role.yaml +++ b/charts/aiven-operator/templates/cluster_role.yaml @@ -58,6 +58,26 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - clickhousedatabases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - clickhousedatabases/status + verbs: + - get + - patch + - update - apiGroups: - aiven.io resources: diff --git a/charts/aiven-operator/templates/mutating_webhook_configuration.yaml b/charts/aiven-operator/templates/mutating_webhook_configuration.yaml index bd459fd5..525ecca8 100644 --- a/charts/aiven-operator/templates/mutating_webhook_configuration.yaml +++ b/charts/aiven-operator/templates/mutating_webhook_configuration.yaml @@ -49,6 +49,26 @@ webhooks: resources: - clickhouses sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "aiven-operator.fullname" . }}-webhook-service + namespace: {{ include "aiven-operator.namespace" . }} + path: /mutate-aiven-io-v1alpha1-clickhousedatabase + failurePolicy: Fail + name: mclickhousedatabase.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - clickhousedatabases + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/charts/aiven-operator/templates/validating_webhook_configuration.yaml b/charts/aiven-operator/templates/validating_webhook_configuration.yaml index cffbafc1..5e004c74 100644 --- a/charts/aiven-operator/templates/validating_webhook_configuration.yaml +++ b/charts/aiven-operator/templates/validating_webhook_configuration.yaml @@ -51,6 +51,27 @@ webhooks: resources: - clickhouses sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "aiven-operator.fullname" . }}-webhook-service + namespace: {{ include "aiven-operator.namespace" . }} + path: /validate-aiven-io-v1alpha1-clickhousedatabase + failurePolicy: Fail + name: vclickhousedatabase.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - clickhousedatabases + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/config/crd/bases/aiven.io_clickhousedatabases.yaml b/config/crd/bases/aiven.io_clickhousedatabases.yaml new file mode 100644 index 00000000..63af2f79 --- /dev/null +++ b/config/crd/bases/aiven.io_clickhousedatabases.yaml @@ -0,0 +1,173 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: clickhousedatabases.aiven.io +spec: + group: aiven.io + names: + kind: ClickhouseDatabase + listKind: ClickhouseDatabaseList + plural: clickhousedatabases + singular: clickhousedatabase + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.serviceName + name: Service Name + type: string + - jsonPath: .spec.project + name: Project + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClickhouseDatabase is the Schema for the databases 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: ClickhouseDatabaseSpec defines the desired state of ClickhouseDatabase + properties: + authSecretRef: + description: Authentication reference to Aiven token in a secret + properties: + key: + minLength: 1 + type: string + name: + minLength: 1 + type: string + required: + - key + - name + type: object + project: + description: Project to link the database to + format: ^[a-zA-Z0-9_-]*$ + maxLength: 63 + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + serviceName: + description: Clickhouse service to link the database to + maxLength: 63 + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + terminationProtection: + description: + It is a Kubernetes side deletion protections, which prevents + the database from being deleted by Kubernetes. It is recommended + to enable this for any production databases containing critical + data. + type: boolean + required: + - project + - serviceName + type: object + status: + description: ClickhouseDatabaseStatus defines the observed state of ClickhouseDatabase + properties: + conditions: + description: + Conditions represent the latest available observations + of an ClickhouseDatabase state + 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 + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 0ac45af9..9abb343f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,6 +22,7 @@ resources: - bases/aiven.io_mysqls.yaml - bases/aiven.io_cassandras.yaml - bases/aiven.io_grafanas.yaml + - bases/aiven.io_clickhousedatabases.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -46,6 +47,7 @@ patchesStrategicMerge: - patches/webhook_in_mysqls.yaml - patches/webhook_in_cassandras.yaml - patches/webhook_in_grafanas.yaml + - patches/webhook_in_clickhousedatabases.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -69,6 +71,7 @@ patchesStrategicMerge: - patches/cainjection_in_mysqls.yaml - patches/cainjection_in_cassandras.yaml - patches/cainjection_in_grafanas.yaml + - patches/cainjection_in_clickhousedatabases.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_clickhousedatabases.yaml b/config/crd/patches/cainjection_in_clickhousedatabases.yaml new file mode 100644 index 00000000..11cadfc2 --- /dev/null +++ b/config/crd/patches/cainjection_in_clickhousedatabases.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: clickhousedatabases.aiven.io diff --git a/config/crd/patches/webhook_in_clickhousedatabases.yaml b/config/crd/patches/webhook_in_clickhousedatabases.yaml new file mode 100644 index 00000000..42ae522c --- /dev/null +++ b/config/crd/patches/webhook_in_clickhousedatabases.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhousedatabases.aiven.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/clickhousedatabase_editor_role.yaml b/config/rbac/clickhousedatabase_editor_role.yaml new file mode 100644 index 00000000..5171b0f2 --- /dev/null +++ b/config/rbac/clickhousedatabase_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit clickhousedatabases. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: clickhousedatabase-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: aiven-operator + app.kubernetes.io/part-of: aiven-operator + app.kubernetes.io/managed-by: kustomize + name: clickhousedatabase-editor-role +rules: + - apiGroups: + - aiven.io + resources: + - clickhousedatabases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - clickhousedatabases/status + verbs: + - get diff --git a/config/rbac/clickhousedatabase_viewer_role.yaml b/config/rbac/clickhousedatabase_viewer_role.yaml new file mode 100644 index 00000000..e7d84c2b --- /dev/null +++ b/config/rbac/clickhousedatabase_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view clickhousedatabases. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: clickhousedatabase-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: aiven-operator + app.kubernetes.io/part-of: aiven-operator + app.kubernetes.io/managed-by: kustomize + name: clickhousedatabase-viewer-role +rules: + - apiGroups: + - aiven.io + resources: + - clickhousedatabases + verbs: + - get + - list + - watch + - apiGroups: + - aiven.io + resources: + - clickhousedatabases/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 6c73a4f1..fa177872 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -56,6 +56,26 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - clickhousedatabases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - clickhousedatabases/status + verbs: + - get + - patch + - update - apiGroups: - aiven.io resources: diff --git a/config/samples/_v1alpha1_clickhousedatabase.yaml b/config/samples/_v1alpha1_clickhousedatabase.yaml new file mode 100644 index 00000000..00c6459a --- /dev/null +++ b/config/samples/_v1alpha1_clickhousedatabase.yaml @@ -0,0 +1,12 @@ +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + labels: + app.kubernetes.io/name: clickhousedatabase + app.kubernetes.io/instance: clickhousedatabase-sample + app.kubernetes.io/part-of: aiven-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: aiven-operator + name: clickhousedatabase-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 4ac5c48e..498af1a2 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -20,4 +20,5 @@ resources: - _v1alpha1_mysql.yaml - _v1alpha1_cassandra.yaml - _v1alpha1_grafana.yaml + - _v1alpha1_clickhousedatabase.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index f1395456..7e0084c2 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -45,6 +45,26 @@ webhooks: resources: - clickhouses sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-aiven-io-v1alpha1-clickhousedatabase + failurePolicy: Fail + name: mclickhousedatabase.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - clickhousedatabases + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -434,6 +454,27 @@ webhooks: resources: - clickhouses sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-aiven-io-v1alpha1-clickhousedatabase + failurePolicy: Fail + name: vclickhousedatabase.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - clickhousedatabases + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/controllers/clickhousedatabase_controller.go b/controllers/clickhousedatabase_controller.go new file mode 100644 index 00000000..01e41c4c --- /dev/null +++ b/controllers/clickhousedatabase_controller.go @@ -0,0 +1,132 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package controllers + +import ( + "context" + "fmt" + "strconv" + + "github.com/aiven/aiven-go-client/v2" + avngen "github.com/aiven/go-client-codegen" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/aiven/aiven-operator/api/v1alpha1" +) + +// ClickhouseDatabaseReconciler reconciles a ClickhouseDatabase object +type ClickhouseDatabaseReconciler struct { + Controller +} + +func newClickhouseDatabaseReconciler(c Controller) reconcilerType { + return &ClickhouseDatabaseReconciler{Controller: c} +} + +// ClickhouseDatabaseHandler handles an Aiven ClickhouseDatabase +type ClickhouseDatabaseHandler struct{} + +// +kubebuilder:rbac:groups=aiven.io,resources=clickhousedatabases,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=aiven.io,resources=clickhousedatabases/status,verbs=get;update;patch + +func (r *ClickhouseDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.reconcileInstance(ctx, req, &ClickhouseDatabaseHandler{}, &v1alpha1.ClickhouseDatabase{}) +} + +func (r *ClickhouseDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.ClickhouseDatabase{}). + Complete(r) +} + +func (h *ClickhouseDatabaseHandler) createOrUpdate(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object, refs []client.Object) error { + db, err := h.convert(obj) + if err != nil { + return err + } + + _, err = avn.ClickhouseDatabase.Get(ctx, db.Spec.Project, db.Spec.ServiceName, db.Name) + if isNotFound(err) { + err = avn.ClickhouseDatabase.Create(ctx, db.Spec.Project, db.Spec.ServiceName, db.Name) + } + + if err != nil { + return fmt.Errorf("cannot create clickhouse database on Aiven side: %w", err) + } + + meta.SetStatusCondition(&db.Status.Conditions, + getInitializedCondition("Created", + "Instance was created or update on Aiven side")) + + meta.SetStatusCondition(&db.Status.Conditions, + getRunningCondition(metav1.ConditionUnknown, "Created", + "Instance was created or update on Aiven side, status remains unknown")) + + metav1.SetMetaDataAnnotation(&db.ObjectMeta, + processedGenerationAnnotation, strconv.FormatInt(db.GetGeneration(), formatIntBaseDecimal)) + + return nil +} + +func (h *ClickhouseDatabaseHandler) delete(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (bool, error) { + db, err := h.convert(obj) + if err != nil { + return false, err + } + + if fromAnyPointer(db.Spec.TerminationProtection) { + return false, errTerminationProtectionOn + } + + err = avn.ClickhouseDatabase.Delete(ctx, db.Spec.Project, db.Spec.ServiceName, db.Name) + if err != nil && !isNotFound(err) { + return false, err + } + + return true, nil +} + +func (h *ClickhouseDatabaseHandler) get(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (*corev1.Secret, error) { + db, err := h.convert(obj) + if err != nil { + return nil, err + } + + _, err = avn.ClickhouseDatabase.Get(ctx, db.Spec.Project, db.Spec.ServiceName, db.Name) + if err != nil { + return nil, err + } + + meta.SetStatusCondition(&db.Status.Conditions, + getRunningCondition(metav1.ConditionTrue, "CheckRunning", + "Instance is running on Aiven side")) + + metav1.SetMetaDataAnnotation(&db.ObjectMeta, instanceIsRunningAnnotation, "true") + + return nil, nil +} + +func (h *ClickhouseDatabaseHandler) checkPreconditions(ctx context.Context, avn *aiven.Client, avnGen avngen.Client, obj client.Object) (bool, error) { + db, err := h.convert(obj) + if err != nil { + return false, err + } + + meta.SetStatusCondition(&db.Status.Conditions, + getInitializedCondition("Preconditions", "Checking preconditions")) + + return checkServiceIsRunning(ctx, avn, avnGen, db.Spec.Project, db.Spec.ServiceName) +} + +func (h *ClickhouseDatabaseHandler) convert(i client.Object) (*v1alpha1.ClickhouseDatabase, error) { + db, ok := i.(*v1alpha1.ClickhouseDatabase) + if !ok { + return nil, fmt.Errorf("cannot convert object to ClickhouseDatabase") + } + + return db, nil +} diff --git a/controllers/setup.go b/controllers/setup.go index 103ef1d1..144c89c0 100644 --- a/controllers/setup.go +++ b/controllers/setup.go @@ -26,6 +26,7 @@ func SetupControllers(mgr ctrl.Manager, defaultToken, kubeVersion, operatorVersi builders := map[string]reconcilerBuilder{ "Cassandra": newCassandraReconciler, "Clickhouse": newClickhouseReconciler, + "ClickhouseDatabase": newClickhouseDatabaseReconciler, "ClickhouseUser": newClickhouseUserReconciler, "ConnectionPool": newConnectionPoolReconciler, "Database": newDatabaseReconciler, diff --git a/docs/docs/api-reference/clickhousedatabase.md b/docs/docs/api-reference/clickhousedatabase.md new file mode 100644 index 00000000..1dd8d863 --- /dev/null +++ b/docs/docs/api-reference/clickhousedatabase.md @@ -0,0 +1,57 @@ +--- +title: "ClickhouseDatabase" +--- + +## Usage example + +```yaml +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: my-db +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-project-name + serviceName: my-clickhouse +``` + +## ClickhouseDatabase {: #ClickhouseDatabase } + +ClickhouseDatabase is the Schema for the databases API. + +**Required** + +- [`apiVersion`](#apiVersion-property){: name='apiVersion-property'} (string). Value `aiven.io/v1alpha1`. +- [`kind`](#kind-property){: name='kind-property'} (string). Value `ClickhouseDatabase`. +- [`metadata`](#metadata-property){: name='metadata-property'} (object). Data that identifies the object, including a `name` string and optional `namespace`. +- [`spec`](#spec-property){: name='spec-property'} (object). ClickhouseDatabaseSpec defines the desired state of ClickhouseDatabase. See below for [nested schema](#spec). + +## spec {: #spec } + +_Appears on [`ClickhouseDatabase`](#ClickhouseDatabase)._ + +ClickhouseDatabaseSpec defines the desired state of ClickhouseDatabase. + +**Required** + +- [`project`](#spec.project-property){: name='spec.project-property'} (string, Immutable, MaxLength: 63, Format: `^[a-zA-Z0-9_-]*$`). Project to link the database to. +- [`serviceName`](#spec.serviceName-property){: name='spec.serviceName-property'} (string, Immutable, MaxLength: 63). Clickhouse service to link the database to. + +**Optional** + +- [`authSecretRef`](#spec.authSecretRef-property){: name='spec.authSecretRef-property'} (object). Authentication reference to Aiven token in a secret. See below for [nested schema](#spec.authSecretRef). +- [`terminationProtection`](#spec.terminationProtection-property){: name='spec.terminationProtection-property'} (boolean). It is a Kubernetes side deletion protections, which prevents the database from being deleted by Kubernetes. It is recommended to enable this for any production databases containing critical data. + +## authSecretRef {: #spec.authSecretRef } + +_Appears on [`spec`](#spec)._ + +Authentication reference to Aiven token in a secret. + +**Required** + +- [`key`](#spec.authSecretRef.key-property){: name='spec.authSecretRef.key-property'} (string, MinLength: 1). +- [`name`](#spec.authSecretRef.name-property){: name='spec.authSecretRef.name-property'} (string, MinLength: 1). diff --git a/docs/docs/api-reference/connectionpool.md b/docs/docs/api-reference/connectionpool.md index 57c539e6..e6be5ba9 100644 --- a/docs/docs/api-reference/connectionpool.md +++ b/docs/docs/api-reference/connectionpool.md @@ -23,7 +23,7 @@ spec: baz: egg project: aiven-project-name - serviceName: google-europe-west1 + serviceName: my-service databaseName: my-db username: my-user poolMode: transaction diff --git a/docs/docs/api-reference/database.md b/docs/docs/api-reference/database.md index 11c5aaa4..b1d02405 100644 --- a/docs/docs/api-reference/database.md +++ b/docs/docs/api-reference/database.md @@ -15,7 +15,7 @@ spec: key: token project: aiven-project-name - serviceName: google-europe-west1 + serviceName: my-service lcCtype: en_US.UTF-8 lcCollate: en_US.UTF-8 diff --git a/docs/docs/api-reference/examples/clickhousedatabase.yaml b/docs/docs/api-reference/examples/clickhousedatabase.yaml new file mode 100644 index 00000000..87363065 --- /dev/null +++ b/docs/docs/api-reference/examples/clickhousedatabase.yaml @@ -0,0 +1,11 @@ +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: my-db +spec: + authSecretRef: + name: aiven-token + key: token + + project: aiven-project-name + serviceName: my-clickhouse diff --git a/docs/docs/api-reference/examples/connectionpool.yaml b/docs/docs/api-reference/examples/connectionpool.yaml index 224d93cc..e8879c61 100644 --- a/docs/docs/api-reference/examples/connectionpool.yaml +++ b/docs/docs/api-reference/examples/connectionpool.yaml @@ -16,7 +16,7 @@ spec: baz: egg project: aiven-project-name - serviceName: google-europe-west1 + serviceName: my-service databaseName: my-db username: my-user poolMode: transaction diff --git a/docs/docs/api-reference/examples/database.yaml b/docs/docs/api-reference/examples/database.yaml index cb9d2a7f..72922c7b 100644 --- a/docs/docs/api-reference/examples/database.yaml +++ b/docs/docs/api-reference/examples/database.yaml @@ -8,7 +8,7 @@ spec: key: token project: aiven-project-name - serviceName: google-europe-west1 + serviceName: my-service lcCtype: en_US.UTF-8 lcCollate: en_US.UTF-8 diff --git a/tests/clickhouse_test.go b/tests/clickhouse_test.go index 05251c0b..fdbea573 100644 --- a/tests/clickhouse_test.go +++ b/tests/clickhouse_test.go @@ -11,19 +11,19 @@ import ( clickhouseuserconfig "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/clickhouse" ) -func getClickhouseYaml(project, name, cloudName string) string { +func getClickhouseYaml(project, cloudName, chName, dbName string) string { return fmt.Sprintf(` apiVersion: aiven.io/v1alpha1 kind: Clickhouse metadata: - name: %[2]s + name: %[3]s spec: authSecretRef: name: aiven-token key: token project: %[1]s - cloudName: %[3]s + cloudName: %[2]s plan: startup-16 tags: @@ -36,7 +36,20 @@ spec: description: bar - network: 10.20.0.0/16 -`, project, name, cloudName) +--- + +apiVersion: aiven.io/v1alpha1 +kind: ClickhouseDatabase +metadata: + name: %[4]s +spec: + authSecretRef: + name: aiven-token + key: token + + project: %[1]s + serviceName: %[3]s +`, project, cloudName, chName, dbName) } func TestClickhouse(t *testing.T) { @@ -47,8 +60,9 @@ func TestClickhouse(t *testing.T) { ctx, cancel := testCtx() defer cancel() - name := randName("clickhouse") - yml := getClickhouseYaml(cfg.Project, name, cfg.PrimaryCloudName) + chName := randName("clickhouse") + dbName := randName("clickhouse") + yml := getClickhouseYaml(cfg.Project, cfg.PrimaryCloudName, chName, dbName) s := NewSession(ctx, k8sClient, cfg.Project) // Cleans test afterward @@ -60,10 +74,10 @@ func TestClickhouse(t *testing.T) { // Waits kube objects ch := new(v1alpha1.Clickhouse) - require.NoError(t, s.GetRunning(ch, name)) + require.NoError(t, s.GetRunning(ch, chName)) // THEN - chAvn, err := avnClient.Services.Get(ctx, cfg.Project, name) + chAvn, err := avnClient.Services.Get(ctx, cfg.Project, chName) require.NoError(t, err) assert.Equal(t, chAvn.Name, ch.GetName()) assert.Equal(t, "RUNNING", ch.Status.State) @@ -71,7 +85,7 @@ func TestClickhouse(t *testing.T) { assert.Equal(t, chAvn.Plan, ch.Spec.Plan) assert.Equal(t, chAvn.CloudName, ch.Spec.CloudName) assert.Equal(t, map[string]string{"env": "test", "instance": "foo"}, ch.Spec.Tags) - chResp, err := avnClient.ServiceTags.Get(ctx, cfg.Project, name) + chResp, err := avnClient.ServiceTags.Get(ctx, cfg.Project, chName) require.NoError(t, err) assert.Equal(t, chResp.Tags, ch.Spec.Tags) @@ -108,4 +122,13 @@ func TestClickhouse(t *testing.T) { assert.NotEmpty(t, secret.Data["CLICKHOUSE_USER"]) assert.NotEmpty(t, secret.Data["CLICKHOUSE_PASSWORD"]) assert.NotEmpty(t, secret.Data["CLICKHOUSE_CA_CERT"]) + + // Validates ClickhouseDatabase + db := new(v1alpha1.ClickhouseDatabase) + require.NoError(t, s.GetRunning(db, dbName)) + + dbAvn, err := avnClient.ClickhouseDatabase.Get(ctx, cfg.Project, chName, dbName) + require.NoError(t, err) + assert.Equal(t, dbName, db.GetName()) + assert.Equal(t, dbAvn.Name, db.GetName()) }