diff --git a/api/v1alpha1/valkeycluster_types.go b/api/v1alpha1/valkeycluster_types.go index 6611a9a..d20418c 100644 --- a/api/v1alpha1/valkeycluster_types.go +++ b/api/v1alpha1/valkeycluster_types.go @@ -30,8 +30,9 @@ type ValkeyClusterSpec struct { // Override the default Valkey image Image string `json:"image,omitempty"` - // Override the default Valkey exporter image - ExporterImage string `json:"exporterImage,omitempty"` + // Exporter Configuration options + // +optional + Exporter ExporterSpec `json:"exporter,omitempty"` // The number of shards groups. Each shard group contains one primary and N replicas. // +kubebuilder:validation:Minimum=1 @@ -45,10 +46,6 @@ type ValkeyClusterSpec struct { // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` - // Override resource requirements for the Valkey exporter container in each pod - // +optional - ExporterResources corev1.ResourceRequirements `json:"exporterResources,omitempty"` - // Tolerations to apply to the pods // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` @@ -63,6 +60,19 @@ type ValkeyClusterSpec struct { Affinity *corev1.Affinity `json:"affinity,omitempty"` } +type ExporterSpec struct { + // Override the default Exporter image + Image string `json:"image,omitempty"` + + // Override resource requirements for the Valkey exporter container in each pod + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Enable or disable the exporter sidecar container + // +kubebuilder:default=true + Enabled bool `json:"enabled,omitempty"` +} + // ValkeyClusterStatus defines the observed state of ValkeyCluster. type ValkeyClusterStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c67c2b8..d5dbf07 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExporterSpec) DeepCopyInto(out *ExporterSpec) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExporterSpec. +func (in *ExporterSpec) DeepCopy() *ExporterSpec { + if in == nil { + return nil + } + out := new(ExporterSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValkeyCluster) DeepCopyInto(out *ValkeyCluster) { *out = *in @@ -88,8 +104,8 @@ func (in *ValkeyClusterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValkeyClusterSpec) DeepCopyInto(out *ValkeyClusterSpec) { *out = *in + in.Exporter.DeepCopyInto(&out.Exporter) in.Resources.DeepCopyInto(&out.Resources) - in.ExporterResources.DeepCopyInto(&out.ExporterResources) if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]v1.Toleration, len(*in)) diff --git a/config/crd/bases/valkey.io_valkeyclusters.yaml b/config/crd/bases/valkey.io_valkeyclusters.yaml index a7097a0..5ad15dc 100644 --- a/config/crd/bases/valkey.io_valkeyclusters.yaml +++ b/config/crd/bases/valkey.io_valkeyclusters.yaml @@ -972,67 +972,75 @@ spec: x-kubernetes-list-type: atomic type: object type: object - exporterImage: - description: Override the default Valkey exporter image - type: string - exporterResources: - description: Override resource requirements for the Valkey exporter - container in each pod + exporter: + description: Exporter Configuration options properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. + enabled: + default: true + description: Enable or disable the exporter sidecar container + type: boolean + image: + description: Override the default Exporter image + type: string + resources: + description: Override resource requirements for the Valkey exporter + container in each pod + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. - This field depends on the - DynamicResourceAllocation feature gate. + This field depends on the + DynamicResourceAllocation feature gate. - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object type: object type: object image: diff --git a/internal/controller/deployment.go b/internal/controller/deployment.go new file mode 100644 index 0000000..62ee452 --- /dev/null +++ b/internal/controller/deployment.go @@ -0,0 +1,48 @@ +package controller + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + valkeyv1 "valkey.io/valkey-operator/api/v1alpha1" +) + +func (r *ValkeyClusterReconciler) upsertDeployment(ctx context.Context, cluster *valkeyv1.ValkeyCluster, shard int, salt string) error { + logger := log.FromContext(ctx) + + existingDeployment := &appsv1.Deployment{} + err := r.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: cluster.Name}, existingDeployment) + if err != nil { + if apierrors.IsNotFound(err) { + // Deployment does not exist, create it + newDeployment := r.deploymentForValkeyCluster(cluster, shard, salt) + logger.Info("Creating a new Deployment", "Deployment.Namespace", newDeployment.Namespace, "Deployment.Name", newDeployment.Name) + if err := r.Create(ctx, newDeployment); err != nil { + logger.Error(err, "Failed to create new Deployment", "Deployment.Namespace", newDeployment.Namespace, "Deployment.Name", newDeployment.Name) + return err + } + } + logger.Error(err, "Failed to get Deployment") + return err + } + // Deployment exists, update it if necessary + logger.Info("Deployment already exists, skipping creation", "Deployment.Namespace", existingDeployment.Namespace, "Deployment.Name", existingDeployment.Name) + + return nil +} + +func (r *ValkeyClusterReconciler) deploymentForValkeyCluster(cluster *valkeyv1.ValkeyCluster, shard int, salt string) *appsv1.Deployment { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: cluster.Name, + Namespace: cluster.Namespace, + Labels: labels(cluster, shard, salt), + }, + } + return deployment +} diff --git a/internal/controller/utils.go b/internal/controller/utils.go new file mode 100644 index 0000000..d86f7f7 --- /dev/null +++ b/internal/controller/utils.go @@ -0,0 +1,35 @@ +package controller + +import ( + "fmt" + "maps" + + valkeyv1 "valkey.io/valkey-operator/api/v1alpha1" +) + +func labels(cluster *valkeyv1.ValkeyCluster, shard int, salt string) map[string]string { + if cluster.Labels == nil { + cluster.Labels = make(map[string]string) + } + l := maps.Clone(cluster.Labels) + l["app.kubernetes.io/name"] = "valkey" + l["app.kubernetes.io/instance"] = cluster.Name + l["app.kubernetes.io/managed-by"] = "valkey-operator" + l["app.kubernetes.io/part-of"] = "valkey" + l["app.kubernetes.io/component"] = "valkey-cluster" + if shard >= 0 { + l["valkey.io/shard"] = fmt.Sprintf("%d", shard) + } + if salt != "" { + l["valkey.io/salt"] = salt + } + return l +} + +func l(cluster *valkeyv1.ValkeyCluster) map[string]string { + return labels(cluster, -1, "") +} + +func annotations(cluster *valkeyv1.ValkeyCluster) map[string]string { + return maps.Clone(cluster.Annotations) +} diff --git a/internal/controller/utils_test.go b/internal/controller/utils_test.go new file mode 100644 index 0000000..ec29200 --- /dev/null +++ b/internal/controller/utils_test.go @@ -0,0 +1,106 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + valkeyv1 "valkey.io/valkey-operator/api/v1alpha1" +) + +func TestLabels(t *testing.T) { + testLabels := map[string]string{ + "app": "valkey", + } + cluster := &valkeyv1.ValkeyCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource", + Namespace: "default", + Labels: testLabels, + }, + } + result := labels(cluster, -1, "") + if testLabels["app"] != result["app"] { + t.Errorf("Expected %v, got %v", testLabels["app"], result["app"]) + } + if result["app.kubernetes.io/name"] != "valkey" { + t.Errorf("Expected %v, got %v", "valkey", result["app.kubernetes.io/name"]) + } + if result["app.kubernetes.io/instance"] != "test-resource" { + t.Errorf("Expected %v, got %v", "test-resource", result["app.kubernetes.io/instance"]) + } + result["app.kubernetes.io/component"] = "metrics" + if result["app.kubernetes.io/component"] != "metrics" { + t.Errorf("Expected %v, got %v", "metrics", result["app.kubernetes.io/component"]) + } + result2 := labels(cluster, -1, "") + if result2["app.kubernetes.io/component"] != "valkey-cluster" { + t.Errorf("Expected %v, got %v", "valkey-cluster", result2["app.kubernetes.io/component"]) + } + + if result["valkey.io/salt"] != "" { + t.Errorf("Expected empty salt label, got %v", result["valkey.io/salt"]) + } + if result["valkey.io/shard"] != "" { + t.Errorf("Expected empty shard label, got %v", result["valkey.io/shard"]) + } + + result3 := labels(cluster, 2, "XYZ") + if result3["valkey.io/salt"] != "XYZ" { + t.Errorf("Expected salt label XYZ, got %v", result3["valkey.io/salt"]) + } + if result3["valkey.io/shard"] != "2" { + t.Errorf("Expected shard label 2, got %v", result3["valkey.io/shard"]) + } +} + +func TestL(t *testing.T) { + cluster := &valkeyv1.ValkeyCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource", + Namespace: "default", + }, + } + result := l(cluster) + if result["app.kubernetes.io/name"] != "valkey" { + t.Errorf("Expected %v, got %v", "valkey", result["app.kubernetes.io/name"]) + } + if result["app.kubernetes.io/instance"] != "test-resource" { + t.Errorf("Expected %v, got %v", "test-resource", result["app.kubernetes.io/instance"]) + } + if result["app.kubernetes.io/component"] != "valkey-cluster" { + t.Errorf("Expected %v, got %v", "valkey-cluster", result["app.kubernetes.io/component"]) + } +} + +func TestAnnotations(t *testing.T) { + testAnnotations := map[string]string{ + "app": "valkey", + } + cluster := &valkeyv1.ValkeyCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-resource", + Namespace: "default", + Annotations: testAnnotations, + }, + } + result := annotations(cluster) + if testAnnotations["app"] != result["app"] { + t.Errorf("Expected %v, got %v", testAnnotations["app"], result["app"]) + } +}