Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a05d5d6
(feat): Extra Valkey Config
utdrmac Jan 9, 2026
7241a35
corrected map name
utdrmac Jan 9, 2026
05af544
update sample cr
utdrmac Jan 9, 2026
8919932
e2e fix; revert func name
utdrmac Jan 10, 2026
f066a91
config as map
utdrmac Jan 10, 2026
cc614b7
add TODO comment, and e2e test
utdrmac Jan 10, 2026
88cf17a
add TODO comment, and e2e test
utdrmac Jan 10, 2026
02808a3
fix lint catch
utdrmac Jan 10, 2026
59bcd2e
e2e lint
utdrmac Jan 10, 2026
3a1709a
merge main
utdrmac Jan 12, 2026
8dce649
merge upstream; expand e2e client commands
utdrmac Jan 12, 2026
26e809e
Merge branch 'main' into valkeyConfig
utdrmac Jan 15, 2026
f487a3c
refactor server config
utdrmac Jan 20, 2026
7780156
forgot file
utdrmac Jan 20, 2026
3db1fd2
verify original hash
utdrmac Jan 20, 2026
cc6b210
merge main
utdrmac Jan 20, 2026
28712d8
merge origin
utdrmac Jan 20, 2026
8a75a87
fix merge issue
utdrmac Jan 21, 2026
a2c1818
test check text changed
utdrmac Jan 22, 2026
3736d67
Merge branch 'main' into ghvalkeyconfig
utdrmac Feb 10, 2026
925e406
update Record events; add scripts to hash
utdrmac Feb 11, 2026
932a3dd
missing scripts in original check
utdrmac Feb 11, 2026
ac3ee7a
merge upstream main
utdrmac Mar 21, 2026
074afe4
handle modules in config
utdrmac Mar 27, 2026
8b16ccf
github linting
utdrmac Mar 27, 2026
790854f
Merge branch 'valkey-io:main' into valkeyConfig
utdrmac Mar 27, 2026
7077bb5
removed modules config. see #122
utdrmac Mar 27, 2026
d85bbd2
removed Modules types
utdrmac Mar 27, 2026
2d9d3c7
corrected test
utdrmac Mar 27, 2026
224a39f
fix e2e tests
utdrmac Mar 27, 2026
245c64f
fix valkeynode tests
utdrmac Mar 28, 2026
91070a6
merge upstream:main
utdrmac Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint

## Tool Versions
KUSTOMIZE_VERSION ?= v5.8.1
Expand Down
13 changes: 13 additions & 0 deletions api/v1alpha1/valkeycluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ const (
ClusterStateFailed ClusterState = "Failed"
)

// This list defines specific Valkey server configuration parameters that cannot
// be overridden by a user-supplied configuration within the CR. Doing so would
// potentially break the operator's behavior, which could result in data loss, or
// a non-functioning cluster
var NonUserOverrideConfigParameters = []string{
"cluster-enabled",
"aclfile",
}

// ValkeyClusterSpec defines the desired state of ValkeyCluster.
type ValkeyClusterSpec struct {

Expand Down Expand Up @@ -88,6 +97,10 @@ type ValkeyClusterSpec struct {
// Additional containers or overrides for existing containers, applied using strategic merge patch
// +optional
Containers []corev1.Container `json:"containers,omitempty"`

// Additional Valkey configuration parameters
// +optional
Config map[string]string `json:"config,omitempty"`
}

type ExporterSpec struct {
Expand Down
5 changes: 3 additions & 2 deletions api/v1alpha1/valkeynode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ type ValkeyNodeSpec struct {
// +optional
Exporter ExporterSpec `json:"exporter,omitempty"`

// ScriptsConfigMapName specifies the name of the ConfigMap that contains the scripts for the ValkeyNode.
// UsersConfigMapName specifies the name of the ConfigMap that contains the
// scripts, and Valkey config for the ValkeyNode.
// +optional
ScriptsConfigMapName string `json:"scriptsConfigMapName,omitempty"`
UsersConfigMapName string `json:"usersConfigMapName,omitempty"`

// UsersACLSecretName is the name of the Secret containing the ACL user
// file. When set, mounts a users-acl volume from this Secret so the
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions config/crd/bases/valkey.io_valkeyclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,11 @@ spec:
x-kubernetes-list-type: atomic
type: object
type: object
config:
additionalProperties:
type: string
description: Additional Valkey configuration parameters
type: object
containers:
description: Additional containers or overrides for existing containers,
applied using strategic merge patch
Expand Down
9 changes: 5 additions & 4 deletions config/crd/bases/valkey.io_valkeynodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2639,10 +2639,6 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdheyburn FYI on this change. I renamed it to "better align"(?) with the other changes since the configmap has both config and the scripts.

scriptsConfigMapName:
description: ScriptsConfigMapName specifies the name of the ConfigMap
that contains the scripts for the ValkeyNode.
type: string
tolerations:
description: Tolerations defines the pod tolerations.
items:
Expand Down Expand Up @@ -2689,6 +2685,11 @@ spec:
file. When set, mounts a users-acl volume from this Secret so the
container can load aclfile /config/users/users.acl.
type: string
usersConfigMapName:
description: |-
UsersConfigMapName specifies the name of the ConfigMap that contains the
scripts, and Valkey config for the ValkeyNode.
type: string
workloadType:
default: StatefulSet
description: |-
Expand Down
3 changes: 3 additions & 0 deletions config/samples/v1alpha1_valkeycluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ metadata:
spec:
shards: 3
replicas: 1
config:
maxmemory: 50mb
maxmemory-policy: allkeys-lfu
resources:
requests:
memory: "256Mi"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
k8s.io/apimachinery v0.35.0
k8s.io/client-go v0.35.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/yaml v1.6.0
)

require (
Expand Down Expand Up @@ -98,5 +99,4 @@ require (
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
224 changes: 224 additions & 0 deletions internal/controller/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
Copyright 2025 Valkey Contributors.

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 (
"context"
"crypto/sha256"
"embed"
"fmt"
"maps"
"slices"
"strings"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
logf "sigs.k8s.io/controller-runtime/pkg/log"
valkeyiov1alpha1 "valkey.io/valkey-operator/api/v1alpha1"
)

const (
scriptsHashKey = "valkey.io/script-hash"
configHashKey = "valkey.io/config-hash"
configFileKey = "valkey.conf"

readinessScriptKey = "readiness-check.sh"
livenessScriptKey = "liveness-check.sh"

// This hash should be updated whenever the contents of either script changes, which would
// coincide with operator version bump.
// $ cat internal/controller/scripts/{liveness-check.sh,readiness-check.sh} | sha256sum
scriptsHash = "8531132f52ac311772dfcb45c107c34ab05e719a0df644cc332512277b564346"

// Average-ish length of Valkey parameter + value
averageParameterLength = 20
)

//go:embed scripts/*
var scripts embed.FS

func getConfigMapName(clusterName string) string {
return clusterName + "-config"
}

// Return a base config of parameters that users shouldn't be able to override
func getBaseConfig() string {
return `# Base operator config
cluster-enabled yes
protected-mode no
cluster-node-timeout 2000
aclfile /config/users/users.acl
`
}

func getUserConfig(ctx context.Context, cluster *valkeyiov1alpha1.ValkeyCluster) string {

specConfig := cluster.Spec.Config

// Exit early if nothing
if len(specConfig) == 0 {
return ""
}

log := logf.FromContext(ctx)

// Build the config
var configBuilder strings.Builder
configBuilder.Grow(len(specConfig) * averageParameterLength)
writeConfigLine(&configBuilder, "#", "Extra Config")

// Sort the config keys to keep consistent processing order
sortedKeys := slices.Sorted(maps.Keys(specConfig))

for _, param := range sortedKeys {

if slices.Contains(valkeyiov1alpha1.NonUserOverrideConfigParameters, param) {
log.Error(nil, "Prohibited valkey server config", "parameter", param)
continue
}

writeConfigLine(&configBuilder, param, specConfig[param])
}

return configBuilder.String()
}

// Create or update a default valkey.conf
// If additional config is provided, append to the default map
func (r *ValkeyClusterReconciler) upsertConfigMap(ctx context.Context, cluster *valkeyiov1alpha1.ValkeyCluster) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this file to configmap.go

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

log := logf.FromContext(ctx)

// Embed readiness check script
readiness, err := scripts.ReadFile("scripts/readiness-check.sh")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is nitpicking, but does it make sense to have scripts in a configmap named mycluster-config? That to me indicates that the ConfigMap contains only configurations.

I would argue to keep -config out of the resource name and just keep it to the name of the cluster.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your suggestion to move the scripts to their own configMap? I have no objection with that.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to leave as is. We can refactor later on

if err != nil {
return fmt.Errorf("reading embedded readiness-check.sh: %w", err)
}

// Embed liveness check script
liveness, err := scripts.ReadFile("scripts/liveness-check.sh")
if err != nil {
return fmt.Errorf("reading embedded liveness-check.sh: %w", err)
}

// Get base config
var newConfigBuilder strings.Builder
newConfigBuilder.WriteString(getBaseConfig())

// User-provided config from spec
newConfigBuilder.WriteString(getUserConfig(ctx, cluster))

// Final string version of the config
newServerConfig := newConfigBuilder.String()

// Calculate hash of constructed configMap contents (ie: updated scripts, changed/added parameters)
newServerConfigHash := fmt.Sprintf("%x", sha256.Sum256([]byte(newServerConfig)))

// Look for, and fetch existing configMap for this cluster
serverConfigMapName := getConfigMapName(cluster.Name)
serverConfigMap := &corev1.ConfigMap{}
if err := r.Get(ctx, types.NamespacedName{
Name: serverConfigMapName,
Namespace: cluster.Namespace,
}, serverConfigMap); err != nil {
if !apierrors.IsNotFound(err) {
log.Error(err, "failed to fetch server configmap")
return err
}

// ConfigMap not found; This happens on cluster init
log.V(2).Info("creating server configMap", "name", serverConfigMapName)

// Create configMap object with contents
serverConfigMap.ObjectMeta = metav1.ObjectMeta{
Name: serverConfigMapName,
Namespace: cluster.Namespace,
Labels: labels(cluster),
Annotations: map[string]string{
configHashKey: newServerConfigHash,
scriptsHashKey: scriptsHash,
},
}
serverConfigMap.Data = map[string]string{
readinessScriptKey: string(readiness),
livenessScriptKey: string(liveness),
configFileKey: newServerConfig,
}

// Register ownership of the configMap
if err := controllerutil.SetControllerReference(cluster, serverConfigMap, r.Scheme); err != nil {
log.Error(err, "Failed to grab ownership of server configMap")
r.Recorder.Eventf(cluster, nil, corev1.EventTypeWarning, "ConfigMapCreationFailed", "UpsertConfigMap", "Failed to grab ownership of server configMap: %v", err)
return err
}

// Create the configMap
if err := r.Create(ctx, serverConfigMap); err != nil {
log.Error(err, "Failed to create server configMap")
r.Recorder.Eventf(cluster, nil, corev1.EventTypeWarning, "ConfigMapCreationFailed", "UpsertConfigMap", "Failed to create server configMap: %v", err)
return err
}

r.Recorder.Eventf(cluster, nil, corev1.EventTypeNormal, "ConfigMapCreated", "UpsertConfigMap", "Created server configMap")

// All good; new configMap with contents created
return nil
}

// ConfigMap exists

// Compare scripts hash in existing configMap to const value in operator; update scripts contents if different
updatedScripts := upsertAnnotation(serverConfigMap, scriptsHashKey, scriptsHash)
if updatedScripts {
log.V(1).Info("updated readiness, and liveness scripts")
serverConfigMap.Data[readinessScriptKey] = string(readiness)
serverConfigMap.Data[livenessScriptKey] = string(liveness)
}

// If the generated config contents hash (from above) matches the hash of the current
// config contents, and we did not update the scripts contents, exit early
if !updatedScripts && !upsertAnnotation(serverConfigMap, configHashKey, newServerConfigHash) {
log.V(1).Info("server config unchanged")
return nil
}

// Update the configMap with the generated config contents
serverConfigMap.Data[configFileKey] = newServerConfig

// Update
if err := r.Update(ctx, serverConfigMap); err != nil {
log.Error(err, "Failed to update server configMap")
r.Recorder.Eventf(cluster, nil, corev1.EventTypeWarning, "ConfigMapUpdateFailed", "UpsertConfigMap", "Failed to update server configMap: %v", err)
return err
}

r.Recorder.Eventf(cluster, nil, corev1.EventTypeNormal, "ConfigMapUpdated", "UpsertConfigMap", "Synchronized server configMap")

// All is good. configMap was updated with new contents.
return nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think updated configmap will be auto mounted in the existing deployments during update. Did u test this

Copy link
Copy Markdown
Collaborator

@bjosv bjosv Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seem to update the configfile in the pods when I test it on kind minikube now, but it takes some time before the file is getting updated.

But, I now see that we are not using SubPath in the VolumeMounts on containers..
We probably should (?) since both /scripts and /configs contains all files from the configmap.
The problem then will be that the volume mount will not receive updates when the ConfigMap changes though..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating the configmap is one thing, but the Valkey instances will not use these updates anyway in the current form. Is that what you where thinking on @sandeepkunusoth ?
Maybe the update part in this PR could be moved to its own PR, which could include the pod restart/config reload depending on how we decide.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that we should omit live applying changes to the config for another PR

}

// Helper function to write a config line in the form of "parameter value\n" to a strings.Builder
func writeConfigLine(builder *strings.Builder, name, value string) {
builder.WriteString(name)
builder.WriteString(" ")
builder.WriteString(value)
builder.WriteString("\n")
}
Loading