Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
124 changes: 124 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Valkey Operator is a production-grade Kubernetes operator that automates deployment and management of [Valkey](https://valkey.io/) (Redis-compatible) instances. It supports three architectures: **Cluster**, **Sentinel/Failover**, and **Standalone**.

## Common Commands

```bash
# Build
make build # Build manager and helper binaries
make manifests # Regenerate CRDs, ClusterRole, WebhookConfiguration (run after API changes)
make generate # Regenerate DeepCopy methods (run after API type changes)

# Test
make test # Run unit tests with coverage
make test-e2e # Run end-to-end tests (requires Kind cluster)
go test ./internal/builder/clusterbuilder/... -run TestFoo # Run a single test or package

# Lint & Format
make lint # golangci-lint
make lint-fix # golangci-lint with auto-fix
make fmt # go fmt
make vet # go vet

# Docs
make docs # Generate API docs
make docs-serve # Serve docs at http://localhost:8080
```

After modifying types in `/api/`, always run `make generate && make manifests`.

## Architecture

### API Groups

Two API groups with different abstraction levels:
- **`valkey.buf.red`** (`/api/v1alpha1/`) — fine-grained control: `Cluster`, `Sentinel`, `Failover`, `User`
- **`rds.valkey.buf.red`** (`/api/rds/v1alpha1/`) — high-level simplified API: `Valkey`
- **`/api/core/`** — shared types reused across both groups (Access, Storage, Exporter, etc.)

### Key Packages

| Package | Role |
|---|---|
| `/internal/controller/` | Reconcilers for each CRD; entry points for K8s watch events |
| `/internal/builder/` | Constructs K8s resources (StatefulSets, Services, ConfigMaps) per architecture |
| `/internal/ops/` | Rule-based OpEngine that orchestrates multi-step operations |
| `/internal/actor/` | Async actor/command pattern (Requeue, Pause, Abort, Success) |
| `/internal/valkey/` | Valkey topology models for cluster, sentinel, and failover |
| `/internal/webhook/` | Validating/mutating webhooks gated by `ENABLE_WEBHOOKS` env var |
| `/pkg/kubernetes/` | K8s client wrappers and utilities |
| `/cmd/main.go` | Operator manager entry point |
| `/cmd/helper/main.go` | Helper binary for init containers and management commands |

### Reconciliation Flow

1. **Controller** watches CRD, calls `Reconcile()`
2. **Builder** packages construct/diff K8s resources
3. **OpEngine** runs rule-based checks to determine next operation
4. **Actors** execute async tasks and return commands (Requeue, Pause, Abort)
5. **Finalizers** handle cleanup on deletion (`ResourceCleanFinalizer`)

### Operations Engine

The `OpEngine` in `/internal/ops/` uses a rule-based system:
- Rules inspect instance state and return the next command
- `MaxCallDepth = 15` prevents infinite recursion
- Separate rule engines per architecture: `/internal/ops/cluster/`, `/internal/ops/sentinel/`, `/internal/ops/failover/`

### Standard Labels

```go
ManagedByLabelKey = "app.kubernetes.io/managed-by"
ArchLabelKey = "valkeyarch"
RoleLabelKey = "valkey.buf.red/role"
ChecksumLabelKey = "valkey.buf.red/checksum"
```

Checksums on ConfigMaps trigger pod restarts when config changes.

## Key Constants

```go
DefaultValkeyServerPort = 6379
DefaultValkeyServerBusPort = 16379 // Cluster bus
DefaultValkeySentinelPort = 26379
DefaultRequeueDuration = 15 * time.Second
DefaultReconcileTimeout = 5 * time.Minute
```

## Testing Patterns

- **Unit tests**: Ginkgo v2 + Gomega, colocated with source (`*_test.go`)
- **Mocking**: `miniredis` and `redigomock` for Valkey; `envtest` for K8s controller tests
- **E2E tests**: `/test/e2e/`, require a running Kind cluster
- `KUBEBUILDER_ASSETS` env var points to K8s API server binaries for envtest

## Environment Variables

| Variable | Purpose |
|---|---|
| `ENABLE_WEBHOOKS` | Set to `"false"` to disable validating webhooks |
| `KUBEBUILDER_ASSETS` | Path to envtest binaries (used in tests) |

## Operator Flags

```
--leader-elect Enable leader election for HA
--webhook-cert-path Directory containing webhook TLS certs
--health-probe-bind-address Health/readiness probe address (default ":8081")
--metrics-bind-address Metrics endpoint (default "0" = disabled)
```

## Deployment

Uses Kustomize (`/config/`). Key overlays:
- `/config/default/` — standard deployment with webhooks
- `/config/crd/` — CRD manifests only
- `/config/samples/` — example CRs for all architectures

Requires cert-manager for webhook TLS certificates.
6 changes: 4 additions & 2 deletions api/rds/v1alpha1/valkey_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ type ValkeyExporter struct {

// ValkeySpec defines the desired state of Valkey
type ValkeySpec struct {
// Version supports 7.2, 8.0, 8.1
// +kubebuilder:validation:Enum="7.2";"8.0";"8.1"
// Version specifies the Valkey version to deploy.
// Stable: 7.2, 8.0, 8.1, 8.2, 9.0
// Preview/RC (not recommended for production): 9.1
// +kubebuilder:validation:Enum="7.2";"8.0";"8.1";"8.2";"9.0";"9.1"
Version string `json:"version"`

// Arch supports cluster, sentinel
Expand Down
14 changes: 11 additions & 3 deletions config/crd/bases/rds.valkey.buf.red_valkeys.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3323,9 +3323,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand Down Expand Up @@ -3395,9 +3396,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand All @@ -3415,11 +3417,17 @@ spec:
type: object
type: array
version:
description: Version supports 7.2, 8.0, 8.1
description: |-
Version specifies the Valkey version to deploy.
Stable: 7.2, 8.0, 8.1, 8.2, 9.0
Preview/RC (not recommended for production): 9.1
enum:
- "7.2"
- "8.0"
- "8.1"
- "8.2"
- "9.0"
- "9.1"
type: string
required:
- arch
Expand Down
3 changes: 2 additions & 1 deletion config/crd/bases/valkey.buf.red_clusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1735,9 +1735,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand Down
6 changes: 4 additions & 2 deletions config/crd/bases/valkey.buf.red_failovers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3287,9 +3287,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand Down Expand Up @@ -3358,9 +3359,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand Down
3 changes: 2 additions & 1 deletion config/crd/bases/valkey.buf.red_sentinels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1645,9 +1645,10 @@ spec:
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).
type: string
tolerationSeconds:
description: |-
Expand Down
2 changes: 1 addition & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: controller
newName: chideat/valkey-operator
newTag: latest
newTag: v2.0.0
commonAnnotations:
defaultExporterImageName: oliver006/redis_exporter
defaultExporterVersion: v1.67.0-alpine
Expand Down
2 changes: 1 addition & 1 deletion internal/builder/clusterbuilder/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func GenerateInstanceService(cluster *v1alpha1.Cluster) *corev1.Service {
return svc
}

func GenerateNodePortSerivce(cluster *v1alpha1.Cluster, name string, labels map[string]string, port int32) *corev1.Service {
func GenerateNodePortService(cluster *v1alpha1.Cluster, name string, labels map[string]string, port int32) *corev1.Service {
clientPort := corev1.ServicePort{Name: "client", Port: 6379, NodePort: port}
selectorLabels := map[string]string{
builder.PodNameLabelKey: name,
Expand Down
2 changes: 1 addition & 1 deletion internal/builder/clusterbuilder/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ func TestGenerateNodePortService(t *testing.T) {
}

// Generate the NodePort service
svc := GenerateNodePortSerivce(cluster, tt.nodeName, labels, tt.nodePort)
svc := GenerateNodePortService(cluster, tt.nodeName, labels, tt.nodePort)

// Verify the service
require.NotNil(t, svc)
Expand Down
2 changes: 1 addition & 1 deletion internal/ops/cluster/actor/actor_ensure_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ func (a *actorEnsureResource) ensureValkeyNodePortService(ctx context.Context, c
continue
}
port := newPorts[0]
svc := clusterbuilder.GenerateNodePortSerivce(cr, serviceName, labels, port)
svc := clusterbuilder.GenerateNodePortService(cr, serviceName, labels, port)
if err = a.client.CreateService(ctx, svc.Namespace, svc); err != nil {
a.logger.Error(err, "create nodeport service failed", "target", client.ObjectKeyFromObject(svc))
return actor.NewResultWithValue(cops.CommandRequeue, err)
Expand Down
17 changes: 17 additions & 0 deletions internal/webhook/rds/v1alpha1/valkey_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/chideat/valkey-operator/internal/webhook/rds/v1alpha1/helper"
"github.com/chideat/valkey-operator/internal/webhook/rds/v1alpha1/validation"
"github.com/chideat/valkey-operator/pkg/slot"
"github.com/chideat/valkey-operator/pkg/version"
"sigs.k8s.io/controller-runtime/pkg/client"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -275,6 +276,22 @@ func (v *ValkeyCustomValidator) ValidateCreate(ctx context.Context, inst *rdsv1a

// ValidateUpdate implements admission.Validator so a webhook will be registered for the type inst.
func (v *ValkeyCustomValidator) ValidateUpdate(ctx context.Context, oldInst, newInst *rdsv1alpha1.Valkey) (warns admission.Warnings, err error) {
var oldVersion string
if oldInst != nil {
oldVersion = oldInst.Spec.Version
}
logger.Info("Validation for inst upon update", "name", newInst.GetName(),
"oldVersion", oldVersion, "newVersion", newInst.Spec.Version)

oldVer, oldErr := version.ParseValkeyVersion(oldVersion)
newVer, newErr := version.ParseValkeyVersion(newInst.Spec.Version)
if oldErr == nil && newErr == nil && newVer.Compare(oldVer) < 0 {
return warns, fmt.Errorf(
"version downgrade from %s to %s is not allowed; "+
"delete and recreate the instance to downgrade",
oldVersion, newInst.Spec.Version)
}

ctx = context.WithValue(ctx, actionKey, "update")
return v.ValidateCreate(ctx, newInst)
}
Expand Down
51 changes: 32 additions & 19 deletions pkg/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import (
var (
MinTLSSupportedVersion, _ = semver.NewVersion("7.4.0")
MinACLSupportedVersion, _ = semver.NewVersion("7.4.0")

// ValkeyVersion-typed gates (used with IsAtLeast)
MinVectorSetsVersion = ValkeyVersion("8.0")
MinLatencyTrackingVersion = ValkeyVersion("9.0")
MinLazyfreeUserFlushVersion = ValkeyVersion("9.0")
)

type ValkeyVersion string
Expand All @@ -47,31 +52,44 @@ func (v ValkeyVersion) CustomConfigs(arch core.Arch) map[string]string {
return nil
}

ret := map[string]string{}
ret["ignore-warnings"] = "ARM64-COW-BUG"
ret := map[string]string{
"ignore-warnings": "ARM64-COW-BUG",
}

if arch == core.ValkeyCluster {
ret["cluster-allow-replica-migration"] = "no"
ret["cluster-migration-barrier"] = "10"
}

// Valkey 9.0+: latency tracking on by default upstream; make explicit
if v.IsAtLeast(MinLatencyTrackingVersion) {
ret["latency-tracking"] = "yes"
}

// Valkey 9.0+: lazyfree-lazy-user-flush default changed
if v.IsAtLeast(MinLazyfreeUserFlushVersion) {
ret["lazyfree-lazy-user-flush"] = "yes"
}

return ret
}

// Compare conpare two version
// Compare compares two versions.
//
// if v > other, return 1
// if v < other, return -1
// if v == other, return 0
// if error occurred, return -2
func (v ValkeyVersion) Compare(other ValkeyVersion) int {
if v == "" && other == "" {
return 0
}
if v == "" {
if other == "" {
return 0
}
return -1
} else if other == "" {
}
if other == "" {
return 1
}

v1, err := semver.NewVersion(string(v))
if err != nil {
return -2
Expand All @@ -80,17 +98,12 @@ func (v ValkeyVersion) Compare(other ValkeyVersion) int {
if err != nil {
return -2
}
if v1.Major() > v2.Major() {
return 1
} else if v1.Major() < v2.Major() {
return -1
}
if v1.Minor() > v2.Minor() {
return 1
} else if v1.Minor() < v2.Minor() {
return -1
}
return 0
return v1.Compare(v2)
}

// IsAtLeast returns true if v >= minVersion according to Compare.
func (v ValkeyVersion) IsAtLeast(minVersion ValkeyVersion) bool {
return v.Compare(minVersion) >= 0
}

// ParseVersion
Expand Down
Loading
Loading