diff --git a/CHANGELOG.md b/CHANGELOG.md index cc4c2725..d4c4867f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Settings - Add `Flink` field `userConfig.custom_code`, type `boolean`: Enable to upload Custom JARs for Flink applications +- Add kind: `Valkey` ## v0.26.0 - 2024-11-21 diff --git a/PROJECT b/PROJECT index caf11ecb..6bd5a777 100644 --- a/PROJECT +++ b/PROJECT @@ -304,4 +304,17 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: aiven.io + kind: Valkey + path: github.com/aiven/aiven-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + conversion: true + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/api/v1alpha1/setup_webhooks.go b/api/v1alpha1/setup_webhooks.go index af91e07b..44b2a755 100644 --- a/api/v1alpha1/setup_webhooks.go +++ b/api/v1alpha1/setup_webhooks.go @@ -67,6 +67,9 @@ func SetupWebhooks(mgr ctrl.Manager) error { if err := (&Grafana{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("webhook Grafana: %w", err) } + if err := (&Valkey{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("webhook Valkey: %w", err) + } //+kubebuilder:scaffold:builder return nil diff --git a/api/v1alpha1/userconfig/service/valkey/valkey.go b/api/v1alpha1/userconfig/service/valkey/valkey.go new file mode 100644 index 00000000..f468f130 --- /dev/null +++ b/api/v1alpha1/userconfig/service/valkey/valkey.go @@ -0,0 +1,185 @@ +// Code generated by user config generator. DO NOT EDIT. +// +kubebuilder:object:generate=true + +package valkeyuserconfig + +// CIDR address block, either as a string, or in a dict with an optional description field +type IpFilter struct { + // +kubebuilder:validation:MaxLength=1024 + // Description for IP filter list entry + Description *string `groups:"create,update" json:"description,omitempty"` + + // +kubebuilder:validation:MaxLength=43 + // CIDR address block + Network string `groups:"create,update" json:"network"` +} + +// Migrate data from existing server +type Migration struct { + // +kubebuilder:validation:MaxLength=63 + // Database name for bootstrapping the initial connection + Dbname *string `groups:"create,update" json:"dbname,omitempty"` + + // +kubebuilder:validation:MaxLength=255 + // Hostname or IP address of the server where to migrate data from + Host string `groups:"create,update" json:"host"` + + // +kubebuilder:validation:MaxLength=2048 + // Comma-separated list of databases, which should be ignored during migration (supported by MySQL and PostgreSQL only at the moment) + IgnoreDbs *string `groups:"create,update" json:"ignore_dbs,omitempty"` + + // +kubebuilder:validation:MaxLength=2048 + // Comma-separated list of database roles, which should be ignored during migration (supported by PostgreSQL only at the moment) + IgnoreRoles *string `groups:"create,update" json:"ignore_roles,omitempty"` + + // +kubebuilder:validation:Enum="dump";"replication" + // The migration method to be used (currently supported only by Redis, Dragonfly, MySQL and PostgreSQL service types) + Method *string `groups:"create,update" json:"method,omitempty"` + + // +kubebuilder:validation:MaxLength=256 + // Password for authentication with the server where to migrate data from + Password *string `groups:"create,update" json:"password,omitempty"` + + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // Port number of the server where to migrate data from + Port int `groups:"create,update" json:"port"` + + // The server where to migrate data from is secured with SSL + Ssl *bool `groups:"create,update" json:"ssl,omitempty"` + + // +kubebuilder:validation:MaxLength=256 + // User name for authentication with the server where to migrate data from + Username *string `groups:"create,update" json:"username,omitempty"` +} + +// Allow access to selected service ports from private networks +type PrivateAccess struct { + // Allow clients to connect to prometheus with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations + Prometheus *bool `groups:"create,update" json:"prometheus,omitempty"` + + // Allow clients to connect to valkey with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations + Valkey *bool `groups:"create,update" json:"valkey,omitempty"` +} + +// Allow access to selected service components through Privatelink +type PrivatelinkAccess struct { + // Enable prometheus + Prometheus *bool `groups:"create,update" json:"prometheus,omitempty"` + + // Enable valkey + Valkey *bool `groups:"create,update" json:"valkey,omitempty"` +} + +// Allow access to selected service ports from the public Internet +type PublicAccess struct { + // Allow clients to connect to prometheus from the public internet for service nodes that are in a project VPC or another type of private network + Prometheus *bool `groups:"create,update" json:"prometheus,omitempty"` + + // Allow clients to connect to valkey from the public internet for service nodes that are in a project VPC or another type of private network + Valkey *bool `groups:"create,update" json:"valkey,omitempty"` +} +type ValkeyUserConfig struct { + // +kubebuilder:validation:MaxItems=1 + // Additional Cloud Regions for Backup Replication + AdditionalBackupRegions []string `groups:"create,update" json:"additional_backup_regions,omitempty"` + + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=23 + // The hour of day (in UTC) when backup for the service is started. New backup is only started if previous backup has already completed. + BackupHour *int `groups:"create,update" json:"backup_hour,omitempty"` + + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=59 + // The minute of an hour when backup for the service is started. New backup is only started if previous backup has already completed. + BackupMinute *int `groups:"create,update" json:"backup_minute,omitempty"` + + // +kubebuilder:validation:MaxItems=1024 + // Allow incoming connections from CIDR address block, e.g. '10.20.0.0/16' + IpFilter []*IpFilter `groups:"create,update" json:"ip_filter,omitempty"` + + // Migrate data from existing server + Migration *Migration `groups:"create,update" json:"migration,omitempty"` + + // Allow access to selected service ports from private networks + PrivateAccess *PrivateAccess `groups:"create,update" json:"private_access,omitempty"` + + // Allow access to selected service components through Privatelink + PrivatelinkAccess *PrivatelinkAccess `groups:"create,update" json:"privatelink_access,omitempty"` + + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z][-a-z0-9]{0,63}$|^$` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // Name of another project to fork a service from. This has effect only when a new service is being created. + ProjectToForkFrom *string `groups:"create" json:"project_to_fork_from,omitempty"` + + // Allow access to selected service ports from the public Internet + PublicAccess *PublicAccess `groups:"create,update" json:"public_access,omitempty"` + + // +kubebuilder:validation:MaxLength=128 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9-_:.]+$` + // Name of the basebackup to restore in forked service + RecoveryBasebackupName *string `groups:"create,update" json:"recovery_basebackup_name,omitempty"` + + // Store logs for the service so that they are available in the HTTP API and console. + ServiceLog *bool `groups:"create,update" json:"service_log,omitempty"` + + // +kubebuilder:validation:MaxLength=64 + // +kubebuilder:validation:Pattern=`^[a-z][-a-z0-9]{0,63}$|^$` + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // Name of another service to fork from. This has effect only when a new service is being created. + ServiceToForkFrom *string `groups:"create" json:"service_to_fork_from,omitempty"` + + // Use static public IP addresses + StaticIps *bool `groups:"create,update" json:"static_ips,omitempty"` + + // +kubebuilder:validation:Enum="allchannels";"resetchannels" + // Determines default pub/sub channels' ACL for new users if ACL is not supplied. When this option is not defined, all_channels is assumed to keep backward compatibility. This option doesn't affect Valkey configuration acl-pubsub-default. + ValkeyAclChannelsDefault *string `groups:"create,update" json:"valkey_acl_channels_default,omitempty"` + + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=32 + // Set Valkey IO thread count. Changing this will cause a restart of the Valkey service. + ValkeyIoThreads *int `groups:"create,update" json:"valkey_io_threads,omitempty"` + + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=120 + // LFU maxmemory-policy counter decay time in minutes + ValkeyLfuDecayTime *int `groups:"create,update" json:"valkey_lfu_decay_time,omitempty"` + + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=100 + // Counter logarithm factor for volatile-lfu and allkeys-lfu maxmemory-policies + ValkeyLfuLogFactor *int `groups:"create,update" json:"valkey_lfu_log_factor,omitempty"` + + // +kubebuilder:validation:Enum="allkeys-lfu";"allkeys-lru";"allkeys-random";"noeviction";"volatile-lfu";"volatile-lru";"volatile-random";"volatile-ttl" + // Valkey maxmemory-policy + ValkeyMaxmemoryPolicy *string `groups:"create,update" json:"valkey_maxmemory_policy,omitempty"` + + // +kubebuilder:validation:MaxLength=32 + // +kubebuilder:validation:Pattern=`^[KEg\$lshzxentdmA]*$` + // Set notify-keyspace-events option + ValkeyNotifyKeyspaceEvents *string `groups:"create,update" json:"valkey_notify_keyspace_events,omitempty"` + + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=128 + // Set number of Valkey databases. Changing this will cause a restart of the Valkey service. + ValkeyNumberOfDatabases *int `groups:"create,update" json:"valkey_number_of_databases,omitempty"` + + // +kubebuilder:validation:Enum="off";"rdb" + // When persistence is 'rdb', Valkey does RDB dumps each 10 minutes if any key is changed. Also RDB dumps are done according to backup schedule for backup purposes. When persistence is 'off', no RDB dumps and backups are done, so data can be lost at any moment if service is restarted for any reason, or if service is powered off. Also service can't be forked. + ValkeyPersistence *string `groups:"create,update" json:"valkey_persistence,omitempty"` + + // +kubebuilder:validation:Minimum=32 + // +kubebuilder:validation:Maximum=512 + // Set output buffer limit for pub / sub clients in MB. The value is the hard limit, the soft limit is 1/4 of the hard limit. When setting the limit, be mindful of the available memory in the selected service plan. + ValkeyPubsubClientOutputBufferLimit *int `groups:"create,update" json:"valkey_pubsub_client_output_buffer_limit,omitempty"` + + // Require SSL to access Valkey + ValkeySsl *bool `groups:"create,update" json:"valkey_ssl,omitempty"` + + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=2073600 + // Valkey idle connection timeout in seconds + ValkeyTimeout *int `groups:"create,update" json:"valkey_timeout,omitempty"` +} diff --git a/api/v1alpha1/userconfig/service/valkey/zz_generated.deepcopy.go b/api/v1alpha1/userconfig/service/valkey/zz_generated.deepcopy.go new file mode 100644 index 00000000..58f99ad9 --- /dev/null +++ b/api/v1alpha1/userconfig/service/valkey/zz_generated.deepcopy.go @@ -0,0 +1,293 @@ +//go:build !ignore_autogenerated + +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +// Code generated by controller-gen. DO NOT EDIT. + +package valkeyuserconfig + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IpFilter) DeepCopyInto(out *IpFilter) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IpFilter. +func (in *IpFilter) DeepCopy() *IpFilter { + if in == nil { + return nil + } + out := new(IpFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Migration) DeepCopyInto(out *Migration) { + *out = *in + if in.Dbname != nil { + in, out := &in.Dbname, &out.Dbname + *out = new(string) + **out = **in + } + if in.IgnoreDbs != nil { + in, out := &in.IgnoreDbs, &out.IgnoreDbs + *out = new(string) + **out = **in + } + if in.IgnoreRoles != nil { + in, out := &in.IgnoreRoles, &out.IgnoreRoles + *out = new(string) + **out = **in + } + if in.Method != nil { + in, out := &in.Method, &out.Method + *out = new(string) + **out = **in + } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(string) + **out = **in + } + if in.Ssl != nil { + in, out := &in.Ssl, &out.Ssl + *out = new(bool) + **out = **in + } + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Migration. +func (in *Migration) DeepCopy() *Migration { + if in == nil { + return nil + } + out := new(Migration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrivateAccess) DeepCopyInto(out *PrivateAccess) { + *out = *in + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(bool) + **out = **in + } + if in.Valkey != nil { + in, out := &in.Valkey, &out.Valkey + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrivateAccess. +func (in *PrivateAccess) DeepCopy() *PrivateAccess { + if in == nil { + return nil + } + out := new(PrivateAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrivatelinkAccess) DeepCopyInto(out *PrivatelinkAccess) { + *out = *in + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(bool) + **out = **in + } + if in.Valkey != nil { + in, out := &in.Valkey, &out.Valkey + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrivatelinkAccess. +func (in *PrivatelinkAccess) DeepCopy() *PrivatelinkAccess { + if in == nil { + return nil + } + out := new(PrivatelinkAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicAccess) DeepCopyInto(out *PublicAccess) { + *out = *in + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(bool) + **out = **in + } + if in.Valkey != nil { + in, out := &in.Valkey, &out.Valkey + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicAccess. +func (in *PublicAccess) DeepCopy() *PublicAccess { + if in == nil { + return nil + } + out := new(PublicAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValkeyUserConfig) DeepCopyInto(out *ValkeyUserConfig) { + *out = *in + if in.AdditionalBackupRegions != nil { + in, out := &in.AdditionalBackupRegions, &out.AdditionalBackupRegions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.BackupHour != nil { + in, out := &in.BackupHour, &out.BackupHour + *out = new(int) + **out = **in + } + if in.BackupMinute != nil { + in, out := &in.BackupMinute, &out.BackupMinute + *out = new(int) + **out = **in + } + if in.IpFilter != nil { + in, out := &in.IpFilter, &out.IpFilter + *out = make([]*IpFilter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(IpFilter) + (*in).DeepCopyInto(*out) + } + } + } + if in.Migration != nil { + in, out := &in.Migration, &out.Migration + *out = new(Migration) + (*in).DeepCopyInto(*out) + } + if in.PrivateAccess != nil { + in, out := &in.PrivateAccess, &out.PrivateAccess + *out = new(PrivateAccess) + (*in).DeepCopyInto(*out) + } + if in.PrivatelinkAccess != nil { + in, out := &in.PrivatelinkAccess, &out.PrivatelinkAccess + *out = new(PrivatelinkAccess) + (*in).DeepCopyInto(*out) + } + if in.ProjectToForkFrom != nil { + in, out := &in.ProjectToForkFrom, &out.ProjectToForkFrom + *out = new(string) + **out = **in + } + if in.PublicAccess != nil { + in, out := &in.PublicAccess, &out.PublicAccess + *out = new(PublicAccess) + (*in).DeepCopyInto(*out) + } + if in.RecoveryBasebackupName != nil { + in, out := &in.RecoveryBasebackupName, &out.RecoveryBasebackupName + *out = new(string) + **out = **in + } + if in.ServiceLog != nil { + in, out := &in.ServiceLog, &out.ServiceLog + *out = new(bool) + **out = **in + } + if in.ServiceToForkFrom != nil { + in, out := &in.ServiceToForkFrom, &out.ServiceToForkFrom + *out = new(string) + **out = **in + } + if in.StaticIps != nil { + in, out := &in.StaticIps, &out.StaticIps + *out = new(bool) + **out = **in + } + if in.ValkeyAclChannelsDefault != nil { + in, out := &in.ValkeyAclChannelsDefault, &out.ValkeyAclChannelsDefault + *out = new(string) + **out = **in + } + if in.ValkeyIoThreads != nil { + in, out := &in.ValkeyIoThreads, &out.ValkeyIoThreads + *out = new(int) + **out = **in + } + if in.ValkeyLfuDecayTime != nil { + in, out := &in.ValkeyLfuDecayTime, &out.ValkeyLfuDecayTime + *out = new(int) + **out = **in + } + if in.ValkeyLfuLogFactor != nil { + in, out := &in.ValkeyLfuLogFactor, &out.ValkeyLfuLogFactor + *out = new(int) + **out = **in + } + if in.ValkeyMaxmemoryPolicy != nil { + in, out := &in.ValkeyMaxmemoryPolicy, &out.ValkeyMaxmemoryPolicy + *out = new(string) + **out = **in + } + if in.ValkeyNotifyKeyspaceEvents != nil { + in, out := &in.ValkeyNotifyKeyspaceEvents, &out.ValkeyNotifyKeyspaceEvents + *out = new(string) + **out = **in + } + if in.ValkeyNumberOfDatabases != nil { + in, out := &in.ValkeyNumberOfDatabases, &out.ValkeyNumberOfDatabases + *out = new(int) + **out = **in + } + if in.ValkeyPersistence != nil { + in, out := &in.ValkeyPersistence, &out.ValkeyPersistence + *out = new(string) + **out = **in + } + if in.ValkeyPubsubClientOutputBufferLimit != nil { + in, out := &in.ValkeyPubsubClientOutputBufferLimit, &out.ValkeyPubsubClientOutputBufferLimit + *out = new(int) + **out = **in + } + if in.ValkeySsl != nil { + in, out := &in.ValkeySsl, &out.ValkeySsl + *out = new(bool) + **out = **in + } + if in.ValkeyTimeout != nil { + in, out := &in.ValkeyTimeout, &out.ValkeyTimeout + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeyUserConfig. +func (in *ValkeyUserConfig) DeepCopy() *ValkeyUserConfig { + if in == nil { + return nil + } + out := new(ValkeyUserConfig) + in.DeepCopyInto(out) + return out +} diff --git a/api/v1alpha1/valkey_types.go b/api/v1alpha1/valkey_types.go new file mode 100644 index 00000000..d9886ada --- /dev/null +++ b/api/v1alpha1/valkey_types.go @@ -0,0 +1,72 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + valkeyuserconfig "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/valkey" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ValkeySpec defines the desired state of Valkey +type ValkeySpec struct { + ServiceCommonSpec `json:",inline"` + + // Valkey specific user configuration options + UserConfig *valkeyuserconfig.ValkeyUserConfig `json:"userConfig,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Valkey is the Schema for the valkeys API +// Info "Exposes secret keys": `VALKEY_HOST`, `VALKEY_PORT`, `VALKEY_USER`, `VALKEY_PASSWORD` +// +kubebuilder:printcolumn:name="Project",type="string",JSONPath=".spec.project" +// +kubebuilder:printcolumn:name="Region",type="string",JSONPath=".spec.cloudName" +// +kubebuilder:printcolumn:name="Plan",type="string",JSONPath=".spec.plan" +// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state" +type Valkey struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ValkeySpec `json:"spec,omitempty"` + Status ServiceStatus `json:"status,omitempty"` +} + +var _ AivenManagedObject = &Valkey{} + +func (in *Valkey) AuthSecretRef() *AuthSecretReference { + return in.Spec.AuthSecretRef +} + +func (in *Valkey) Conditions() *[]metav1.Condition { + return &in.Status.Conditions +} + +func (in *Valkey) NoSecret() bool { + return in.Spec.ConnInfoSecretTargetDisabled != nil && *in.Spec.ConnInfoSecretTargetDisabled +} + +func (in *Valkey) GetRefs() []*ResourceReferenceObject { + return in.Spec.GetRefs(in.GetNamespace()) +} + +func (in *Valkey) GetConnInfoSecretTarget() ConnInfoSecretTarget { + return in.Spec.ConnInfoSecretTarget +} + +//+kubebuilder:object:root=true + +// ValkeyList contains a list of Valkey +type ValkeyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Valkey `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Valkey{}, &ValkeyList{}) +} diff --git a/api/v1alpha1/valkey_webhook.go b/api/v1alpha1/valkey_webhook.go new file mode 100644 index 00000000..820c4b42 --- /dev/null +++ b/api/v1alpha1/valkey_webhook.go @@ -0,0 +1,62 @@ +// 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 valkeylog = logf.Log.WithName("valkey-resource") + +func (in *Valkey) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(in). + Complete() +} + +//+kubebuilder:webhook:path=/mutate-aiven-io-v1alpha1-valkey,mutating=true,failurePolicy=fail,groups=aiven.io,resources=valkey,verbs=create;update,versions=v1alpha1,name=mvalkey.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &Valkey{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (in *Valkey) Default() { + valkeylog.Info("default", "name", in.Name) +} + +//+kubebuilder:webhook:verbs=create;update;delete,path=/validate-aiven-io-v1alpha1-valkey,mutating=false,failurePolicy=fail,groups=aiven.io,resources=valkey,versions=v1alpha1,name=vvalkey.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Validator = &Valkey{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (in *Valkey) ValidateCreate() error { + valkeylog.Info("validate create", "name", in.Name) + + return in.Spec.Validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (in *Valkey) ValidateUpdate(old runtime.Object) error { + valkeylog.Info("validate update", "name", in.Name) + return in.Spec.Validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (in *Valkey) ValidateDelete() error { + valkeylog.Info("validate delete", "name", in.Name) + + if in.Spec.TerminationProtection != nil && *in.Spec.TerminationProtection { + return errors.New("cannot delete Valkey service, termination protection is on") + } + + if in.Spec.ProjectVPCID != "" && in.Spec.ProjectVPCRef != nil { + return errors.New("cannot use both projectVpcId and projectVPCRef") + } + + return nil +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e4873e38..735d2392 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -44,6 +44,7 @@ import ( opensearch "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/opensearch" pg "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/pg" redis "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/redis" + valkey "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/valkey" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -3142,3 +3143,83 @@ func (in *ServiceUserStatus) DeepCopy() *ServiceUserStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Valkey) DeepCopyInto(out *Valkey) { + *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 Valkey. +func (in *Valkey) DeepCopy() *Valkey { + if in == nil { + return nil + } + out := new(Valkey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Valkey) 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 *ValkeyList) DeepCopyInto(out *ValkeyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Valkey, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeyList. +func (in *ValkeyList) DeepCopy() *ValkeyList { + if in == nil { + return nil + } + out := new(ValkeyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValkeyList) 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 *ValkeySpec) DeepCopyInto(out *ValkeySpec) { + *out = *in + in.ServiceCommonSpec.DeepCopyInto(&out.ServiceCommonSpec) + if in.UserConfig != nil { + in, out := &in.UserConfig, &out.UserConfig + *out = new(valkey.ValkeyUserConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeySpec. +func (in *ValkeySpec) DeepCopy() *ValkeySpec { + if in == nil { + return nil + } + out := new(ValkeySpec) + in.DeepCopyInto(out) + return out +} diff --git a/charts/aiven-operator-crds/templates/aiven.io_valkeys.yaml b/charts/aiven-operator-crds/templates/aiven.io_valkeys.yaml new file mode 100644 index 00000000..188ed2bb --- /dev/null +++ b/charts/aiven-operator-crds/templates/aiven.io_valkeys.yaml @@ -0,0 +1,591 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: valkeys.aiven.io +spec: + group: aiven.io + names: + kind: Valkey + listKind: ValkeyList + plural: valkeys + singular: valkey + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.project + name: Project + type: string + - jsonPath: .spec.cloudName + name: Region + type: string + - jsonPath: .spec.plan + name: Plan + type: string + - jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Valkey is the Schema for the valkeys API + Info "Exposes secret keys": `VALKEY_HOST`, `VALKEY_PORT`, `VALKEY_USER`, `VALKEY_PASSWORD` + 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: ValkeySpec defines the desired state of Valkey + 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 + cloudName: + description: Cloud the service runs in. + maxLength: 256 + type: string + connInfoSecretTarget: + description: Secret configuration. + properties: + annotations: + additionalProperties: + type: string + description: Annotations added to the secret + type: object + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + description: Labels added to the secret + type: object + x-kubernetes-preserve-unknown-fields: true + name: + description: + Name of the secret resource to be created. By default, + it is equal to the resource name + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + prefix: + description: |- + Prefix for the secret's keys. + Added "as is" without any transformations. + By default, is equal to the kind name in uppercase + underscore, e.g. `KAFKA_`, `REDIS_`, etc. + type: string + required: + - name + type: object + connInfoSecretTargetDisabled: + description: + When true, the secret containing connection information + will not be created, defaults to false. This field cannot be changed + after resource creation. + type: boolean + x-kubernetes-validations: + - message: connInfoSecretTargetDisabled is immutable. + rule: self == oldSelf + disk_space: + description: |- + The disk space of the service, possible values depend on the service type, the cloud provider and the project. + Reducing will result in the service re-balancing. + The removal of this field does not change the value. + pattern: (?i)^[1-9][0-9]*(GiB|G)?$ + type: string + maintenanceWindowDow: + description: + Day of week when maintenance operations should be performed. + One monday, tuesday, wednesday, etc. + enum: + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - sunday + type: string + maintenanceWindowTime: + description: + Time of day when maintenance operations should be performed. + UTC time in HH:mm:ss format. + maxLength: 8 + type: string + plan: + description: Subscription plan. + maxLength: 128 + type: string + project: + description: Identifies the project this resource belongs to + maxLength: 63 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + projectVPCRef: + description: + ProjectVPCRef reference to ProjectVPC resource to use + its ID as ProjectVPCID automatically + properties: + name: + minLength: 1 + type: string + namespace: + minLength: 1 + type: string + required: + - name + type: object + projectVpcId: + description: Identifier of the VPC the service should be in, if any. + maxLength: 36 + type: string + serviceIntegrations: + description: + Service integrations to specify when creating a service. + Not applied after initial service creation + items: + description: + Service integrations to specify when creating a service. + Not applied after initial service creation + properties: + integrationType: + enum: + - read_replica + type: string + sourceServiceName: + maxLength: 64 + minLength: 1 + type: string + required: + - integrationType + - sourceServiceName + type: object + maxItems: 1 + type: array + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + tags: + additionalProperties: + type: string + description: + Tags are key-value pairs that allow you to categorize + services. + type: object + technicalEmails: + description: + Defines the email addresses that will receive alerts + about upcoming maintenance updates or warnings about service instability. + items: + properties: + email: + description: Email address. + pattern: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ + type: string + required: + - email + type: object + maxItems: 10 + type: array + terminationProtection: + description: + Prevent service from being deleted. It is recommended + to have this enabled for all services. + type: boolean + userConfig: + description: Valkey specific user configuration options + properties: + additional_backup_regions: + description: Additional Cloud Regions for Backup Replication + items: + type: string + maxItems: 1 + type: array + backup_hour: + description: + The hour of day (in UTC) when backup for the service + is started. New backup is only started if previous backup has + already completed. + maximum: 23 + minimum: 0 + type: integer + backup_minute: + description: + The minute of an hour when backup for the service + is started. New backup is only started if previous backup has + already completed. + maximum: 59 + minimum: 0 + type: integer + ip_filter: + description: + Allow incoming connections from CIDR address block, + e.g. '10.20.0.0/16' + items: + description: + CIDR address block, either as a string, or in a + dict with an optional description field + properties: + description: + description: Description for IP filter list entry + maxLength: 1024 + type: string + network: + description: CIDR address block + maxLength: 43 + type: string + required: + - network + type: object + maxItems: 1024 + type: array + migration: + description: Migrate data from existing server + properties: + dbname: + description: Database name for bootstrapping the initial connection + maxLength: 63 + type: string + host: + description: + Hostname or IP address of the server where to + migrate data from + maxLength: 255 + type: string + ignore_dbs: + description: + Comma-separated list of databases, which should + be ignored during migration (supported by MySQL and PostgreSQL + only at the moment) + maxLength: 2048 + type: string + ignore_roles: + description: + Comma-separated list of database roles, which + should be ignored during migration (supported by PostgreSQL + only at the moment) + maxLength: 2048 + type: string + method: + description: + The migration method to be used (currently supported + only by Redis, Dragonfly, MySQL and PostgreSQL service types) + enum: + - dump + - replication + type: string + password: + description: + Password for authentication with the server where + to migrate data from + maxLength: 256 + type: string + port: + description: + Port number of the server where to migrate data + from + maximum: 65535 + minimum: 1 + type: integer + ssl: + description: + The server where to migrate data from is secured + with SSL + type: boolean + username: + description: + User name for authentication with the server + where to migrate data from + maxLength: 256 + type: string + required: + - host + - port + type: object + private_access: + description: + Allow access to selected service ports from private + networks + properties: + prometheus: + description: + Allow clients to connect to prometheus with a + DNS name that always resolves to the service's private IP + addresses. Only available in certain network locations + type: boolean + valkey: + description: + Allow clients to connect to valkey with a DNS + name that always resolves to the service's private IP addresses. + Only available in certain network locations + type: boolean + type: object + privatelink_access: + description: + Allow access to selected service components through + Privatelink + properties: + prometheus: + description: Enable prometheus + type: boolean + valkey: + description: Enable valkey + type: boolean + type: object + project_to_fork_from: + description: + Name of another project to fork a service from. This + has effect only when a new service is being created. + maxLength: 63 + pattern: ^[a-z][-a-z0-9]{0,63}$|^$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + public_access: + description: + Allow access to selected service ports from the public + Internet + properties: + prometheus: + description: + Allow clients to connect to prometheus from the + public internet for service nodes that are in a project + VPC or another type of private network + type: boolean + valkey: + description: + Allow clients to connect to valkey from the public + internet for service nodes that are in a project VPC or + another type of private network + type: boolean + type: object + recovery_basebackup_name: + description: Name of the basebackup to restore in forked service + maxLength: 128 + pattern: ^[a-zA-Z0-9-_:.]+$ + type: string + service_log: + description: + Store logs for the service so that they are available + in the HTTP API and console. + type: boolean + service_to_fork_from: + description: + Name of another service to fork from. This has effect + only when a new service is being created. + maxLength: 64 + pattern: ^[a-z][-a-z0-9]{0,63}$|^$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + static_ips: + description: Use static public IP addresses + type: boolean + valkey_acl_channels_default: + description: + Determines default pub/sub channels' ACL for new + users if ACL is not supplied. When this option is not defined, + all_channels is assumed to keep backward compatibility. This + option doesn't affect Valkey configuration acl-pubsub-default. + enum: + - allchannels + - resetchannels + type: string + valkey_io_threads: + description: + Set Valkey IO thread count. Changing this will cause + a restart of the Valkey service. + maximum: 32 + minimum: 1 + type: integer + valkey_lfu_decay_time: + description: LFU maxmemory-policy counter decay time in minutes + maximum: 120 + minimum: 1 + type: integer + valkey_lfu_log_factor: + description: + Counter logarithm factor for volatile-lfu and allkeys-lfu + maxmemory-policies + maximum: 100 + minimum: 0 + type: integer + valkey_maxmemory_policy: + description: Valkey maxmemory-policy + enum: + - allkeys-lfu + - allkeys-lru + - allkeys-random + - noeviction + - volatile-lfu + - volatile-lru + - volatile-random + - volatile-ttl + type: string + valkey_notify_keyspace_events: + description: Set notify-keyspace-events option + maxLength: 32 + pattern: ^[KEg\$lshzxentdmA]*$ + type: string + valkey_number_of_databases: + description: + Set number of Valkey databases. Changing this will + cause a restart of the Valkey service. + maximum: 128 + minimum: 1 + type: integer + valkey_persistence: + description: + When persistence is 'rdb', Valkey does RDB dumps + each 10 minutes if any key is changed. Also RDB dumps are done + according to backup schedule for backup purposes. When persistence + is 'off', no RDB dumps and backups are done, so data can be + lost at any moment if service is restarted for any reason, or + if service is powered off. Also service can't be forked. + enum: + - "off" + - rdb + type: string + valkey_pubsub_client_output_buffer_limit: + description: + Set output buffer limit for pub / sub clients in + MB. The value is the hard limit, the soft limit is 1/4 of the + hard limit. When setting the limit, be mindful of the available + memory in the selected service plan. + maximum: 512 + minimum: 32 + type: integer + valkey_ssl: + description: Require SSL to access Valkey + type: boolean + valkey_timeout: + description: Valkey idle connection timeout in seconds + maximum: 2073600 + minimum: 0 + type: integer + type: object + required: + - plan + - project + type: object + x-kubernetes-validations: + - message: + connInfoSecretTargetDisabled can only be set during resource + creation. + rule: has(oldSelf.connInfoSecretTargetDisabled) == has(self.connInfoSecretTargetDisabled) + status: + description: ServiceStatus defines the observed state of service + properties: + conditions: + description: + Conditions represent the latest available observations + of a service state + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + state: + description: Service state + type: string + 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 5488d1f6..c716b5bf 100644 --- a/charts/aiven-operator/templates/cluster_role.yaml +++ b/charts/aiven-operator/templates/cluster_role.yaml @@ -753,6 +753,32 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - valkeys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - valkeys/finalizers + verbs: + - update + - apiGroups: + - aiven.io + resources: + - valkeys/status + verbs: + - get + - patch + - update - apiGroups: - coordination.k8s.io resources: diff --git a/charts/aiven-operator/templates/mutating_webhook_configuration.yaml b/charts/aiven-operator/templates/mutating_webhook_configuration.yaml index 8f476f85..ed9de652 100644 --- a/charts/aiven-operator/templates/mutating_webhook_configuration.yaml +++ b/charts/aiven-operator/templates/mutating_webhook_configuration.yaml @@ -429,5 +429,26 @@ webhooks: - serviceusers sideEffects: None {{- include "aiven-operator.webhookNamespaceSelector" . | indent 4 }} + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "aiven-operator.fullname" . }}-webhook-service + namespace: {{ include "aiven-operator.namespace" . }} + path: /mutate-aiven-io-v1alpha1-valkey + failurePolicy: Fail + name: mvalkey.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - valkey + sideEffects: None + {{- include "aiven-operator.webhookNamespaceSelector" . | indent 4 }} {{- end }} diff --git a/charts/aiven-operator/templates/validating_webhook_configuration.yaml b/charts/aiven-operator/templates/validating_webhook_configuration.yaml index db5832ef..6b5e58cc 100644 --- a/charts/aiven-operator/templates/validating_webhook_configuration.yaml +++ b/charts/aiven-operator/templates/validating_webhook_configuration.yaml @@ -444,5 +444,27 @@ webhooks: - serviceusers sideEffects: None {{- include "aiven-operator.webhookNamespaceSelector" . | indent 4 }} + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "aiven-operator.fullname" . }}-webhook-service + namespace: {{ include "aiven-operator.namespace" . }} + path: /validate-aiven-io-v1alpha1-valkey + failurePolicy: Fail + name: vvalkey.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - valkey + sideEffects: None + {{- include "aiven-operator.webhookNamespaceSelector" . | indent 4 }} {{- end }} diff --git a/config/crd/bases/aiven.io_valkeys.yaml b/config/crd/bases/aiven.io_valkeys.yaml new file mode 100644 index 00000000..188ed2bb --- /dev/null +++ b/config/crd/bases/aiven.io_valkeys.yaml @@ -0,0 +1,591 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: valkeys.aiven.io +spec: + group: aiven.io + names: + kind: Valkey + listKind: ValkeyList + plural: valkeys + singular: valkey + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.project + name: Project + type: string + - jsonPath: .spec.cloudName + name: Region + type: string + - jsonPath: .spec.plan + name: Plan + type: string + - jsonPath: .status.state + name: State + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Valkey is the Schema for the valkeys API + Info "Exposes secret keys": `VALKEY_HOST`, `VALKEY_PORT`, `VALKEY_USER`, `VALKEY_PASSWORD` + 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: ValkeySpec defines the desired state of Valkey + 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 + cloudName: + description: Cloud the service runs in. + maxLength: 256 + type: string + connInfoSecretTarget: + description: Secret configuration. + properties: + annotations: + additionalProperties: + type: string + description: Annotations added to the secret + type: object + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + description: Labels added to the secret + type: object + x-kubernetes-preserve-unknown-fields: true + name: + description: + Name of the secret resource to be created. By default, + it is equal to the resource name + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + prefix: + description: |- + Prefix for the secret's keys. + Added "as is" without any transformations. + By default, is equal to the kind name in uppercase + underscore, e.g. `KAFKA_`, `REDIS_`, etc. + type: string + required: + - name + type: object + connInfoSecretTargetDisabled: + description: + When true, the secret containing connection information + will not be created, defaults to false. This field cannot be changed + after resource creation. + type: boolean + x-kubernetes-validations: + - message: connInfoSecretTargetDisabled is immutable. + rule: self == oldSelf + disk_space: + description: |- + The disk space of the service, possible values depend on the service type, the cloud provider and the project. + Reducing will result in the service re-balancing. + The removal of this field does not change the value. + pattern: (?i)^[1-9][0-9]*(GiB|G)?$ + type: string + maintenanceWindowDow: + description: + Day of week when maintenance operations should be performed. + One monday, tuesday, wednesday, etc. + enum: + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - sunday + type: string + maintenanceWindowTime: + description: + Time of day when maintenance operations should be performed. + UTC time in HH:mm:ss format. + maxLength: 8 + type: string + plan: + description: Subscription plan. + maxLength: 128 + type: string + project: + description: Identifies the project this resource belongs to + maxLength: 63 + pattern: ^[a-zA-Z0-9_-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + projectVPCRef: + description: + ProjectVPCRef reference to ProjectVPC resource to use + its ID as ProjectVPCID automatically + properties: + name: + minLength: 1 + type: string + namespace: + minLength: 1 + type: string + required: + - name + type: object + projectVpcId: + description: Identifier of the VPC the service should be in, if any. + maxLength: 36 + type: string + serviceIntegrations: + description: + Service integrations to specify when creating a service. + Not applied after initial service creation + items: + description: + Service integrations to specify when creating a service. + Not applied after initial service creation + properties: + integrationType: + enum: + - read_replica + type: string + sourceServiceName: + maxLength: 64 + minLength: 1 + type: string + required: + - integrationType + - sourceServiceName + type: object + maxItems: 1 + type: array + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + tags: + additionalProperties: + type: string + description: + Tags are key-value pairs that allow you to categorize + services. + type: object + technicalEmails: + description: + Defines the email addresses that will receive alerts + about upcoming maintenance updates or warnings about service instability. + items: + properties: + email: + description: Email address. + pattern: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ + type: string + required: + - email + type: object + maxItems: 10 + type: array + terminationProtection: + description: + Prevent service from being deleted. It is recommended + to have this enabled for all services. + type: boolean + userConfig: + description: Valkey specific user configuration options + properties: + additional_backup_regions: + description: Additional Cloud Regions for Backup Replication + items: + type: string + maxItems: 1 + type: array + backup_hour: + description: + The hour of day (in UTC) when backup for the service + is started. New backup is only started if previous backup has + already completed. + maximum: 23 + minimum: 0 + type: integer + backup_minute: + description: + The minute of an hour when backup for the service + is started. New backup is only started if previous backup has + already completed. + maximum: 59 + minimum: 0 + type: integer + ip_filter: + description: + Allow incoming connections from CIDR address block, + e.g. '10.20.0.0/16' + items: + description: + CIDR address block, either as a string, or in a + dict with an optional description field + properties: + description: + description: Description for IP filter list entry + maxLength: 1024 + type: string + network: + description: CIDR address block + maxLength: 43 + type: string + required: + - network + type: object + maxItems: 1024 + type: array + migration: + description: Migrate data from existing server + properties: + dbname: + description: Database name for bootstrapping the initial connection + maxLength: 63 + type: string + host: + description: + Hostname or IP address of the server where to + migrate data from + maxLength: 255 + type: string + ignore_dbs: + description: + Comma-separated list of databases, which should + be ignored during migration (supported by MySQL and PostgreSQL + only at the moment) + maxLength: 2048 + type: string + ignore_roles: + description: + Comma-separated list of database roles, which + should be ignored during migration (supported by PostgreSQL + only at the moment) + maxLength: 2048 + type: string + method: + description: + The migration method to be used (currently supported + only by Redis, Dragonfly, MySQL and PostgreSQL service types) + enum: + - dump + - replication + type: string + password: + description: + Password for authentication with the server where + to migrate data from + maxLength: 256 + type: string + port: + description: + Port number of the server where to migrate data + from + maximum: 65535 + minimum: 1 + type: integer + ssl: + description: + The server where to migrate data from is secured + with SSL + type: boolean + username: + description: + User name for authentication with the server + where to migrate data from + maxLength: 256 + type: string + required: + - host + - port + type: object + private_access: + description: + Allow access to selected service ports from private + networks + properties: + prometheus: + description: + Allow clients to connect to prometheus with a + DNS name that always resolves to the service's private IP + addresses. Only available in certain network locations + type: boolean + valkey: + description: + Allow clients to connect to valkey with a DNS + name that always resolves to the service's private IP addresses. + Only available in certain network locations + type: boolean + type: object + privatelink_access: + description: + Allow access to selected service components through + Privatelink + properties: + prometheus: + description: Enable prometheus + type: boolean + valkey: + description: Enable valkey + type: boolean + type: object + project_to_fork_from: + description: + Name of another project to fork a service from. This + has effect only when a new service is being created. + maxLength: 63 + pattern: ^[a-z][-a-z0-9]{0,63}$|^$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + public_access: + description: + Allow access to selected service ports from the public + Internet + properties: + prometheus: + description: + Allow clients to connect to prometheus from the + public internet for service nodes that are in a project + VPC or another type of private network + type: boolean + valkey: + description: + Allow clients to connect to valkey from the public + internet for service nodes that are in a project VPC or + another type of private network + type: boolean + type: object + recovery_basebackup_name: + description: Name of the basebackup to restore in forked service + maxLength: 128 + pattern: ^[a-zA-Z0-9-_:.]+$ + type: string + service_log: + description: + Store logs for the service so that they are available + in the HTTP API and console. + type: boolean + service_to_fork_from: + description: + Name of another service to fork from. This has effect + only when a new service is being created. + maxLength: 64 + pattern: ^[a-z][-a-z0-9]{0,63}$|^$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + static_ips: + description: Use static public IP addresses + type: boolean + valkey_acl_channels_default: + description: + Determines default pub/sub channels' ACL for new + users if ACL is not supplied. When this option is not defined, + all_channels is assumed to keep backward compatibility. This + option doesn't affect Valkey configuration acl-pubsub-default. + enum: + - allchannels + - resetchannels + type: string + valkey_io_threads: + description: + Set Valkey IO thread count. Changing this will cause + a restart of the Valkey service. + maximum: 32 + minimum: 1 + type: integer + valkey_lfu_decay_time: + description: LFU maxmemory-policy counter decay time in minutes + maximum: 120 + minimum: 1 + type: integer + valkey_lfu_log_factor: + description: + Counter logarithm factor for volatile-lfu and allkeys-lfu + maxmemory-policies + maximum: 100 + minimum: 0 + type: integer + valkey_maxmemory_policy: + description: Valkey maxmemory-policy + enum: + - allkeys-lfu + - allkeys-lru + - allkeys-random + - noeviction + - volatile-lfu + - volatile-lru + - volatile-random + - volatile-ttl + type: string + valkey_notify_keyspace_events: + description: Set notify-keyspace-events option + maxLength: 32 + pattern: ^[KEg\$lshzxentdmA]*$ + type: string + valkey_number_of_databases: + description: + Set number of Valkey databases. Changing this will + cause a restart of the Valkey service. + maximum: 128 + minimum: 1 + type: integer + valkey_persistence: + description: + When persistence is 'rdb', Valkey does RDB dumps + each 10 minutes if any key is changed. Also RDB dumps are done + according to backup schedule for backup purposes. When persistence + is 'off', no RDB dumps and backups are done, so data can be + lost at any moment if service is restarted for any reason, or + if service is powered off. Also service can't be forked. + enum: + - "off" + - rdb + type: string + valkey_pubsub_client_output_buffer_limit: + description: + Set output buffer limit for pub / sub clients in + MB. The value is the hard limit, the soft limit is 1/4 of the + hard limit. When setting the limit, be mindful of the available + memory in the selected service plan. + maximum: 512 + minimum: 32 + type: integer + valkey_ssl: + description: Require SSL to access Valkey + type: boolean + valkey_timeout: + description: Valkey idle connection timeout in seconds + maximum: 2073600 + minimum: 0 + type: integer + type: object + required: + - plan + - project + type: object + x-kubernetes-validations: + - message: + connInfoSecretTargetDisabled can only be set during resource + creation. + rule: has(oldSelf.connInfoSecretTargetDisabled) == has(self.connInfoSecretTargetDisabled) + status: + description: ServiceStatus defines the observed state of service + properties: + conditions: + description: + Conditions represent the latest available observations + of a service state + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + state: + description: Service state + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5c66e8c6..c31d62c9 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -28,6 +28,7 @@ resources: - bases/aiven.io_clickhousegrants.yaml - bases/aiven.io_serviceintegrationendpoints.yaml - bases/aiven.io_flinks.yaml + - bases/aiven.io_valkeys.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -53,6 +54,7 @@ patchesStrategicMerge: - patches/webhook_in_grafanas.yaml - patches/webhook_in_serviceintegrationendpoints.yaml - patches/webhook_in_flinks.yaml + - patches/webhook_in_valkeys.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -77,6 +79,7 @@ patchesStrategicMerge: - patches/cainjection_in_grafanas.yaml - patches/cainjection_in_serviceintegrationendpoints.yaml - patches/cainjection_in_flinks.yaml + - patches/cainjection_in_valkeys.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_valkeys.yaml b/config/crd/patches/cainjection_in_valkeys.yaml new file mode 100644 index 00000000..da058859 --- /dev/null +++ b/config/crd/patches/cainjection_in_valkeys.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: valkeys.aiven.io diff --git a/config/crd/patches/webhook_in_valkeys.yaml b/config/crd/patches/webhook_in_valkeys.yaml new file mode 100644 index 00000000..91db8198 --- /dev/null +++ b/config/crd/patches/webhook_in_valkeys.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valkeys.aiven.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a187ebf6..d3294155 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -750,6 +750,34 @@ rules: - get - patch - update + - apiGroups: + - aiven.io + resources: + - valkey + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - valkey/finalizers + verbs: + - create + - get + - update + - apiGroups: + - aiven.io + resources: + - valkey/status + verbs: + - get + - patch + - update - apiGroups: - coordination.k8s.io resources: diff --git a/config/rbac/valkey_editor_role.yaml b/config/rbac/valkey_editor_role.yaml new file mode 100644 index 00000000..e2af3a74 --- /dev/null +++ b/config/rbac/valkey_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit valkeys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: valkey-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: valkey-editor-role +rules: + - apiGroups: + - aiven.io + resources: + - valkeys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - aiven.io + resources: + - valkeys/status + verbs: + - get diff --git a/config/rbac/valkey_viewer_role.yaml b/config/rbac/valkey_viewer_role.yaml new file mode 100644 index 00000000..e2749a4f --- /dev/null +++ b/config/rbac/valkey_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view valkeys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: valkey-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: valkey-viewer-role +rules: + - apiGroups: + - aiven.io + resources: + - valkeys + verbs: + - get + - list + - watch + - apiGroups: + - aiven.io + resources: + - valkeys/status + verbs: + - get diff --git a/config/samples/_v1alpha1_valkey.yaml b/config/samples/_v1alpha1_valkey.yaml new file mode 100644 index 00000000..6e1d48ee --- /dev/null +++ b/config/samples/_v1alpha1_valkey.yaml @@ -0,0 +1,12 @@ +apiVersion: aiven.io/v1alpha1 +kind: Valkey +metadata: + labels: + app.kubernetes.io/name: valkey + app.kubernetes.io/instance: valkey-sample + app.kubernetes.io/part-of: aiven-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: aiven-operator + name: valkey-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 5d4b02b8..bf88583e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -25,4 +25,5 @@ resources: - _v1alpha1_clickhouserole.yaml - _v1alpha1_serviceintegrationendpoint.yaml - _v1alpha1_flink.yaml + - _v1alpha1_valkey.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 90621fa8..f70f998a 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -404,6 +404,26 @@ webhooks: resources: - serviceusers sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-aiven-io-v1alpha1-valkey + failurePolicy: Fail + name: mvalkey.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - valkey + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -825,3 +845,24 @@ webhooks: resources: - serviceusers sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-aiven-io-v1alpha1-valkey + failurePolicy: Fail + name: vvalkey.kb.io + rules: + - apiGroups: + - aiven.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - valkey + sideEffects: None diff --git a/controllers/setup.go b/controllers/setup.go index 1c39b9c3..ca03f6fa 100644 --- a/controllers/setup.go +++ b/controllers/setup.go @@ -50,6 +50,7 @@ func SetupControllers(mgr ctrl.Manager, defaultToken, kubeVersion, operatorVersi "ServiceIntegration": newServiceIntegrationReconciler, "ServiceIntegrationEndpoint": newServiceIntegrationEndpointReconciler, "ServiceUser": newServiceUserReconciler, + "Valkey": newValkeyReconciler, } for k, v := range builders { diff --git a/controllers/valkey_controller.go b/controllers/valkey_controller.go new file mode 100644 index 00000000..db9695b5 --- /dev/null +++ b/controllers/valkey_controller.go @@ -0,0 +1,99 @@ +// Copyright (c) 2024 Aiven, Helsinki, Finland. https://aiven.io/ + +package controllers + +import ( + "context" + "fmt" + + "github.com/aiven/aiven-go-client/v2" + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/service" + corev1 "k8s.io/api/core/v1" + 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" +) + +// ValkeyReconciler reconciles a Valkey object +type ValkeyReconciler struct { + Controller +} + +func newValkeyReconciler(c Controller) reconcilerType { + return &ValkeyReconciler{Controller: c} +} + +type ValkeyHandler struct{} + +//+kubebuilder:rbac:groups=aiven.io,resources=valkey,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=aiven.io,resources=valkey/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=aiven.io,resources=valkey/finalizers,verbs=get;create;update + +func (r *ValkeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.reconcileInstance(ctx, req, newGenericServiceHandler(newValkeyAdapter), &v1alpha1.Valkey{}) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ValkeyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Valkey{}). + Owns(&corev1.Secret{}). + Complete(r) +} + +func newValkeyAdapter(_ *aiven.Client, object client.Object) (serviceAdapter, error) { + valkey, ok := object.(*v1alpha1.Valkey) + if !ok { + return nil, fmt.Errorf("object is not of type v1alpha1.Valkey") + } + return &valkeyAdapter{valkey}, nil +} + +// valkeyAdapter handles an Aiven Valkey service +type valkeyAdapter struct { + *v1alpha1.Valkey +} + +func (a *valkeyAdapter) getObjectMeta() *metav1.ObjectMeta { + return &a.ObjectMeta +} + +func (a *valkeyAdapter) getServiceStatus() *v1alpha1.ServiceStatus { + return &a.Status +} + +func (a *valkeyAdapter) getServiceCommonSpec() *v1alpha1.ServiceCommonSpec { + return &a.Spec.ServiceCommonSpec +} + +func (a *valkeyAdapter) getUserConfig() any { + return a.Spec.UserConfig +} + +func (a *valkeyAdapter) newSecret(ctx context.Context, s *service.ServiceGetOut) (*corev1.Secret, error) { + prefix := getSecretPrefix(a) + stringData := map[string]string{ + prefix + "HOST": s.ServiceUriParams["host"], + prefix + "PASSWORD": s.ServiceUriParams["password"], + prefix + "PORT": s.ServiceUriParams["port"], + prefix + "SSL": s.ServiceUriParams["ssl"], + prefix + "USER": s.ServiceUriParams["user"], + } + + return newSecret(a, stringData, false), nil +} + +func (a *valkeyAdapter) getServiceType() string { + return "valkey" +} + +func (a *valkeyAdapter) getDiskSpace() string { + return a.Spec.DiskSpace +} + +func (a *valkeyAdapter) performUpgradeTaskIfNeeded(ctx context.Context, avn avngen.Client, old *service.ServiceGetOut) error { + return nil +} diff --git a/docs/docs/api-reference/examples/valkey.yaml b/docs/docs/api-reference/examples/valkey.yaml new file mode 100644 index 00000000..1161824f --- /dev/null +++ b/docs/docs/api-reference/examples/valkey.yaml @@ -0,0 +1,32 @@ +apiVersion: aiven.io/v1alpha1 +kind: Valkey +metadata: + name: my-valkey +spec: + authSecretRef: + name: aiven-token + key: token + + connInfoSecretTarget: + name: my-valkey-secret + annotations: + foo: bar + labels: + baz: egg + + project: my-aiven-project + cloudName: google-europe-west1 + plan: startup-4 + + maintenanceWindowDow: sunday + maintenanceWindowTime: 11:00:00 + + tags: + env: test + instance: foo + + userConfig: + ip_filter: + - network: 0.0.0.0/32 + description: bar + - network: 10.20.0.0/16 diff --git a/docs/docs/api-reference/valkey.md b/docs/docs/api-reference/valkey.md new file mode 100644 index 00000000..d9079d7f --- /dev/null +++ b/docs/docs/api-reference/valkey.md @@ -0,0 +1,294 @@ +--- +title: "Valkey" +--- + +## Usage example + +??? example + ```yaml + apiVersion: aiven.io/v1alpha1 + kind: Valkey + metadata: + name: my-valkey + spec: + authSecretRef: + name: aiven-token + key: token + + connInfoSecretTarget: + name: valkey-secret + annotations: + foo: bar + labels: + baz: egg + + project: my-aiven-project + cloudName: google-europe-west1 + plan: startup-4 + + maintenanceWindowDow: sunday + maintenanceWindowTime: 11:00:00 + + tags: + env: test + instance: foo + + userConfig: + ip_filter: + - network: 0.0.0.0/32 + description: bar + - network: 10.20.0.0/16 + ``` + +!!! info + To create this resource, a `Secret` containing Aiven token must be [created](/aiven-operator/authentication.html) first. + +Apply the resource with: + +```shell +kubectl apply -f example.yaml +``` + +Verify the newly created `Valkey`: + +```shell +kubectl get valkeys my-valkey +``` + +The output is similar to the following: +```shell +Name Project Region Plan State +my-valkey my-aiven-project google-europe-west1 startup-4 RUNNING +``` + +To view the details of the `Secret`, use the following command: +```shell +kubectl describe secret valkey-secret +``` + +You can use the [jq](https://github.com/jqlang/jq) to quickly decode the `Secret`: + +```shell +kubectl get secret valkey-secret -o json | jq '.data | map_values(@base64d)' +``` + +The output is similar to the following: + +```{ .json .no-copy } +{ + "VALKEY_HOST": "", + "VALKEY_PORT": "", + "VALKEY_USER": "", + "VALKEY_PASSWORD": "", +} +``` + +## Valkey {: #Valkey } + +Valkey is the Schema for the valkeys API + +!!! Info "Exposes secret keys" + + `VALKEY_HOST`, `VALKEY_PORT`, `VALKEY_USER`, `VALKEY_PASSWORD`. + +**Required** + +- [`apiVersion`](#apiVersion-property){: name='apiVersion-property'} (string). Value `aiven.io/v1alpha1`. +- [`kind`](#kind-property){: name='kind-property'} (string). Value `Valkey`. +- [`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). ValkeySpec defines the desired state of Valkey. See below for [nested schema](#spec). + +## spec {: #spec } + +_Appears on [`Valkey`](#Valkey)._ + +ValkeySpec defines the desired state of Valkey. + +**Required** + +- [`plan`](#spec.plan-property){: name='spec.plan-property'} (string, MaxLength: 128). Subscription plan. +- [`project`](#spec.project-property){: name='spec.project-property'} (string, Immutable, Pattern: `^[a-zA-Z0-9_-]+$`, MaxLength: 63). Identifies the project this resource belongs 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). +- [`cloudName`](#spec.cloudName-property){: name='spec.cloudName-property'} (string, MaxLength: 256). Cloud the service runs in. +- [`connInfoSecretTarget`](#spec.connInfoSecretTarget-property){: name='spec.connInfoSecretTarget-property'} (object). Secret configuration. See below for [nested schema](#spec.connInfoSecretTarget). +- [`connInfoSecretTargetDisabled`](#spec.connInfoSecretTargetDisabled-property){: name='spec.connInfoSecretTargetDisabled-property'} (boolean, Immutable). When true, the secret containing connection information will not be created, defaults to false. This field cannot be changed after resource creation. +- [`disk_space`](#spec.disk_space-property){: name='spec.disk_space-property'} (string, Pattern: `(?i)^[1-9][0-9]*(GiB|G)?$`). The disk space of the service, possible values depend on the service type, the cloud provider and the project. +Reducing will result in the service re-balancing. +The removal of this field does not change the value. +- [`maintenanceWindowDow`](#spec.maintenanceWindowDow-property){: name='spec.maintenanceWindowDow-property'} (string, Enum: `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`). Day of week when maintenance operations should be performed. One monday, tuesday, wednesday, etc. +- [`maintenanceWindowTime`](#spec.maintenanceWindowTime-property){: name='spec.maintenanceWindowTime-property'} (string, MaxLength: 8). Time of day when maintenance operations should be performed. UTC time in HH:mm:ss format. +- [`projectVPCRef`](#spec.projectVPCRef-property){: name='spec.projectVPCRef-property'} (object). ProjectVPCRef reference to ProjectVPC resource to use its ID as ProjectVPCID automatically. See below for [nested schema](#spec.projectVPCRef). +- [`projectVpcId`](#spec.projectVpcId-property){: name='spec.projectVpcId-property'} (string, MaxLength: 36). Identifier of the VPC the service should be in, if any. +- [`serviceIntegrations`](#spec.serviceIntegrations-property){: name='spec.serviceIntegrations-property'} (array of objects, Immutable, MaxItems: 1). Service integrations to specify when creating a service. Not applied after initial service creation. See below for [nested schema](#spec.serviceIntegrations). +- [`tags`](#spec.tags-property){: name='spec.tags-property'} (object, AdditionalProperties: string). Tags are key-value pairs that allow you to categorize services. +- [`technicalEmails`](#spec.technicalEmails-property){: name='spec.technicalEmails-property'} (array of objects, MaxItems: 10). Defines the email addresses that will receive alerts about upcoming maintenance updates or warnings about service instability. See below for [nested schema](#spec.technicalEmails). +- [`terminationProtection`](#spec.terminationProtection-property){: name='spec.terminationProtection-property'} (boolean). Prevent service from being deleted. It is recommended to have this enabled for all services. +- [`userConfig`](#spec.userConfig-property){: name='spec.userConfig-property'} (object). Valkey specific user configuration options. See below for [nested schema](#spec.userConfig). + +## 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). + +## connInfoSecretTarget {: #spec.connInfoSecretTarget } + +_Appears on [`spec`](#spec)._ + +Secret configuration. + +**Required** + +- [`name`](#spec.connInfoSecretTarget.name-property){: name='spec.connInfoSecretTarget.name-property'} (string, Immutable). Name of the secret resource to be created. By default, it is equal to the resource name. + +**Optional** + +- [`annotations`](#spec.connInfoSecretTarget.annotations-property){: name='spec.connInfoSecretTarget.annotations-property'} (object, AdditionalProperties: string). Annotations added to the secret. +- [`labels`](#spec.connInfoSecretTarget.labels-property){: name='spec.connInfoSecretTarget.labels-property'} (object, AdditionalProperties: string). Labels added to the secret. +- [`prefix`](#spec.connInfoSecretTarget.prefix-property){: name='spec.connInfoSecretTarget.prefix-property'} (string). Prefix for the secret's keys. +Added "as is" without any transformations. +By default, is equal to the kind name in uppercase + underscore, e.g. `KAFKA_`, `REDIS_`, etc. + +## projectVPCRef {: #spec.projectVPCRef } + +_Appears on [`spec`](#spec)._ + +ProjectVPCRef reference to ProjectVPC resource to use its ID as ProjectVPCID automatically. + +**Required** + +- [`name`](#spec.projectVPCRef.name-property){: name='spec.projectVPCRef.name-property'} (string, MinLength: 1). + +**Optional** + +- [`namespace`](#spec.projectVPCRef.namespace-property){: name='spec.projectVPCRef.namespace-property'} (string, MinLength: 1). + +## serviceIntegrations {: #spec.serviceIntegrations } + +_Appears on [`spec`](#spec)._ + +Service integrations to specify when creating a service. Not applied after initial service creation. + +**Required** + +- [`integrationType`](#spec.serviceIntegrations.integrationType-property){: name='spec.serviceIntegrations.integrationType-property'} (string, Enum: `read_replica`). +- [`sourceServiceName`](#spec.serviceIntegrations.sourceServiceName-property){: name='spec.serviceIntegrations.sourceServiceName-property'} (string, MinLength: 1, MaxLength: 64). + +## technicalEmails {: #spec.technicalEmails } + +_Appears on [`spec`](#spec)._ + +Defines the email addresses that will receive alerts about upcoming maintenance updates or warnings about service instability. + +**Required** + +- [`email`](#spec.technicalEmails.email-property){: name='spec.technicalEmails.email-property'} (string). Email address. + +## userConfig {: #spec.userConfig } + +_Appears on [`spec`](#spec)._ + +Valkey specific user configuration options. + +**Optional** + +- [`additional_backup_regions`](#spec.userConfig.additional_backup_regions-property){: name='spec.userConfig.additional_backup_regions-property'} (array of strings, MaxItems: 1). Additional Cloud Regions for Backup Replication. +- [`backup_hour`](#spec.userConfig.backup_hour-property){: name='spec.userConfig.backup_hour-property'} (integer, Minimum: 0, Maximum: 23). The hour of day (in UTC) when backup for the service is started. New backup is only started if previous backup has already completed. +- [`backup_minute`](#spec.userConfig.backup_minute-property){: name='spec.userConfig.backup_minute-property'} (integer, Minimum: 0, Maximum: 59). The minute of an hour when backup for the service is started. New backup is only started if previous backup has already completed. +- [`ip_filter`](#spec.userConfig.ip_filter-property){: name='spec.userConfig.ip_filter-property'} (array of objects, MaxItems: 1024). Allow incoming connections from CIDR address block, e.g. `10.20.0.0/16`. See below for [nested schema](#spec.userConfig.ip_filter). +- [`migration`](#spec.userConfig.migration-property){: name='spec.userConfig.migration-property'} (object). Migrate data from existing server. See below for [nested schema](#spec.userConfig.migration). +- [`private_access`](#spec.userConfig.private_access-property){: name='spec.userConfig.private_access-property'} (object). Allow access to selected service ports from private networks. See below for [nested schema](#spec.userConfig.private_access). +- [`privatelink_access`](#spec.userConfig.privatelink_access-property){: name='spec.userConfig.privatelink_access-property'} (object). Allow access to selected service components through Privatelink. See below for [nested schema](#spec.userConfig.privatelink_access). +- [`project_to_fork_from`](#spec.userConfig.project_to_fork_from-property){: name='spec.userConfig.project_to_fork_from-property'} (string, Immutable, Pattern: `^[a-z][-a-z0-9]{0,63}$|^$`, MaxLength: 63). Name of another project to fork a service from. This has effect only when a new service is being created. +- [`public_access`](#spec.userConfig.public_access-property){: name='spec.userConfig.public_access-property'} (object). Allow access to selected service ports from the public Internet. See below for [nested schema](#spec.userConfig.public_access). +- [`recovery_basebackup_name`](#spec.userConfig.recovery_basebackup_name-property){: name='spec.userConfig.recovery_basebackup_name-property'} (string, Pattern: `^[a-zA-Z0-9-_:.]+$`, MaxLength: 128). Name of the basebackup to restore in forked service. +- [`service_log`](#spec.userConfig.service_log-property){: name='spec.userConfig.service_log-property'} (boolean). Store logs for the service so that they are available in the HTTP API and console. +- [`service_to_fork_from`](#spec.userConfig.service_to_fork_from-property){: name='spec.userConfig.service_to_fork_from-property'} (string, Immutable, Pattern: `^[a-z][-a-z0-9]{0,63}$|^$`, MaxLength: 64). Name of another service to fork from. This has effect only when a new service is being created. +- [`static_ips`](#spec.userConfig.static_ips-property){: name='spec.userConfig.static_ips-property'} (boolean). Use static public IP addresses. +- [`valkey_acl_channels_default`](#spec.userConfig.valkey_acl_channels_default-property){: name='spec.userConfig.valkey_acl_channels_default-property'} (string, Enum: `allchannels`, `resetchannels`). Determines default pub/sub channels' ACL for new users if ACL is not supplied. When this option is not defined, all_channels is assumed to keep backward compatibility. This option doesn't affect Valkey configuration acl-pubsub-default. +- [`valkey_io_threads`](#spec.userConfig.valkey_io_threads-property){: name='spec.userConfig.valkey_io_threads-property'} (integer, Minimum: 1, Maximum: 32). Set Valkey IO thread count. Changing this will cause a restart of the Valkey service. +- [`valkey_lfu_decay_time`](#spec.userConfig.valkey_lfu_decay_time-property){: name='spec.userConfig.valkey_lfu_decay_time-property'} (integer, Minimum: 1, Maximum: 120). LFU maxmemory-policy counter decay time in minutes. +- [`valkey_lfu_log_factor`](#spec.userConfig.valkey_lfu_log_factor-property){: name='spec.userConfig.valkey_lfu_log_factor-property'} (integer, Minimum: 0, Maximum: 100). Counter logarithm factor for volatile-lfu and allkeys-lfu maxmemory-policies. +- [`valkey_maxmemory_policy`](#spec.userConfig.valkey_maxmemory_policy-property){: name='spec.userConfig.valkey_maxmemory_policy-property'} (string, Enum: `allkeys-lfu`, `allkeys-lru`, `allkeys-random`, `noeviction`, `volatile-lfu`, `volatile-lru`, `volatile-random`, `volatile-ttl`). Valkey maxmemory-policy. +- [`valkey_notify_keyspace_events`](#spec.userConfig.valkey_notify_keyspace_events-property){: name='spec.userConfig.valkey_notify_keyspace_events-property'} (string, Pattern: `^[KEg\$lshzxentdmA]*$`, MaxLength: 32). Set notify-keyspace-events option. +- [`valkey_number_of_databases`](#spec.userConfig.valkey_number_of_databases-property){: name='spec.userConfig.valkey_number_of_databases-property'} (integer, Minimum: 1, Maximum: 128). Set number of Valkey databases. Changing this will cause a restart of the Valkey service. +- [`valkey_persistence`](#spec.userConfig.valkey_persistence-property){: name='spec.userConfig.valkey_persistence-property'} (string, Enum: `off`, `rdb`). When persistence is `rdb`, Valkey does RDB dumps each 10 minutes if any key is changed. Also RDB dumps are done according to backup schedule for backup purposes. When persistence is `off`, no RDB dumps and backups are done, so data can be lost at any moment if service is restarted for any reason, or if service is powered off. Also service can't be forked. +- [`valkey_pubsub_client_output_buffer_limit`](#spec.userConfig.valkey_pubsub_client_output_buffer_limit-property){: name='spec.userConfig.valkey_pubsub_client_output_buffer_limit-property'} (integer, Minimum: 32, Maximum: 512). Set output buffer limit for pub / sub clients in MB. The value is the hard limit, the soft limit is 1/4 of the hard limit. When setting the limit, be mindful of the available memory in the selected service plan. +- [`valkey_ssl`](#spec.userConfig.valkey_ssl-property){: name='spec.userConfig.valkey_ssl-property'} (boolean). Require SSL to access Valkey. +- [`valkey_timeout`](#spec.userConfig.valkey_timeout-property){: name='spec.userConfig.valkey_timeout-property'} (integer, Minimum: 0, Maximum: 2073600). Valkey idle connection timeout in seconds. + +### ip_filter {: #spec.userConfig.ip_filter } + +_Appears on [`spec.userConfig`](#spec.userConfig)._ + +CIDR address block, either as a string, or in a dict with an optional description field. + +**Required** + +- [`network`](#spec.userConfig.ip_filter.network-property){: name='spec.userConfig.ip_filter.network-property'} (string, MaxLength: 43). CIDR address block. + +**Optional** + +- [`description`](#spec.userConfig.ip_filter.description-property){: name='spec.userConfig.ip_filter.description-property'} (string, MaxLength: 1024). Description for IP filter list entry. + +### migration {: #spec.userConfig.migration } + +_Appears on [`spec.userConfig`](#spec.userConfig)._ + +Migrate data from existing server. + +**Required** + +- [`host`](#spec.userConfig.migration.host-property){: name='spec.userConfig.migration.host-property'} (string, MaxLength: 255). Hostname or IP address of the server where to migrate data from. +- [`port`](#spec.userConfig.migration.port-property){: name='spec.userConfig.migration.port-property'} (integer, Minimum: 1, Maximum: 65535). Port number of the server where to migrate data from. + +**Optional** + +- [`dbname`](#spec.userConfig.migration.dbname-property){: name='spec.userConfig.migration.dbname-property'} (string, MaxLength: 63). Database name for bootstrapping the initial connection. +- [`ignore_dbs`](#spec.userConfig.migration.ignore_dbs-property){: name='spec.userConfig.migration.ignore_dbs-property'} (string, MaxLength: 2048). Comma-separated list of databases, which should be ignored during migration (supported by MySQL and PostgreSQL only at the moment). +- [`ignore_roles`](#spec.userConfig.migration.ignore_roles-property){: name='spec.userConfig.migration.ignore_roles-property'} (string, MaxLength: 2048). Comma-separated list of database roles, which should be ignored during migration (supported by PostgreSQL only at the moment). +- [`method`](#spec.userConfig.migration.method-property){: name='spec.userConfig.migration.method-property'} (string, Enum: `dump`, `replication`). The migration method to be used (currently supported only by Redis, Dragonfly, MySQL and PostgreSQL service types). +- [`password`](#spec.userConfig.migration.password-property){: name='spec.userConfig.migration.password-property'} (string, MaxLength: 256). Password for authentication with the server where to migrate data from. +- [`ssl`](#spec.userConfig.migration.ssl-property){: name='spec.userConfig.migration.ssl-property'} (boolean). The server where to migrate data from is secured with SSL. +- [`username`](#spec.userConfig.migration.username-property){: name='spec.userConfig.migration.username-property'} (string, MaxLength: 256). User name for authentication with the server where to migrate data from. + +### private_access {: #spec.userConfig.private_access } + +_Appears on [`spec.userConfig`](#spec.userConfig)._ + +Allow access to selected service ports from private networks. + +**Optional** + +- [`prometheus`](#spec.userConfig.private_access.prometheus-property){: name='spec.userConfig.private_access.prometheus-property'} (boolean). Allow clients to connect to prometheus with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations. +- [`valkey`](#spec.userConfig.private_access.valkey-property){: name='spec.userConfig.private_access.valkey-property'} (boolean). Allow clients to connect to valkey with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations. + +### privatelink_access {: #spec.userConfig.privatelink_access } + +_Appears on [`spec.userConfig`](#spec.userConfig)._ + +Allow access to selected service components through Privatelink. + +**Optional** + +- [`prometheus`](#spec.userConfig.privatelink_access.prometheus-property){: name='spec.userConfig.privatelink_access.prometheus-property'} (boolean). Enable prometheus. +- [`valkey`](#spec.userConfig.privatelink_access.valkey-property){: name='spec.userConfig.privatelink_access.valkey-property'} (boolean). Enable valkey. + +### public_access {: #spec.userConfig.public_access } + +_Appears on [`spec.userConfig`](#spec.userConfig)._ + +Allow access to selected service ports from the public Internet. + +**Optional** + +- [`prometheus`](#spec.userConfig.public_access.prometheus-property){: name='spec.userConfig.public_access.prometheus-property'} (boolean). Allow clients to connect to prometheus from the public internet for service nodes that are in a project VPC or another type of private network. +- [`valkey`](#spec.userConfig.public_access.valkey-property){: name='spec.userConfig.public_access.valkey-property'} (boolean). Allow clients to connect to valkey from the public internet for service nodes that are in a project VPC or another type of private network. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b84a3868..59f74a8d 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -114,3 +114,4 @@ nav: - api-reference/serviceintegration.md - api-reference/serviceintegrationendpoint.md - api-reference/serviceuser.md + - api-reference/valkey.md diff --git a/main.go b/main.go index da706762..9942dfae 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ import ( //+kubebuilder:scaffold:imports ) -//go:generate go run ./generators/userconfigs/... --services mysql,cassandra,flink,grafana,pg,kafka,redis,clickhouse,opensearch,kafka_connect +//go:generate go run ./generators/userconfigs/... --services mysql,cassandra,flink,grafana,pg,kafka,redis,clickhouse,opensearch,kafka_connect,valkey //go:generate go run ./generators/userconfigs/... --integrations autoscaler,clickhouse_kafka,clickhouse_postgresql,datadog,kafka_connect,kafka_logs,kafka_mirrormaker,logs,metrics,external_aws_cloudwatch_metrics //go:generate go run ./generators/userconfigs/... --integration-endpoints autoscaler,datadog,external_aws_cloudwatch_logs,external_aws_cloudwatch_metrics,external_elasticsearch_logs,external_google_cloud_bigquery,external_google_cloud_logging,external_kafka,external_opensearch_logs,external_postgresql,external_schema_registry,jolokia,prometheus,rsyslog diff --git a/tests/valkey_test.go b/tests/valkey_test.go new file mode 100644 index 00000000..e5bb1901 --- /dev/null +++ b/tests/valkey_test.go @@ -0,0 +1,81 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aiven/aiven-operator/api/v1alpha1" + valkeyuserconfig "github.com/aiven/aiven-operator/api/v1alpha1/userconfig/service/valkey" +) + +func TestValkey(t *testing.T) { + t.Parallel() + defer recoverPanic(t) + + // GIVEN + ctx, cancel := testCtx() + defer cancel() + + name := randName("valkey") + yml, err := loadExampleYaml("valkey.yaml", map[string]string{ + "google-europe-west1": cfg.PrimaryCloudName, + "my-aiven-project": cfg.Project, + "my-valkey": name, + }) + require.NoError(t, err) + + s := NewSession(ctx, k8sClient, cfg.Project) + + // Cleans test afterward + defer s.Destroy(t) + + // WHEN + // Applies given manifest + require.NoError(t, s.Apply(yml)) + + // Waits kube objects + rs := new(v1alpha1.Valkey) + require.NoError(t, s.GetRunning(rs, name)) + + // THEN + rsAvn, err := avnGen.ServiceGet(ctx, cfg.Project, name) + require.NoError(t, err) + assert.Equal(t, rsAvn.ServiceName, rs.GetName()) + assert.Equal(t, serviceRunningState, rs.Status.State) + assert.Contains(t, serviceRunningStatesAiven, rsAvn.State) + assert.Equal(t, rsAvn.Plan, rs.Spec.Plan) + assert.Equal(t, rsAvn.CloudName, rs.Spec.CloudName) + assert.Equal(t, map[string]string{"env": "test", "instance": "foo"}, rs.Spec.Tags) + rsResp, err := avnClient.ServiceTags.Get(ctx, cfg.Project, name) + require.NoError(t, err) + assert.Equal(t, rsResp.Tags, rs.Spec.Tags) + + // UserConfig test + require.NotNil(t, rs.Spec.UserConfig) + + // Validates ip filters + require.Len(t, rs.Spec.UserConfig.IpFilter, 2) + + // First entry + assert.Equal(t, "0.0.0.0/32", rs.Spec.UserConfig.IpFilter[0].Network) + assert.Equal(t, "bar", *rs.Spec.UserConfig.IpFilter[0].Description) + + // Second entry + assert.Equal(t, "10.20.0.0/16", rs.Spec.UserConfig.IpFilter[1].Network) + assert.Nil(t, rs.Spec.UserConfig.IpFilter[1].Description) + + // Compares with Aiven ip_filter + var ipFilterAvn []*valkeyuserconfig.IpFilter + require.NoError(t, castInterface(rsAvn.UserConfig["ip_filter"], &ipFilterAvn)) + assert.Equal(t, ipFilterAvn, rs.Spec.UserConfig.IpFilter) + + // Secrets test + secret, err := s.GetSecret(rs.GetName() + "-secret") + require.NoError(t, err) + assert.NotEmpty(t, secret.Data["VALKEY_HOST"]) + assert.NotEmpty(t, secret.Data["VALKEY_PORT"]) + assert.NotEmpty(t, secret.Data["VALKEY_USER"]) + assert.NotEmpty(t, secret.Data["VALKEY_PASSWORD"]) +}