├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── samples │ ├── kustomization.yaml │ └── v1_valkey.yaml ├── default │ ├── manager_metrics_patch.yaml │ ├── metrics_service.yaml │ └── kustomization.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── rbac │ ├── service_account.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── valkey_viewer_role.yaml │ ├── valkey_editor_role.yaml │ ├── leader_election_role.yaml │ ├── kustomization.yaml │ └── role.yaml └── crd │ ├── kustomizeconfig.yaml │ └── kustomization.yaml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── test.yaml │ ├── build.yaml │ ├── publish.yaml │ ├── scan.yaml │ └── image.yaml ├── Dockerfile.controller ├── hack ├── issuer.yaml ├── ingress.yaml ├── boilerplate.go.txt ├── minikube-pvc-hack.yaml ├── prometheus.yaml ├── minikube.sh └── postgres-operator.yaml ├── .dockerignore ├── Dockerfile.sidecar ├── cmd ├── sidecar │ ├── root.go │ ├── bootstrap.go │ ├── version.go │ └── daemon.go └── manager │ └── main.go ├── valkey.yml.tpl ├── valkey.yaml ├── PROJECT ├── internal ├── controller │ ├── scripts │ │ ├── ping_liveness_local.sh │ │ ├── ping_liveness_local_tls.sh │ │ ├── ping_readiness_local.sh │ │ └── ping_readiness_local_tls.sh │ ├── cluster.go │ ├── valkey_controller_test.go │ ├── suite_test.go │ └── valkey_controller_utils_test.go └── sidecar │ └── init.go ├── cfg └── config.go ├── .gitignore ├── .golangci.yml ├── test ├── e2e │ ├── e2e_suite_test.go │ └── e2e_test.go └── utils │ └── utils.go ├── Dockerfile.valkey ├── api └── v1 │ ├── groupversion_info.go │ ├── zz_generated.deepcopy.go │ └── valkey_types.go ├── README.md ├── go.mod ├── docs └── ROADMAP.md ├── LICENSE.md ├── Makefile └── go.sum /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: @hyperspike 4 | -------------------------------------------------------------------------------- /Dockerfile.controller: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static:nonroot 2 | COPY manager /manager 3 | USER 65532:65532 4 | 5 | ENTRYPOINT ["/manager"] 6 | -------------------------------------------------------------------------------- /hack/issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: selfsigned 5 | spec: 6 | selfSigned: {} 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /Dockerfile.sidecar: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static:nonroot 2 | 3 | COPY sidecar /sidecar 4 | USER 65532:65532 5 | 6 | ENTRYPOINT ["/sidecar"] 7 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - v1_valkey.yaml 4 | # +kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint securely 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8080 5 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: localhost:5000/valkey-operator 8 | newTag: "1" 9 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/samples/v1_valkey.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: hyperspike.io/v1 2 | kind: Valkey 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: valkey-sample 8 | spec: 9 | volumePermissions: true 10 | -------------------------------------------------------------------------------- /cmd/sidecar/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var rootCmd = &cobra.Command{ 8 | Use: "sidecar", 9 | Short: "The Valkey Sidecar", 10 | Long: "A tool to manage Valkey clusters within Kubernetes", 11 | } 12 | 13 | func main() { 14 | if err := rootCmd.Execute(); err != nil { 15 | panic(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hack/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | hsts: "false" 4 | allow-snippet-annotations: "true" 5 | kind: ConfigMap 6 | metadata: 7 | labels: 8 | app.kubernetes.io/component: controller 9 | app.kubernetes.io/instance: ingress-nginx 10 | app.kubernetes.io/name: ingress-nginx 11 | name: ingress-nginx-controller 12 | namespace: ingress-nginx 13 | -------------------------------------------------------------------------------- /valkey.yml.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: hyperspike.io/v1 2 | kind: Valkey 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: keyval 8 | spec: 9 | volumePermissions: true 10 | prometheus: @PROMETHEUS@ 11 | tls: @TLS@ 12 | certIssuer: selfsigned 13 | prometheusLabels: 14 | prometheus: "1" 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "docker" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "gomod" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | groups: 16 | kubernetes: 17 | patterns: 18 | - k8s.io/* 19 | 20 | -------------------------------------------------------------------------------- /cmd/sidecar/bootstrap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "hyperspike.io/valkey-operator/internal/sidecar" 6 | ) 7 | 8 | var bootstrapCmd = &cobra.Command{ 9 | Use: "bootstrap", 10 | Long: "Bootstrap", 11 | Short: "Bootstrap", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | sidecar.SetVolumePermissions() 14 | }, 15 | } 16 | 17 | func init() { 18 | rootCmd.AddCommand(bootstrapCmd) 19 | } 20 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: valkey-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: http 13 | port: 8080 14 | protocol: TCP 15 | targetPort: 8080 16 | selector: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /valkey.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: hyperspike.io/v1 2 | kind: Valkey 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: valkey-operator 6 | app.kubernetes.io/managed-by: kustomize 7 | name: vk2 8 | spec: 9 | nodes: 3 10 | volumePermissions: true 11 | prometheus: false 12 | tls: true 13 | certIssuer: selfsigned 14 | prometheusLabels: 15 | prometheus: prometheus 16 | storage: 17 | spec: 18 | resources: 19 | requests: 20 | storage: 2Gi 21 | accessModes: 22 | - ReadWriteOnce 23 | -------------------------------------------------------------------------------- /config/rbac/valkey_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view valkeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: valkey-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: valkey-viewer-role 9 | rules: 10 | - apiGroups: 11 | - hyperspike.io 12 | resources: 13 | - valkeys 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - apiGroups: 19 | - hyperspike.io 20 | resources: 21 | - valkeys/status 22 | verbs: 23 | - get 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Setup Go ${{ matrix.go-version }} 15 | uses: actions/setup-go@v6 16 | with: 17 | go-version: 1.25 18 | # You can test your matrix by printing the current Go version 19 | - name: Display Go version 20 | run: go version 21 | - name: Test it! 22 | run: make V=1 test 23 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: hyperspike.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: valkey-operator 9 | repo: hyperspike.io/valkey-operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: hyperspike.io 16 | kind: Valkey 17 | path: hyperspike.io/valkey-operator/api/v1 18 | version: v1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/rbac/valkey_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit valkeys. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: valkey-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: valkey-editor-role 9 | rules: 10 | - apiGroups: 11 | - hyperspike.io 12 | resources: 13 | - valkeys 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - hyperspike.io 24 | resources: 25 | - valkeys/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: valkey-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: http # Ensure this is the name of the port that exposes HTTP metrics 15 | scheme: http 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/controller/scripts/ping_liveness_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | if [ ! -z "$VALKEY_PASSWORD" ]; then export REDISCLI_AUTH=$VALKEY_PASSWORD; fi; 4 | response=$( 5 | timeout --foreground -s 15 $1 \ 6 | valkey-cli \ 7 | -h localhost \ 8 | -p $VALKEY_PORT_NUMBER \ 9 | ping 10 | ) 11 | if [ "$?" -eq "124" ]; then 12 | echo "Timed out" 13 | exit 1 14 | fi 15 | responseFirstWord=$(echo $response | head -n1 | awk '{print $1;}') 16 | if [ "$response" != "PONG" ] && [ "$responseFirstWord" != "LOADING" ] && [ "$responseFirstWord" != "MASTERDOWN" ]; then 17 | echo "$response" 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /cfg/config.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | var ( 4 | // Default Settings 5 | DefaultSidecarImage string 6 | DefaultValkeyImage string 7 | DefaultNodes int32 = 3 8 | ) 9 | 10 | type Config struct { 11 | // The default clusterwide prometheus exporter image to use 12 | SidecarImage string `json:"exporterImage"` 13 | // The default clusterwide valkey image to use 14 | ValkeyImage string `json:"valkeyImage"` 15 | // The default number of nodes to use 16 | Nodes int32 `json:"nodes"` 17 | } 18 | 19 | func Defaults() *Config { 20 | return &Config{ 21 | SidecarImage: DefaultSidecarImage, 22 | ValkeyImage: DefaultValkeyImage, 23 | Nodes: DefaultNodes, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | dist/* 9 | Dockerfile.cross 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Kubernetes Generated files - skip generated files, except for vendored files 21 | !vendor/**/zz_generated.* 22 | 23 | # editor and IDE paraphernalia 24 | .idea 25 | .vscode 26 | *.swp 27 | *.swo 28 | *~ 29 | 30 | .cni-north.yaml 31 | .coredns.yaml 32 | .ingress.yaml 33 | blank.yaml 34 | cilium/ 35 | /manager 36 | /sidecar 37 | valkey-operator/ 38 | valkey-operator-*-chart.tgz 39 | -------------------------------------------------------------------------------- /internal/controller/scripts/ping_liveness_local_tls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ ! -z "$VALKEY_PASSWORD" ]; then export REDISCLI_AUTH=$VALKEY_PASSWORD; fi; 5 | 6 | response=$( 7 | timeout --foreground -s 15 $1 \ 8 | valkey-cli \ 9 | -h localhost \ 10 | -p $VALKEY_TLS_PORT_NUMBER \ 11 | --tls \ 12 | --cacert $VALKEY_TLS_CA_FILE \ 13 | ping 14 | #--cert $VALKEY_TLS_CERT_FILE \ 15 | #--key $VALKEY_TLS_KEY_FILE \ 16 | ) 17 | 18 | if [ "$?" -eq "124" ]; then 19 | echo "Timed out" 20 | exit 1 21 | fi 22 | 23 | responseFirstWord=$(echo $response | head -n1 | awk '{print $1;}') 24 | if [ "$response" != "PONG" ] && [ "$responseFirstWord" != "LOADING" ] && [ "$responseFirstWord" != "MASTERDOWN" ]; then 25 | echo "$response" 26 | exit 1 27 | fi 28 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: valkey-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # For each CRD, "Editor" and "Viewer" roles are scaffolded by 13 | # default, aiding admins in cluster management. Those roles are 14 | # not used by the Project itself. You can comment the following lines 15 | # if you do not want those helpers be installed with your Project. 16 | - valkey_editor_role.yaml 17 | - valkey_viewer_role.yaml 18 | 19 | -------------------------------------------------------------------------------- /cmd/sidecar/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | /* 12 | BuildVersion, BuildDate, BuildCommitSha are filled in by the build script 13 | */ 14 | Version = "<<< filled in by build >>>" 15 | BuildDate = "<<< filled in by build >>>" 16 | Commit = "<<< filled in by build >>>" 17 | ) 18 | 19 | var versionCmd = &cobra.Command{ 20 | Use: "version", 21 | Short: "Print package versions", 22 | Long: `Print package versions`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | fmt.Println("Version: ", Version) 25 | fmt.Println("Build Date: ", BuildDate) 26 | fmt.Println("Commit: ", Commit) 27 | fmt.Println("Go Version: ", runtime.Version()) 28 | }, 29 | } 30 | 31 | func init() { 32 | rootCmd.AddCommand(versionCmd) 33 | } 34 | -------------------------------------------------------------------------------- /internal/sidecar/init.go: -------------------------------------------------------------------------------- 1 | package sidecar 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // Set permissions on valkey data volume 10 | func SetVolumePermissions() { 11 | dir := os.Getenv("DATA_DIR") 12 | if dir == "" { 13 | dir = "/data" 14 | } 15 | user := os.Getenv("VALKEY_USER") 16 | if user == "" { 17 | user = "1001" 18 | } 19 | group := os.Getenv("VALKEY_GROUP") 20 | if group == "" { 21 | group = user 22 | } 23 | uid, err := strconv.Atoi(user) 24 | if err != nil { 25 | fmt.Println("Failed to convert user to int: ", err) 26 | os.Exit(1) 27 | } 28 | gid, err := strconv.Atoi(group) 29 | if err != nil { 30 | fmt.Println("Failed to convert group to int: ", err) 31 | os.Exit(1) 32 | } 33 | if err := os.Chown(dir, uid, gid); err != nil { 34 | fmt.Println("Failed to chown data dir: ", err) 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hack/minikube-pvc-hack.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: minikube-pv-hack 5 | --- 6 | apiVersion: apps/v1 7 | kind: DaemonSet 8 | metadata: 9 | name: minikube-pv-hack 10 | namespace: minikube-pv-hack 11 | spec: 12 | selector: 13 | matchLabels: 14 | name: minikube-pv-hack 15 | template: 16 | metadata: 17 | labels: 18 | name: minikube-pv-hack 19 | spec: 20 | terminationGracePeriodSeconds: 0 21 | containers: 22 | - name: minikube-pv-hack 23 | image: registry.access.redhat.com/ubi8:latest 24 | command: 25 | - bash 26 | - -c 27 | - | 28 | while : ; do 29 | chmod 777 /target/* 30 | sleep 1 31 | done 32 | volumeMounts: 33 | - name: host-vol 34 | mountPath: /target 35 | volumes: 36 | - name: host-vol 37 | hostPath: 38 | path: /tmp/hostpath-provisioner/default 39 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | allow-parallel-runners: true 4 | linters: 5 | default: none 6 | enable: 7 | - copyloopvar 8 | - dupl 9 | - errcheck 10 | - ginkgolinter 11 | - goconst 12 | - gocyclo 13 | - govet 14 | - ineffassign 15 | - lll 16 | - misspell 17 | - nakedret 18 | - prealloc 19 | - staticcheck 20 | - unconvert 21 | - unparam 22 | - unused 23 | settings: 24 | lll: 25 | line-length: 256 26 | exclusions: 27 | generated: lax 28 | rules: 29 | - linters: 30 | - lll 31 | path: api/* 32 | - linters: 33 | - dupl 34 | - lll 35 | path: internal/* 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - goimports 44 | exclusions: 45 | generated: lax 46 | paths: 47 | - third_party$ 48 | - builtin$ 49 | - examples$ 50 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | ) 26 | 27 | // Run e2e tests using the Ginkgo runner. 28 | func TestE2E(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | _, _ = fmt.Fprintf(GinkgoWriter, "Starting valkey-operator suite\n") 31 | RunSpecs(t, "e2e suite") 32 | } 33 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/hyperspike.io_valkeys.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 12 | 13 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 14 | # patches here are for enabling the CA injection for each CRD 15 | #- path: patches/cainjection_in_valkeys.yaml 16 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 17 | 18 | # [WEBHOOK] To enable webhook, uncomment the following section 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | 21 | #configurations: 22 | #- kustomizeconfig.yaml 23 | -------------------------------------------------------------------------------- /internal/controller/scripts/ping_readiness_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | VALKEY_STATUS_FILE=/tmp/.valkey_cluster_check 5 | if [ ! -z "$VALKEY_PASSWORD" ]; then export REDISCLI_AUTH=$VALKEY_PASSWORD; fi; 6 | response=$( 7 | timeout --foreground -s 15 $1 \ 8 | valkey-cli \ 9 | -h localhost \ 10 | -p $VALKEY_PORT_NUMBER \ 11 | ping 12 | ) 13 | if [ "$?" -eq "124" ]; then 14 | echo "Timed out" 15 | exit 1 16 | fi 17 | if [ "$response" != "PONG" ]; then 18 | echo "$response" 19 | exit 1 20 | fi 21 | nodes=$(echo $VALKEY_NODES | wc -w) 22 | if [ ! -f "$VALKEY_STATUS_FILE" ] && [ "$nodes" != "1" ]; then 23 | response=$( 24 | timeout --foreground -s 15 $1 \ 25 | valkey-cli \ 26 | -h localhost \ 27 | -p $VALKEY_PORT_NUMBER \ 28 | CLUSTER INFO | grep cluster_state | tr -d '[:space:]' 29 | ) 30 | if [ "$?" -eq "124" ]; then 31 | echo "Timed out" 32 | exit 1 33 | fi 34 | if [ "$response" != "cluster_state:ok" ]; then 35 | echo "$response" 36 | exit 1 37 | else 38 | touch "$VALKEY_STATUS_FILE" 39 | fi 40 | fi 41 | -------------------------------------------------------------------------------- /Dockerfile.valkey: -------------------------------------------------------------------------------- 1 | FROM alpine:3.23.2 AS builder 2 | 3 | ARG VALKEY_VERSION=8.1.4 4 | 5 | WORKDIR /home/valkey 6 | 7 | RUN apk add --no-cache --virtual .build-deps \ 8 | git=2.49.1-r0 \ 9 | coreutils=9.7-r1 \ 10 | linux-headers=6.14.2-r0 \ 11 | musl-dev=1.2.5-r10 \ 12 | openssl-dev=3.5.4-r0 \ 13 | gcc=14.2.0-r6 \ 14 | curl=8.14.1-r2 \ 15 | make=4.4.1-r3 \ 16 | && curl -L https://github.com/valkey-io/valkey/archive/refs/tags/${VALKEY_VERSION}.tar.gz -o valkey.tar.gz \ 17 | && tar -xzf valkey.tar.gz --strip-components=1 \ 18 | && make PREFIX=/usr BUILD_TLS=yes \ 19 | && make install BUILD_TLS=yes PREFIX=/home/valkey/build 20 | 21 | FROM alpine:3.23.2 AS valkey 22 | 23 | RUN apk add --no-cache \ 24 | openssl=3.5.4-r0 \ 25 | ca-certificates=20250911-r0 \ 26 | coreutils=9.7-r1 \ 27 | && addgroup -S valkey -g 1009 \ 28 | && adduser -S -G valkey valkey -u 1009 \ 29 | && mkdir /etc/valkey \ 30 | && chown valkey:valkey /etc/valkey \ 31 | && mkdir /var/lib/valkey \ 32 | && chown valkey:valkey /var/lib/valkey 33 | 34 | COPY --from=builder /home/valkey/build/ /usr/ 35 | 36 | USER valkey 37 | -------------------------------------------------------------------------------- /internal/controller/scripts/ping_readiness_local_tls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | VALKEY_STATUS_FILE=/tmp/.valkey_cluster_check 5 | if [ ! -z "$VALKEY_PASSWORD" ]; then export REDISCLI_AUTH=$VALKEY_PASSWORD; fi; 6 | 7 | response=$( 8 | timeout --foreground -s 15 $1 \ 9 | valkey-cli \ 10 | -h localhost \ 11 | -p $VALKEY_TLS_PORT_NUMBER \ 12 | --tls \ 13 | --cacert $VALKEY_TLS_CA_FILE \ 14 | ping 15 | #--cert $VALKEY_TLS_CERT_FILE \ 16 | #--key $VALKEY_TLS_KEY_FILE \ 17 | ) 18 | 19 | if [ "$?" -eq "124" ]; then 20 | echo "Timed out" 21 | exit 1 22 | fi 23 | 24 | if [ "$response" != "PONG" ]; then 25 | echo "$response" 26 | exit 1 27 | fi 28 | count=$(echo $VALKEY_NODES | wc -w) 29 | if [ ! -f "$VALKEY_STATUS_FILE" ] && [ "$count" != "1" ]; then 30 | response=$( 31 | timeout --foreground -s 15 $1 \ 32 | valkey-cli \ 33 | -h localhost \ 34 | -p $VALKEY_TLS_PORT_NUMBER \ 35 | --tls \ 36 | --cacert $VALKEY_TLS_CA_FILE \ 37 | CLUSTER INFO | grep cluster_state | tr -d '[:space:]' 38 | #--cert $VALKEY_TLS_CERT_FILE \ 39 | #--key $VALKEY_TLS_KEY_FILE \ 40 | ) 41 | if [ "$?" -eq "124" ]; then 42 | echo "Timed out" 43 | exit 1 44 | fi 45 | if [ "$response" != "cluster_state:ok" ]; then 46 | echo "$response" 47 | exit 1 48 | else 49 | touch "$VALKEY_STATUS_FILE" 50 | fi 51 | fi 52 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=hyperspike.io 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "hyperspike.io", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Setup Go ${{ matrix.go-version }} 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version: 1.25 21 | # You can test your matrix by printing the current Go version 22 | - name: Display Go version 23 | run: go version 24 | - name: Build it 25 | run: make V=1 26 | build-operator-container: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v6 30 | - name: Setup Go ${{ matrix.go-version }} 31 | uses: actions/setup-go@v6 32 | with: 33 | go-version: 1.25 34 | - name: Build it 35 | run: make V=1 36 | - name: Extract metadata (Operator tags, labels) for Docker 37 | id: meta_operator 38 | uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 39 | with: 40 | images: ${{ env.REGISTRY }}/hyperspike/valkey-operator:${{ github.sha }} 41 | - name: Build Operator image 42 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 43 | id: docker_build_operator 44 | with: 45 | file: Dockerfile.controller 46 | context: . 47 | push: false 48 | visibility: public 49 | tags: ${{ steps.meta_operator.outputs.images }} 50 | labels: ${{ steps.meta_operator.outputs.labels }} 51 | -------------------------------------------------------------------------------- /hack/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: prometheus 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRole 8 | metadata: 9 | name: prometheus 10 | rules: 11 | - apiGroups: [""] 12 | resources: 13 | - nodes 14 | - nodes/metrics 15 | - services 16 | - endpoints 17 | - pods 18 | verbs: ["get", "list", "watch"] 19 | - apiGroups: [""] 20 | resources: 21 | - configmaps 22 | verbs: ["get"] 23 | - apiGroups: 24 | - discovery.k8s.io 25 | resources: 26 | - endpointslices 27 | verbs: ["get", "list", "watch"] 28 | - apiGroups: 29 | - networking.k8s.io 30 | resources: 31 | - ingresses 32 | verbs: ["get", "list", "watch"] 33 | - nonResourceURLs: ["/metrics"] 34 | verbs: ["get"] 35 | --- 36 | apiVersion: rbac.authorization.k8s.io/v1 37 | kind: ClusterRoleBinding 38 | metadata: 39 | name: prometheus 40 | roleRef: 41 | apiGroup: rbac.authorization.k8s.io 42 | kind: ClusterRole 43 | name: prometheus 44 | subjects: 45 | - kind: ServiceAccount 46 | name: prometheus 47 | namespace: default 48 | --- 49 | apiVersion: monitoring.coreos.com/v1 50 | kind: Prometheus 51 | metadata: 52 | name: prometheus 53 | spec: 54 | image: quay.io/prometheus/prometheus:v3.0.0 55 | serviceAccountName: prometheus 56 | serviceMonitorSelector: 57 | matchLabels: 58 | prometheus: prometheus 59 | resources: 60 | requests: 61 | memory: 400Mi 62 | enableAdminAPI: true 63 | --- 64 | apiVersion: v1 65 | kind: Service 66 | metadata: 67 | name: prometheus 68 | spec: 69 | type: ClusterIP 70 | ports: 71 | - name: web 72 | port: 9090 73 | protocol: TCP 74 | targetPort: web 75 | selector: 76 | prometheus: prometheus 77 | -------------------------------------------------------------------------------- /internal/controller/cluster.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "slices" 7 | 8 | valkeyClient "github.com/valkey-io/valkey-go" 9 | ) 10 | 11 | // valkeyCluster represents a Valkey cluster. It contains a list of shards, each with its own nodes. 12 | type valkeyCluster struct { 13 | shards []*valkeyShard 14 | } 15 | 16 | // valkeyShard represents a shard in the Valkey cluster. It contains slot information and a list of nodes. 17 | type valkeyShard struct { 18 | id int 19 | slotMin int 20 | slotMax int 21 | nodes []*valkeyNode 22 | } 23 | 24 | // valkeyNode represents a node in the Valkey cluster. 25 | type valkeyNode struct { 26 | // id is the node id in the Valkey cluster 27 | id string 28 | // name is the pod name in Kubernetes 29 | name string 30 | // ip is the pod ip in Kubernetes 31 | ip string 32 | // port is the port of the Valkey service 33 | port int 34 | // flags are the Valkey flags for this pod 35 | flags []string 36 | // primary is the id of the primary node when this node is a replica 37 | primary string 38 | // connected is true when the pod is reachable from the operator 39 | connected bool 40 | // shard is the id of the shard this node belongs to 41 | shard int 42 | // client is the Valkey client for this node, if connected. 43 | client valkeyClient.Client 44 | } 45 | 46 | // isPrimary checks if this node is a primary node for the shard in the Valkey cluster. 47 | func (vn *valkeyNode) isPrimary() bool { 48 | return slices.Contains(vn.flags, "master") 49 | } 50 | 51 | // stsPodIndex extracts the pod index number from the pod name. The pod name is expected to be in 52 | // the format -. The pod index is the pod number from a StatefulSet. 53 | func stsPodIndex(podName string) (int, error) { 54 | pattern := `.*-(\d+)$` 55 | re := regexp.MustCompile(pattern) 56 | 57 | matches := re.FindStringSubmatch(podName) 58 | if len(matches) < 2 { 59 | return 0, fmt.Errorf("no number found in pod name: %s", podName) 60 | } 61 | 62 | // Convert the captured group to an integer 63 | var number int 64 | _, err := fmt.Sscanf(matches[1], "%d", &number) 65 | if err != nil { 66 | return 0, fmt.Errorf("failed to parse number: %w", err) 67 | } 68 | 69 | return number, nil 70 | } 71 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - secrets 12 | - serviceaccounts 13 | - services 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - "" 24 | resources: 25 | - events 26 | verbs: 27 | - create 28 | - patch 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - pods 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - apps 39 | resources: 40 | - deployments 41 | - statefulsets 42 | verbs: 43 | - create 44 | - delete 45 | - get 46 | - list 47 | - patch 48 | - update 49 | - watch 50 | - apiGroups: 51 | - cert-manager.io 52 | resources: 53 | - certificates 54 | verbs: 55 | - create 56 | - delete 57 | - get 58 | - list 59 | - patch 60 | - update 61 | - watch 62 | - apiGroups: 63 | - cert-manager.io 64 | resources: 65 | - clusterissuers 66 | - issuers 67 | verbs: 68 | - get 69 | - list 70 | - watch 71 | - apiGroups: 72 | - hyperspike.io 73 | resources: 74 | - valkeys 75 | verbs: 76 | - create 77 | - delete 78 | - get 79 | - list 80 | - patch 81 | - update 82 | - watch 83 | - apiGroups: 84 | - hyperspike.io 85 | resources: 86 | - valkeys/finalizers 87 | verbs: 88 | - update 89 | - apiGroups: 90 | - hyperspike.io 91 | resources: 92 | - valkeys/status 93 | verbs: 94 | - get 95 | - patch 96 | - update 97 | - apiGroups: 98 | - monitoring.coreos.com 99 | resources: 100 | - servicemonitors 101 | verbs: 102 | - create 103 | - delete 104 | - get 105 | - list 106 | - patch 107 | - update 108 | - watch 109 | - apiGroups: 110 | - policy 111 | resources: 112 | - poddisruptionbudgets 113 | verbs: 114 | - create 115 | - delete 116 | - get 117 | - list 118 | - patch 119 | - update 120 | - watch 121 | - apiGroups: 122 | - storage.k8s.io 123 | resources: 124 | - storageclasses 125 | verbs: 126 | - get 127 | - list 128 | - watch 129 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'release-*' 7 | - 'master' 8 | - 'main' 9 | tags: 10 | - 'v*' 11 | - '!pkg*' 12 | 13 | permissions: 14 | contents: write 15 | packages: write 16 | # id-token: write # Uncomment this line if you want to do cosign signing 17 | 18 | jobs: 19 | publish: 20 | runs-on: ubuntu-latest 21 | if: github.event_name == 'push' && github.ref_type == 'tag' 22 | steps: 23 | - run: | 24 | gh release create ${{ github.ref_name }} --title "Release ${{ github.ref_name }}" --generate-notes -R $GITHUB_REPOSITORY 25 | env: 26 | GH_TOKEN: ${{ github.token }} 27 | upload-assets: 28 | runs-on: ubuntu-latest 29 | needs: publish 30 | name: Upload release assets 31 | permissions: 32 | contents: write 33 | packages: write 34 | id-token: write 35 | attestations: write 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v6 39 | - name: Install Go 40 | uses: actions/setup-go@v6 41 | with: 42 | go-version: 1.25 43 | check-latest: true 44 | - name: Build Installer 45 | run: make build-installer IMG=ghcr.io/${{ github.repository_owner }}/valkey-operator:${{ github.ref_name }} 46 | - name: Attest 47 | uses: actions/attest-build-provenance@v3 48 | id: attest 49 | with: 50 | subject-path: | 51 | dist/install.yaml 52 | - name: Upload dist/install.yaml to release 53 | uses: svenstaro/upload-release-action@v2 54 | with: 55 | repo_token: ${{ secrets.GITHUB_TOKEN }} 56 | file: dist/install.yaml 57 | asset_name: install.yaml 58 | tag: ${{ github.ref }} 59 | overwrite: true 60 | publish-helm: 61 | runs-on: ubuntu-latest 62 | needs: publish 63 | name: Publish Helm Chart 64 | permissions: 65 | contents: write 66 | packages: write 67 | id-token: write 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v6 71 | - name: Install Go 72 | uses: actions/setup-go@v6 73 | with: 74 | go-version: 1.25 75 | check-latest: true 76 | - name: Log in to the Container registry 77 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 78 | with: 79 | registry: ghcr.io 80 | username: ${{ github.actor }} 81 | password: ${{ secrets.GITHUB_TOKEN }} 82 | - name: Package and Upload Helm Chart 83 | run: make helm-publish V=1 IMG=ghcr.io/${{ github.repository_owner }}/valkey-operator:${{ github.ref_name }} 84 | -------------------------------------------------------------------------------- /internal/controller/valkey_controller_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | /* 5 | Copyright 2024. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package controller 21 | 22 | import ( 23 | "context" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/types" 29 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 30 | 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | 33 | hyperspikeiov1 "hyperspike.io/valkey-operator/api/v1" 34 | ) 35 | 36 | var _ = Describe("Valkey Controller", func() { 37 | Context("When reconciling a resource", func() { 38 | const resourceName = "test-resource" 39 | 40 | ctx := context.Background() 41 | 42 | typeNamespacedName := types.NamespacedName{ 43 | Name: resourceName, 44 | Namespace: "default", // TODO(user):Modify as needed 45 | } 46 | valkey := &hyperspikeiov1.Valkey{} 47 | 48 | BeforeEach(func() { 49 | By("creating the custom resource for the Kind Valkey") 50 | err := k8sClient.Get(ctx, typeNamespacedName, valkey) 51 | if err != nil && errors.IsNotFound(err) { 52 | resource := &hyperspikeiov1.Valkey{ 53 | ObjectMeta: metav1.ObjectMeta{ 54 | Name: resourceName, 55 | Namespace: "default", 56 | }, 57 | // TODO(user): Specify other spec details if needed. 58 | } 59 | Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 60 | } 61 | }) 62 | 63 | AfterEach(func() { 64 | // TODO(user): Cleanup logic after each test, like removing the resource instance. 65 | resource := &hyperspikeiov1.Valkey{} 66 | err := k8sClient.Get(ctx, typeNamespacedName, resource) 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | By("Cleanup the specific resource instance Valkey") 70 | Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 71 | }) 72 | It("should successfully reconcile the resource", func() { 73 | By("Reconciling the created resource") 74 | controllerReconciler := &ValkeyReconciler{ 75 | Client: k8sClient, 76 | Scheme: k8sClient.Scheme(), 77 | } 78 | 79 | _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 80 | NamespacedName: typeNamespacedName, 81 | }) 82 | Expect(err).NotTo(HaveOccurred()) 83 | // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 84 | // Example: If you expect a certain status condition after reconciliation, verify it here. 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | /* 5 | Copyright 2024. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package controller 21 | 22 | import ( 23 | "fmt" 24 | "path/filepath" 25 | "runtime" 26 | "testing" 27 | 28 | . "github.com/onsi/ginkgo/v2" 29 | . "github.com/onsi/gomega" 30 | 31 | "k8s.io/client-go/kubernetes/scheme" 32 | "k8s.io/client-go/rest" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/envtest" 35 | logf "sigs.k8s.io/controller-runtime/pkg/log" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | 38 | hyperspikeiov1 "hyperspike.io/valkey-operator/api/v1" 39 | // +kubebuilder:scaffold:imports 40 | ) 41 | 42 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 43 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 44 | 45 | var cfg *rest.Config 46 | var k8sClient client.Client 47 | var testEnv *envtest.Environment 48 | 49 | func TestControllers(t *testing.T) { 50 | RegisterFailHandler(Fail) 51 | 52 | RunSpecs(t, "Controller Suite") 53 | } 54 | 55 | var _ = BeforeSuite(func() { 56 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 57 | 58 | By("bootstrapping test environment") 59 | testEnv = &envtest.Environment{ 60 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 61 | ErrorIfCRDPathMissing: true, 62 | 63 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 64 | // without call the makefile target test. If not informed it will look for the 65 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 66 | // Note that you must have the required binaries setup under the bin directory to perform 67 | // the tests directly. When we run make test it will be setup and used automatically. 68 | BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", 69 | fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), 70 | } 71 | 72 | var err error 73 | // cfg is defined in this file globally. 74 | cfg, err = testEnv.Start() 75 | Expect(err).NotTo(HaveOccurred()) 76 | Expect(cfg).NotTo(BeNil()) 77 | 78 | err = hyperspikeiov1.AddToScheme(scheme.Scheme) 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | // +kubebuilder:scaffold:scheme 82 | 83 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 84 | Expect(err).NotTo(HaveOccurred()) 85 | Expect(k8sClient).NotTo(BeNil()) 86 | 87 | }) 88 | 89 | var _ = AfterSuite(func() { 90 | By("tearing down the test environment") 91 | err := testEnv.Stop() 92 | Expect(err).NotTo(HaveOccurred()) 93 | }) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # valkey-operator 2 | 3 | Provision [valkey](https://valkey.io) (redis) clusters 4 | 5 | ## Description 6 | 7 | This operator creates valkey clusters and makes them available to other services on the k8s cluster 8 | 9 | See the following link for more information on available Custom Resource Options: [https://doc.crds.dev/github.com/hyperspike/valkey-operator](https://doc.crds.dev/github.com/hyperspike/valkey-operator) 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - go version v1.22.0+ 16 | - docker version 17.03+. 17 | - kubectl version v1.11.3+. 18 | - Access to a Kubernetes v1.11.3+ cluster. 19 | 20 | ### Quick Start 21 | 22 | Deploy kubernetes locally using minikube, and install the controller: 23 | 24 | ```sh 25 | make quickstart 26 | ``` 27 | 28 | and optionally, turn on TLS and Prometheus: 29 | 30 | ```sh 31 | make quickstart TLS=1 PROMETHEUS=1 32 | ``` 33 | 34 | ### To Uninstall 35 | 36 | ```sh 37 | make minikube-delete 38 | ``` 39 | 40 | ## Project Distribution 41 | 42 | ### Vanilla Kubernetes Manifests 43 | 44 | To install the valkey-operator, all you need to do is run the following command: 45 | 46 | ```sh 47 | LATEST=$(curl -s https://api.github.com/repos/hyperspike/valkey-operator/releases/latest | jq -cr .tag_name) 48 | curl -sL https://github.com/hyperspike/valkey-operator/releases/download/$LATEST/install.yaml | kubectl create -f - 49 | ``` 50 | 51 | ### Helm 52 | 53 | ```sh 54 | LATEST=$(curl -s https://api.github.com/repos/hyperspike/valkey-operator/releases/latest | jq -cr .tag_name) 55 | helm install valkey-operator --namespace valkey-operator-system --create-namespace oci://ghcr.io/hyperspike/valkey-operator --version ${LATEST}-chart 56 | ``` 57 | 58 | ### Verifying the container image 59 | 60 | ```sh 61 | LATEST=$(curl -s https://api.github.com/repos/hyperspike/valkey-operator/releases/latest | jq -cr .tag_name) 62 | cosign verify ghcr.io/hyperspike/valkey-operator:$LATEST --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity https://github.com/hyperspike/valkey-operator/.github/workflows/image.yaml@refs/tags/$LATEST 63 | ``` 64 | 65 | ## Contributing 66 | 67 | **NOTE:** Run `make help` for more information on all potential `make` targets 68 | 69 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 70 | 71 | Spool up a local cluster: 72 | 73 | ```sh 74 | make minikube 75 | ``` 76 | 77 | Proxy to the registry on the local cluster: 78 | 79 | ```sh 80 | make registry-proxy 81 | ``` 82 | 83 | And deploy the operator: 84 | 85 | ```sh 86 | export REGISTRY=localhost:5000; export VERSION=1; \ 87 | make docker-build; make docker-push; \ 88 | make build-installer; kubectl apply -f dist/install.yaml 89 | ``` 90 | 91 | ## License 92 | 93 | Copyright 2024. 94 | 95 | Licensed under the Apache License, Version 2.0 (the "License"); 96 | you may not use this file except in compliance with the License. 97 | You may obtain a copy of the License at 98 | 99 | http://www.apache.org/licenses/LICENSE-2.0 100 | 101 | Unless required by applicable law or agreed to in writing, software 102 | distributed under the License is distributed on an "AS IS" BASIS, 103 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 104 | See the License for the specific language governing permissions and 105 | limitations under the License. 106 | -------------------------------------------------------------------------------- /.github/workflows/scan.yaml: -------------------------------------------------------------------------------- 1 | name: Scan 2 | 3 | # Run workflow each time code is pushed to your repository and on a schedule. 4 | # The scheduled workflow runs every at 00:00 on Sunday UTC time. 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | 14 | permissions: 15 | contents: read 16 | security-events: write 17 | actions: read 18 | 19 | jobs: 20 | hadolint: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | - uses: hadolint/hadolint-action@v3.3.0 25 | with: 26 | dockerfile: Dockerfile.valkey 27 | - uses: hadolint/hadolint-action@v3.3.0 28 | with: 29 | dockerfile: Dockerfile.controller 30 | - uses: hadolint/hadolint-action@v3.3.0 31 | with: 32 | dockerfile: Dockerfile.sidecar 33 | gosec: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Check out code into the Go module directory 37 | uses: actions/checkout@v6 38 | - name: Security Scan 39 | uses: securego/gosec@master 40 | with: 41 | # we let the report trigger content trigger a failure using the GitHub Security features. 42 | args: '-fmt sarif -out results.sarif ./...' 43 | - name: Upload SARIF file 44 | if: always() 45 | uses: github/codeql-action/upload-sarif@v4 46 | with: 47 | # Path to SARIF file relative to the root of the repository 48 | sarif_file: results.sarif 49 | golangci: 50 | name: lint 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/setup-go@v6 54 | with: 55 | go-version: 1.25 56 | - uses: actions/checkout@v6 57 | - name: golangci-lint 58 | uses: golangci/golangci-lint-action@v9 59 | with: 60 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 61 | version: v2.4.0 62 | 63 | # Optional: working directory, useful for monorepos 64 | # working-directory: somedir 65 | 66 | # Optional: golangci-lint command line arguments. 67 | # args: --issues-exit-code=0 68 | args: --timeout=5m --config=.golangci.yml 69 | 70 | # Optional: show only new issues if it's a pull request. The default value is `false`. 71 | # only-new-issues: true 72 | 73 | # Optional: if set to true then the all caching functionality will be complete disabled, 74 | # takes precedence over all other caching options. 75 | # skip-cache: true 76 | 77 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 78 | # skip-pkg-cache: true 79 | 80 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 81 | # skip-build-cache: true 82 | trivy: 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v6 86 | 87 | - name: Build the Valkey image 88 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 89 | id: docker_build_valkey 90 | with: 91 | file: Dockerfile.valkey 92 | context: . 93 | push: false 94 | tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey:${{ github.SHA }} 95 | - name: Run Trivy vulnerability scanner 96 | uses: aquasecurity/trivy-action@master 97 | with: 98 | image-ref: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey:${{ github.SHA }} 99 | format: 'sarif' 100 | output: 'trivy-results.sarif' 101 | - name: Upload Trivy scan results to GitHub Security tab 102 | uses: github/codeql-action/upload-sarif@v4 103 | if: always() 104 | with: 105 | sarif_file: trivy-results.sarif 106 | -------------------------------------------------------------------------------- /internal/controller/valkey_controller_utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "testing" 21 | 22 | hyperspikeiov1 "hyperspike.io/valkey-operator/api/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | ) 26 | 27 | func TestLabels(t *testing.T) { 28 | testLabels := map[string]string{ 29 | "app": "valkey", 30 | } 31 | valkey := &hyperspikeiov1.Valkey{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "test-resource", 34 | Namespace: "default", 35 | Labels: testLabels, 36 | }, 37 | } 38 | result := labels(valkey) 39 | if testLabels["app"] != result["app"] { 40 | t.Errorf("Expected %v, got %v", testLabels["app"], result["app"]) 41 | } 42 | if result["app.kubernetes.io/name"] != "valkey" { 43 | t.Errorf("Expected %v, got %v", "valkey", result["app.kubernetes.io/name"]) 44 | } 45 | if result["app.kubernetes.io/instance"] != "test-resource" { 46 | t.Errorf("Expected %v, got %v", "test-resource", result["app.kubernetes.io/instance"]) 47 | } 48 | result["app.kubernetes.io/component"] = Metrics 49 | result2 := labels(valkey) 50 | if result["app.kubernetes.io/component"] != "metrics" { 51 | t.Errorf("Expected %v, got %v", "metrics", result["app.kubernetes.io/component"]) 52 | } 53 | if result2["app.kubernetes.io/component"] != "valkey" { 54 | t.Errorf("Expected %v, got %v", "valkey", result["app.kubernetes.io/component"]) 55 | } 56 | } 57 | 58 | func TestAnnotations(t *testing.T) { 59 | testAnnotations := map[string]string{ 60 | "app": "valkey", 61 | } 62 | valkey := &hyperspikeiov1.Valkey{ 63 | ObjectMeta: metav1.ObjectMeta{ 64 | Name: "test-resource", 65 | Namespace: "default", 66 | Annotations: testAnnotations, 67 | }, 68 | } 69 | result := annotations(valkey) 70 | if testAnnotations["app"] != result["app"] { 71 | t.Errorf("Expected %v, got %v", testAnnotations["app"], result["app"]) 72 | } 73 | } 74 | 75 | func TestServicePasswordKey(t *testing.T) { 76 | valkey := &hyperspikeiov1.Valkey{ 77 | ObjectMeta: metav1.ObjectMeta{ 78 | Name: "test-resource", 79 | Namespace: "default", 80 | }, 81 | } 82 | result := getServicePasswordKey(valkey) 83 | if result != "password" { 84 | t.Errorf("Expected %v, got %v", "test-resource", result) 85 | } 86 | valkey.Spec.ServicePassword = &corev1.SecretKeySelector{ 87 | Key: "test-password", 88 | } 89 | result = getServicePasswordKey(valkey) 90 | if result != "test-password" { 91 | t.Errorf("Expected %v, got %v", "test-password", result) 92 | } 93 | } 94 | 95 | func TestServicePasswordName(t *testing.T) { 96 | valkey := &hyperspikeiov1.Valkey{ 97 | ObjectMeta: metav1.ObjectMeta{ 98 | Name: "test-resource", 99 | Namespace: "default", 100 | }, 101 | } 102 | result := getServicePasswordName(valkey) 103 | if result != "test-resource" { 104 | t.Errorf("Expected %v, got %v", "test-resource", result) 105 | } 106 | valkey.Spec.ServicePassword = &corev1.SecretKeySelector{ 107 | LocalObjectReference: corev1.LocalObjectReference{ 108 | Name: "test-password", 109 | }, 110 | } 111 | result = getServicePasswordName(valkey) 112 | if result != "test-password" { 113 | t.Errorf("Expected %v, got %v", "test-password", result) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module hyperspike.io/valkey-operator 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | github.com/cert-manager/cert-manager v1.19.1 7 | github.com/google/go-cmp v0.7.0 8 | github.com/oliver006/redis_exporter v1.80.1 9 | github.com/onsi/ginkgo/v2 v2.27.2 10 | github.com/onsi/gomega v1.38.2 11 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0 12 | github.com/prometheus/client_golang v1.23.2 13 | github.com/spf13/cobra v1.10.1 14 | github.com/valkey-io/valkey-go v1.0.68 15 | go.uber.org/zap v1.27.1 16 | k8s.io/api v0.34.3 17 | k8s.io/apimachinery v0.34.3 18 | k8s.io/client-go v0.34.3 19 | sigs.k8s.io/controller-runtime v0.22.4 20 | ) 21 | 22 | require ( 23 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 24 | github.com/beorn7/perks v1.0.1 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/emicklei/go-restful/v3 v3.13.0 // indirect 28 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 29 | github.com/fsnotify/fsnotify v1.9.0 // indirect 30 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 31 | github.com/go-logr/logr v1.4.3 // indirect 32 | github.com/go-logr/zapr v1.3.0 // indirect 33 | github.com/go-openapi/jsonpointer v0.22.1 // indirect 34 | github.com/go-openapi/jsonreference v0.21.2 // indirect 35 | github.com/go-openapi/swag v0.23.1 // indirect 36 | github.com/go-openapi/swag/jsonname v0.25.1 // indirect 37 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/gomodule/redigo v1.9.3 // indirect 40 | github.com/google/btree v1.1.3 // indirect 41 | github.com/google/gnostic-models v0.7.0 // indirect 42 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 43 | github.com/google/uuid v1.6.0 // indirect 44 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/klauspost/compress v1.18.0 // indirect 48 | github.com/mailru/easyjson v0.9.0 // indirect 49 | github.com/mna/redisc v1.4.0 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 52 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 53 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 54 | github.com/prometheus/client_model v0.6.2 // indirect 55 | github.com/prometheus/common v0.66.1 // indirect 56 | github.com/prometheus/procfs v0.17.0 // indirect 57 | github.com/sirupsen/logrus v1.9.3 // indirect 58 | github.com/spf13/pflag v1.0.10 // indirect 59 | github.com/x448/float16 v0.8.4 // indirect 60 | go.uber.org/multierr v1.11.0 // indirect 61 | go.yaml.in/yaml/v2 v2.4.3 // indirect 62 | go.yaml.in/yaml/v3 v3.0.4 // indirect 63 | golang.org/x/mod v0.27.0 // indirect 64 | golang.org/x/net v0.44.0 // indirect 65 | golang.org/x/oauth2 v0.31.0 // indirect 66 | golang.org/x/sync v0.17.0 // indirect 67 | golang.org/x/sys v0.36.0 // indirect 68 | golang.org/x/term v0.35.0 // indirect 69 | golang.org/x/text v0.29.0 // indirect 70 | golang.org/x/time v0.13.0 // indirect 71 | golang.org/x/tools v0.36.0 // indirect 72 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 73 | google.golang.org/protobuf v1.36.9 // indirect 74 | gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 75 | gopkg.in/inf.v0 v0.9.1 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | k8s.io/apiextensions-apiserver v0.34.2 // indirect 78 | k8s.io/klog/v2 v2.130.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 80 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect 81 | sigs.k8s.io/gateway-api v1.4.0 // indirect 82 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 83 | sigs.k8s.io/randfill v1.0.0 // indirect 84 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 85 | sigs.k8s.io/yaml v1.6.0 // indirect 86 | ) 87 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## v0.1.0 4 | 5 | ### Important Features 6 | 7 | 1. Init all nodes with distinct config epocs, (1, 2, 3, etc. rather than 0, 0, 0...). This is similar to what `valkey-cli --cluster create` does, but it's probably better to implement it in the operator rather than calling out to `cluster-cli`. 8 | 2. CLUSTER MEET and K8S redisness-probe after meet. 9 | 3. Check that the node is in the cluster (e.g. using CLUSTER NODES). 10 | 4. Migrate slots to it or start replicating from another node. 11 | 5. Rediness gate (on the pod) until a replica has finished loading/replicating. 12 | 6. Check that the node is part of the cluster and has slots 13 | * Upgrade, without impact on the redundancy: 14 | 1. Add one new upgraded pod and make it a replica of an existing primary. 15 | 2. Upgrade all other replica nodes of this primary, one by one or a few at a time. (This can be done be removing an old and adding a new upgraded one in its place.) 16 | 3. Trigger a manual failover to one of the replicas. 17 | 4. Remove the old primary. 18 | * Anti-affinity: 19 | * Replicas and primary of the same shard need be spread out across different K8s workers. 20 | * Primaries should be evenly distributed across K8s workers. 21 | * K8s worker upgrade, without service impact. Similar to upgrading Valkey, this is done without any reduced redundancy. 22 | 1. Add a new worker. 23 | 2. Add replicas to it. 24 | 3. For the primaries on the old worker, trigger manual failover to replicas on the new worker. 25 | 4. Remove the nodes on the old worker from the valkey cluster using CLUSTER FORGET. 26 | 4. Remove the old worker. 27 | * Orchestrate scaling. Scale in/out without service impact. 28 | 1. Add new primary and replicas (if scaling out). 29 | 2. Move cluster slots (valkey keys) to it from the other shards, to spread the data evenly among the primaries. 30 | 3. Remove nodes (if scaling in). 31 | * Future: Auto-scaling. Check which shards have more traffic, CPU and/or memory usage than the others and balance the keys among the shards to adjust these metrics. (Such metrics were added in Valkey 8 for this purpose. I believe they will use it in AWS.) 32 | * There are ideas about whether it would be possible to use K8s Horizontal Pod Autoscaler. (Requires 33 | ReplicaSet? One ReplicaSet per worker? Some smart way to use labels on the pods?) 34 | 35 | When you start a pod, I don't know if you can fully control on which worker the pod is started. I suppose it depends how the pods are managed (deployment, stateful set, or just manually by the operator). You may be able to add labels on pods and set up rule according to that. If you can't fully control on which worker a pod is stared, you can at least manipulate the valkey cluster by moving replicas between primaries and by triggering manual failover. 36 | The main problem with stateful set, according to the people I've talked to, is the netsplit scenario. Assume some pod is not reachable by the operator, but it's still alive on a different worker and writing to its disk storage. In this case, the stateful set will never delete the pod, because it can't guarantee that it's not writing to its disk. Maybe you can force-delete it? Is that a good way? If it's actually writing to a disk, it might not be a good idea to start a new pod writing to the same storage. Also, it's not desirable to have potential old zombie pods around. 37 | For this reason, they're using only diskless replication, no storage and no stateful sets. With this setup, a pod can be deleted at any time, even if the operator doesn't know if it's alive or not. 38 | It might be possible to do all this with stateful set, or with multiple stateful sets, but you don't get this for free just by using stateful set. Apparently, you get the most control by just managing the pods directly from the operator. 39 | 40 | ## Existing Operators 41 | 42 | * [IBM](https://ibm.github.io/operator-for-redis-cluster/) 43 | * [Amadeus](https://github.com/AmadeusITGroup/Redis-Operator) 44 | * [Ops Tree](https://ot-container-kit.github.io/redis-operator/#/quickstart/quickstart) 45 | 46 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os/exec" 22 | "time" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | 27 | "hyperspike.io/valkey-operator/test/utils" 28 | ) 29 | 30 | const namespace = "valkey-operator-system" 31 | 32 | var _ = Describe("controller", Ordered, func() { 33 | BeforeAll(func() { 34 | By("installing prometheus operator") 35 | Expect(utils.InstallPrometheusOperator()).To(Succeed()) 36 | 37 | By("installing the cert-manager") 38 | Expect(utils.InstallCertManager()).To(Succeed()) 39 | 40 | By("creating manager namespace") 41 | cmd := exec.Command("kubectl", "create", "ns", namespace) 42 | _, _ = utils.Run(cmd) 43 | }) 44 | 45 | AfterAll(func() { 46 | By("uninstalling the Prometheus manager bundle") 47 | utils.UninstallPrometheusOperator() 48 | 49 | By("uninstalling the cert-manager bundle") 50 | utils.UninstallCertManager() 51 | 52 | By("removing manager namespace") 53 | cmd := exec.Command("kubectl", "delete", "ns", namespace) 54 | _, _ = utils.Run(cmd) 55 | }) 56 | 57 | Context("Operator", func() { 58 | It("should run successfully", func() { 59 | var controllerPodName string 60 | var err error 61 | 62 | // projectimage stores the name of the image used in the example 63 | var projectimage = "example.com/valkey-operator:v0.0.1" 64 | 65 | By("building the manager(Operator) image") 66 | cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) 67 | _, err = utils.Run(cmd) 68 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 69 | 70 | By("loading the the manager(Operator) image on Kind") 71 | err = utils.LoadImageToKindClusterWithName(projectimage) 72 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 73 | 74 | By("installing CRDs") 75 | cmd = exec.Command("make", "install") 76 | _, err = utils.Run(cmd) 77 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 78 | 79 | By("deploying the controller-manager") 80 | cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) 81 | _, err = utils.Run(cmd) 82 | ExpectWithOffset(1, err).NotTo(HaveOccurred()) 83 | 84 | By("validating that the controller-manager pod is running as expected") 85 | verifyControllerUp := func() error { 86 | // Get pod name 87 | 88 | cmd = exec.Command("kubectl", "get", 89 | "pods", "-l", "control-plane=controller-manager", 90 | "-o", "go-template={{ range .items }}"+ 91 | "{{ if not .metadata.deletionTimestamp }}"+ 92 | "{{ .metadata.name }}"+ 93 | "{{ \"\\n\" }}{{ end }}{{ end }}", 94 | "-n", namespace, 95 | ) 96 | 97 | podOutput, err := utils.Run(cmd) 98 | ExpectWithOffset(2, err).NotTo(HaveOccurred()) 99 | podNames := utils.GetNonEmptyLines(string(podOutput)) 100 | if len(podNames) != 1 { 101 | return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) 102 | } 103 | controllerPodName = podNames[0] 104 | ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) 105 | 106 | // Validate pod status 107 | cmd = exec.Command("kubectl", "get", 108 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 109 | "-n", namespace, 110 | ) 111 | status, err := utils.Run(cmd) 112 | ExpectWithOffset(2, err).NotTo(HaveOccurred()) 113 | if string(status) != "Running" { 114 | return fmt.Errorf("controller pod in %s status", status) 115 | } 116 | return nil 117 | } 118 | EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) 119 | 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: valkey-operator 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: v1 11 | kind: ConfigMap 12 | metadata: 13 | name: config 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: valkey-operator 18 | app.kubernetes.io/managed-by: kustomize 19 | data: 20 | nodes: "3" 21 | valkeyImage: "ghcr.io/hyperspike/valkey:8.1.4" 22 | exporterImage: "ghcr.io/hyperspike/valkey-sidecar:v0.0.51" 23 | --- 24 | apiVersion: apps/v1 25 | kind: Deployment 26 | metadata: 27 | name: controller-manager 28 | namespace: system 29 | labels: 30 | control-plane: controller-manager 31 | app.kubernetes.io/name: valkey-operator 32 | app.kubernetes.io/managed-by: kustomize 33 | spec: 34 | selector: 35 | matchLabels: 36 | control-plane: controller-manager 37 | replicas: 1 38 | template: 39 | metadata: 40 | annotations: 41 | kubectl.kubernetes.io/default-container: manager 42 | labels: 43 | control-plane: controller-manager 44 | spec: 45 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 46 | # according to the platforms which are supported by your solution. 47 | # It is considered best practice to support multiple architectures. You can 48 | # build your manager image using the makefile target docker-buildx. 49 | # affinity: 50 | # nodeAffinity: 51 | # requiredDuringSchedulingIgnoredDuringExecution: 52 | # nodeSelectorTerms: 53 | # - matchExpressions: 54 | # - key: kubernetes.io/arch 55 | # operator: In 56 | # values: 57 | # - amd64 58 | # - arm64 59 | # - ppc64le 60 | # - s390x 61 | # - key: kubernetes.io/os 62 | # operator: In 63 | # values: 64 | # - linux 65 | securityContext: 66 | runAsNonRoot: true 67 | # TODO(user): For common cases that do not require escalating privileges 68 | # it is recommended to ensure that all your Pods/Containers are restrictive. 69 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 70 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 71 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 72 | # seccompProfile: 73 | # type: RuntimeDefault 74 | containers: 75 | - command: 76 | - /manager 77 | args: 78 | - --leader-elect=false 79 | - --health-probe-bind-address=:8081 80 | image: controller:latest 81 | name: manager 82 | env: 83 | - name: APP_NAME 84 | valueFrom: 85 | fieldRef: 86 | fieldPath: metadata.labels['app.kubernetes.io/name'] 87 | - name: INSTANCE_NAME 88 | valueFrom: 89 | fieldRef: 90 | fieldPath: metadata.labels['app.kubernetes.io/instance'] 91 | securityContext: 92 | allowPrivilegeEscalation: false 93 | capabilities: 94 | drop: 95 | - "ALL" 96 | livenessProbe: 97 | httpGet: 98 | path: /healthz 99 | port: 8081 100 | initialDelaySeconds: 15 101 | periodSeconds: 20 102 | readinessProbe: 103 | httpGet: 104 | path: /readyz 105 | port: 8081 106 | initialDelaySeconds: 5 107 | periodSeconds: 10 108 | # TODO(user): Configure the resources accordingly based on the project requirements. 109 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 110 | resources: 111 | limits: 112 | cpu: 500m 113 | memory: 128Mi 114 | requests: 115 | cpu: 10m 116 | memory: 64Mi 117 | serviceAccountName: controller-manager 118 | terminationGracePeriodSeconds: 10 119 | -------------------------------------------------------------------------------- /test/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "strings" 24 | 25 | "github.com/onsi/ginkgo/v2" 26 | ) 27 | 28 | const ( 29 | prometheusOperatorVersion = "v0.72.0" 30 | prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + 31 | "releases/download/%s/bundle.yaml" 32 | 33 | certmanagerVersion = "v1.14.4" 34 | certmanagerURLTmpl = "https://github.com/jetstack/cert-manager/releases/download/%s/cert-manager.yaml" 35 | ) 36 | 37 | func warnError(err error) { 38 | _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "warning: %v\n", err) 39 | } 40 | 41 | // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. 42 | func InstallPrometheusOperator() error { 43 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 44 | cmd := exec.Command("kubectl", "create", "-f", url) // #nosec G204 45 | _, err := Run(cmd) 46 | return err 47 | } 48 | 49 | // Run executes the provided command within this context 50 | func Run(cmd *exec.Cmd) ([]byte, error) { 51 | dir, _ := GetProjectDir() 52 | cmd.Dir = dir 53 | 54 | if err := os.Chdir(cmd.Dir); err != nil { 55 | _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "chdir dir: %s\n", err) 56 | } 57 | 58 | cmd.Env = append(os.Environ(), "GO111MODULE=on") 59 | command := strings.Join(cmd.Args, " ") 60 | _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "running: %s\n", command) 61 | output, err := cmd.CombinedOutput() 62 | if err != nil { 63 | return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) 64 | } 65 | 66 | return output, nil 67 | } 68 | 69 | // UninstallPrometheusOperator uninstalls the prometheus 70 | func UninstallPrometheusOperator() { 71 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 72 | cmd := exec.Command("kubectl", "delete", "-f", url) // #nosec G204 73 | if _, err := Run(cmd); err != nil { 74 | warnError(err) 75 | } 76 | } 77 | 78 | // UninstallCertManager uninstalls the cert manager 79 | func UninstallCertManager() { 80 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 81 | cmd := exec.Command("kubectl", "delete", "-f", url) // #nosec G204 82 | if _, err := Run(cmd); err != nil { 83 | warnError(err) 84 | } 85 | } 86 | 87 | // InstallCertManager installs the cert manager bundle. 88 | func InstallCertManager() error { 89 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 90 | cmd := exec.Command("kubectl", "apply", "-f", url) // #nosec G204 91 | if _, err := Run(cmd); err != nil { 92 | return err 93 | } 94 | // Wait for cert-manager-webhook to be ready, which can take time if cert-manager 95 | // was re-installed after uninstalling on a cluster. 96 | cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", 97 | "--for", "condition=Available", 98 | "--namespace", "cert-manager", 99 | "--timeout", "5m", 100 | ) 101 | 102 | _, err := Run(cmd) 103 | return err 104 | } 105 | 106 | // LoadImageToKindCluster loads a local docker image to the kind cluster 107 | func LoadImageToKindClusterWithName(name string) error { 108 | cluster := "kind" 109 | if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { 110 | cluster = v 111 | } 112 | kindOptions := []string{"load", "docker-image", name, "--name", cluster} 113 | cmd := exec.Command("kind", kindOptions...) // #nosec G204 114 | _, err := Run(cmd) 115 | return err 116 | } 117 | 118 | // GetNonEmptyLines converts given command output string into individual objects 119 | // according to line breakers, and ignores the empty elements in it. 120 | func GetNonEmptyLines(output string) []string { 121 | var res []string 122 | elements := strings.Split(output, "\n") 123 | for _, element := range elements { 124 | if element != "" { 125 | res = append(res, element) 126 | } 127 | } 128 | 129 | return res 130 | } 131 | 132 | // GetProjectDir will return the directory where the project is 133 | func GetProjectDir() (string, error) { 134 | wd, err := os.Getwd() 135 | if err != nil { 136 | return wd, err 137 | } 138 | wd = strings.ReplaceAll(wd, "/test/e2e", "") 139 | return wd, nil 140 | } 141 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: valkey-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: valkey-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | # [METRICS] To enable the controller manager metrics service, uncomment the following line. 29 | #- metrics_service.yaml 30 | 31 | # Uncomment the patches line if you enable Metrics, and/or are using webhooks and cert-manager 32 | #patches: 33 | # [METRICS] The following patch will enable the metrics endpoint. Ensure that you also protect this endpoint. 34 | # More info: https://book.kubebuilder.io/reference/metrics 35 | # If you want to expose the metric endpoint of your controller-manager uncomment the following line. 36 | #- path: manager_metrics_patch.yaml 37 | # target: 38 | # kind: Deployment 39 | 40 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 41 | # crd/kustomization.yaml 42 | #- path: manager_webhook_patch.yaml 43 | 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 45 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 46 | # 'CERTMANAGER' needs to be enabled to use ca injection 47 | #- path: webhookcainjection_patch.yaml 48 | 49 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 50 | # Uncomment the following replacements to add the cert-manager CA injection annotations 51 | #replacements: 52 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 53 | # kind: Certificate 54 | # group: cert-manager.io 55 | # version: v1 56 | # name: serving-cert # this name should match the one in certificate.yaml 57 | # fieldPath: .metadata.namespace # namespace of the certificate CR 58 | # targets: 59 | # - select: 60 | # kind: ValidatingWebhookConfiguration 61 | # fieldPaths: 62 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 63 | # options: 64 | # delimiter: '/' 65 | # index: 0 66 | # create: true 67 | # - select: 68 | # kind: MutatingWebhookConfiguration 69 | # fieldPaths: 70 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 71 | # options: 72 | # delimiter: '/' 73 | # index: 0 74 | # create: true 75 | # - select: 76 | # kind: CustomResourceDefinition 77 | # fieldPaths: 78 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 79 | # options: 80 | # delimiter: '/' 81 | # index: 0 82 | # create: true 83 | # - source: 84 | # kind: Certificate 85 | # group: cert-manager.io 86 | # version: v1 87 | # name: serving-cert # this name should match the one in certificate.yaml 88 | # fieldPath: .metadata.name 89 | # targets: 90 | # - select: 91 | # kind: ValidatingWebhookConfiguration 92 | # fieldPaths: 93 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 94 | # options: 95 | # delimiter: '/' 96 | # index: 1 97 | # create: true 98 | # - select: 99 | # kind: MutatingWebhookConfiguration 100 | # fieldPaths: 101 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 102 | # options: 103 | # delimiter: '/' 104 | # index: 1 105 | # create: true 106 | # - select: 107 | # kind: CustomResourceDefinition 108 | # fieldPaths: 109 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 110 | # options: 111 | # delimiter: '/' 112 | # index: 1 113 | # create: true 114 | # - source: # Add cert-manager annotation to the webhook Service 115 | # kind: Service 116 | # version: v1 117 | # name: webhook-service 118 | # fieldPath: .metadata.name # namespace of the service 119 | # targets: 120 | # - select: 121 | # kind: Certificate 122 | # group: cert-manager.io 123 | # version: v1 124 | # fieldPaths: 125 | # - .spec.dnsNames.0 126 | # - .spec.dnsNames.1 127 | # options: 128 | # delimiter: '.' 129 | # index: 0 130 | # create: true 131 | # - source: 132 | # kind: Service 133 | # version: v1 134 | # name: webhook-service 135 | # fieldPath: .metadata.namespace # namespace of the service 136 | # targets: 137 | # - select: 138 | # kind: Certificate 139 | # group: cert-manager.io 140 | # version: v1 141 | # fieldPaths: 142 | # - .spec.dnsNames.0 143 | # - .spec.dnsNames.1 144 | # options: 145 | # delimiter: '.' 146 | # index: 1 147 | # create: true 148 | -------------------------------------------------------------------------------- /hack/minikube.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | export CILIUM_CLI_MODE=classic 5 | export SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 6 | 7 | 8 | bootcluster_linux() { 9 | name=$1 10 | clusterid=$2 11 | podcidr=$3 12 | servicecidr=$4 13 | 14 | helm repo add cilium https://helm.cilium.io/ || true 15 | helm repo update cilium 16 | helm pull cilium/cilium --untar 17 | 18 | echo "--- 19 | kind: ConfigMap 20 | apiVersion: v1 21 | metadata: 22 | name: blank-cni 23 | namespace: kube-system 24 | data: 25 | description: This CM intentioanlly left blank to fake out minikube/kubeadm 26 | " > blank.yaml 27 | minikube start --memory 4g --container-runtime=cri-o \ 28 | --kubernetes-version=v${K8S_VERSION} \ 29 | --extra-config kubeadm.pod-network-cidr=$podcidr \ 30 | --service-cluster-ip-range $servicecidr \ 31 | --extra-config kubeadm.skip-phases=addon/kube-proxy \ 32 | --network north-south \ 33 | --subnet 10.59.0.0/16 \ 34 | --host-only-cidr 10.59.0.0/16 \ 35 | --dns-domain cluster.$name --cni=blank.yaml --profile=$name 36 | 37 | echo "--- 38 | apiVersion: v1 39 | kind: ConfigMap 40 | metadata: 41 | name: bgp-config 42 | namespace: kube-system 43 | data: 44 | config.yaml: | 45 | peers: [] 46 | #- peer-address: 10.0.0.1 47 | # peer-asn: 64512 48 | # my-asn: 64512 49 | address-pools: [] 50 | #- name: default 51 | # protocol: bgp 52 | # addresses: 53 | # - 192.168.39.0/24 54 | --- 55 | " > .cni-$name.yaml 56 | helm template cilium cilium --validate --version ${CILIUM_VERSION} --namespace kube-system \ 57 | --set cluster.name=$name \ 58 | --set cluster.id=$clusterid \ 59 | --set externalIPs.enabled=true \ 60 | --set ipam.operator.clusterPoolIPv4PodCIDRList=$podcidr \ 61 | --set ipv4NativeRoutingCIDR=10.0.0.0/8 \ 62 | --set routingMode="native" \ 63 | --set autoDirectNodeRoutes=true \ 64 | --set operator.replicas=1 \ 65 | --set socketLB.enabled=true \ 66 | --set kubeProxyReplacement=true \ 67 | --set k8sServiceHost=$(minikube ip -p $name) \ 68 | --set hubble.relay.enabled=true \ 69 | --set hubble.peerService.clusterDomain=cluster.$name \ 70 | --set hubble.ui.enabled=true \ 71 | --set hubble.metrics.enableOpenMetrics=true \ 72 | --set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \ 73 | --set ingressController.enabled=true \ 74 | --set ingressController.loadbalancerMode=dedicated \ 75 | --set ingressController.secretsNamespace.name=kube-system \ 76 | --set ingressController.secretsNamespace.create=false \ 77 | --set localRedirectPolicy=true \ 78 | --set l2announcements.enabled=true \ 79 | --set l2podAnnouncements.enabled=true \ 80 | --set loadBalancer.l7.backend=envoy \ 81 | --set-string extraConfig.enable-envoy-config=true \ 82 | --set socketLB.hostNamespaceOnly=true \ 83 | --set hostPort.enabled=true \ 84 | --set externalIPs.enabled=true \ 85 | --set bgpControlPlane.enabled=true \ 86 | --set endpointRoutes.enabled=true \ 87 | --set nodePort.enabled=true \ 88 | --set k8sServicePort=8443 >> .cni-$name.yaml 89 | #--set authentication.enabled=true \ 90 | #--set authentication.mutual.spire.enabled=true \ 91 | #--set authentication.mutual.spire.install.enabled=true \ 92 | #--set authentication.mutual.spire.serverAddress=spire-server.cilium-spire.svc.cluster.$name:8081 \ 93 | #--set tunnel=disabled \ 94 | #--set tunnelProtocol="" \ 95 | #--set endpointStatus.enabled=true \ 96 | #--set endpointStatus.status=policy \ 97 | #--set clustermesh.useAPIServer=true \ 98 | #--set clustermesh.config.enabled=true \ 99 | #--set encryption.enabled=true \ 100 | #--set encryption.type=wireguard \ 101 | #--set encryption.nodeEncryption=true \ 102 | #--set encryption.wireguard.userspaceFallback=true \ 103 | #--set clustermesh.config.domain=$name.mesh.cilium.io \ 104 | #--set clustermesh.apiserver.kvstoremesh.enabled=true \ 105 | #--set clustermesh.apiserver.replicas=3 \ 106 | minikube kubectl -p $name -- apply -f .cni-$name.yaml 107 | minikube node add -p $name 108 | minikube node add -p $name 109 | # sleep 15 #@TODO build a watch loop 110 | } 111 | 112 | bootcluster_macos() { 113 | name=$1 114 | clusterid=$2 115 | podcidr=$3 116 | servicecidr=$4 117 | 118 | minikube start --memory 4g --container-runtime=cri-o \ 119 | --kubernetes-version=v${K8S_VERSION} \ 120 | --extra-config kubeadm.pod-network-cidr=$podcidr \ 121 | --service-cluster-ip-range $servicecidr \ 122 | --network north-south \ 123 | --subnet 10.59.0.0/16 \ 124 | --host-only-cidr 10.59.0.0/16 \ 125 | --dns-domain cluster.$name --cni=cilium --profile=$name 126 | } 127 | 128 | addons() { 129 | kubectl delete pod -l k8s-app=kube-dns -n kube-system 130 | minikube addons enable registry -p north --images='Registry=docker.io/registry:2.8.3,KubeRegistryProxy=gcr.io/k8s-minikube/kube-registry-proxy:0.0.9' 131 | # use the addon, but through a tunnel 132 | #minikube addons enable ingress -p north 133 | #kubectl get svc -n ingress-nginx ingress-nginx-controller -o yaml > .ingress.yaml 134 | #sed -i'' -e 's/NodePort/LoadBalancer/' -e '/allocateNode/d' .ingress.yaml 135 | #kubectl apply -f .ingress.yaml 136 | #kubectl apply -f scripts/ingress.yaml 137 | #kubectl delete po -n ingress-nginx -l app.kubernetes.io/component=controller 138 | kubectl delete pod -l k8s-app=kube-dns -n kube-system 139 | kubectl get deployment -n kube-system coredns -o yaml > .coredns.yaml 140 | sed -i'' -e 's/\(replicas:\).*/\1\ 2/' .coredns.yaml 141 | kubectl apply -f .coredns.yaml 142 | #kubectl apply -f $SCRIPT_DIR/postgres-operator.yaml 143 | #kubectl apply -f $SCRIPT_DIR/minikube-pvc-hack.yaml 144 | if [ ! -z ${TLS} ]; then 145 | LATEST=$(curl -s curl https://api.github.com/repos/cert-manager/cert-manager/releases/latest | jq -cr .tag_name) 146 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/${LATEST}/cert-manager.yaml 147 | rc=1 148 | tries=0 149 | while [ $rc -ne 0 ] && [ $tries -ne 25 ]; do 150 | sleep 1 151 | kubectl apply -f $SCRIPT_DIR/issuer.yaml 152 | rc=$? 153 | tries=$((tries+1)) 154 | done 155 | if [ $rc -ne 0 ]; then 156 | echo "Failed to create cert-manager issuer" 157 | exit 1 158 | fi 159 | fi 160 | if [ ! -z ${PROMETHEUS} ]; then 161 | LATEST=$(curl -s https://api.github.com/repos/prometheus-operator/prometheus-operator/releases/latest | jq -cr .tag_name) 162 | curl -sL https://github.com/prometheus-operator/prometheus-operator/releases/download/${LATEST}/bundle.yaml | kubectl create -f - 163 | kubectl apply -f $SCRIPT_DIR/prometheus.yaml 164 | fi 165 | } 166 | 167 | OS=$(uname) 168 | 169 | if [ "$OS" = "Darwin" ]; then 170 | bootcluster_macos north 1 10.60.0.0/16 10.96.0.0/16 171 | elif [ "$OS" = "Linux" ]; then 172 | bootcluster_linux north 1 10.60.0.0/16 10.96.0.0/16 173 | else 174 | echo "Unsupported OS" 175 | exit 1 176 | fi 177 | 178 | addons 179 | -------------------------------------------------------------------------------- /api/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2024. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *ExternalAccess) DeepCopyInto(out *ExternalAccess) { 31 | *out = *in 32 | if in.Proxy != nil { 33 | in, out := &in.Proxy, &out.Proxy 34 | *out = new(ProxySettings) 35 | (*in).DeepCopyInto(*out) 36 | } 37 | if in.LoadBalancer != nil { 38 | in, out := &in.LoadBalancer, &out.LoadBalancer 39 | *out = new(LoadBalancerSettings) 40 | (*in).DeepCopyInto(*out) 41 | } 42 | } 43 | 44 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAccess. 45 | func (in *ExternalAccess) DeepCopy() *ExternalAccess { 46 | if in == nil { 47 | return nil 48 | } 49 | out := new(ExternalAccess) 50 | in.DeepCopyInto(out) 51 | return out 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *LoadBalancerSettings) DeepCopyInto(out *LoadBalancerSettings) { 56 | *out = *in 57 | if in.Annotations != nil { 58 | in, out := &in.Annotations, &out.Annotations 59 | *out = make(map[string]string, len(*in)) 60 | for key, val := range *in { 61 | (*out)[key] = val 62 | } 63 | } 64 | } 65 | 66 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerSettings. 67 | func (in *LoadBalancerSettings) DeepCopy() *LoadBalancerSettings { 68 | if in == nil { 69 | return nil 70 | } 71 | out := new(LoadBalancerSettings) 72 | in.DeepCopyInto(out) 73 | return out 74 | } 75 | 76 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 77 | func (in *ProxySettings) DeepCopyInto(out *ProxySettings) { 78 | *out = *in 79 | if in.Resources != nil { 80 | in, out := &in.Resources, &out.Resources 81 | *out = new(corev1.ResourceRequirements) 82 | (*in).DeepCopyInto(*out) 83 | } 84 | if in.Annotations != nil { 85 | in, out := &in.Annotations, &out.Annotations 86 | *out = make(map[string]string, len(*in)) 87 | for key, val := range *in { 88 | (*out)[key] = val 89 | } 90 | } 91 | if in.Replicas != nil { 92 | in, out := &in.Replicas, &out.Replicas 93 | *out = new(int32) 94 | **out = **in 95 | } 96 | } 97 | 98 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxySettings. 99 | func (in *ProxySettings) DeepCopy() *ProxySettings { 100 | if in == nil { 101 | return nil 102 | } 103 | out := new(ProxySettings) 104 | in.DeepCopyInto(out) 105 | return out 106 | } 107 | 108 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 109 | func (in *Valkey) DeepCopyInto(out *Valkey) { 110 | *out = *in 111 | out.TypeMeta = in.TypeMeta 112 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 113 | in.Spec.DeepCopyInto(&out.Spec) 114 | in.Status.DeepCopyInto(&out.Status) 115 | } 116 | 117 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Valkey. 118 | func (in *Valkey) DeepCopy() *Valkey { 119 | if in == nil { 120 | return nil 121 | } 122 | out := new(Valkey) 123 | in.DeepCopyInto(out) 124 | return out 125 | } 126 | 127 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 128 | func (in *Valkey) DeepCopyObject() runtime.Object { 129 | if c := in.DeepCopy(); c != nil { 130 | return c 131 | } 132 | return nil 133 | } 134 | 135 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 136 | func (in *ValkeyList) DeepCopyInto(out *ValkeyList) { 137 | *out = *in 138 | out.TypeMeta = in.TypeMeta 139 | in.ListMeta.DeepCopyInto(&out.ListMeta) 140 | if in.Items != nil { 141 | in, out := &in.Items, &out.Items 142 | *out = make([]Valkey, len(*in)) 143 | for i := range *in { 144 | (*in)[i].DeepCopyInto(&(*out)[i]) 145 | } 146 | } 147 | } 148 | 149 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeyList. 150 | func (in *ValkeyList) DeepCopy() *ValkeyList { 151 | if in == nil { 152 | return nil 153 | } 154 | out := new(ValkeyList) 155 | in.DeepCopyInto(out) 156 | return out 157 | } 158 | 159 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 160 | func (in *ValkeyList) DeepCopyObject() runtime.Object { 161 | if c := in.DeepCopy(); c != nil { 162 | return c 163 | } 164 | return nil 165 | } 166 | 167 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 168 | func (in *ValkeySpec) DeepCopyInto(out *ValkeySpec) { 169 | *out = *in 170 | if in.PrometheusLabels != nil { 171 | in, out := &in.PrometheusLabels, &out.PrometheusLabels 172 | *out = make(map[string]string, len(*in)) 173 | for key, val := range *in { 174 | (*out)[key] = val 175 | } 176 | } 177 | if in.Storage != nil { 178 | in, out := &in.Storage, &out.Storage 179 | *out = new(corev1.PersistentVolumeClaim) 180 | (*in).DeepCopyInto(*out) 181 | } 182 | if in.Resources != nil { 183 | in, out := &in.Resources, &out.Resources 184 | *out = new(corev1.ResourceRequirements) 185 | (*in).DeepCopyInto(*out) 186 | } 187 | if in.ExternalAccess != nil { 188 | in, out := &in.ExternalAccess, &out.ExternalAccess 189 | *out = new(ExternalAccess) 190 | (*in).DeepCopyInto(*out) 191 | } 192 | if in.ServicePassword != nil { 193 | in, out := &in.ServicePassword, &out.ServicePassword 194 | *out = new(corev1.SecretKeySelector) 195 | (*in).DeepCopyInto(*out) 196 | } 197 | if in.Tolerations != nil { 198 | in, out := &in.Tolerations, &out.Tolerations 199 | *out = make([]corev1.Toleration, len(*in)) 200 | for i := range *in { 201 | (*in)[i].DeepCopyInto(&(*out)[i]) 202 | } 203 | } 204 | if in.NodeSelector != nil { 205 | in, out := &in.NodeSelector, &out.NodeSelector 206 | *out = make(map[string]string, len(*in)) 207 | for key, val := range *in { 208 | (*out)[key] = val 209 | } 210 | } 211 | } 212 | 213 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeySpec. 214 | func (in *ValkeySpec) DeepCopy() *ValkeySpec { 215 | if in == nil { 216 | return nil 217 | } 218 | out := new(ValkeySpec) 219 | in.DeepCopyInto(out) 220 | return out 221 | } 222 | 223 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 224 | func (in *ValkeyStatus) DeepCopyInto(out *ValkeyStatus) { 225 | *out = *in 226 | if in.Conditions != nil { 227 | in, out := &in.Conditions, &out.Conditions 228 | *out = make([]metav1.Condition, len(*in)) 229 | for i := range *in { 230 | (*in)[i].DeepCopyInto(&(*out)[i]) 231 | } 232 | } 233 | } 234 | 235 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValkeyStatus. 236 | func (in *ValkeyStatus) DeepCopy() *ValkeyStatus { 237 | if in == nil { 238 | return nil 239 | } 240 | out := new(ValkeyStatus) 241 | in.DeepCopyInto(out) 242 | return out 243 | } 244 | -------------------------------------------------------------------------------- /cmd/manager/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "crypto/tls" 21 | "flag" 22 | "os" 23 | "strconv" 24 | "strings" 25 | 26 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 27 | // to ensure that exec-entrypoint and run can make use of them. 28 | "k8s.io/client-go/kubernetes" 29 | _ "k8s.io/client-go/plugin/pkg/client/auth" 30 | "k8s.io/client-go/rest" 31 | 32 | "k8s.io/apimachinery/pkg/runtime" 33 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 34 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 35 | ctrl "sigs.k8s.io/controller-runtime" 36 | "sigs.k8s.io/controller-runtime/pkg/healthz" 37 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 38 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 39 | "sigs.k8s.io/controller-runtime/pkg/webhook" 40 | 41 | certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" 42 | monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 43 | hyperspikeiov1 "hyperspike.io/valkey-operator/api/v1" 44 | "hyperspike.io/valkey-operator/cfg" 45 | "hyperspike.io/valkey-operator/internal/controller" 46 | corev1 "k8s.io/api/core/v1" 47 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 48 | // +kubebuilder:scaffold:imports 49 | ) 50 | 51 | var ( 52 | scheme = runtime.NewScheme() 53 | setupLog = ctrl.Log.WithName("setup") 54 | ) 55 | 56 | var ( 57 | Version string 58 | Commit string 59 | ) 60 | 61 | func init() { 62 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 63 | 64 | utilruntime.Must(hyperspikeiov1.AddToScheme(scheme)) 65 | utilruntime.Must(monitoringv1.AddToScheme(scheme)) 66 | utilruntime.Must(certv1.AddToScheme(scheme)) 67 | // +kubebuilder:scaffold:scheme 68 | } 69 | 70 | func getOperatorNamespace() (string, error) { 71 | nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 72 | if err != nil { 73 | return "", err 74 | } 75 | ns := strings.TrimSpace(string(nsBytes)) 76 | return ns, nil 77 | } 78 | 79 | func main() { 80 | var metricsAddr string 81 | var enableLeaderElection bool 82 | var probeAddr string 83 | var secureMetrics bool 84 | var enableHTTP2 bool 85 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metric endpoint binds to. "+ 86 | "Use the port :8080. If not set, it will be 0 in order to disable the metrics server") 87 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 88 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 89 | "Enable leader election for controller manager. "+ 90 | "Enabling this will ensure there is only one active controller manager.") 91 | flag.BoolVar(&secureMetrics, "metrics-secure", false, 92 | "If set the metrics endpoint is served securely") 93 | flag.BoolVar(&enableHTTP2, "enable-http2", false, 94 | "If set, HTTP/2 will be enabled for the metrics and webhook servers") 95 | opts := zap.Options{ 96 | Development: true, 97 | } 98 | opts.BindFlags(flag.CommandLine) 99 | flag.Parse() 100 | 101 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 102 | 103 | setupLog = setupLog.WithValues("version", Version, "commit", Commit) 104 | 105 | // if the enable-http2 flag is false (the default), http/2 should be disabled 106 | // due to its vulnerabilities. More specifically, disabling http/2 will 107 | // prevent from being vulnerable to the HTTP/2 Stream Cancellation and 108 | // Rapid Reset CVEs. For more information see: 109 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 110 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 111 | disableHTTP2 := func(c *tls.Config) { 112 | setupLog.Info("disabling http/2") 113 | c.NextProtos = []string{"http/1.1"} 114 | } 115 | 116 | tlsOpts := []func(*tls.Config){} 117 | if !enableHTTP2 { 118 | tlsOpts = append(tlsOpts, disableHTTP2) 119 | } 120 | 121 | webhookServer := webhook.NewServer(webhook.Options{ 122 | TLSOpts: tlsOpts, 123 | }) 124 | 125 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 126 | Scheme: scheme, 127 | Metrics: metricsserver.Options{ 128 | BindAddress: metricsAddr, 129 | SecureServing: secureMetrics, 130 | TLSOpts: tlsOpts, 131 | }, 132 | WebhookServer: webhookServer, 133 | HealthProbeBindAddress: probeAddr, 134 | LeaderElection: enableLeaderElection, 135 | LeaderElectionID: "4ca2c877.hyperspike.io", 136 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 137 | // when the Manager ends. This requires the binary to immediately end when the 138 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 139 | // speeds up voluntary leader transitions as the new leader don't have to wait 140 | // LeaseDuration time first. 141 | // 142 | // In the default scaffold provided, the program ends immediately after 143 | // the manager stops, so would be fine to enable this option. However, 144 | // if you are doing or is intended to do any operation such as perform cleanups 145 | // after the manager stops then its usage might be unsafe. 146 | // LeaderElectionReleaseOnCancel: true, 147 | }) 148 | if err != nil { 149 | setupLog.Error(err, "unable to start manager") 150 | os.Exit(1) 151 | } 152 | 153 | ctx := ctrl.SetupSignalHandler() 154 | // k8sClient := 155 | instanceName := os.Getenv("INSTANCE_NAME") 156 | if instanceName == "" { 157 | instanceName = os.Getenv("APP_NAME") 158 | if instanceName == "" { 159 | instanceName = "valkey-operator" 160 | } 161 | } 162 | cmName := instanceName + "-config" 163 | cfgMap := &corev1.ConfigMap{ 164 | ObjectMeta: metav1.ObjectMeta{ 165 | Name: cmName, 166 | }, 167 | } 168 | k8sCfg, err := rest.InClusterConfig() 169 | if err != nil { 170 | setupLog.Error(err, "failed to get in-cluster config") 171 | } 172 | // creates the clientset 173 | clientset, err := kubernetes.NewForConfig(k8sCfg) 174 | if err != nil { 175 | setupLog.Error(err, "failed to get in-cluster config") 176 | } 177 | ns, err := getOperatorNamespace() 178 | if err != nil { 179 | setupLog.Error(err, "failed to get namespace") 180 | } 181 | cfgMap, err = clientset.CoreV1().ConfigMaps(ns).Get(ctx, cmName, metav1.GetOptions{}) 182 | if err != nil { 183 | setupLog.Error(err, "failed to get global config") 184 | } 185 | config := cfg.Defaults() 186 | for k, v := range cfgMap.Data { 187 | if k == "exporterImage" && v != "" { 188 | config.SidecarImage = v 189 | } 190 | if k == "valkeyImage" && v != "" { 191 | config.ValkeyImage = v 192 | } 193 | if k == "nodes" && v != "" { 194 | n, _ := strconv.ParseInt(v, 10, 32) 195 | config.Nodes = int32(n) 196 | } 197 | } 198 | 199 | if err = (&controller.ValkeyReconciler{ 200 | Client: mgr.GetClient(), 201 | Scheme: mgr.GetScheme(), 202 | GlobalConfig: config, 203 | Recorder: mgr.GetEventRecorderFor("valkey-controller"), 204 | }).SetupWithManager(mgr); err != nil { 205 | setupLog.Error(err, "unable to create controller", "controller", "Valkey") 206 | os.Exit(1) 207 | } 208 | // +kubebuilder:scaffold:builder 209 | 210 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 211 | setupLog.Error(err, "unable to set up health check") 212 | os.Exit(1) 213 | } 214 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 215 | setupLog.Error(err, "unable to set up ready check") 216 | os.Exit(1) 217 | } 218 | 219 | setupLog.Info("starting manager") 220 | if err := mgr.Start(ctx); err != nil { 221 | setupLog.Error(err, "problem running manager") 222 | os.Exit(1) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /hack/postgres-operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: postgres-operator 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | name: postgres-operator 10 | namespace: postgres-operator 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRole 14 | metadata: 15 | name: postgres-operator 16 | rules: 17 | - apiGroups: 18 | - acid.zalan.do 19 | resources: 20 | - postgresqls 21 | - postgresqls/status 22 | - operatorconfigurations 23 | verbs: 24 | - create 25 | - delete 26 | - deletecollection 27 | - get 28 | - list 29 | - patch 30 | - update 31 | - watch 32 | - apiGroups: 33 | - acid.zalan.do 34 | resources: 35 | - postgresteams 36 | verbs: 37 | - get 38 | - list 39 | - watch 40 | - apiGroups: 41 | - apiextensions.k8s.io 42 | resources: 43 | - customresourcedefinitions 44 | verbs: 45 | - create 46 | - get 47 | - patch 48 | - update 49 | - apiGroups: 50 | - "" 51 | resources: 52 | - configmaps 53 | verbs: 54 | - get 55 | - apiGroups: 56 | - "" 57 | resources: 58 | - events 59 | verbs: 60 | - create 61 | - get 62 | - list 63 | - patch 64 | - update 65 | - watch 66 | - apiGroups: 67 | - "" 68 | resources: 69 | - endpoints 70 | verbs: 71 | - create 72 | - delete 73 | - deletecollection 74 | - get 75 | - list 76 | - patch 77 | - update 78 | - watch 79 | - apiGroups: 80 | - "" 81 | resources: 82 | - secrets 83 | verbs: 84 | - create 85 | - delete 86 | - get 87 | - update 88 | - apiGroups: 89 | - "" 90 | resources: 91 | - nodes 92 | verbs: 93 | - get 94 | - list 95 | - watch 96 | - apiGroups: 97 | - "" 98 | resources: 99 | - persistentvolumeclaims 100 | verbs: 101 | - delete 102 | - get 103 | - list 104 | - patch 105 | - update 106 | - apiGroups: 107 | - "" 108 | resources: 109 | - persistentvolumes 110 | verbs: 111 | - get 112 | - list 113 | - update 114 | - apiGroups: 115 | - "" 116 | resources: 117 | - pods 118 | verbs: 119 | - delete 120 | - get 121 | - list 122 | - patch 123 | - update 124 | - watch 125 | - apiGroups: 126 | - "" 127 | resources: 128 | - pods/exec 129 | verbs: 130 | - create 131 | - apiGroups: 132 | - "" 133 | resources: 134 | - services 135 | verbs: 136 | - create 137 | - delete 138 | - get 139 | - patch 140 | - update 141 | - apiGroups: 142 | - apps 143 | resources: 144 | - statefulsets 145 | - deployments 146 | verbs: 147 | - create 148 | - delete 149 | - get 150 | - list 151 | - patch 152 | - apiGroups: 153 | - batch 154 | resources: 155 | - cronjobs 156 | verbs: 157 | - create 158 | - delete 159 | - get 160 | - list 161 | - patch 162 | - update 163 | - apiGroups: 164 | - "" 165 | resources: 166 | - namespaces 167 | verbs: 168 | - get 169 | - apiGroups: 170 | - policy 171 | resources: 172 | - poddisruptionbudgets 173 | verbs: 174 | - create 175 | - delete 176 | - get 177 | - apiGroups: 178 | - "" 179 | resources: 180 | - serviceaccounts 181 | verbs: 182 | - get 183 | - create 184 | - apiGroups: 185 | - rbac.authorization.k8s.io 186 | resources: 187 | - rolebindings 188 | verbs: 189 | - get 190 | - create 191 | --- 192 | apiVersion: rbac.authorization.k8s.io/v1 193 | kind: ClusterRole 194 | metadata: 195 | name: postgres-pod 196 | rules: 197 | - apiGroups: 198 | - "" 199 | resources: 200 | - endpoints 201 | verbs: 202 | - create 203 | - delete 204 | - deletecollection 205 | - get 206 | - list 207 | - patch 208 | - update 209 | - watch 210 | - apiGroups: 211 | - "" 212 | resources: 213 | - pods 214 | verbs: 215 | - get 216 | - list 217 | - patch 218 | - update 219 | - watch 220 | - apiGroups: 221 | - "" 222 | resources: 223 | - services 224 | verbs: 225 | - create 226 | --- 227 | apiVersion: rbac.authorization.k8s.io/v1 228 | kind: ClusterRoleBinding 229 | metadata: 230 | name: postgres-operator 231 | roleRef: 232 | apiGroup: rbac.authorization.k8s.io 233 | kind: ClusterRole 234 | name: postgres-operator 235 | subjects: 236 | - kind: ServiceAccount 237 | name: postgres-operator 238 | namespace: postgres-operator 239 | --- 240 | apiVersion: v1 241 | data: 242 | api_port: "8080" 243 | aws_region: eu-central-1 244 | cluster_domain: cluster.north 245 | cluster_history_entries: "1000" 246 | cluster_labels: application:spilo 247 | cluster_name_label: north 248 | connection_pooler_default_cpu_limit: "1" 249 | connection_pooler_default_cpu_request: 500m 250 | connection_pooler_default_memory_limit: 100Mi 251 | connection_pooler_default_memory_request: 100Mi 252 | connection_pooler_image: registry.opensource.zalan.do/acid/pgbouncer:master-32 253 | crd_categories: all 254 | db_hosted_zone: db.example.com 255 | debug_logging: "true" 256 | default_cpu_limit: "1" 257 | default_cpu_request: 100m 258 | default_memory_limit: 500Mi 259 | default_memory_request: 100Mi 260 | docker_image: ghcr.io/zalando/spilo-16:3.2-p2 261 | enable_ebs_gp3_migration: "false" 262 | enable_finalizers: "false" 263 | enable_master_load_balancer: "false" 264 | enable_master_pooler_load_balancer: "false" 265 | enable_password_rotation: "false" 266 | enable_patroni_failsafe_mode: "false" 267 | enable_persistent_volume_claim_deletion: "true" 268 | enable_pgversion_env_var: "true" 269 | enable_readiness_probe: "false" 270 | enable_replica_load_balancer: "false" 271 | enable_replica_pooler_load_balancer: "false" 272 | enable_spilo_wal_path_compat: "true" 273 | enable_team_id_clustername_prefix: "false" 274 | enable_team_member_deprecation: "false" 275 | enable_teams_api: "false" 276 | external_traffic_policy: Cluster 277 | logical_backup_docker_image: registry.opensource.zalan.do/acid/logical-backup:v1.11.0 278 | logical_backup_job_prefix: logical-backup- 279 | logical_backup_provider: s3 280 | logical_backup_s3_bucket: my-bucket-url 281 | logical_backup_s3_sse: AES256 282 | logical_backup_schedule: 30 00 * * * 283 | major_version_upgrade_mode: manual 284 | master_dns_name_format: '{cluster}.{namespace}.{hostedzone}' 285 | patroni_api_check_interval: 1s 286 | patroni_api_check_timeout: 5s 287 | pdb_name_format: postgres-{cluster}-pdb 288 | persistent_volume_claim_retention_policy: when_deleted:retain,when_scaled:retain 289 | pod_deletion_wait_timeout: 10m 290 | pod_label_wait_timeout: 10m 291 | pod_management_policy: ordered_ready 292 | pod_role_label: spilo-role 293 | pod_service_account_name: postgres-pod 294 | pod_terminate_grace_period: 5m 295 | ready_wait_interval: 3s 296 | ready_wait_timeout: 30s 297 | repair_period: 5m 298 | replica_dns_name_format: '{cluster}-repl.{namespace}.{hostedzone}' 299 | replication_username: standby 300 | resource_check_interval: 3s 301 | resource_check_timeout: 10m 302 | resync_period: 30m 303 | ring_log_lines: "100" 304 | role_deletion_suffix: _deleted 305 | secret_name_template: '{username}.{cluster}.credentials.{tprkind}.{tprgroup}' 306 | share_pgsocket_with_sidecars: "false" 307 | spilo_allow_privilege_escalation: "true" 308 | spilo_privileged: "false" 309 | storage_resize_mode: pvc 310 | super_username: postgres 311 | watched_namespace: '*' 312 | workers: "8" 313 | kind: ConfigMap 314 | metadata: 315 | name: postgres-operator 316 | namespace: postgres-operator 317 | --- 318 | apiVersion: v1 319 | kind: Service 320 | metadata: 321 | name: postgres-operator 322 | namespace: postgres-operator 323 | spec: 324 | ports: 325 | - port: 8080 326 | protocol: TCP 327 | targetPort: 8080 328 | selector: 329 | name: postgres-operator 330 | type: ClusterIP 331 | --- 332 | apiVersion: apps/v1 333 | kind: Deployment 334 | metadata: 335 | labels: 336 | application: postgres-operator 337 | name: postgres-operator 338 | namespace: postgres-operator 339 | spec: 340 | replicas: 1 341 | selector: 342 | matchLabels: 343 | name: postgres-operator 344 | strategy: 345 | type: Recreate 346 | template: 347 | metadata: 348 | labels: 349 | name: postgres-operator 350 | spec: 351 | containers: 352 | - env: 353 | - name: CONFIG_MAP_NAME 354 | value: postgres-operator 355 | image: ghcr.io/zalando/postgres-operator:v1.12.1 356 | imagePullPolicy: IfNotPresent 357 | name: postgres-operator 358 | resources: 359 | limits: 360 | cpu: 500m 361 | memory: 500Mi 362 | requests: 363 | cpu: 100m 364 | memory: 250Mi 365 | securityContext: 366 | allowPrivilegeEscalation: false 367 | readOnlyRootFilesystem: true 368 | runAsNonRoot: true 369 | runAsUser: 1000 370 | serviceAccountName: postgres-operator 371 | -------------------------------------------------------------------------------- /api/v1/valkey_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // ValkeySpec defines the desired state of Valkey 28 | type ValkeySpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // Image to use 33 | Image string `json:"image,omitempty"` 34 | 35 | // Exporter Image to use 36 | ExporterImage string `json:"exporterImage,omitempty"` 37 | 38 | // Number of shards. Each node is a primary 39 | // +kubebuilder:default:=3 40 | Shards int32 `json:"nodes,omitempty"` 41 | 42 | // Number of replicas for each node. 43 | // 44 | // Note: This field currently creates extra primary nodes. 45 | // Follow https://github.com/hyperspike/valkey-operator/issues/186 for details 46 | // 47 | // +kubebuilder:default:=0 48 | Replicas int32 `json:"replicas,omitempty"` 49 | 50 | // Turn on an init container to set permissions on the persistent volume 51 | // +kubebuilder:default:=false 52 | VolumePermissions bool `json:"volumePermissions"` 53 | 54 | // TLS Support 55 | // +kubebuilder:default:=false 56 | // +optional 57 | TLS bool `json:"tls,omitempty"` 58 | // Certificate Issuer 59 | // +optional 60 | CertIssuer string `json:"certIssuer,omitempty"` 61 | // Certificate Issuer Type 62 | // +kubebuilder:default:="ClusterIssuer" 63 | // +kubebuilder:validation:Enum=ClusterIssuer;Issuer 64 | // +optional 65 | CertIssuerType string `json:"certIssuerType,omitempty"` 66 | 67 | // Enable prometheus 68 | // +kubebuilder:default:=false 69 | Prometheus bool `json:"prometheus"` 70 | // Extra prometheus labels for operator targeting 71 | // +optional 72 | PrometheusLabels map[string]string `json:"prometheusLabels,omitempty"` 73 | 74 | // ServiceMonitor Enabled. The service monitor is a custom resource which tells 75 | // other Prometheus components how to scrape metrics from the valkey cluster 76 | // +kubebuilder:default:=false 77 | ServiceMonitor bool `json:"serviceMonitor"` 78 | 79 | // Cluster Domain - used for DNS 80 | // +kubebuilder:default:=cluster.local 81 | ClusterDomain string `json:"clusterDomain"` 82 | 83 | // Persistent volume claim. The kind and metadata can be omitted, but the spec 84 | // is necessary. 85 | // +optional 86 | Storage *corev1.PersistentVolumeClaim `json:"storage,omitempty"` 87 | 88 | // Resources requirements and limits for the Valkey Server container 89 | // +optional 90 | Resources *corev1.ResourceRequirements `json:"resources,omitempty"` 91 | 92 | // External access configuration 93 | // +optional 94 | ExternalAccess *ExternalAccess `json:"externalAccess,omitempty"` 95 | 96 | // Anonymous Auth. 97 | // 98 | // If true, clients can login without providing a password. If 99 | // false, the the operator will configure the valkey server to use a password. It 100 | // will either create a Secret holding the password or, if ServicePassword is set, 101 | // use an existing secret. 102 | // 103 | // +kubebuilder:default:=false 104 | AnonymousAuth bool `json:"anonymousAuth"` 105 | 106 | // Service Password is a SecretKeySelector that points to a data key in a Secret. Look for 107 | // SecretKeySelector in [Kubernetes Pod Documentation] for details 108 | // 109 | // This field is optional. If ServicePassword is not set and 110 | // [ValkeySpec.AnonymousAuth] is false, then the operator will create a secret 111 | // in with the same name and namespace as the custom resource, with a "password" data key 112 | // and a random 16-character password value. 113 | // 114 | // https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables 115 | // +optional 116 | ServicePassword *corev1.SecretKeySelector `json:"servicePassword,omitempty"` 117 | 118 | // Tolerations 119 | // +optional 120 | Tolerations []corev1.Toleration `json:"tolerations,omitempty"` 121 | 122 | // Node Selector 123 | // +optional 124 | NodeSelector map[string]string `json:"nodeSelector,omitempty"` 125 | 126 | // Which endpoint is shown as the preferred endpoint valid values are 'ip', 'hostname', or 'unknown-endpoint'. 127 | // +kubebuilder:default:="ip" 128 | // +kubebuilder:validation:Enum=ip;hostname;unknown-endpoint 129 | // +optional 130 | ClusterPreferredEndpointType string `json:"clusterPreferredEndpointType,omitempty"` 131 | 132 | // PlatformManagedSecurityContext delegates security context management to the platform. 133 | // When true, the operator omits the following fields from pod and container security contexts, 134 | // allowing the platform (e.g., OpenShift) to manage them via SCCs or Pod Security Standards: 135 | // - RunAsUser, RunAsGroup, FSGroup (user/group IDs) 136 | // - FSGroupChangePolicy, SupplementalGroups 137 | // - SELinuxOptions 138 | // When false (default), these fields are set to explicit values (e.g., 1001 for user/group IDs). 139 | // +kubebuilder:default:=false 140 | // +optional 141 | PlatformManagedSecurityContext bool `json:"platformManagedSecurityContext,omitempty"` 142 | } 143 | 144 | // ExternalAccess defines the external access configuration 145 | type ExternalAccess struct { 146 | // Enable external access 147 | // +kubebuilder:default:=false 148 | Enabled bool `json:"enabled"` 149 | 150 | // External access type 151 | // LoadBalancer or Proxy, the LoadBalancer type will create a LoadBalancer service for each Valkey Shard (master node) 152 | // The Proxy type will create a single LoadBalancer service and use an envoy proxy to route traffic to the Valkey Shards 153 | // +kubebuilder:default:=Proxy 154 | // +kubebuilder:validation:Enum=LoadBalancer;Proxy 155 | Type string `json:"type"` 156 | 157 | // Proxy Settings 158 | // +optional 159 | Proxy *ProxySettings `json:"proxy,omitempty"` 160 | 161 | // LoadBalancer Settings 162 | LoadBalancer *LoadBalancerSettings `json:"loadBalancer,omitempty"` 163 | 164 | // Cert Issuer for external access TLS certificate 165 | // +optional 166 | CertIssuer string `json:"certIssuer,omitempty"` 167 | 168 | // Cert Issuer Type for external access TLS certificate 169 | // +kubebuilder:default:="ClusterIssuer" 170 | // +kubebuilder:validation:Enum=ClusterIssuer;Issuer 171 | // +optional 172 | CertIssuerType string `json:"certIssuerType,omitempty"` 173 | 174 | // Support External DNS 175 | // +kubebuilder:default:=false 176 | // +optional 177 | ExternalDNS bool `json:"externalDNS,omitempty"` 178 | } 179 | 180 | // ProxySettings defines the proxy settings 181 | type ProxySettings struct { 182 | // Image to use for the proxy 183 | // +kubebuilder:default:="envoyproxy/envoy:v1.32.1" 184 | // +optional 185 | Image string `json:"image,omitempty"` 186 | // Resources requirements and limits for the proxy container 187 | // +optional 188 | Resources *corev1.ResourceRequirements `json:"resources,omitempty"` 189 | 190 | // Extra Envoy configuration 191 | // +optional 192 | ExtraConfig string `json:"extraConfig,omitempty"` 193 | 194 | // Annotations for the proxy service 195 | // +optional 196 | Annotations map[string]string `json:"annotations,omitempty"` 197 | 198 | // Replicas for the proxy 199 | // +kubebuilder:default:=1 200 | Replicas *int32 `json:"replicas"` 201 | 202 | // External Hostname for the proxy 203 | // +optional 204 | Hostname string `json:"hostname,omitempty"` 205 | } 206 | 207 | // LoadBalancerSettings defines the load balancer settings 208 | type LoadBalancerSettings struct { 209 | // Annotations for the load balancer service 210 | // +optional 211 | Annotations map[string]string `json:"annotations,omitempty"` 212 | } 213 | 214 | // ValkeyStatus defines the observed state of Valkey 215 | type ValkeyStatus struct { 216 | // Important: Run "make" to regenerate code after modifying this file 217 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` 218 | Ready bool `json:"ready"` 219 | } 220 | 221 | // +kubebuilder:object:root=true 222 | // +kubebuilder:subresource:status 223 | // +kubebuilder:resource:shortName=vk 224 | 225 | // Valkey is the Schema for the valkeys API 226 | // +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=`.status.ready` 227 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 228 | // +kubebuilder:printcolumn:name="Nodes",type="integer",JSONPath=".spec.nodes" 229 | // +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas" 230 | // +kubebuilder:printcolumn:name="Volumme Permissions",type="boolean",priority=1,JSONPath=".spec.volumePermissions" 231 | // +kubebuilder:printcolumn:name="Image",type="string",priority=1,JSONPath=".spec.image" 232 | type Valkey struct { 233 | metav1.TypeMeta `json:",inline"` 234 | metav1.ObjectMeta `json:"metadata,omitempty"` 235 | 236 | Spec ValkeySpec `json:"spec,omitempty"` 237 | Status ValkeyStatus `json:"status,omitempty"` 238 | } 239 | 240 | // +kubebuilder:object:root=true 241 | 242 | // ValkeyList contains a list of Valkey 243 | type ValkeyList struct { 244 | metav1.TypeMeta `json:",inline"` 245 | metav1.ListMeta `json:"metadata,omitempty"` 246 | Items []Valkey `json:"items"` 247 | } 248 | 249 | func init() { 250 | SchemeBuilder.Register(&Valkey{}, &ValkeyList{}) 251 | } 252 | -------------------------------------------------------------------------------- /.github/workflows/image.yaml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Create and publish a Docker image 7 | 8 | on: 9 | push: 10 | tags: 11 | - '*' 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | VALKEY_VERSION: 8.1.4 17 | RELEASE_VERSION: ${{ github.ref_name }} 18 | 19 | jobs: 20 | build-and-push-image: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | platform: 26 | - linux/amd64 27 | - linux/arm64 28 | permissions: 29 | contents: read # for trivy scan upload 30 | packages: write 31 | id-token: write 32 | security-events: write 33 | attestations: write 34 | 35 | steps: 36 | - name: Prepare 37 | run: | 38 | platform=${{ matrix.platform }} 39 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 40 | - name: Checkout repository 41 | uses: actions/checkout@v6 42 | 43 | - name: Set up QEMU 44 | uses: docker/setup-qemu-action@v3 45 | - name: Set up Docker Buildx 46 | uses: docker/setup-buildx-action@v3 47 | 48 | - name: Log in to the Container registry 49 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 50 | with: 51 | registry: ${{ env.REGISTRY }} 52 | username: ${{ github.actor }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: Extract metadata (Controller tags, labels) for Docker 56 | id: meta_controller 57 | uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 58 | with: 59 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }} 60 | - name: Extract metadata (Sidecar tags, labels) for Docker 61 | id: meta_sidecar 62 | uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 63 | with: 64 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar:${{ env.RELEASE_VERSION }} 65 | - name: Extract metadata (Valkey tags, labels) for Docker 66 | id: meta_valkey 67 | uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 68 | with: 69 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey:${{ env.VALKEY_VERSION }} 70 | 71 | - name: Setup Go ${{ matrix.go-version }} 72 | uses: actions/setup-go@v6 73 | with: 74 | go-version: 1.25 75 | # You can test your matrix by printing the current Go version 76 | - name: Display Go version 77 | run: go version 78 | - name: Build it 79 | id: go_build 80 | run: | 81 | platform=${{ matrix.platform }} 82 | echo "Building for $GOOS/$GOARCH" 83 | make V=1 GOOS=${platform%/*} GOARCH=${platform#*/} 84 | - name: Build and push Docker image 85 | id: docker_build_controller 86 | uses: docker/build-push-action@v6 87 | with: 88 | file: Dockerfile.controller 89 | context: . 90 | visibility: public 91 | platforms: ${{ matrix.platform }} 92 | labels: ${{ steps.meta_manager.outputs.labels }} 93 | outputs: type=image,"name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}",push-by-digest=true,name-canonical=true,push=true 94 | - name: Build and push Sidecar image 95 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 96 | id: docker_build_sidecar 97 | with: 98 | file: Dockerfile.sidecar 99 | context: . 100 | visibility: public 101 | platforms: ${{ matrix.platform }} 102 | labels: ${{ steps.meta_sidecar.outputs.labels }} 103 | outputs: type=image,"name=${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar",push-by-digest=true,name-canonical=true,push=true 104 | - name: Build and push Valkey image 105 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 106 | id: docker_build_valkey 107 | with: 108 | file: Dockerfile.valkey 109 | context: . 110 | visibility: public 111 | platforms: ${{ matrix.platform }} 112 | labels: ${{ steps.meta_valkey.outputs.labels }} 113 | outputs: type=image,"name=${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey",push-by-digest=true,name-canonical=true,push=true 114 | 115 | - name: Set up Cosign 116 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 117 | 118 | - name: Sign Controller image with GitHub OIDC Token 119 | run: | 120 | cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}@${{ steps.docker_build_controller.outputs.digest }} 121 | - name: Sign Sidecar image with GitHub OIDC Token 122 | run: | 123 | cosign sign --yes ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar:${{ env.RELEASE_VERSION }}@${{ steps.docker_build_sidecar.outputs.digest }} 124 | - name: Sign Valkey image with GitHub OIDC Token 125 | run: | 126 | cosign sign --yes ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey:${{ env.VALKEY_VERSION }}@${{ steps.docker_build_valkey.outputs.digest }} 127 | 128 | - name: Attest the Controller image 129 | uses: actions/attest-build-provenance@v3 130 | id: attest_controller 131 | with: 132 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 133 | subject-digest: ${{ steps.docker_build_controller.outputs.digest }} 134 | push-to-registry: true 135 | - name: Attest the Sidecar image 136 | uses: actions/attest-build-provenance@v3 137 | id: attest_sidecar 138 | with: 139 | subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar 140 | subject-digest: ${{ steps.docker_build_sidecar.outputs.digest }} 141 | push-to-registry: true 142 | - name: Attest the Valkey image 143 | uses: actions/attest-build-provenance@v3 144 | id: attest_valkey 145 | with: 146 | subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey 147 | subject-digest: ${{ steps.docker_build_valkey.outputs.digest }} 148 | push-to-registry: true 149 | - name: Export digest 150 | run: | 151 | mkdir -p ${{ runner.temp }}/digests/controller 152 | mkdir -p ${{ runner.temp }}/digests/sidecar 153 | mkdir -p ${{ runner.temp }}/digests/valkey 154 | digest="${{ steps.docker_build_controller.outputs.digest }}" 155 | touch "${{ runner.temp }}/digests/controller/${digest#sha256:}" 156 | digest="${{ steps.docker_build_sidecar.outputs.digest }}" 157 | touch "${{ runner.temp }}/digests/sidecar/${digest#sha256:}" 158 | digest="${{ steps.docker_build_valkey.outputs.digest }}" 159 | touch "${{ runner.temp }}/digests/valkey/${digest#sha256:}" 160 | - name: Upload digest 161 | uses: actions/upload-artifact@v5 162 | with: 163 | name: digests-${{ env.PLATFORM_PAIR }} 164 | path: ${{ runner.temp }}/digests/* 165 | if-no-files-found: error 166 | retention-days: 1 167 | merge: 168 | runs-on: ubuntu-latest 169 | needs: 170 | - build-and-push-image 171 | permissions: 172 | contents: read # for trivy scan upload 173 | packages: write 174 | id-token: write 175 | security-events: write 176 | steps: 177 | - name: Download digests 178 | uses: actions/download-artifact@v6 179 | with: 180 | path: ${{ runner.temp }}/digests 181 | pattern: digests-* 182 | merge-multiple: true 183 | 184 | - name: Log in to the Container registry 185 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef 186 | with: 187 | registry: ${{ env.REGISTRY }} 188 | username: ${{ github.actor }} 189 | password: ${{ secrets.GITHUB_TOKEN }} 190 | 191 | - name: Set up Docker Buildx 192 | uses: docker/setup-buildx-action@v3 193 | 194 | - name: Docker meta 195 | id: meta_controller 196 | uses: docker/metadata-action@v5 197 | with: 198 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 199 | tags: ${{ env.RELEASE_VERSION }} 200 | - name: Create manifest list and push 201 | working-directory: ${{ runner.temp }}/digests 202 | run: | 203 | cd controller 204 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 205 | $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) 206 | - name: Docker meta 207 | id: meta_sidecar 208 | uses: docker/metadata-action@v5 209 | with: 210 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar 211 | tags: ${{ env.RELEASE_VERSION }} 212 | - name: Create manifest list and push 213 | working-directory: ${{ runner.temp }}/digests 214 | run: | 215 | cd sidecar 216 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 217 | $(printf '${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar@sha256:%s ' *) 218 | - name: Docker meta 219 | id: meta_valkey 220 | uses: docker/metadata-action@v5 221 | with: 222 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey 223 | tags: ${{ env.VALKEY_VERSION }} 224 | - name: Create manifest list and push 225 | working-directory: ${{ runner.temp }}/digests 226 | run: | 227 | cd valkey 228 | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ 229 | $(printf '${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey@sha256:%s ' *) 230 | 231 | - name: Inspect image 232 | run: | 233 | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta_controller.outputs.version }} 234 | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey-sidecar:${{ steps.meta_sidecar.outputs.version }} 235 | docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ github.repository_owner }}/valkey:${{ steps.meta_valkey.outputs.version }} 236 | 237 | - name: Run Trivy vulnerability scanner 238 | uses: aquasecurity/trivy-action@master 239 | with: 240 | image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.RELEASE_VERSION }}' 241 | format: 'sarif' 242 | output: 'trivy-results.sarif' 243 | 244 | - name: Upload Trivy scan results to GitHub Security tab 245 | uses: github/codeql-action/upload-sarif@v4 246 | if: always() 247 | with: 248 | sarif_file: 'trivy-results.sarif' 249 | #env: 250 | # GITHUB_TOKEN: ${{ secrets.TOKEN }} 251 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright 2024 Daniel Molik 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | 197 | -------------------------------------------------------------------------------- /cmd/sidecar/daemon.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "net/http" 24 | "os" 25 | "os/signal" 26 | "runtime" 27 | "strconv" 28 | "syscall" 29 | "time" 30 | 31 | "github.com/oliver006/redis_exporter/exporter" 32 | "github.com/prometheus/client_golang/prometheus" 33 | "github.com/spf13/cobra" 34 | "go.uber.org/zap" 35 | ) 36 | 37 | var daemonCmd = &cobra.Command{ 38 | Use: "daemon", 39 | Short: "Start the Redis metrics exporter", 40 | Long: `Start the Redis metrics exporter`, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | daemon() 43 | }, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(daemonCmd) 48 | } 49 | 50 | func getEnv(key string, defaultVal string) string { 51 | if envVal, ok := os.LookupEnv(key); ok { 52 | return envVal 53 | } 54 | return defaultVal 55 | } 56 | 57 | func getEnvBool(key string, defaultVal bool) bool { 58 | if envVal, ok := os.LookupEnv(key); ok { 59 | envBool, err := strconv.ParseBool(envVal) 60 | if err == nil { 61 | return envBool 62 | } 63 | } 64 | return defaultVal 65 | } 66 | 67 | func getEnvInt64(key string, defaultVal int64) int64 { 68 | if envVal, ok := os.LookupEnv(key); ok { 69 | envInt64, err := strconv.ParseInt(envVal, 10, 64) 70 | if err == nil { 71 | return envInt64 72 | } 73 | } 74 | return defaultVal 75 | } 76 | 77 | func daemon() { 78 | log := zap.NewExample().Sugar() 79 | defer func() { 80 | if err := log.Sync(); err != nil { 81 | fmt.Println("Error syncing log", err) 82 | } 83 | }() 84 | var ( 85 | redisAddr = flag.String("redis.addr", getEnv("REDIS_ADDR", "redis://localhost:6379"), "Address of the Redis instance to scrape") 86 | redisUser = flag.String("redis.user", getEnv("REDIS_USER", ""), "User name to use for authentication (Redis ACL for Redis 6.0 and newer)") 87 | redisPwd = flag.String("redis.password", getEnv("REDIS_PASSWORD", ""), "Password of the Redis instance to scrape") 88 | redisPwdFile = flag.String("redis.password-file", getEnv("REDIS_PASSWORD_FILE", ""), "Password file of the Redis instance to scrape") 89 | namespace = flag.String("namespace", getEnv("REDIS_EXPORTER_NAMESPACE", "redis"), "Namespace for metrics") 90 | checkKeys = flag.String("check-keys", getEnv("REDIS_EXPORTER_CHECK_KEYS", ""), "Comma separated list of key-patterns to export value and length/size, searched for with SCAN") 91 | checkSingleKeys = flag.String("check-single-keys", getEnv("REDIS_EXPORTER_CHECK_SINGLE_KEYS", ""), "Comma separated list of single keys to export value and length/size") 92 | checkKeyGroups = flag.String("check-key-groups", getEnv("REDIS_EXPORTER_CHECK_KEY_GROUPS", ""), "Comma separated list of lua regex for grouping keys") 93 | checkStreams = flag.String("check-streams", getEnv("REDIS_EXPORTER_CHECK_STREAMS", ""), "Comma separated list of stream-patterns to export info about streams, groups and consumers, searched for with SCAN") 94 | checkSingleStreams = flag.String("check-single-streams", getEnv("REDIS_EXPORTER_CHECK_SINGLE_STREAMS", ""), "Comma separated list of single streams to export info about streams, groups and consumers") 95 | streamsExcludeConsumerMetrics = flag.Bool("streams-exclude-consumer-metrics", getEnvBool("REDIS_EXPORTER_STREAMS_EXCLUDE_CONSUMER_METRICS", false), "Don't collect per consumer metrics for streams (decreases cardinality)") 96 | countKeys = flag.String("count-keys", getEnv("REDIS_EXPORTER_COUNT_KEYS", ""), "Comma separated list of patterns to count (eg: 'db0=production_*,db3=sessions:*'), searched for with SCAN") 97 | checkKeysBatchSize = flag.Int64("check-keys-batch-size", 98 | getEnvInt64("REDIS_EXPORTER_CHECK_KEYS_BATCH_SIZE", 1000), 99 | "Approximate number of keys to process in each execution, larger value speeds up scanning.\nWARNING: Still Redis is a single-threaded app, huge COUNT can affect production environment.") 100 | listenAddress = flag.String("web.listen-address", getEnv("REDIS_EXPORTER_WEB_LISTEN_ADDRESS", ":9121"), "Address to listen on for web interface and telemetry.") 101 | metricPath = flag.String("web.telemetry-path", getEnv("REDIS_EXPORTER_WEB_TELEMETRY_PATH", "/metrics"), "Path under which to expose metrics.") 102 | configCommand = flag.String("config-command", getEnv("REDIS_EXPORTER_CONFIG_COMMAND", "CONFIG"), "What to use for the CONFIG command, set to \"-\" to skip config metrics extraction") 103 | connectionTimeout = flag.String("connection-timeout", getEnv("REDIS_EXPORTER_CONNECTION_TIMEOUT", "15s"), "Timeout for connection to Redis instance") 104 | tlsClientKeyFile = flag.String("tls-client-key-file", getEnv("REDIS_EXPORTER_TLS_CLIENT_KEY_FILE", ""), "Name of the client key file (including full path) if the server requires TLS client authentication") 105 | tlsClientCertFile = flag.String("tls-client-cert-file", getEnv("REDIS_EXPORTER_TLS_CLIENT_CERT_FILE", ""), "Name of the client certificate file (including full path) if the server requires TLS client authentication") 106 | tlsCaCertFile = flag.String("tls-ca-cert-file", getEnv("REDIS_EXPORTER_TLS_CA_CERT_FILE", ""), "Name of the CA certificate file (including full path) if the server requires TLS client authentication") 107 | tlsServerKeyFile = flag.String("tls-server-key-file", getEnv("REDIS_EXPORTER_TLS_SERVER_KEY_FILE", ""), "Name of the server key file (including full path) if the web interface and telemetry should use TLS") 108 | tlsServerCertFile = flag.String("tls-server-cert-file", getEnv("REDIS_EXPORTER_TLS_SERVER_CERT_FILE", ""), "Name of the server certificate file (including full path) if the web interface and telemetry should use TLS") 109 | tlsServerCaCertFile = flag.String("tls-server-ca-cert-file", 110 | getEnv("REDIS_EXPORTER_TLS_SERVER_CA_CERT_FILE", ""), 111 | "Name of the CA certificate file (including full path) if the web interface and telemetry should require TLS client authentication") 112 | tlsServerMinVersion = flag.String("tls-server-min-version", getEnv("REDIS_EXPORTER_TLS_SERVER_MIN_VERSION", "TLS1.2"), "Minimum TLS version that is acceptable by the web interface and telemetry when using TLS") 113 | maxDistinctKeyGroups = flag.Int64("max-distinct-key-groups", 114 | getEnvInt64("REDIS_EXPORTER_MAX_DISTINCT_KEY_GROUPS", 100), 115 | "The maximum number of distinct key groups with the most memory utilization to present as distinct metrics per database, the leftover key groups will be aggregated in the 'overflow' bucket") 116 | setClientName = flag.Bool("set-client-name", getEnvBool("REDIS_EXPORTER_SET_CLIENT_NAME", true), "Whether to set client name to redis_exporter") 117 | isTile38 = flag.Bool("is-tile38", getEnvBool("REDIS_EXPORTER_IS_TILE38", false), "Whether to scrape Tile38 specific metrics") 118 | isCluster = flag.Bool("is-cluster", getEnvBool("REDIS_EXPORTER_IS_CLUSTER", false), "Whether this is a redis cluster (Enable this if you need to fetch key level data on a Redis Cluster).") 119 | exportClientList = flag.Bool("export-client-list", getEnvBool("REDIS_EXPORTER_EXPORT_CLIENT_LIST", false), "Whether to scrape Client List specific metrics") 120 | exportClientPort = flag.Bool("export-client-port", 121 | getEnvBool("REDIS_EXPORTER_EXPORT_CLIENT_PORT", false), 122 | "Whether to include the client's port when exporting the client list. Warning: including the port increases the number of metrics generated and will make your Prometheus server take up more memory") 123 | showVersion = flag.Bool("version", false, "Show version information and exit") 124 | redisMetricsOnly = flag.Bool("redis-only-metrics", getEnvBool("REDIS_EXPORTER_REDIS_ONLY_METRICS", false), "Whether to also export go runtime metrics") 125 | pingOnConnect = flag.Bool("ping-on-connect", getEnvBool("REDIS_EXPORTER_PING_ON_CONNECT", false), "Whether to ping the redis instance after connecting") 126 | inclConfigMetrics = flag.Bool("include-config-metrics", getEnvBool("REDIS_EXPORTER_INCL_CONFIG_METRICS", false), "Whether to include all config settings as metrics") 127 | inclModulesMetrics = flag.Bool("include-modules-metrics", getEnvBool("REDIS_EXPORTER_INCL_MODULES_METRICS", false), "Whether to collect Redis Modules metrics") 128 | disableExportingKeyValues = flag.Bool("disable-exporting-key-values", getEnvBool("REDIS_EXPORTER_DISABLE_EXPORTING_KEY_VALUES", false), "Whether to disable values of keys stored in redis as labels or not when using check-keys/check-single-key") 129 | excludeLatencyHistogramMetrics = flag.Bool("exclude-latency-histogram-metrics", getEnvBool("REDIS_EXPORTER_EXCLUDE_LATENCY_HISTOGRAM_METRICS", false), "Do not try to collect latency histogram metrics") 130 | redactConfigMetrics = flag.Bool("redact-config-metrics", getEnvBool("REDIS_EXPORTER_REDACT_CONFIG_METRICS", true), "Whether to redact config settings that include potentially sensitive information like passwords") 131 | inclSystemMetrics = flag.Bool("include-system-metrics", getEnvBool("REDIS_EXPORTER_INCL_SYSTEM_METRICS", false), "Whether to include system metrics like e.g. redis_total_system_memory_bytes") 132 | skipTLSVerification = flag.Bool("skip-tls-verification", getEnvBool("REDIS_EXPORTER_SKIP_TLS_VERIFICATION", false), "Whether to to skip TLS verification") 133 | ) 134 | flag.Parse() 135 | 136 | log.Infof("Redis Metrics Exporter %s build date: %s sha1: %s Go: %s GOOS: %s GOARCH: %s", 137 | Version, BuildDate, Commit, 138 | runtime.Version(), 139 | runtime.GOOS, 140 | runtime.GOARCH, 141 | ) 142 | if *showVersion { 143 | return 144 | } 145 | 146 | to, err := time.ParseDuration(*connectionTimeout) 147 | if err != nil { 148 | log.Fatalf("Couldn't parse connection timeout duration, err: %s", err) 149 | } 150 | 151 | passwordMap := make(map[string]string) 152 | if *redisPwd == "" && *redisPwdFile != "" { 153 | passwordMap, err = exporter.LoadPwdFile(*redisPwdFile) 154 | if err != nil { 155 | log.Fatalf("Error loading redis passwords from file %s, err: %s", *redisPwdFile, err) 156 | } 157 | } 158 | 159 | registry := prometheus.NewRegistry() 160 | if !*redisMetricsOnly { 161 | registry = prometheus.DefaultRegisterer.(*prometheus.Registry) 162 | } 163 | 164 | exp, err := exporter.NewRedisExporter( 165 | *redisAddr, 166 | exporter.Options{ 167 | User: *redisUser, 168 | Password: *redisPwd, 169 | PasswordMap: passwordMap, 170 | Namespace: *namespace, 171 | ConfigCommandName: *configCommand, 172 | CheckKeys: *checkKeys, 173 | CheckSingleKeys: *checkSingleKeys, 174 | CheckKeysBatchSize: *checkKeysBatchSize, 175 | CheckKeyGroups: *checkKeyGroups, 176 | MaxDistinctKeyGroups: *maxDistinctKeyGroups, 177 | CheckStreams: *checkStreams, 178 | CheckSingleStreams: *checkSingleStreams, 179 | StreamsExcludeConsumerMetrics: *streamsExcludeConsumerMetrics, 180 | CountKeys: *countKeys, 181 | InclSystemMetrics: *inclSystemMetrics, 182 | InclConfigMetrics: *inclConfigMetrics, 183 | DisableExportingKeyValues: *disableExportingKeyValues, 184 | ExcludeLatencyHistogramMetrics: *excludeLatencyHistogramMetrics, 185 | RedactConfigMetrics: *redactConfigMetrics, 186 | SetClientName: *setClientName, 187 | IsTile38: *isTile38, 188 | IsCluster: *isCluster, 189 | InclModulesMetrics: *inclModulesMetrics, 190 | ExportClientList: *exportClientList, 191 | ExportClientsInclPort: *exportClientPort, 192 | SkipTLSVerification: *skipTLSVerification, 193 | ClientCertFile: *tlsClientCertFile, 194 | ClientKeyFile: *tlsClientKeyFile, 195 | CaCertFile: *tlsCaCertFile, 196 | ConnectionTimeouts: to, 197 | MetricsPath: *metricPath, 198 | RedisMetricsOnly: *redisMetricsOnly, 199 | PingOnConnect: *pingOnConnect, 200 | RedisPwdFile: *redisPwdFile, 201 | Registry: registry, 202 | BuildInfo: exporter.BuildInfo{ 203 | Version: Version, 204 | CommitSha: Commit, 205 | Date: BuildDate, 206 | }, 207 | }, 208 | ) 209 | if err != nil { 210 | log.Fatal(err) 211 | } 212 | 213 | // Verify that initial client keypair and CA are accepted 214 | if (*tlsClientCertFile != "") != (*tlsClientKeyFile != "") { 215 | log.Fatal("TLS client key file and cert file should both be present") 216 | } 217 | _, err = exp.CreateClientTLSConfig() 218 | if err != nil { 219 | log.Fatal(err) 220 | } 221 | 222 | log.Infof("Providing metrics at %s%s", *listenAddress, *metricPath) 223 | log.Debugf("Configured redis addr: %#v", *redisAddr) 224 | server := &http.Server{ 225 | Addr: *listenAddress, 226 | Handler: exp, 227 | ReadTimeout: 5 * time.Second, 228 | WriteTimeout: 10 * time.Second, 229 | IdleTimeout: 120 * time.Second, 230 | } 231 | go func() { 232 | if *tlsServerCertFile != "" && *tlsServerKeyFile != "" { 233 | log.Debugf("Bind as TLS using cert %s and key %s", *tlsServerCertFile, *tlsServerKeyFile) 234 | 235 | tlsConfig, err := exp.CreateServerTLSConfig(*tlsServerCertFile, *tlsServerKeyFile, *tlsServerCaCertFile, *tlsServerMinVersion) 236 | if err != nil { 237 | log.Fatal(err) 238 | } 239 | server.TLSConfig = tlsConfig 240 | if err := server.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { 241 | log.Fatalf("TLS Server error: %v", err) 242 | } 243 | } else { 244 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 245 | log.Fatalf("Server error: %v", err) 246 | } 247 | } 248 | }() 249 | 250 | // graceful shutdown 251 | quit := make(chan os.Signal, 1) 252 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 253 | _quit := <-quit 254 | log.Infof("Received %s signal, exiting", _quit.String()) 255 | // Create a context with a timeout 256 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 257 | defer cancel() 258 | 259 | // Shutdown the HTTP server gracefully 260 | if err := server.Shutdown(ctx); err != nil { 261 | log.Fatalf("Server shutdown failed: %v", err) 262 | } 263 | log.Infof("Server shut down gracefully") 264 | } 265 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | REGISTRY ?= ghcr.io/hyperspike 3 | IMG_CONTROLLER ?= $(REGISTRY)/valkey-operator:$(VERSION) 4 | IMG_SIDECAR ?= $(REGISTRY)/valkey-sidecar:$(VERSION) 5 | IMG_VALKEY ?= $(REGISTRY)/valkey:$(VALKEY_VERSION) 6 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 7 | 8 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 9 | ifeq (,$(shell go env GOBIN)) 10 | GOBIN=$(shell go env GOPATH)/bin 11 | else 12 | GOBIN=$(shell go env GOBIN) 13 | endif 14 | 15 | GO := $(shell which go) 16 | MINIKUBE := $(shell which minikube) 17 | KUBECTL := $(shell which kubectl) 18 | VERSION ?= $(shell if [ ! -z $$(git tag --points-at HEAD) ] ; then git tag --points-at HEAD|cat ; else git rev-parse --short HEAD|cat; fi ) 19 | DATE ?= $(shell date -u +'%Y%m%d') 20 | SHA ?= $(shell git rev-parse --short HEAD) 21 | PKG ?= hyperspike.io/valkey-operator 22 | GOOS ?= linux 23 | GOARCH ?= amd64 24 | 25 | # CONTAINER_TOOL defines the container tool to be used for building images. 26 | # Be aware that the target commands are only tested with Docker which is 27 | # scaffolded by default. However, you might want to replace it to use other 28 | # tools. (i.e. podman) 29 | CONTAINER_TOOL ?= docker 30 | 31 | # Setting SHELL to bash allows bash commands to be executed by recipes. 32 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 33 | SHELL = /usr/bin/env bash -o pipefail 34 | .SHELLFLAGS = -ec 35 | 36 | K8S_VERSION ?= 1.33.4 37 | ENVTEST_K8S_VERSION = $(K8S_VERSION) 38 | CILIUM_VERSION ?= 1.18.2 39 | VALKEY_VERSION ?= 8.1.4 40 | 41 | V ?= 0 42 | ifeq ($(V), 1) 43 | Q = 44 | VV = -v 45 | else 46 | Q = @ 47 | VV = 48 | endif 49 | 50 | .PHONY: all 51 | all: build 52 | 53 | ##@ General 54 | 55 | # The help target prints out all targets with their descriptions organized 56 | # beneath their categories. The categories are represented by '##@' and the 57 | # target descriptions by '##'. The awk command is responsible for reading the 58 | # entire set of makefiles included in this invocation, looking for lines of the 59 | # file as xyz: ## something, and then pretty-format the target and help. Then, 60 | # if there's a line with ##@ something, that gets pretty-printed as a category. 61 | # More info on the usage of ANSI control characters for terminal formatting: 62 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 63 | # More info on the awk command: 64 | # http://linuxcommand.org/lc3_adv_awk.php 65 | 66 | .PHONY: help 67 | help: ## Display this help. 68 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 69 | 70 | ##@ Development 71 | 72 | .PHONY: manifests 73 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 74 | $Q$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 75 | 76 | .PHONY: generate 77 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 78 | $Q$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 79 | 80 | .PHONY: fmt 81 | fmt: ## Run go fmt against code. 82 | $Q$(GO) fmt ./... 83 | 84 | .PHONY: vet 85 | vet: ## Run go vet against code. 86 | $Q$(GO) vet $(VV) ./... 87 | 88 | .PHONY: test 89 | test: manifests generate fmt vet envtest ## Run tests. 90 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 91 | 92 | # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. 93 | .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. 94 | test-e2e: 95 | go test ./test/e2e/ -v -ginkgo.v 96 | 97 | .PHONY: lint 98 | lint: golangci-lint ## Run golangci-lint linter 99 | $Q$(GOLANGCI_LINT) run 100 | 101 | .PHONY: lint-fix 102 | lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 103 | $Q$(GOLANGCI_LINT) run --fix 104 | 105 | .PHONY: gosec 106 | gosec: gosec-bin ## Run gosec scanner 107 | $Q$(GOSEC) ./... 108 | 109 | ##@ Build 110 | 111 | manager: manifests generate fmt vet ## Build manager binary. 112 | $QCGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build $(VV) \ 113 | -trimpath \ 114 | -gcflags all="-N -l -trimpath=/src -trimpath=$(PWD)" \ 115 | -asmflags all="-trimpath=/src -trimpath=$(PWD)" \ 116 | -ldflags "-s -w -X main.BuildDate=$(DATE) -X main.Version=$(VERSION) -X main.Commit=$(SHA) \ 117 | -X $(PKG)/cfg.DefaultSidecarImage=$(IMG_SIDECAR) -X $(PKG)/cfg.DefaultValkeyImage=$(IMG_VALKEY)" \ 118 | -installsuffix cgo \ 119 | -o $@ ./cmd/manager/ 120 | 121 | sidecar: manifests generate fmt vet ## Build sidecar binary. 122 | $QCGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build $(VV) \ 123 | -trimpath \ 124 | -gcflags all="-N -l -trimpath=/src -trimpath=$(PWD)" \ 125 | -asmflags all="-trimpath=/src -trimpath=$(PWD)" \ 126 | -ldflags "-s -w -X main.BuildDate=$(DATE) -X main.Version=$(VERSION) -X main.Commit=$(SHA) \ 127 | -X $(PKG)/cfg.DefaultSidecarImage=$(IMG_SIDECAR) -X $(PKG)/cfg.DefaultValkeyImage=$(IMG_VALKEY)" \ 128 | -installsuffix cgo \ 129 | -o $@ ./cmd/sidecar/ 130 | 131 | build: manager sidecar ## Build manager and sidecar binaries. 132 | 133 | .PHONY: run 134 | run: manifests generate fmt vet ## Run a controller from your host. 135 | go run ./cmd/manager/main.go 136 | 137 | # If you wish to build the manager image targeting other platforms you can use the --platform flag. 138 | # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 139 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 140 | .PHONY: docker-build docker-build-manager docker-build-sidecar docker-build-valkey 141 | docker-build-manager: manager ## Build docker image with the manager. 142 | $(CONTAINER_TOOL) build -t ${IMG_CONTROLLER} -f Dockerfile.controller . 143 | 144 | docker-build-sidecar: sidecar ## Build docker image with the sidecar binary. 145 | $(CONTAINER_TOOL) build -t ${IMG_SIDECAR} -f Dockerfile.sidecar . 146 | 147 | docker-build-valkey: ## Build docker image with the valkey binary. 148 | $(CONTAINER_TOOL) build -t ${IMG_VALKEY} --build-arg VALKEY_VERSION=$(VALKEY_VERSION) -f Dockerfile.valkey . 149 | 150 | docker-build: docker-build-manager docker-build-sidecar docker-build-valkey ## Build docker image with the manager, sidecar and valkey binaries. 151 | 152 | .PHONY: docker-push 153 | docker-push: ## Push docker image with the manager. 154 | $(CONTAINER_TOOL) push ${IMG_CONTROLLER} 155 | $(CONTAINER_TOOL) push ${IMG_SIDECAR} 156 | $(CONTAINER_TOOL) push ${IMG_VALKEY} 157 | 158 | # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 159 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 160 | # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 161 | # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 162 | # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) 163 | # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 164 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 165 | .PHONY: docker-buildx 166 | docker-buildx: ## Build and push docker image for the manager for cross-platform support 167 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 168 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile.controller > Dockerfile.controller.cross 169 | - $(CONTAINER_TOOL) buildx create --name valkey-operator-builder 170 | $(CONTAINER_TOOL) buildx use valkey-operator-builder 171 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG_CONTROLLER} -f Dockerfile.controller.cross . 172 | - $(CONTAINER_TOOL) buildx rm valkey-operator-builder 173 | rm Dockerfile.controller.cross 174 | 175 | .PHONY: build-installer 176 | build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 177 | $Qmkdir -p dist 178 | $Qcd config/manager && $(KUSTOMIZE) edit set image controller=${IMG_CONTROLLER} 179 | $Q$(KUSTOMIZE) build config/default > dist/install.yaml 180 | 181 | ##@ Deployment 182 | 183 | ifndef ignore-not-found 184 | ignore-not-found = false 185 | endif 186 | 187 | .PHONY: install 188 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 189 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 190 | 191 | .PHONY: uninstall 192 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 193 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 194 | 195 | .PHONY: deploy 196 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 197 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG_CONTROLLER} 198 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 199 | 200 | .PHONY: undeploy 201 | undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 202 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 203 | 204 | ##@ Quickstart 205 | 206 | .PHONY: minikube 207 | minikube: ## Spool up a local minikube cluster for development 208 | $QK8S_VERSION=$(K8S_VERSION) \ 209 | CILIUM_VERSION=$(CILIUM_VERSION) \ 210 | CERTMANAGER_VERSION=$(CERTMANAGER_VERSION) \ 211 | TLS=$(TLS) \ 212 | PROMETHEUS=$(PROMETHEUS) \ 213 | hack/minikube.sh 214 | 215 | .PHONY: quickstart install-operator install-cr 216 | quickstart: minikube install-operator install-cr ## Install the operator into the minikube cluster and deploy the sample CR use the TLS and PROMETHEUS variables to enable those features 217 | 218 | install-operator: ## Install the operator into the minikube cluster 219 | $QLATEST=$(shell curl -s https://api.github.com/repos/hyperspike/valkey-operator/releases/latest | jq -Mr .tag_name) \ 220 | && curl -sL https://github.com/hyperspike/valkey-operator/releases/download/$$LATEST/install.yaml | kubectl apply -f - 221 | install-cr: ## Install the sample CR into the minikube cluster 222 | $Q(if [ ! -z $$TLS ] ; then TLS_VALUE=true ; else TLS_VALUE=false ; fi ; if [ ! -z $$PROMETHEUS ] ; then PROMETHEUS_VALUE=true ; else PROMETHEUS_VALUE=false ; fi ; sed -e "s/@TLS@/$$TLS_VALUE/" -e "s/@PROMETHEUS@/$$PROMETHEUS_VALUE/" valkey.yml.tpl | $(KUBECTL) apply -f - ) 223 | 224 | minikube-delete: ## Delete the minikube cluster 225 | $Qminikube delete -p north 226 | 227 | ##@ Dependencies 228 | 229 | ## Location to install dependencies to 230 | LOCALBIN ?= $(shell pwd)/bin 231 | $(LOCALBIN): 232 | mkdir -p $(LOCALBIN) 233 | 234 | ## Tool Binaries 235 | KUBECTL ?= $(shell which kubectl) 236 | KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION) 237 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) 238 | ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) 239 | GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) 240 | HELMIFY ?= $(LOCALBIN)/helmify-$(HELMIFY_VERSION) 241 | HELM ?= $(LOCALBIN)/helm-$(HELM_VERSION) 242 | GOSEC ?= $(LOCALBIN)/gosec-$(GOSEC_VERSION) 243 | 244 | ## Tool Versions 245 | KUSTOMIZE_VERSION ?= v5.4.1 246 | CONTROLLER_TOOLS_VERSION ?= v0.19.0 247 | ENVTEST_VERSION ?= release-0.18 248 | GOLANGCI_LINT_VERSION ?= v1.61.0 249 | HELMIFY_VERSION ?= v0.4.14 250 | HELM_VERSION ?= v3.15.4 251 | GOSEC_VERSION ?= v2.22.1 252 | 253 | helm-gen: manifests kustomize helmify ## Generate Helm chart from Kustomize manifests 254 | $Qcd config/manager && $(KUSTOMIZE) edit set image controller=${IMG_CONTROLLER} 255 | $Q$(KUSTOMIZE) build config/default | $(HELMIFY) -crd-dir valkey-operator 256 | $Qsed s@\\\(app.kubernetes.io/name\\\)@\'\\\1\'@ -i valkey-operator/templates/deployment.yaml 257 | $Qsed s@\\\(app.kubernetes.io/instance\\\)@\'\\\1\'@ -i valkey-operator/templates/deployment.yaml 258 | 259 | helm-package: helm-gen helm ## Package Helm chart 260 | $Q$(HELM) package valkey-operator --app-version $(VERSION) --version $(VERSION)-chart 261 | 262 | helm-publish: helm-package ## Publish Helm chart 263 | $Q$(HELM) push valkey-operator-$(VERSION)-chart.tgz oci://ghcr.io/hyperspike 264 | 265 | .PHONY: tunnel registry-proxy prometheus-proxy 266 | tunnel: ## turn on minikube's tunnel to test ingress and get UI access 267 | $Q$(MINIKUBE) tunnel -p north 268 | 269 | registry-proxy: ## turn on a port to push locally built containers into the cluster 270 | $Q$(KUBECTL) port-forward --namespace kube-system service/registry 5000:80 271 | prometheus-proxy: ## turn on a port to validate prometheus metrics 272 | $Q$(KUBECTL) port-forward --namespace default svc/prometheus 9090:9090 273 | 274 | .PHONY: kustomize 275 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 276 | $(KUSTOMIZE): $(LOCALBIN) 277 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 278 | 279 | .PHONY: controller-gen 280 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 281 | $(CONTROLLER_GEN): $(LOCALBIN) 282 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 283 | 284 | .PHONY: envtest 285 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 286 | $(ENVTEST): $(LOCALBIN) 287 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 288 | 289 | .PHONY: golangci-lint 290 | golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 291 | $(GOLANGCI_LINT): $(LOCALBIN) 292 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) 293 | 294 | .PHONY: helmify 295 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 296 | $(HELMIFY): $(LOCALBIN) 297 | $(call go-install-tool,$(HELMIFY),github.com/arttor/helmify/cmd/helmify,$(HELMIFY_VERSION)) 298 | 299 | .PHONY: helm 300 | helm: $(HELM) ## Download helm locally if necessary. 301 | $(HELM): $(LOCALBIN) 302 | $(call go-install-tool,$(HELM),helm.sh/helm/v3/cmd/helm,$(HELM_VERSION)) 303 | 304 | .PHONY: gosec-bin 305 | gosec-bin: $(GOSEC) ## Download gosec locally if necessary. 306 | $(GOSEC): $(LOCALBIN) 307 | $(call go-install-tool,$(GOSEC),github.com/securego/gosec/v2/cmd/gosec,${GOSEC_VERSION}) 308 | 309 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 310 | # $1 - target path with name of binary (ideally with version) 311 | # $2 - package url which can be installed 312 | # $3 - specific version of package 313 | define go-install-tool 314 | @[ -f $(1) ] || { \ 315 | set -e; \ 316 | package=$(2)@$(3) ;\ 317 | echo "Downloading $${package}" ;\ 318 | GOOS=linux GOARCH=amd64 GOBIN=$(LOCALBIN) go install $${package} ;\ 319 | mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ 320 | } 321 | endef 322 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 2 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cert-manager/cert-manager v1.19.1 h1:Txh8L/nLWTDcb7ZnXuXbTe15BxQnLbLirXmbNk0fGgY= 6 | github.com/cert-manager/cert-manager v1.19.1/go.mod h1:8Ps1VXCQRGKT8zNvLQlhDK1gFKWmYKdIPQFmvTS2JeA= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= 15 | github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 16 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 17 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 18 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 19 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 20 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 21 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 22 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 23 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 24 | github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= 25 | github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= 26 | github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= 27 | github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= 28 | github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= 29 | github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 30 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 31 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 32 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 33 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 34 | github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= 35 | github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= 36 | github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= 37 | github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= 38 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 39 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 40 | github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= 41 | github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= 42 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 43 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 44 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 45 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 46 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 47 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 48 | github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 49 | github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= 50 | github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= 51 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 52 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 53 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 54 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 55 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 56 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 57 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 58 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 59 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 61 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 62 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 63 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 65 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 66 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 67 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 68 | github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= 69 | github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 70 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 71 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 72 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 73 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 74 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 75 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 76 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 77 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 78 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 79 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 80 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 81 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 82 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 83 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 84 | github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= 85 | github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= 86 | github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= 87 | github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 88 | github.com/mna/redisc v1.4.0 h1:rBKXyGO/39SGmYoRKCyzXcBpoMMKqkikg8E1G8YIfSA= 89 | github.com/mna/redisc v1.4.0/go.mod h1:CplIoaSTDi5h9icnj4FLbRgHoNKCHDNJDVRztWDGeSQ= 90 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 93 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 94 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 95 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 97 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 98 | github.com/oliver006/redis_exporter v1.80.1 h1:wD0CTz3SMhXGuDqiqbozJYOhovxUTN58eg9VmEJ7nEE= 99 | github.com/oliver006/redis_exporter v1.80.1/go.mod h1:OsB5jexEZ/LyaPswjiAmKOARK+q0CEG3b8hzeeJvNjQ= 100 | github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= 101 | github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 102 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 103 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 104 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 105 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 106 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 107 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 108 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 109 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0 h1:QK37j5ZUtBwbyZkF4BBAs3bQQ1gYKG8e+g1BdNZBr/M= 110 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.87.0/go.mod h1:WHiLZmOWVop/MoYvRD58LfnPeyE+dcITby/jQjg83Hw= 111 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 112 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 113 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 114 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 115 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 116 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 117 | github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= 118 | github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 119 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 120 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 121 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 122 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 123 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 124 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 125 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 126 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 127 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 128 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 129 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 130 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 131 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 132 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 133 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 134 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 135 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 136 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 137 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 138 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 139 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 140 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 141 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 142 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 143 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 144 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 145 | github.com/valkey-io/valkey-go v1.0.68 h1:bTbfonp49b41DqrF30q+y2JL3gcbjd2IiacFAtO4JBA= 146 | github.com/valkey-io/valkey-go v1.0.68/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY= 147 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 148 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 149 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 150 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 152 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 153 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 154 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 155 | go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 156 | go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 157 | go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 158 | go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 159 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 160 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 161 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 162 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 163 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 164 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 165 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 166 | golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 167 | golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 168 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 169 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 170 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 171 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 172 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 173 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 174 | golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= 175 | golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 176 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 179 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 180 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 181 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 182 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 186 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 187 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 188 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 189 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 190 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 191 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 192 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 193 | golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= 194 | golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 195 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 196 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 197 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 198 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 199 | golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= 200 | golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 201 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 202 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 203 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= 206 | gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 207 | google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 208 | google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 213 | gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 214 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 215 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 216 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 217 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 218 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 219 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 220 | k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= 221 | k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= 222 | k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= 223 | k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= 224 | k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= 225 | k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 226 | k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= 227 | k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= 228 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 229 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 230 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= 231 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 232 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 233 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 234 | sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= 235 | sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= 236 | sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ= 237 | sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= 238 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 239 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 240 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 241 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 242 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 243 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 244 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 245 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 246 | --------------------------------------------------------------------------------