diff --git a/charts/valkey/.helmignore b/charts/valkey/.helmignore new file mode 100644 index 0000000..26f45ea --- /dev/null +++ b/charts/valkey/.helmignore @@ -0,0 +1,28 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +.github/ + +# Unit tests (only ignore root-level tests/, not templates/tests/) +/tests/ \ No newline at end of file diff --git a/charts/valkey/CONTRIBUTING.md b/charts/valkey/CONTRIBUTING.md new file mode 100644 index 0000000..760796d --- /dev/null +++ b/charts/valkey/CONTRIBUTING.md @@ -0,0 +1,326 @@ +# Contributing to the Valkey Helm Chart + +Thank you for your interest in contributing! This guide explains how to set up +your environment, make changes, test them, and submit them for review. It is +aimed at first-time contributors as well as regulars. + +If anything here is unclear or out of date, please open an issue or a pull +request to improve it. + +--- + +## Table of Contents + +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Testing](#testing) +- [Submitting Changes](#submitting-changes) +- [Additional Information](#additional-information) + +--- + +## Getting Started + +### Repository overview + +This repository hosts the Helm charts for deploying [Valkey](https://valkey.io) +(an open-source, high-performance key/value datastore and Redis alternative) on +Kubernetes. It contains two charts: + +| Chart | Path | Description | +| ---------------- | ------------------ | -------------------------------------------------------- | +| `valkey` | `./valkey` | A lightweight chart for deploying Valkey to Kubernetes. | +| `valkey-operator`| `./valkey-operator`| A chart for deploying the Valkey operator. | + +This guide focuses on the **`valkey`** chart. + +### Prerequisites and required tools + +You will need the following tools installed locally: + +| Tool | Minimum version | Purpose | +| ---------------------------------------------------------------------- | --------------- | ---------------------------------------- | +| [Helm](https://helm.sh/docs/intro/install/) | 3.5+ | Linting, templating, and packaging. | +| [helm-unittest](https://github.com/helm-unittest/helm-unittest) plugin | 1.0.2+ | Running the chart unit tests. | +| [Git](https://git-scm.com/) | any recent | Version control and DCO sign-off. | +| [just](https://github.com/casey/just) (optional) | any recent | Convenience task runner (`Justfile`). | +| A Kubernetes cluster (optional) | 1.20+ | End-to-end verification (kind/minikube). | + +Install the `helm-unittest` plugin: + +```bash +helm plugin install https://github.com/helm-unittest/helm-unittest --version 1.0.2 +``` + +### Cloning the repository + +Fork the repository on GitHub, then clone your fork: + +```bash +git clone https://github.com//valkey-helm.git +cd valkey-helm +git remote add upstream https://github.com/valkey-io/valkey-helm.git +``` + +Keeping an `upstream` remote lets you sync with the canonical repository: + +```bash +git fetch upstream +git checkout main +git merge upstream/main +``` + +### Setting up a local development environment + +No build step is required for the chart itself. To confirm your toolchain works, +run the linter and unit tests from the repository root: + +```bash +helm lint ./valkey --strict +helm unittest ./valkey +``` + +If you have `just` installed, you can run all validations at once: + +```bash +just validate +``` + +To exercise the chart against a real cluster, you can use a local one such as +[kind](https://kind.sigs.k8s.io/) or +[minikube](https://minikube.sigs.k8s.io/). + +--- + +## Development Workflow + +### Creating a feature branch + +Always work on a branch off the latest `main`: + +```bash +git checkout main +git pull upstream main +git checkout -b my-feature +``` + +Use a short, descriptive branch name (e.g. `fix-pdb-selector`, +`add-tls-support`). + +### Making changes to charts, templates, or documentation + +Common places you will make changes within `./valkey`: + +- `templates/` — Kubernetes manifest templates (StatefulSet, Services, + ConfigMaps, etc.) and `_helpers.tpl` for shared template logic. +- `values.yaml` — default configuration values exposed to users. +- `values.schema.json` — JSON schema validating user-supplied values. **Keep it + in sync with `values.yaml`** when you add, remove, or rename a value. +- `README.md` — user-facing chart documentation. Update it when you change + configurable values or behavior. +- `tests/` — `helm-unittest` test suites. + +### Following repository coding and documentation conventions + +- Keep templates consistent with the existing style: reuse helpers from + `_helpers.tpl` rather than duplicating logic, and quote values where Helm + requires it. +- Make new behavior **opt-in** and preserve existing defaults so upgrades stay + non-breaking wherever possible. +- Every new or changed configurable value should be reflected in `values.yaml`, + `values.schema.json`, and documented in `README.md`. +- Add or update unit tests in `tests/` for any template change. + +--- + +## Testing + +All of the following run from the repository root. The same checks run in CI, so +running them locally first saves a review cycle. + +### Running lint checks + +```bash +helm lint ./valkey +# or +just lint +``` + +### Validating Helm templates + +Render the templates to make sure they produce valid manifests: + +```bash +helm template valkey ./valkey +# or +just template +``` + +CI additionally renders the chart against multiple Kubernetes versions +(currently `1.28.15`, `1.35.3`, and `1.36.0`). You can mirror this locally: + +```bash +helm template valkey ./valkey --kube-version 1.28.15 +``` + +### Executing repository-specific tests + +The chart uses [`helm-unittest`](https://github.com/helm-unittest/helm-unittest) +for unit tests (suites live in `valkey/tests/`): + +```bash +helm unittest ./valkey +``` + +If you intentionally change rendered output, you may need to update snapshots: + +```bash +helm unittest -u ./valkey +``` + +### Verifying changes locally before submission + +Run the full validation suite before opening a pull request: + +```bash +just validate # runs lint + unit tests +``` + +Optionally, install the chart into a local cluster for an end-to-end check: + +```bash +helm install valkey ./valkey --dry-run --debug # render + validate without applying +helm install valkey ./valkey # actually install (e.g. on kind/minikube) +helm test valkey # run the chart's connection test pod +helm uninstall valkey +``` + +--- + +## Submitting Changes + +### Commit message guidelines + +- Write clear, imperative commit subjects (e.g. "Add TLS support to + StatefulSet", not "added tls"). +- Keep the subject concise; use the body to explain the *why* when it is not + obvious. +- Group related changes into logical commits. + +### DCO sign-off requirements + +This project uses the [Developer Certificate of Origin](https://developercertificate.org/) +(DCO). Every commit must be signed off, certifying that you wrote the code or +otherwise have the right to submit it under the project's license. + +Add a sign-off automatically with the `-s` flag: + +```bash +git commit -s -m "Add TLS support to StatefulSet" +``` + +This appends a trailer to your commit message: + +``` +Signed-off-by: Your Name +``` + +The name and email must match your Git configuration: + +```bash +git config user.name "Your Name" +git config user.email "your.email@example.com" +``` + +If you forget to sign off, amend the most recent commit: + +```bash +git commit --amend -s +``` + +To sign off a series of commits, rebase with the sign-off applied to each: + +```bash +git rebase --signoff main +``` + +### Creating a pull request + +1. Push your branch to your fork: + + ```bash + git push origin my-feature + ``` + +2. Open a pull request against the `main` branch of the upstream repository. +3. In the description, explain **what** changed and **why**, and reference any + related issues (e.g. `Fixes #123`). +4. Confirm the checklist: + - [ ] `helm lint ./valkey` passes. + - [ ] `helm unittest ./valkey` passes. + - [ ] Chart `version` bumped in `Chart.yaml` (if the chart changed). + - [ ] `values.yaml`, `values.schema.json`, and `README.md` updated as needed. + - [ ] All commits are signed off (DCO). + +CI will run linting, unit tests, and multi-version template rendering on your +pull request. Please make sure all checks are green. + +### Addressing review feedback + +- Respond to review comments and push follow-up commits to the same branch — the + pull request updates automatically. +- Keep the discussion focused and prefer additional commits over force-pushes + during active review so reviewers can see what changed. +- Remember to sign off any new commits (`git commit -s`). +- Once approved, a maintainer will merge your pull request. + +--- + +## Additional Information + +### Repository structure overview + +``` +. +├── Justfile # Convenience tasks: lint, test, template, package, validate +├── README.md # Repository overview +├── LICENSE # BSD 3-Clause License +├── .github/ +│ ├── ct.yml # chart-testing config (version-increment enforcement) +│ └── workflows/ # CI: lint, unit tests, template rendering, release +├── valkey/ # The Valkey Helm chart +│ ├── Chart.yaml # Chart metadata and version +│ ├── values.yaml # Default values +│ ├── values.schema.json # Values validation schema +│ ├── templates/ # Kubernetes manifest templates +│ └── tests/ # helm-unittest test suites +└── valkey-operator/ # The Valkey operator chart +``` + +### Release process + +Releases are automated via GitHub Actions: + +- When a change to `valkey/Chart.yaml` (or `valkey-operator/Chart.yaml`) lands on + `main` with a new `version`, the [release workflow](../.github/workflows/release.yml) + runs. +- It uses [chart-releaser](https://github.com/helm/chart-releaser-action) to + package the chart, create a GitHub Release, and publish the packaged chart. +- Charts are also pushed as OCI artifacts to GitHub Container Registry (GHCR). + +As a contributor, your responsibility is simply to bump the chart `version` +correctly; maintainers and automation handle the rest. + +### Links and community resources + +- Valkey project: +- Chart repository: +- Helm documentation: +- helm-unittest: +- Developer Certificate of Origin: +- Slack: [#valkey-helm](https://valkey-oss-developer.slack.com/archives/C09JZ6N2AAV) + on the Valkey OSS developer workspace. + +--- + +Thanks again for contributing to the Valkey Helm Chart! 🎉 diff --git a/charts/valkey/Chart.yaml b/charts/valkey/Chart.yaml new file mode 100644 index 0000000..b785375 --- /dev/null +++ b/charts/valkey/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: valkey +description: A Helm chart for Kubernetes +type: application +version: 0.12.1 +appVersion: "9.1.3" +home: https://valkey.io/valkey-helm/ +sources: + - https://github.com/valkey-io/valkey-helm.git + - https://valkey.io +maintainers: + - name: raven + url: https://github.com/mk-raven + - name: sgissi + url: https://github.com/sgissi + - name: Bloodraven21 + url: https://github.com/Bloodraven21 +icon: https://dyltqmyl993wv.cloudfront.net/assets/stacks/valkey/img/valkey-stack-220x234.png diff --git a/charts/valkey/README.md b/charts/valkey/README.md new file mode 100644 index 0000000..1e12951 --- /dev/null +++ b/charts/valkey/README.md @@ -0,0 +1,408 @@ +# valkey + +![Version: 0.12.1](https://img.shields.io/badge/Version-0.12.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 9.1.3](https://img.shields.io/badge/AppVersion-9.1.3-informational?style=flat-square) + +A Helm chart for Kubernetes + +**Homepage:** + +## Maintainers + +| Name | Url | +| ---- | --- | +| raven | [https://github.com/mk-raven] | +| sgissi | [https://github.com/sgissi] | +| Bloodraven21 | [https://github.com/Bloodraven21] | +## Source Code + +* +* + +## Deployment Modes + +### Standalone Mode (Default) + +Deploy a single Valkey instance: + +```bash +helm install valkey valkey/valkey +``` + +**Services:** + +* `valkey`: Master/read-write service + +### Replication Mode + +Deploy Valkey with master-replica architecture for read scaling and data redundancy: + +```bash +helm install valkey valkey/valkey --set replica.enabled=true --set replica.persistence.size=5Gi +``` + +**IMPORTANT** + +## Cluster Mode + +This chart does not and will not support **Valkey cluster** mode. Managing a clustered topology is fundamentally different from standalone or replicated deployments, and the operational requirements go well beyond what this chart is designed to handle. + +For cluster mode, a separate chart is being developed that uses the valkey-operator to deploy and manage clusters. The operator must be installed first. + +To follow progress or get involved, see the [weekly meeting wiki](https://github.com/valkey-io/valkey-operator/wiki/Weekly-meeting). + +**Services:** + +* `valkey`: Master/write service +* `valkey-read`: Read service (load-balances across all pods) - optional +* `valkey-headless`: Headless service for pod discovery + +**Write Safety Configuration:** + +Ensure data durability by requiring a minimum number of replicas to be in sync before accepting writes: + +```yaml +replica: + minReplicasToWrite: 1 # Require at least 1 replica + minReplicasMaxLag: 10 # Max 10 seconds replication lag +``` + +If fewer than `minReplicasToWrite` replicas are available, the master will reject write operations. + +## Storage + +### Standalone Storage + +Persistence is optional. By default, data is stored in an ephemeral volume and lost on pod restart. + +**Enable persistent storage:** + +```yaml +dataStorage: + enabled: true + requestedSize: 10Gi + className: "fast-ssd" # Optional +``` + +**Use existing PVC:** + +```yaml +dataStorage: + enabled: true + persistentVolumeClaimName: "my-existing-pvc" +``` + +### Replication Storage + +Persistent storage is **mandatory** in replication mode. Without it, the primary might come up with an empty dataset after a restart, all replicas will synchronize with the empty primary and lose their data. See [Valkey Replication Safety](https://valkey.io/topics/replication/#safety-of-replication-when-primary-has-persistence-turned-off) for details. + +```yaml +replica: + enabled: true + persistence: + size: 10Gi # Required + storageClass: "fast-ssd" # Optional +``` + +## Authentication + +This chart supports ACL-based authentication for Valkey. + +**⚠️ IMPORTANT:** When authentication is enabled, the `default` user **MUST** be defined in either `auth.aclUsers` or `auth.aclConfig`. Without a default user, anyone can access the database without credentials. + +### Existing Secret (recommended) + +Reference an existing Kubernetes secret containing user passwords: + +```yaml +auth: + enabled: true + usersExistingSecret: "my-valkey-users" + aclUsers: + default: + permissions: "~* &* +@all" + # Password will be read from secret key "default" (defaults to username) + readonly: + permissions: "~* -@all +@read +ping +info" + passwordKey: "readonly-pwd" # Use custom secret key name +``` + +### Inline Passwords + +Define users directly in your values file with inline passwords: + +```yaml +auth: + enabled: true + aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + readonly: + permissions: "~* -@all +@read +ping +info" + password: "readonly-password" +``` + +**Note:** + +* If `usersExistingSecret` is defined, passwords from the secret will take precedence over inline passwords. + +### Custom ACL Configuration + +You can also provide raw ACL configuration that will be appended after any generated users: + +```yaml +auth: + enabled: true + aclConfig: | + user default on >defaultpassword ~* &* +@all + user guest on nopass ~public:* +@read +``` + +### Replication with Authentication + +When using ACL authentication in replication mode, replicas need credentials to authenticate to the master: + +```yaml +auth: + enabled: true + usersExistingSecret: "my-valkey-users" + aclUsers: + default: + permissions: "~* &* +@all" + replication-user: + permissions: "+psync +replconf +ping" + +replica: + enabled: true + replicas: 2 + replicationUser: "replication-user" # Must be defined in auth.aclUsers +``` + +**Important Notes:** + +* `replica.replicationUser` specifies which ACL user replicas use to authenticate +* This user MUST be defined in `auth.aclUsers` with appropriate permissions +* Minimum permissions: `+psync +replconf +ping` + +## Metrics + +This chart supports Prometheus metrics collection using the [Redis exporter](https://github.com/oliver006/redis_exporter). + +Enable the metrics exporter sidecar: + +```yaml +metrics: + enabled: true +``` + +### Prometheus Operator discovery + +Automated Prometheus discovery using the Prometheus Operator ServiceMonitor: + +```yaml +metrics: + enabled: true + serviceMonitor: + enabled: true +``` + +## PodDisruptionBudget + +A PodDisruptionBudget helps keep enough read-replicas available during voluntary disruptions like node drains or rolling updates. + +**Enable PDB (only works in replicated mode):** + +```yaml +podDisruptionBudget: + enabled: true + maxUnavailable: 1 # Allow at most 1 pod to be unavailable +``` + +**Or use minAvailable to guarantee a specific number of replicas:** + +```yaml +podDisruptionBudget: + enabled: true + minAvailable: 2 # Always keep at least 2 replicas running +``` + +## TLS + +This chart supports TLS encryption for Valkey connections. + +First create a secret containing the certificate public and private keys plus CA public key: + +```shell +kubectl create secret generic valkey-tls-secret --from-file=server.crt --from-file=server.key --from-file=ca.crt +``` + +Enable TLS and provide the name of the secret created above: + +```yaml +tls: + enabled: true + existingSecret: "valkey-tls-secret" +``` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| global.imageRegistry | string | '' | | +| global.imagePullSecrets | list | `[]` | | +| affinity | object | `{}` | | +| auth.aclConfig | string | `""` | | +| auth.aclUsers | object | `{}` | | +| auth.enabled | bool | `false` | | +| auth.usersExistingSecret | string | `""` | | +| dataStorage.accessModes[0] | string | `"ReadWriteOnce"` | | +| dataStorage.annotations | object | `{}` | | +| dataStorage.className | string | `""` | | +| dataStorage.enabled | bool | `false` | | +| dataStorage.keepPvc | bool | `false` | | +| dataStorage.labels | object | `{}` | | +| dataStorage.persistentVolumeClaimName | string | `""` | | +| dataStorage.requestedSize | string | `""` | | +| dataStorage.subPath | string | `""` | | +| dataStorage.volumeName | string | `"valkey-data"` | | +| dataStorage.hostPath | string | `""` | | +| deploymentStrategy | string | `"RollingUpdate"` | | +| env | object | `{}` | | +| extraSecretValkeyConfigs | bool | `false` | | +| extraVolumes | list | `[]` | | +| extraVolumeMounts | list | `[]` | | +| extraValkeyConfigs | list | `[]` | | +| extraValkeySecrets | list | `[]` | | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.registry | string | `""` | | +| image.repository | string | `"docker.io/valkey/valkey"` | | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| initResources | object | `{}` | | +| livenessProbe.customProbe | object | `{}` | Full probe spec to replace the default valkey-cli ping handler and timing | +| livenessProbe.enabled | bool | `true` | | +| livenessProbe.failureThreshold | int | `3` | | +| livenessProbe.initialDelaySeconds | int | `0` | | +| livenessProbe.periodSeconds | int | `10` | | +| livenessProbe.timeoutSeconds | int | `1` | | +| metrics.enabled | bool | `false` | | +| metrics.exporter.args | list | `[]` | | +| metrics.exporter.command | list | `[]` | | +| metrics.exporter.extraEnvs | object | `{}` | | +| metrics.exporter.extraVolumeMounts | list | `[]` | | +| metrics.exporter.image.pullPolicy | string | `"IfNotPresent"` | | +| metrics.exporter.image.repository | string | `"ghcr.io/oliver006/redis_exporter"` | | +| metrics.exporter.image.tag | string | `"v1.79.0"` | | +| metrics.exporter.port | int | `9121` | | +| metrics.exporter.resources | object | `{}` | | +| metrics.exporter.securityContext | object | `{}` | | +| metrics.podMonitor.additionalLabels | object | `{}` | | +| metrics.podMonitor.annotations | object | `{}` | | +| metrics.podMonitor.enabled | bool | `false` | | +| metrics.podMonitor.extraLabels | object | `{}` | | +| metrics.podMonitor.honorLabels | bool | `false` | | +| metrics.podMonitor.interval | string | `"30s"` | | +| metrics.podMonitor.metricRelabelings | list | `[]` | | +| metrics.podMonitor.podTargetLabels | list | `[]` | | +| metrics.podMonitor.port | string | `"metrics"` | | +| metrics.podMonitor.relabelings | list | `[]` | | +| metrics.podMonitor.sampleLimit | bool | `false` | | +| metrics.podMonitor.scrapeTimeout | string | `""` | | +| metrics.podMonitor.targetLimit | bool | `false` | | +| metrics.prometheusRule.enabled | bool | `false` | | +| metrics.prometheusRule.extraAnnotations | object | `{}` | | +| metrics.prometheusRule.extraLabels | object | `{}` | | +| metrics.prometheusRule.rules | list | `[]` | | +| metrics.service.annotations | object | `{}` | | +| metrics.service.enabled | bool | `true` | | +| metrics.service.extraLabels | object | `{}` | | +| metrics.service.ports.http | int | `9121` | | +| metrics.service.type | string | `"ClusterIP"` | | +| metrics.service.appProtocol | string | `""` | | +| metrics.serviceMonitor.additionalLabels | object | `{}` | | +| metrics.serviceMonitor.annotations | object | `{}` | | +| metrics.serviceMonitor.enabled | bool | `false` | | +| metrics.serviceMonitor.extraLabels | object | `{}` | | +| metrics.serviceMonitor.honorLabels | bool | `false` | | +| metrics.serviceMonitor.interval | string | `"30s"` | | +| metrics.serviceMonitor.metricRelabelings | list | `[]` | | +| metrics.serviceMonitor.podTargetLabels | list | `[]` | | +| metrics.serviceMonitor.port | string | `"metrics"` | | +| metrics.serviceMonitor.relabelings | list | `[]` | | +| metrics.serviceMonitor.sampleLimit | bool | `false` | | +| metrics.serviceMonitor.scrapeTimeout | string | `""` | | +| metrics.serviceMonitor.targetLimit | bool | `false` | | +| nameOverride | string | `""` | | +| networkPolicy | object | `{}` | | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| commonLabels | object | `{}` | | +| podDisruptionBudget.enabled | bool | `false` | | +| podDisruptionBudget.minAvailable | int or string | `null` | Minimum pods available during disruptions | +| podDisruptionBudget.maxUnavailable | int or string | `1` | Maximum pods unavailable during disruptions | +| podDisruptionBudget.unhealthyPodEvictionPolicy | string | `null` | Policy for evicting unhealthy pods | +| podSecurityContext.fsGroup | int | `1000` | | +| podSecurityContext.runAsGroup | int | `1000` | | +| podSecurityContext.runAsUser | int | `1000` | | +| priorityClassName | string | `""` | | +| runtimeClassName | string | `""` | RuntimeClassName for the pods (e.g. `gvisor`, `kata-containers`); empty uses the cluster default runtime | +| readinessProbe.customProbe | object | `{}` | Full probe spec to replace the default valkey-cli ping handler and timing | +| readinessProbe.enabled | bool | `false` | Opt-in; the Valkey container had no readiness probe before | +| readinessProbe.failureThreshold | int | `3` | | +| readinessProbe.initialDelaySeconds | int | `0` | | +| readinessProbe.periodSeconds | int | `10` | | +| readinessProbe.successThreshold | int | `1` | | +| readinessProbe.timeoutSeconds | int | `1` | | +| replica.enabled | bool | `false` | | +| replica.replicas | int | `2` | | +| replica.replicationUser | string | `"default"` | | +| replica.disklessSync | bool | `false` | | +| replica.minReplicasToWrite | int | `0` | | +| replica.minReplicasMaxLag | int | `10` | | +| replica.service.enabled | bool | `"true"` | | +| replica.service.type | string | `"ClusterIP"` | | +| replica.service.port | int | `6379` | | +| replica.service.annotations | object | `{}` | | +| replica.service.nodePort | int | `0` | | +| replica.service.clusterIP | string | `""` | | +| replica.service.appProtocol | string | `""` | | +| replica.service.loadBalancerClass | string | `""` | | +| replica.persistence. | | `""` | | +| replica.persistence.size | string | `""` | Required if replica is enabled | +| replica.persistence.storageClass | string | `""` | | +| replica.persistence.accessModes | list | `""` | | +| resources | object | `{}` | | +| securityContext.capabilities.drop[0] | string | `"ALL"` | | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| securityContext.runAsNonRoot | bool | `true` | | +| securityContext.runAsUser | int | `1000` | | +| service.annotations | object | `{}` | | +| service.nodePort | int | `0` | | +| service.port | int | `6379` | | +| service.type | string | `"ClusterIP"` | | +| service.appProtocol | string | `""` | | +| service.loadBalancerClass | string | `""` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.automount | bool | `false` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| startupProbe.customProbe | object | `{}` | Full probe spec to replace the default valkey-cli ping handler and timing | +| startupProbe.enabled | bool | `true` | | +| startupProbe.failureThreshold | int | `3` | | +| startupProbe.initialDelaySeconds | int | `0` | | +| startupProbe.periodSeconds | int | `10` | | +| startupProbe.timeoutSeconds | int | `1` | | +| tls.caPublicKey | string | `"ca.crt"` | | +| tls.dhParamKey | string | `""` | | +| tls.enabled | bool | `false` | | +| tls.existingSecret | string | `""` | | +| tls.requireClientCertificate | bool | `false` | | +| tls.serverKey | string | `"server.key"` | | +| tls.serverPublicKey | string | `"server.crt"` | | +| tolerations | list | `[]` | | +| topologySpreadConstraints | list | `[]` | | +| valkeyConfig | string | `""` | | +| valkeyLogLevel | string | `"notice"` | | +| workloadAnnotations | object | `{}` | | diff --git a/charts/valkey/templates/NOTES.txt b/charts/valkey/templates/NOTES.txt new file mode 100644 index 0000000..07ddb6d --- /dev/null +++ b/charts/valkey/templates/NOTES.txt @@ -0,0 +1,137 @@ +{{/* + NOTES for Valkey Helm Chart + This file is rendered after `helm install` / `helm upgrade`. +*/}} + +⭐ Valkey has been deployed! + +Release: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} +Chart: {{ .Chart.Name }} {{ .Chart.Version }} +App version: {{ .Chart.AppVersion }} + +{{- if .Values.replica.enabled }} +================================================================================ +🔄 REPLICATION MODE +================================================================================ + +Your Valkey deployment is running in REPLICATION mode: +- 1 Master ({{ include "valkey.fullname" . }}-0) +- {{ .Values.replica.replicas }} Replica(s) + +{{- if .Values.replica.service.enabled }} + +READ Operations (Load-balanced across all pods): + Service: {{ include "valkey.fullname" . }}-read + Type: {{ .Values.replica.service.type }} + Port: {{ .Values.replica.service.port }} + +1) In-cluster access: + $ valkey-cli -h {{ include "valkey.fullname" . }}-read -p {{ .Values.replica.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }} GET key + +2) Local access via kubectl port-forward: + $ kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "valkey.fullname" . }}-read 6379:{{ .Values.replica.service.port }} + $ valkey-cli -h 127.0.0.1 -p 6379{{ if .Values.tls.enabled }} --tls{{- end }} GET key +{{ if eq .Values.replica.service.type "LoadBalancer" }} +3) External access (LoadBalancer): + $ export SERVICE_IP=$(kubectl -n {{ .Release.Namespace }} get svc {{ include "valkey.fullname" . }}-read -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + $ valkey-cli -h $SERVICE_IP -p {{ .Values.replica.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }} GET key +{{ else if eq .Values.replica.service.type "NodePort" }} +3) External access (NodePort): + $ export NODE_PORT=$(kubectl -n {{ .Release.Namespace }} get svc {{ include "valkey.fullname" . }}-read -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}') + $ valkey-cli -h $NODE_IP -p $NODE_PORT{{ if .Values.tls.enabled }} --tls{{- end }} GET key +{{ end }} +{{- end }} + +Direct Pod Access: + Master: {{ include "valkey.fullname" . }}-0.{{ include "valkey.headlessServiceName" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }} +{{- range $i := until (int .Values.replica.replicas) }} + Replica: {{ include "valkey.fullname" $ }}-{{ add $i 1 }}.{{ include "valkey.headlessServiceName" $ }}.{{ $.Release.Namespace }}.svc.{{ $.Values.clusterDomain }} +{{- end }} + +WRITE Operations (Master only): + Service: {{ include "valkey.fullname" . }} + Type: {{ .Values.service.type }} + Port: {{ .Values.service.port }} + +1) In-cluster access: + $ valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }} PING + +2) Local access: + $ kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "valkey.fullname" . }} 6379:{{ .Values.service.port }} + $ valkey-cli -h 127.0.0.1 -p 6379{{ if .Values.tls.enabled }} --tls{{- end }} SET key value +{{- else }} +================================================================================ +📦 STANDALONE MODE +================================================================================ + +Service: {{ include "valkey.fullname" . }} +Type: {{ .Values.service.type }} +Port: {{ .Values.service.port }} + +1) In-cluster access + From another Pod: + $ valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }} PING + +2) Local access via kubectl port-forward + $ kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "valkey.fullname" . }} 6379:{{ .Values.service.port }} + In another terminal: + $ valkey-cli -h 127.0.0.1 -p 6379{{ if .Values.tls.enabled }} --tls{{- end }} PING +{{- end }} +{{ if eq .Values.service.type "LoadBalancer" }} +3) External access (LoadBalancer) + $ export SERVICE_IP=$(kubectl -n {{ .Release.Namespace }} get svc {{ include "valkey.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + $ valkey-cli -h $SERVICE_IP -p {{ .Values.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }} PING +{{ else if eq .Values.service.type "NodePort" }} +3) External access (NodePort) + $ export NODE_PORT=$(kubectl -n {{ .Release.Namespace }} get svc {{ include "valkey.fullname" . }} -o jsonpath='{.spec.ports[0].nodePort}') + $ export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}') + $ valkey-cli -h $NODE_IP -p $NODE_PORT{{ if .Values.tls.enabled }} --tls{{- end }} PING +{{ end }} + +{{ if .Values.auth.enabled }} +🔐 Authentication is ENABLED (ACL) +- Provide the username and password from `.auth.aclUsers`. +{{ else }} +🔓 Authentication is DISABLED +- Enable with: `--set auth.enabled=true` and provide `.auth.aclUsers`. +{{ end }} + +✅ Quick test +$ valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }}{{ if .Values.tls.enabled }} --tls{{- end }}{{ if .Values.auth.enabled }} --user -a {{ end }} +valkey> SET foo bar +valkey> GET foo +"bar" + +💾 Persistence +{{- if .Values.replica.enabled }} +- Persistence is ENABLED (required for replication mode). Each instance has its own volume. +- Size: {{ .Values.replica.persistence.size }} +{{- if .Values.replica.persistence.storageClass }} +- Storage class: {{ .Values.replica.persistence.storageClass }} +{{- end }} +- To see PVCs: + $ kubectl -n {{ .Release.Namespace }} get pvc -l app.kubernetes.io/instance={{ .Release.Name }} +{{- else }} +{{ if .Values.dataStorage.enabled -}} +{{ if .Values.dataStorage.hostPath -}} +- Persistence is ENABLED. A hostPath volume is used at path='`{{ .Values.dataStorage.hostPath }}`'. +{{ else -}} +- Persistence is ENABLED. To see it: + $ kubectl -n {{ .Release.Namespace }} get pvc -l app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/name={{ include "valkey.name" . }} +- Note: `dataStorage.keepPvc={{ .Values.dataStorage.keepPvc }}` controls whether the PVC is kept on uninstall. +{{ end -}} +{{ else -}} +- Persistence is DISABLED. Data will not survive Pod restarts. +- Enable with: + `--set dataStorage.enabled=true --set dataStorage.requestedSize=5Gi` (optionally set `dataStorage.className`) + or + `--set dataStorage.enabled=true --set dataStorage.persistentVolumeClaimName=valkey-data-pvc` to use an existing PVC + or + `--set dataStorage.enabled=true --set dataStorage.hostPath=/some/path/` to use a hostPath volume. +{{- end }} +{{- end }} + +🧹 Uninstall +$ helm -n {{ .Release.Namespace }} uninstall {{ .Release.Name }} diff --git a/charts/valkey/templates/_helpers.tpl b/charts/valkey/templates/_helpers.tpl new file mode 100644 index 0000000..bd4e826 --- /dev/null +++ b/charts/valkey/templates/_helpers.tpl @@ -0,0 +1,228 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "valkey.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "valkey.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "valkey.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "valkey.labels" -}} +helm.sh/chart: {{ include "valkey.chart" . }} +{{ include "valkey.selectorLabels" . }} +{{- if or .Values.image.tag .Chart.AppVersion }} +app.kubernetes.io/version: {{ mustRegexReplaceAllLiteral "@sha.*" .Values.image.tag "" | default .Chart.AppVersion | trunc 63 | trimSuffix "-" | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.commonLabels }} +{{- toYaml . | nindent 0 }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "valkey.selectorLabels" -}} +app.kubernetes.io/name: {{ include "valkey.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "valkey.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "valkey.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Returns the Valkey container image +*/}} +{{- define "valkey.image" -}} +{{- include "valkey.common.image" (dict "image" (dict "registry" .Values.image.registry "repository" .Values.image.repository "tag" (.Values.image.tag | default .Chart.AppVersion)) "global" .Values.global) }} +{{- end -}} + +{{/* +Returns the Valkey exporter container image +*/}} +{{- define "valkey.metrics.exporter.image" -}} +{{- include "valkey.common.image" (dict "image" .Values.metrics.exporter.image "global" .Values.global) }} +{{- end -}} + +{{/* +The common image function that renders the container image +*/}} +{{- define "valkey.common.image" -}} +{{- $registryName := .image.registry }} +{{- $repositoryName := .image.repository }} +{{- $tag := .image.tag }} +{{- if .global }} + {{- if .global.imageRegistry }} + {{- $registryName = .global.imageRegistry }} + {{- end }} +{{- end }} +{{- if $registryName }} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag }} +{{- else }} +{{- printf "%s:%s" $repositoryName $tag }} +{{ end }} +{{- end -}} + +{{/* +Returns the Valkey image pull secrets +*/}} +{{- define "valkey.imagePullSecrets" -}} +{{- $pullSecrets := list }} +{{- if .Values.global }} + {{- range .Values.global.imagePullSecrets -}} + {{- $pullSecrets = append $pullSecrets . -}} + {{- end -}} +{{- end -}} +{{- range .Values.imagePullSecrets -}} + {{- $pullSecrets = append $pullSecrets . -}} +{{- end -}} +{{- if (not (empty $pullSecrets)) }} +imagePullSecrets: +{{- range $pullSecrets }} +- name: {{ . }} +{{- end }} +{{- end }} +{{- end -}} + +{{/* +Check if there are any users with inline passwords +*/}} +{{- define "valkey.hasInlinePasswords" -}} +{{- $hasInlinePasswords := false -}} +{{- range $username, $user := .Values.auth.aclUsers -}} + {{- if $user.password -}} + {{- $hasInlinePasswords = true -}} + {{- end -}} +{{- end -}} +{{- $hasInlinePasswords -}} +{{- end -}} + +{{/* +Validate auth configuration +*/}} +{{- define "valkey.validateAuthConfig" -}} +{{- if .Values.auth.enabled }} + {{- if not (or .Values.auth.aclUsers .Values.auth.aclConfig) }} + {{- fail "auth.enabled is true but no authentication method is configured. Please provide auth.aclUsers or auth.aclConfig" }} + {{- end }} + {{- if .Values.auth.aclUsers }} + {{- $hasUsersExistingSecret := .Values.auth.usersExistingSecret }} + {{- if not (hasKey .Values.auth.aclUsers "default") }} + {{- fail "The 'default' user must be defined in auth.aclUsers when authentication is enabled. Without it, anyone can access the database without credentials." }} + {{- end }} + {{- range $username, $user := .Values.auth.aclUsers }} + {{- if not $user.permissions }} + {{- fail (printf "User '%s' in auth.aclUsers must have a 'permissions' field" $username) }} + {{- end }} + {{- if not (or $user.password $hasUsersExistingSecret) }} + {{- fail (printf "User '%s' must have either 'password' field or auth.usersExistingSecret must be set" $username) }} + {{- end }} + {{- if and $user.passwordKey (not $hasUsersExistingSecret) }} + {{- fail (printf "User '%s' has passwordKey but auth.usersExistingSecret is not set" $username) }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- end -}} + +{{/* +Headless service name for replication +*/}} +{{- define "valkey.headlessServiceName" -}} +{{ include "valkey.fullname" . }}-headless +{{- end -}} + +{{/* +Validate replica persistence configuration +*/}} +{{- define "valkey.validateReplicaPersistence" -}} +{{- if .Values.replica.enabled }} + {{- if not .Values.replica.persistence.size }} + {{- fail "Replica mode requires persistent storage. Please set replica.persistence.size (e.g., '5Gi')" }} + {{- end }} +{{- end }} +{{- end -}} + +{{/* +Validate replica authentication configuration +*/}} +{{- define "valkey.validateReplicaAuth" -}} +{{- if and .Values.replica.enabled .Values.auth.enabled }} + {{- if not (hasKey .Values.auth.aclUsers .Values.replica.replicationUser) }} + {{- fail (printf "Replication user '%s' (replica.replicationUser) must be defined in auth.aclUsers. The chart requires this to retrieve the password for replica authentication." .Values.replica.replicationUser) }} + {{- end }} +{{- end }} +{{- end -}} + +{{/* +Render the Valkey server container health probes (startupProbe, livenessProbe, +readinessProbe). Each probe is gated on its own `enabled` flag. When a probe's +`customProbe` map is set it replaces the default handler and timing entirely; +otherwise the default valkey-cli ping exec handler (TLS-aware) is emitted with +whichever timing fields are set on that probe. The command is built as an +argument list and invokes valkey-cli directly (no shell), with the TLS flags +appended only when `tls.enabled` is set. Returns nothing when no probe is +enabled, so callers should guard with `with`. +*/}} +{{- define "valkey.healthProbes" -}} +{{- $cmd := list "valkey-cli" -}} +{{- if $.Values.tls.enabled -}} +{{- $cmd = concat $cmd (list "--cacert" (printf "/tls/%s" $.Values.tls.caPublicKey) "--tls") -}} +{{- end -}} +{{- $cmd = append $cmd "ping" -}} +{{- $probes := dict -}} +{{- range $name := (list "startupProbe" "livenessProbe" "readinessProbe") -}} +{{- $probe := index $.Values $name -}} +{{- if $probe -}} +{{- if $probe.enabled -}} +{{- if $probe.customProbe -}} +{{- $probes = set $probes $name $probe.customProbe -}} +{{- else -}} +{{- $rendered := dict "exec" (dict "command" $cmd) -}} +{{- range $field := (list "initialDelaySeconds" "periodSeconds" "timeoutSeconds" "failureThreshold" "successThreshold") -}} +{{- if hasKey $probe $field -}}{{- $rendered = set $rendered $field (index $probe $field) -}}{{- end -}} +{{- end -}} +{{- $probes = set $probes $name $rendered -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $probes -}} +{{- toYaml $probes -}} +{{- end -}} +{{- end -}} + diff --git a/charts/valkey/templates/configmap.yaml b/charts/valkey/templates/configmap.yaml new file mode 100644 index 0000000..9176d12 --- /dev/null +++ b/charts/valkey/templates/configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.valkeyConfig }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "valkey.fullname" . }}-config + labels: + {{- include "valkey.labels" . | nindent 4 }} +data: + valkey.conf: | + {{- .Values.valkeyConfig | nindent 4 }} +{{- end }} diff --git a/charts/valkey/templates/deploy_valkey.yaml b/charts/valkey/templates/deploy_valkey.yaml new file mode 100644 index 0000000..c5853d4 --- /dev/null +++ b/charts/valkey/templates/deploy_valkey.yaml @@ -0,0 +1,302 @@ +{{- if not .Values.replica.enabled }} +{{- $fullname := include "valkey.fullname" . }} +{{- $storage := .Values.dataStorage }} +{{- $createPVC := and $storage.enabled (not (empty $storage.requestedSize)) (empty $storage.persistentVolumeClaimName) }} +{{- include "valkey.validateAuthConfig" . }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + {{- with .Values.workloadAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: 1 + strategy: + type: {{ .Values.deploymentStrategy }} + selector: + matchLabels: + {{- include "valkey.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "valkey.selectorLabels" . | nindent 8 }} + {{- with .Values.commonLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + checksum/initconfig: {{ include (print $.Template.BasePath "/init_config.yaml") . | sha256sum | trunc 32 }} + {{- if .Values.valkeyConfig }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | trunc 32 }} + {{- end }} + spec: + {{- include "valkey.imagePullSecrets" . | nindent 6 }} + automountServiceAccountToken: {{ .Values.serviceAccount.automount }} + serviceAccountName: {{ include "valkey.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} + {{- if .Values.runtimeClassName }} + runtimeClassName: {{ .Values.runtimeClassName | quote }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: {{ include "valkey.fullname" . }}-init + image: {{ include "valkey.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + command: [ "/scripts/init.sh" ] + volumeMounts: + - name: {{ .Values.dataStorage.volumeName }} + mountPath: /data + {{- if .Values.dataStorage.subPath }} + subPath: {{ .Values.dataStorage.subPath }} + {{- end }} + - name: scripts + mountPath: /scripts + {{- if .Values.valkeyConfig }} + - name: valkey-config + mountPath: /usr/local/etc/valkey/valkey.conf + subPath: valkey.conf + {{- end }} + {{- if .Values.extraSecretValkeyConfigs }} + - name: extravalkeyconfigs-volume + mountPath: /extravalkeyconfigs + {{- end }} + {{- if .Values.auth.enabled }} + - name: valkey-acl + mountPath: /etc/valkey + {{- if .Values.auth.usersExistingSecret }} + - name: valkey-users-secret + mountPath: /valkey-users-secret + readOnly: true + {{- end }} + {{- if or (include "valkey.hasInlinePasswords" . | eq "true") .Values.auth.aclConfig }} + - name: valkey-auth-secret + mountPath: /valkey-auth-secret + readOnly: true + {{- end }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.initResources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "valkey.fullname" . }} + image: {{ include "valkey.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: [ "valkey-server" ] + args: [ "/data/conf/valkey.conf" ] + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + env: + {{- range $key, $val := .Values.env }} + - name: {{ $key }} + value: "{{ $val }}" + {{- end }} + - name: VALKEY_LOG_LEVEL + value: "{{ .Values.valkeyLogLevel }}" + ports: + - name: tcp + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with (include "valkey.healthProbes" .) }} + {{- . | nindent 10 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: {{ .Values.dataStorage.volumeName }} + mountPath: /data + {{- if .Values.dataStorage.subPath }} + subPath: {{ .Values.dataStorage.subPath }} + {{- end }} + {{- if .Values.tls.enabled }} + - name: {{ include "valkey.fullname" . }}-tls + mountPath: /tls + {{- end }} + {{- if .Values.auth.enabled }} + - name: valkey-acl + mountPath: /etc/valkey + {{- end }} + {{- range $secret := .Values.extraValkeySecrets }} + - name: {{ $secret.name }}-valkey + mountPath: {{ $secret.mountPath }} + {{- end }} + {{- range $config := .Values.extraValkeyConfigs }} + - name: {{ $config.name }}-valkey + mountPath: {{ $config.mountPath }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics + image: {{ include "valkey.metrics.exporter.image" . }} + imagePullPolicy: {{ .Values.metrics.exporter.image.pullPolicy | quote }} + {{- with .Values.metrics.exporter.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: metrics + containerPort: {{ .Values.metrics.exporter.port }} + startupProbe: + tcpSocket: + port: metrics + livenessProbe: + tcpSocket: + port: metrics + readinessProbe: + httpGet: + path: / + port: metrics + {{- with .Values.metrics.exporter.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: REDIS_ALIAS + value: {{ include "valkey.fullname" . }} + {{- if .Values.auth.enabled }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.auth.usersExistingSecret }} + {{- $defaultUser := index .Values.auth.aclUsers "default" | default dict }} + {{- $passwordKey := $defaultUser.passwordKey | default "default" }} + name: {{ tpl .Values.auth.usersExistingSecret . }} + key: {{ $passwordKey }} + {{- else }} + name: {{ include "valkey.fullname" . }}-auth + key: default-password + {{- end }} + {{- end }} + {{- range $key, $val := .Values.metrics.exporter.extraEnvs }} + - name: {{ $key }} + value: "{{ $val }}" + {{- end }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: scripts + configMap: + name: {{ include "valkey.fullname" . }}-init-scripts + defaultMode: 0555 + {{- if .Values.auth.enabled }} + - name: valkey-acl + emptyDir: + medium: Memory + {{- end }} + {{- if .Values.valkeyConfig }} + - name: valkey-config + configMap: + name: {{ include "valkey.fullname" . }}-config + {{- end }} + {{- range .Values.extraValkeySecrets }} + - name: {{ .name }}-valkey + secret: + secretName: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- if .Values.tls.enabled }} + - name: {{ include "valkey.fullname" . }}-tls + secret: + secretName: {{ required "An existing secret is required to enable TLS" .Values.tls.existingSecret }} + defaultMode: 0400 + {{- end }} + {{- range .Values.extraValkeyConfigs }} + - name: {{ .name }}-valkey + configMap: + name: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- if .Values.metrics.enabled }} + {{- range .Values.metrics.exporter.extraExporterSecrets }} + - name: {{ .name }}-exporter + secret: + secretName: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- end }} + {{- if .Values.auth.enabled }} + {{- if .Values.auth.usersExistingSecret }} + - name: valkey-users-secret + secret: + secretName: {{ tpl .Values.auth.usersExistingSecret . }} + defaultMode: 0400 + {{- end }} + {{- if or (include "valkey.hasInlinePasswords" . | eq "true") .Values.auth.aclConfig }} + - name: valkey-auth-secret + secret: + secretName: {{ include "valkey.fullname" . }}-auth + defaultMode: 0400 + {{- end }} + {{- end }} + - name: {{ $storage.volumeName }} + {{- if $storage.persistentVolumeClaimName }} + persistentVolumeClaim: + claimName: {{ $storage.persistentVolumeClaimName }} + {{- else if $createPVC }} + persistentVolumeClaim: + claimName: {{ include "valkey.fullname" . }} + {{- else if $storage.hostPath }} + hostPath: + path: {{ $storage.hostPath }} + type: DirectoryOrCreate + {{- else }} + emptyDir: {} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/valkey/templates/init_config.yaml b/charts/valkey/templates/init_config.yaml new file mode 100644 index 0000000..9b0337e --- /dev/null +++ b/charts/valkey/templates/init_config.yaml @@ -0,0 +1,231 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "valkey.fullname" . }}-init-scripts + labels: + {{- include "valkey.labels" . | nindent 4 }} +data: + init.sh: |- + #!/bin/sh + set -eu + + # Default config paths + VALKEY_CONFIG=${VALKEY_CONFIG_PATH:-/data/conf/valkey.conf} + + LOGFILE="/data/init.log" + DATA_DIR="/data/conf" + + # Logging function (outputs to stderr and file) + log() { + echo "$(date) $1" | tee -a "$LOGFILE" >&2 + } + + {{- if .Values.auth.enabled }} + # Function to get password for a user + # Usage: get_user_password [password_key] + # Returns: password via stdout, exits with error if not found + get_user_password() { + username="$1" + password_key="${2:-$username}" + password="" + + {{- if .Values.auth.usersExistingSecret }} + # Try to get password from existing secret first (priority) + if [ -f "/valkey-users-secret/$password_key" ]; then + password=$(cat "/valkey-users-secret/$password_key") + log "Using password from existing secret for user $username" + elif [ -f "/valkey-auth-secret/${username}-password" ]; then + # Fallback to inline password + password=$(cat "/valkey-auth-secret/${username}-password") + log "Using inline password for user $username" + else + log "ERROR: No password found for user $username" + return 1 + fi + {{- else }} + # Use inline password only + if [ -f "/valkey-auth-secret/${username}-password" ]; then + password=$(cat "/valkey-auth-secret/${username}-password") + log "Using inline password for user $username" + else + log "ERROR: No password found for user $username" + return 1 + fi + {{- end }} + + echo "$password" + } + {{- end }} + + # Clean old log if requested + if [ "${KEEP_OLD_LOGS:-false}" != "true" ]; then + rm -f "$LOGFILE" + fi + + if [ -f "$LOGFILE" ]; then + log "Detected restart of this instance ($HOSTNAME)" + fi + + log "Creating configuration in $DATA_DIR..." + mkdir -p "$DATA_DIR" + rm -f "$VALKEY_CONFIG" + + + # Base valkey.conf + log "Generating base valkey.conf" + { +{{- if .Values.tls.enabled }} + echo "tls-cert-file /tls/{{ .Values.tls.serverPublicKey }}" + echo "tls-key-file /tls/{{ .Values.tls.serverKey }}" + echo "tls-ca-cert-file /tls/{{ .Values.tls.caPublicKey }}" + {{- if .Values.tls.dhParamKey }} + echo "tls-dh-params-file /tls/{{ .Values.tls.dhParamKey }}" + {{- end }} + {{- if not .Values.tls.requireClientCertificate }} + echo "tls-auth-clients no" + {{- end }} + echo "port 0" + echo "tls-port 6379" +{{- else }} + echo "port 6379" +{{- end }} + echo "protected-mode no" + echo "bind * -::*" + echo "dir /data" + } >>"$VALKEY_CONFIG" + + {{- if .Values.auth.enabled }} + # Create secure directory for ACL file + log "Creating /etc/valkey directory for ACL file" + mkdir -p /etc/valkey + + # Set aclfile path in valkey.conf + echo "aclfile /etc/valkey/users.acl" >>"$VALKEY_CONFIG" + + # Remove or reset existing ACL file if present (it may be read-only from previous run) + log "Preparing ACL file at /etc/valkey/users.acl" + if [ -f /etc/valkey/users.acl ]; then + log "Removing existing read-only users.acl file" + chmod 0600 /etc/valkey/users.acl + rm -f /etc/valkey/users.acl + fi + + # Create ACL file with secure permissions + touch /etc/valkey/users.acl + chmod 0600 /etc/valkey/users.acl + + {{- if .Values.auth.aclUsers }} + # Generate ACL entries for each user + log "Generating ACL entries for users" + {{- range $username, $user := .Values.auth.aclUsers }} + {{- $passwordKey := $user.passwordKey | default $username }} + + # User: {{ $username }} + PASSWORD=$(get_user_password "{{ $username }}" "{{ $passwordKey }}") || exit 1 + + # Hash the password and write ACL entry + PASSHASH=$(echo -n "$PASSWORD" | sha256sum | cut -f 1 -d " ") + echo "user {{ $username }} on #$PASSHASH {{ $user.permissions }}" >> /etc/valkey/users.acl + + {{- end }} + {{- end }} + + {{- if .Values.auth.aclConfig }} + # Append inline ACL configuration + log "Appending custom ACL configuration" + if [ -f /valkey-auth-secret/aclConfig ]; then + cat /valkey-auth-secret/aclConfig >> /etc/valkey/users.acl + fi + {{- end }} + + # Set final permissions + chmod 0400 /etc/valkey/users.acl + log "ACL file created with 0400 permissions" + {{- end }} + + {{- if .Values.replica.enabled }} + # Replica mode configuration + log "Configuring replication mode" + + # Use POD_INDEX from Kubernetes metadata + POD_INDEX=${POD_INDEX:-0} + IS_MASTER=false + + # Check if this is pod-0 (master) + if [ "$POD_INDEX" = "0" ]; then + IS_MASTER=true + log "This pod (index $POD_INDEX) is configured as MASTER" + else + log "This pod (index $POD_INDEX) is configured as REPLICA" + fi + + # Configure replica settings + if [ "$IS_MASTER" = "false" ]; then + MASTER_HOST="{{ include "valkey.fullname" . }}-0.{{ include "valkey.headlessServiceName" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}" + MASTER_PORT="{{ .Values.service.port }}" + + log "Configuring replica to follow master at $MASTER_HOST:$MASTER_PORT" + + { + echo "" + echo "# Replica Configuration" + echo "replicaof $MASTER_HOST $MASTER_PORT" + echo "replica-announce-ip {{ include "valkey.fullname" . }}-$POD_INDEX.{{ include "valkey.headlessServiceName" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}" + {{- if .Values.replica.disklessSync }} + echo "" + echo "# Diskless replication" + echo "repl-diskless-sync yes" + echo "repl-diskless-sync-delay 5" + {{- end }} + {{- if .Values.auth.enabled }} + echo "" + echo "# Master authentication" + {{- end }} + } >>"$VALKEY_CONFIG" + + {{- if .Values.auth.enabled }} + # Get the password for the replication user + {{- $replUsername := .Values.replica.replicationUser }} + {{- $replUser := index .Values.auth.aclUsers $replUsername }} + {{- $replPasswordKey := $replUser.passwordKey | default $replUsername }} + REPL_PASSWORD=$(get_user_password "{{ $replUsername }}" "{{ $replPasswordKey }}") || exit 1 + + # Write masterauth configuration + echo "masterauth $REPL_PASSWORD" >>"$VALKEY_CONFIG" + echo "masteruser {{ $replUsername }}" >>"$VALKEY_CONFIG" + log "Configured masterauth with user {{ $replUsername }}" + {{- end }} + + {{- if .Values.tls.enabled }} + # TLS for replication + { + echo "" + echo "# TLS for replication" + echo "tls-replication yes" + } >>"$VALKEY_CONFIG" + log "Enabled TLS for replication" + {{- end }} + fi + + {{- if gt (int .Values.replica.minReplicasToWrite) 0 }} + # Write safety - require minimum healthy replicas + { + echo "" + echo "# Minimum replicas for write operations" + echo "min-replicas-to-write {{ .Values.replica.minReplicasToWrite }}" + echo "min-replicas-max-lag {{ .Values.replica.minReplicasMaxLag }}" + } >>"$VALKEY_CONFIG" + log "Configured write safety: require {{ .Values.replica.minReplicasToWrite }} replicas with max {{ .Values.replica.minReplicasMaxLag }}s lag" + {{- end }} + {{- end }} + + # Append extra configs if present + if [ -f /usr/local/etc/valkey/valkey.conf ]; then + log "Appending /usr/local/etc/valkey/valkey.conf" + cat /usr/local/etc/valkey/valkey.conf >>"$VALKEY_CONFIG" + fi + if [ -d /extravalkeyconfigs ]; then + log "Appending files in /extravalkeyconfigs/" + cat /extravalkeyconfigs/* >>"$VALKEY_CONFIG" + fi + diff --git a/charts/valkey/templates/metrics-svc.yaml b/charts/valkey/templates/metrics-svc.yaml new file mode 100644 index 0000000..ffd71d7 --- /dev/null +++ b/charts/valkey/templates/metrics-svc.yaml @@ -0,0 +1,29 @@ +{{- if and .Values.metrics.enabled .Values.metrics.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ printf "%s-metrics" (include "valkey.fullname" .) }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/component: metrics + app.kubernetes.io/part-of: valkey + {{- with .Values.metrics.service.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + {{- with .Values.metrics.service.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.metrics.service.type }} + ports: + - name: metrics + port: {{ .Values.metrics.service.ports.http }} + protocol: TCP + targetPort: metrics + {{- if .Values.metrics.service.appProtocol }} + appProtocol: {{ .Values.metrics.service.appProtocol }} + {{- end }} + selector: + {{- include "valkey.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/valkey/templates/netpolicy.yaml b/charts/valkey/templates/netpolicy.yaml new file mode 100644 index 0000000..f65c504 --- /dev/null +++ b/charts/valkey/templates/netpolicy.yaml @@ -0,0 +1,33 @@ +{{- with .Values.networkPolicy }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "valkey.fullname" $ }} + {{- with .labels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + podSelector: + matchLabels: + {{- include "valkey.selectorLabels" $ | nindent 6 }} + policyTypes: + {{- if .ingress }} + - Ingress + {{- end }} + {{- if .egress }} + - Egress + {{- end }} + {{- with .ingress }} + ingress: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .egress }} + egress: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/valkey/templates/poddisruptionbudget.yaml b/charts/valkey/templates/poddisruptionbudget.yaml new file mode 100644 index 0000000..ff12352 --- /dev/null +++ b/charts/valkey/templates/poddisruptionbudget.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.podDisruptionBudget.enabled .Values.replica.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} +spec: + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "valkey.selectorLabels" . | nindent 6 }} + {{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} +{{- end }} diff --git a/charts/valkey/templates/podmonitor.yaml b/charts/valkey/templates/podmonitor.yaml new file mode 100644 index 0000000..3a22d6b --- /dev/null +++ b/charts/valkey/templates/podmonitor.yaml @@ -0,0 +1,53 @@ +{{- if and .Values.metrics.enabled .Values.metrics.podMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/part-of: valkey + app.kubernetes.io/component: podmonitor + {{- with .Values.metrics.podMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.metrics.podMonitor.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + podMetricsEndpoints: + - port: {{ .Values.metrics.podMonitor.port }} + {{- with .Values.metrics.podMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.metrics.podMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .Values.metrics.podMonitor.honorLabels }} + honorLabels: {{ . }} + {{- end }} + {{- with .Values.metrics.podMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.metrics.podMonitor.metricRelabelings }} + metricRelabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.metrics.podMonitor.podTargetLabels }} + podTargetLabels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.metrics.podMonitor.sampleLimit -}} + sampleLimit: {{ . }} + {{- end }} + {{- with .Values.metrics.podMonitor.targetLimit -}} + targetLimit: {{ . }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "valkey.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/valkey/templates/prometheusrules.yaml b/charts/valkey/templates/prometheusrules.yaml new file mode 100644 index 0000000..ab3ba4a --- /dev/null +++ b/charts/valkey/templates/prometheusrules.yaml @@ -0,0 +1,22 @@ +{{- if and .Values.metrics.enabled .Values.metrics.prometheusRule.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/part-of: valkey + {{- if .Values.metrics.prometheusRule.extraLabels }} + {{- toYaml .Values.metrics.prometheusRule.extraLabels | nindent 4 }} + {{- end }} + {{- with .Values.metrics.prometheusRule.extraAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- with .Values.metrics.prometheusRule.rules }} + groups: + - name: {{ include "valkey.fullname" $ }} + rules: {{ tpl (toYaml .) $ | nindent 8 }} +{{- end }} +{{- end }} diff --git a/charts/valkey/templates/pvc.yaml b/charts/valkey/templates/pvc.yaml new file mode 100644 index 0000000..aa20859 --- /dev/null +++ b/charts/valkey/templates/pvc.yaml @@ -0,0 +1,30 @@ +{{- if and .Values.dataStorage.enabled (not .Values.replica.enabled) (not (empty .Values.dataStorage.requestedSize)) (empty .Values.dataStorage.persistentVolumeClaimName) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + {{- with .Values.dataStorage.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if or .Values.dataStorage.keepPvc .Values.dataStorage.annotations }} + annotations: + {{- if .Values.dataStorage.keepPvc }} + "helm.sh/resource-policy": keep + {{- end }} + {{- with .Values.dataStorage.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + accessModes: + {{- toYaml .Values.dataStorage.accessModes | nindent 4 }} + volumeMode: Filesystem + resources: + requests: + storage: {{ .Values.dataStorage.requestedSize }} + {{- if .Values.dataStorage.className }} + storageClassName: {{ .Values.dataStorage.className }} + {{- end }} +{{- end }} diff --git a/charts/valkey/templates/secret.yaml b/charts/valkey/templates/secret.yaml new file mode 100644 index 0000000..3e90978 --- /dev/null +++ b/charts/valkey/templates/secret.yaml @@ -0,0 +1,20 @@ +{{- if .Values.auth.enabled }} +{{- if or (include "valkey.hasInlinePasswords" . | eq "true") .Values.auth.aclConfig }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "valkey.fullname" . }}-auth + labels: + {{- include "valkey.labels" . | nindent 4 }} +type: Opaque +data: + {{- range $username, $user := .Values.auth.aclUsers }} + {{- if $user.password }} + {{ $username }}-password: {{ $user.password | b64enc }} + {{- end }} + {{- end }} + {{- if .Values.auth.aclConfig }} + aclConfig: {{ .Values.auth.aclConfig | b64enc }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/valkey/templates/service-headless.yaml b/charts/valkey/templates/service-headless.yaml new file mode 100644 index 0000000..733ca68 --- /dev/null +++ b/charts/valkey/templates/service-headless.yaml @@ -0,0 +1,20 @@ +{{- if .Values.replica.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "valkey.fullname" . }}-headless + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/component: headless +spec: + type: ClusterIP + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: tcp + port: {{ .Values.service.port }} + targetPort: tcp + protocol: TCP + selector: + {{- include "valkey.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/valkey/templates/service-read.yaml b/charts/valkey/templates/service-read.yaml new file mode 100644 index 0000000..49ec54e --- /dev/null +++ b/charts/valkey/templates/service-read.yaml @@ -0,0 +1,37 @@ +{{- if and .Values.replica.enabled .Values.replica.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "valkey.fullname" . }}-read + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/component: read + {{- with .Values.replica.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.replica.service.type }} + {{- if .Values.replica.service.clusterIP }} + clusterIP: {{ .Values.replica.service.clusterIP }} + {{- end }} + {{- if .Values.replica.service.loadBalancerClass }} + loadBalancerClass: {{ .Values.replica.service.loadBalancerClass }} + {{- end }} + {{- if .Values.replica.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{ .Values.replica.service.loadBalancerSourceRanges }} + {{- end }} + ports: + - name: tcp + port: {{ .Values.replica.service.port }} + targetPort: tcp + protocol: TCP + {{- if and (eq .Values.replica.service.type "NodePort") .Values.replica.service.nodePort }} + nodePort: {{ .Values.replica.service.nodePort }} + {{- end }} + {{- if .Values.replica.service.appProtocol }} + appProtocol: {{ .Values.replica.service.appProtocol }} + {{- end }} + selector: + {{- include "valkey.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/valkey/templates/service.yaml b/charts/valkey/templates/service.yaml new file mode 100644 index 0000000..1e78682 --- /dev/null +++ b/charts/valkey/templates/service.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if .Values.service.clusterIP }} + clusterIP: {{ .Values.service.clusterIP }} + {{- end }} + {{- if .Values.service.loadBalancerClass }} + loadBalancerClass: {{ .Values.service.loadBalancerClass }} + {{- end }} + {{- if .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges }} + {{- end }} + ports: + - port: {{ .Values.service.port }} + targetPort: tcp + protocol: TCP + name: tcp + {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + {{- if .Values.service.appProtocol }} + appProtocol: {{ .Values.service.appProtocol }} + {{- end }} + selector: + {{- include "valkey.selectorLabels" . | nindent 4 }} + {{- if .Values.replica.enabled }} + statefulset.kubernetes.io/pod-name: {{ include "valkey.fullname" . }}-0 + {{- end }} diff --git a/charts/valkey/templates/serviceaccount.yaml b/charts/valkey/templates/serviceaccount.yaml new file mode 100644 index 0000000..e057715 --- /dev/null +++ b/charts/valkey/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "valkey.serviceAccountName" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/valkey/templates/servicemonitor.yaml b/charts/valkey/templates/servicemonitor.yaml new file mode 100644 index 0000000..e46d8e6 --- /dev/null +++ b/charts/valkey/templates/servicemonitor.yaml @@ -0,0 +1,54 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled .Values.metrics.service.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + app.kubernetes.io/part-of: valkey + app.kubernetes.io/component: service-monitor + {{- with .Values.metrics.serviceMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: {{ .Values.metrics.serviceMonitor.port }} + {{- with .Values.metrics.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.honorLabels }} + honorLabels: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.metricRelabelings }} + metricRelabelings: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.podTargetLabels }} + podTargetLabels: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.sampleLimit }} + sampleLimit: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.targetLimit }} + targetLimit: {{ . }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "valkey.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: metrics +{{- end }} diff --git a/charts/valkey/templates/statefulset.yaml b/charts/valkey/templates/statefulset.yaml new file mode 100644 index 0000000..08b72e8 --- /dev/null +++ b/charts/valkey/templates/statefulset.yaml @@ -0,0 +1,304 @@ +{{- if .Values.replica.enabled }} +{{- include "valkey.validateAuthConfig" . }} +{{- include "valkey.validateReplicaPersistence" . }} +{{- include "valkey.validateReplicaAuth" . }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "valkey.fullname" . }} + labels: + {{- include "valkey.labels" . | nindent 4 }} + {{- with .Values.workloadAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + serviceName: {{ include "valkey.fullname" . }}-headless + replicas: {{ add (int .Values.replica.replicas) 1 }} + podManagementPolicy: OrderedReady + {{- if .Values.replica.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + {{- toYaml .Values.replica.persistentVolumeClaimRetentionPolicy | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "valkey.selectorLabels" . | nindent 6 }} + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: valkey-data + spec: + accessModes: {{ toYaml .Values.replica.persistence.accessModes | nindent 8 }} + {{- if .Values.replica.persistence.storageClass }} + storageClassName: {{ .Values.replica.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.replica.persistence.size | quote }} + template: + metadata: + labels: + {{- include "valkey.selectorLabels" . | nindent 8 }} + {{- with .Values.commonLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + checksum/initconfig: {{ include (print $.Template.BasePath "/init_config.yaml") . | sha256sum | trunc 32 | quote }} + {{- if .Values.valkeyConfig }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | trunc 32 | quote }} + {{- end }} + spec: + {{- (include "valkey.imagePullSecrets" .) | nindent 6 }} + automountServiceAccountToken: {{ .Values.serviceAccount.automount }} + serviceAccountName: {{ include "valkey.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} + {{- if .Values.runtimeClassName }} + runtimeClassName: {{ .Values.runtimeClassName | quote }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: {{ include "valkey.fullname" . }}-init + image: {{ include "valkey.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + command: [ "/scripts/init.sh" ] + env: + - name: POD_INDEX + valueFrom: + fieldRef: + fieldPath: metadata.labels['apps.kubernetes.io/pod-index'] + volumeMounts: + - name: valkey-data + mountPath: /data + - name: scripts + mountPath: /scripts + {{- if .Values.valkeyConfig }} + - name: valkey-config + mountPath: /usr/local/etc/valkey/valkey.conf + subPath: valkey.conf + {{- end }} + {{- if .Values.extraSecretValkeyConfigs }} + - name: extravalkeyconfigs-volume + mountPath: /extravalkeyconfigs + {{- end }} + {{- if .Values.auth.enabled }} + - name: valkey-acl + mountPath: /etc/valkey + {{- if .Values.auth.usersExistingSecret }} + - name: valkey-users-secret + mountPath: /valkey-users-secret + readOnly: true + {{- end }} + {{- if or (include "valkey.hasInlinePasswords" . | eq "true") .Values.auth.aclConfig }} + - name: valkey-auth-secret + mountPath: /valkey-auth-secret + readOnly: true + {{- end }} + {{- end }} + {{- with .Values.initResources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraInitContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "valkey.fullname" . }} + image: {{ include "valkey.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: [ "valkey-server" ] + args: [ "/data/conf/valkey.conf" ] + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + env: + - name: POD_INDEX + valueFrom: + fieldRef: + fieldPath: metadata.labels['apps.kubernetes.io/pod-index'] + {{- range $key, $val := .Values.env }} + - name: {{ $key }} + value: "{{ $val }}" + {{- end }} + - name: VALKEY_LOG_LEVEL + value: "{{ .Values.valkeyLogLevel }}" + ports: + - name: tcp + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with (include "valkey.healthProbes" .) }} + {{- . | nindent 10 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: valkey-data + mountPath: /data + {{- if .Values.tls.enabled }} + - name: {{ include "valkey.fullname" . }}-tls + mountPath: /tls + {{- end }} + {{- if .Values.auth.enabled }} + - name: valkey-acl + mountPath: /etc/valkey + {{- end }} + {{- range $secret := .Values.extraValkeySecrets }} + - name: {{ $secret.name }}-valkey + mountPath: {{ $secret.mountPath }} + {{- end }} + {{- range $config := .Values.extraValkeyConfigs }} + - name: {{ $config.name }}-valkey + mountPath: {{ $config.mountPath }} + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics + image: {{ include "valkey.metrics.exporter.image" . }} + imagePullPolicy: {{ .Values.metrics.exporter.image.pullPolicy | quote }} + {{- with .Values.metrics.exporter.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: metrics + containerPort: {{ .Values.metrics.exporter.port }} + startupProbe: + tcpSocket: + port: metrics + livenessProbe: + tcpSocket: + port: metrics + readinessProbe: + httpGet: + path: / + port: metrics + {{- with .Values.metrics.exporter.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.metrics.exporter.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: REDIS_ALIAS + value: {{ include "valkey.fullname" . }} + {{- if .Values.auth.enabled }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.auth.usersExistingSecret }} + {{- $defaultUser := index .Values.auth.aclUsers "default" | default dict }} + {{- $passwordKey := $defaultUser.passwordKey | default "default" }} + name: {{ tpl .Values.auth.usersExistingSecret . }} + key: {{ $passwordKey }} + {{- else }} + name: {{ include "valkey.fullname" . }}-auth + key: default-password + {{- end }} + {{- end }} + {{- range $key, $val := .Values.metrics.exporter.extraEnvs }} + - name: {{ $key }} + value: "{{ $val }}" + {{- end }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: scripts + configMap: + name: {{ include "valkey.fullname" . }}-init-scripts + defaultMode: 0555 + {{- if .Values.auth.enabled }} + - name: valkey-acl + emptyDir: + medium: Memory + {{- end }} + {{- if .Values.valkeyConfig }} + - name: valkey-config + configMap: + name: {{ include "valkey.fullname" . }}-config + {{- end }} + {{- range .Values.extraValkeySecrets }} + - name: {{ .name }}-valkey + secret: + secretName: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- if .Values.tls.enabled }} + - name: {{ include "valkey.fullname" . }}-tls + secret: + secretName: {{ required "An existing secret is required to enable TLS" .Values.tls.existingSecret }} + defaultMode: 0400 + {{- end }} + {{- range .Values.extraValkeyConfigs }} + - name: {{ .name }}-valkey + configMap: + name: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- if .Values.metrics.enabled }} + {{- range .Values.metrics.exporter.extraExporterSecrets }} + - name: {{ .name }}-exporter + secret: + secretName: {{ .name }} + defaultMode: {{ .defaultMode | default 0440 }} + {{- end }} + {{- end }} + {{- if .Values.auth.enabled }} + {{- if .Values.auth.usersExistingSecret }} + - name: valkey-users-secret + secret: + secretName: {{ tpl .Values.auth.usersExistingSecret . }} + defaultMode: 0400 + {{- end }} + {{- if or (include "valkey.hasInlinePasswords" . | eq "true") .Values.auth.aclConfig }} + - name: valkey-auth-secret + secret: + secretName: {{ include "valkey.fullname" . }}-auth + defaultMode: 0400 + {{- end }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/valkey/templates/tests/auth.yaml b/charts/valkey/templates/tests/auth.yaml new file mode 100644 index 0000000..e814dc0 --- /dev/null +++ b/charts/valkey/templates/tests/auth.yaml @@ -0,0 +1,144 @@ +{{- if .Values.auth.enabled }} +{{- if include "valkey.hasInlinePasswords" . | eq "true" }} +{{- $firstUsername := "" }} +{{- range $username, $user := .Values.auth.aclUsers }} + {{- if $user.password }} + {{- if not $firstUsername }} + {{- $firstUsername = $username }} + {{- end }} + {{- end }} +{{- end }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "valkey.fullname" . }}-test-auth-generated + labels: + {{- include "valkey.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: test-auth + image: {{ include "valkey.image" . | quote }} + command: + - sh + - -c + - | + set -e + echo "Testing authentication with user {{ $firstUsername }} (inline passwords)..." + + # Extract password from secret + PASSWORD=$(cat /valkey-auth/{{ $firstUsername }}-password) + USERNAME="{{ $firstUsername }}" + + {{- if .Values.tls.enabled }} + # TLS flags + TLS_FLAGS="--tls --cacert /tls/{{ .Values.tls.caPublicKey }}" + {{- else }} + TLS_FLAGS="" + {{- end }} + + # Test authentication + PING_RESULT=$(valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }} $TLS_FLAGS --user "$USERNAME" --pass "$PASSWORD" PING) + if [ "$PING_RESULT" != "PONG" ]; then + echo "✗ Authentication test failed: expected 'PONG', got '$PING_RESULT'" + exit 1 + fi + echo "✓ PING successful: $PING_RESULT" + + # Test that we can set and get a value (if permissions allow) + if valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }} $TLS_FLAGS --user "$USERNAME" --pass "$PASSWORD" SET test-key-generated "test-value" 2>/dev/null; then + VALUE=$(valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }} $TLS_FLAGS --user "$USERNAME" --pass "$PASSWORD" GET test-key-generated) + if [ "$VALUE" = "test-value" ]; then + echo "✓ Authentication test passed with write permissions" + exit 0 + else + echo "✗ Authentication test failed: expected 'test-value', got '$VALUE'" + exit 1 + fi + else + echo "✓ Authentication test passed (read-only or restricted permissions)" + exit 0 + fi + volumeMounts: + - name: valkey-auth + mountPath: /valkey-auth + readOnly: true + {{- if .Values.tls.enabled }} + - name: valkey-tls + mountPath: /tls + readOnly: true + {{- end }} + volumes: + - name: valkey-auth + secret: + secretName: {{ include "valkey.fullname" . }}-auth + {{- if .Values.tls.enabled }} + - name: valkey-tls + secret: + secretName: {{ .Values.tls.existingSecret }} + {{- end }} +{{- end }} +{{- end }} +--- +{{- if and .Values.auth.enabled .Values.auth.usersExistingSecret }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "valkey.fullname" . }}-test-auth-existing + labels: + {{- include "valkey.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: test-auth + image: {{ include "valkey.image" . | quote }} + command: + - sh + - -c + - | + set -e + echo "Testing authentication with usersExistingSecret..." + + {{- if .Values.tls.enabled }} + # TLS flags + TLS_FLAGS="--tls --cacert /tls/{{ .Values.tls.caPublicKey }}" + {{- else }} + TLS_FLAGS="" + {{- end }} + + # Test basic connection (no auth - will fail if auth is properly configured) + PING_RESULT=$(valkey-cli -h {{ include "valkey.fullname" . }} -p {{ .Values.service.port }} $TLS_FLAGS PING 2>&1 || true) + if [ "$PING_RESULT" = "PONG" ]; then + echo "✗ Authentication test failed: server allows unauthenticated access" + exit 1 + fi + + echo "✓ Authentication is enforced (unauthenticated access denied)" + echo "✓ Received expected error: $PING_RESULT" + echo "⚠ Manual verification recommended for usersExistingSecret configuration" + exit 0 + volumeMounts: + - name: valkey-users-secret + mountPath: /valkey-users-secret + readOnly: true + {{- if .Values.tls.enabled }} + - name: valkey-tls + mountPath: /tls + readOnly: true + {{- end }} + volumes: + - name: valkey-users-secret + secret: + secretName: {{ tpl .Values.auth.usersExistingSecret . }} + {{- if .Values.tls.enabled }} + - name: valkey-tls + secret: + secretName: {{ .Values.tls.existingSecret }} + {{- end }} +{{- end }} diff --git a/charts/valkey/tests/auth_validation_test.yaml b/charts/valkey/tests/auth_validation_test.yaml new file mode 100644 index 0000000..62066c9 --- /dev/null +++ b/charts/valkey/tests/auth_validation_test.yaml @@ -0,0 +1,244 @@ +suite: auth configuration validation +templates: + - templates/deploy_valkey.yaml + - templates/statefulset.yaml + - templates/init_config.yaml +tests: + - it: should fail when auth enabled but no method configured + set: + auth.enabled: true + auth.aclUsers: {} + auth.aclConfig: "" + template: templates/deploy_valkey.yaml + asserts: + - failedTemplate: + errorPattern: "auth.enabled is true but no authentication method.*" + + - it: should fail when aclUsers defined but no default user + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + template: templates/deploy_valkey.yaml + asserts: + - failedTemplate: + errorPattern: "The 'default' user must be defined in auth.aclUsers.*" + + - it: should fail when user has no permissions + set: + auth.enabled: true + auth.aclUsers: + default: + password: "test" + template: templates/deploy_valkey.yaml + asserts: + - failedTemplate: + errorPattern: "User 'default' in auth.aclUsers must have a 'permissions' field" + + - it: should fail when user has no password and no usersExistingSecret + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + template: templates/deploy_valkey.yaml + asserts: + - failedTemplate: + errorPattern: "User 'default' must have either 'password' field or auth.usersExistingSecret must be set" + + - it: should fail when user has passwordKey but no usersExistingSecret + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "test" + passwordKey: "default-pwd" + template: templates/deploy_valkey.yaml + asserts: + - failedTemplate: + errorPattern: "User 'default' has passwordKey but auth.usersExistingSecret is not set" + + - it: should succeed with aclUsers and inline passwords only + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + readonly: + permissions: "~* +@read" + password: "readonly-password" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed with aclUsers and usersExistingSecret only + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + readonly: + permissions: "~* +@read" + passwordKey: "readonly-pwd" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed with templated usersExistingSecret + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret-{{ .Release.Name }}" + auth.aclUsers: + default: + permissions: "~* &* +@all" + readonly: + permissions: "~* +@read" + passwordKey: "readonly-pwd" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed with mixed inline and existing secret passwords + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "fallback-password" + passwordKey: "default-pwd" + readonly: + permissions: "~* +@read" + password: "readonly-password" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed when auth enabled with aclConfig only + set: + auth.enabled: true + auth.aclConfig: "user default on >password ~* &* +@all" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed with aclUsers and aclConfig combined + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + auth.aclConfig: "user guest on nopass ~public:* +@read" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should succeed when auth disabled + set: + auth.enabled: false + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + + - it: should fail when replica enabled with auth and replicationUser not in aclUsers + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "repl-user" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + admin: + permissions: "~* &* +@all" + password: "admin-password" + template: templates/statefulset.yaml + asserts: + - failedTemplate: + errorPattern: "Replication user 'repl-user'.*must be defined in auth.aclUsers.*" + + - it: should succeed when replica enabled with auth and replicationUser exists in aclUsers + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "repl-user" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + repl-user: + permissions: "+psync +replconf +ping" + password: "repl-password" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + + - it: should succeed when replica enabled with auth and default user as replicationUser + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "default" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + + - it: should succeed when replica enabled but auth disabled (no replication user check) + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "repl-user" + auth.enabled: false + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + + - it: should fail when replica enabled with aclConfig but replicationUser not in aclUsers + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "repl-user" + auth.enabled: true + auth.aclConfig: "user default on >password ~* &* +@all\nuser repl-user on >password +psync +replconf +ping" + template: templates/statefulset.yaml + asserts: + - failedTemplate: + errorPattern: "Replication user 'repl-user'.*must be defined in auth.aclUsers.*" + + - it: should fail when replica enabled with mixed aclUsers and aclConfig but replicationUser only in aclConfig + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "repl-user" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + auth.aclConfig: "user repl-user on >password +psync +replconf +ping" + template: templates/statefulset.yaml + asserts: + - failedTemplate: + errorPattern: "Replication user 'repl-user'.*must be defined in auth.aclUsers.*" diff --git a/charts/valkey/tests/deployment_test.yaml b/charts/valkey/tests/deployment_test.yaml new file mode 100644 index 0000000..419ab69 --- /dev/null +++ b/charts/valkey/tests/deployment_test.yaml @@ -0,0 +1,491 @@ +suite: deployment configuration +templates: + - templates/deploy_valkey.yaml + - templates/init_config.yaml +tests: + - it: should not have auth volumes when auth disabled + set: + auth.enabled: false + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - notExists: + path: spec.template.spec.volumes[?(@.name == "valkey-users-secret")] + - notExists: + path: spec.template.spec.volumes[?(@.name == "valkey-auth-secret")] + - notExists: + path: spec.template.spec.initContainers[0].volumeMounts[?(@.name == "valkey-users-secret")] + - notExists: + path: spec.template.spec.initContainers[0].volumeMounts[?(@.name == "valkey-auth-secret")] + + - it: should have generated volume with inline passwords + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: valkey-auth-secret + secret: + secretName: RELEASE-NAME-valkey-auth + defaultMode: 0400 + - contains: + path: spec.template.spec.initContainers[0].volumeMounts + content: + name: valkey-auth-secret + mountPath: /valkey-auth-secret + readOnly: true + + - it: should have users-secret volume with usersExistingSecret + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-custom-secret + defaultMode: 0400 + - contains: + path: spec.template.spec.initContainers[0].volumeMounts + content: + name: valkey-users-secret + mountPath: /valkey-users-secret + readOnly: true + + - it: should render users-secret volume with templated usersExistingSecret + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret-{{ .Release.Name }}" + auth.aclUsers: + default: + permissions: "~* &* +@all" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-custom-secret-RELEASE-NAME + defaultMode: 0400 + + - it: should have both volumes with mixed passwords + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + readonly: + permissions: "~* +@read" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-custom-secret + defaultMode: 0400 + - contains: + path: spec.template.spec.volumes + content: + name: valkey-auth-secret + secret: + secretName: RELEASE-NAME-valkey-auth + defaultMode: 0400 + + - it: should have generated volume with aclConfig only + set: + auth.enabled: true + auth.aclConfig: "user default on >password ~* &* +@all" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: valkey-auth-secret + secret: + secretName: RELEASE-NAME-valkey-auth + defaultMode: 0400 + + - it: should use custom image + set: + image.registry: custom-registry + image.repository: valkey/valkey + image.tag: "7.0.0" + template: templates/deploy_valkey.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: custom-registry/valkey/valkey:7.0.0 + + - it: should have service account + set: + serviceAccount.create: true + serviceAccount.name: "custom-sa" + template: templates/deploy_valkey.yaml + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: custom-sa + + - it: should use image pull policy for containers and initContainers + set: + image.pullPolicy: Always + template: templates/deploy_valkey.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: Always + + - it: should always have annotations block even without podAnnotations + set: + podAnnotations: null + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - exists: + path: spec.template.metadata.annotations + - exists: + path: spec.template.metadata.annotations["checksum/initconfig"] + + - it: should include podAnnotations when provided + set: + podAnnotations: + prometheus.io/scrape: "true" + custom.annotation: "value" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.metadata.annotations["prometheus.io/scrape"] + value: "true" + - equal: + path: spec.template.metadata.annotations["custom.annotation"] + value: value + - exists: + path: spec.template.metadata.annotations["checksum/initconfig"] + + - it: should use image pull policy for containers, initContainers and metrics + set: + image.pullPolicy: Always + metrics.enabled: true + metrics.exporter.image.pullPolicy: Always + template: templates/deploy_valkey.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.containers[1].imagePullPolicy + value: Always + + - it: should not have imagePullSecrets when not configured + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - notExists: + path: spec.template.spec.imagePullSecrets + + - it: should render imagePullSecrets from values.imagePullSecrets + set: + imagePullSecrets: + - myregistrykey + - anotherregistrykey + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: myregistrykey + - name: anotherregistrykey + + - it: should render imagePullSecrets from global.imagePullSecrets + set: + global.imagePullSecrets: + - globalregistrykey + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: globalregistrykey + + - it: should merge global and local imagePullSecrets + set: + global.imagePullSecrets: + - globalregistrykey + imagePullSecrets: + - localregistrykey + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: globalregistrykey + - name: localregistrykey + + - it: should include extra containers from overlay + set: + extraContainers: + - name: nginx-sidecar + image: registry.hub.docker.com/nginx/nginx:stable-alpine + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.containers[1].name + value: nginx-sidecar + + - it: should render extraVolumes + set: + extraVolumes: + - name: extra-config + configMap: + name: my-config + - name: extra-secret + secret: + secretName: my-secret + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: extra-config + configMap: + name: my-config + - contains: + path: spec.template.spec.volumes + content: + name: extra-secret + secret: + secretName: my-secret + + - it: should render extraVolumeMounts for main container + set: + extraVolumeMounts: + - name: extra-config + mountPath: /extra-config + readOnly: true + - name: extra-data + mountPath: /extra/data + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-config + mountPath: /extra-config + readOnly: true + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-data + mountPath: /extra/data + + - it: should render both extraVolumes and extraVolumeMounts together + set: + extraVolumes: + - name: extra-config + configMap: + name: my-config + extraVolumeMounts: + - name: extra-config + mountPath: /extra-config + readOnly: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.volumes + content: + name: extra-config + configMap: + name: my-config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-config + mountPath: /extra-config + readOnly: true + + - it: should include workload annotations when provided + set: + workloadAnnotations: + example.com/annotation: "test-value" + custom.annotation: "custom-value" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: metadata.annotations["example.com/annotation"] + value: "test-value" + - equal: + path: metadata.annotations["custom.annotation"] + value: "custom-value" + + - it: should not have REDIS_PASSWORD env in metrics exporter when auth is disabled + set: + auth.enabled: false + metrics.enabled: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - notContains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + + - it: should set REDIS_PASSWORD from generated secret in metrics exporter when auth is enabled + set: + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + metrics.enabled: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-valkey-auth + key: default-password + + - it: should set REDIS_PASSWORD from existing secret in metrics exporter when usersExistingSecret is set + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + metrics.enabled: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret + key: default + + - it: should render templated usersExistingSecret in metrics exporter + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret-{{ .Release.Name }}" + auth.aclUsers: + default: + permissions: "~* &* +@all" + metrics.enabled: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret-RELEASE-NAME + key: default + + - it: should set REDIS_PASSWORD using custom passwordKey in metrics exporter when usersExistingSecret is set + set: + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + passwordKey: "my-password-key" + metrics.enabled: true + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret + key: my-password-key + + - it: should not set runtimeClassName when empty + set: + runtimeClassName: "" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - notExists: + path: spec.template.spec.runtimeClassName + + - it: should set runtimeClassName when provided + set: + runtimeClassName: "gvisor" + template: templates/deploy_valkey.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: spec.template.spec.runtimeClassName + value: "gvisor" \ No newline at end of file diff --git a/charts/valkey/tests/healthprobes_test.yaml b/charts/valkey/tests/healthprobes_test.yaml new file mode 100644 index 0000000..765271a --- /dev/null +++ b/charts/valkey/tests/healthprobes_test.yaml @@ -0,0 +1,87 @@ +suite: health probes configuration +templates: + - templates/deploy_valkey.yaml + - templates/statefulset.yaml + - templates/init_config.yaml +tests: + - it: renders default startup and liveness probes on the Deployment valkey container + template: templates/deploy_valkey.yaml + asserts: + - exists: + path: spec.template.spec.containers[0].startupProbe + - exists: + path: spec.template.spec.containers[0].livenessProbe + - notExists: + path: spec.template.spec.containers[0].readinessProbe + - equal: + path: spec.template.spec.containers[0].livenessProbe.exec.command + value: [ "valkey-cli", "ping" ] + - equal: + path: spec.template.spec.containers[0].startupProbe.periodSeconds + value: 10 + - equal: + path: spec.template.spec.containers[0].livenessProbe.failureThreshold + value: 3 + + - it: renders default probes on the StatefulSet valkey container + template: templates/statefulset.yaml + set: + replica.enabled: true + replica.persistence.size: "5Gi" + asserts: + - exists: + path: spec.template.spec.containers[0].startupProbe + - exists: + path: spec.template.spec.containers[0].livenessProbe + - notExists: + path: spec.template.spec.containers[0].readinessProbe + + - it: enables the readiness probe with its success threshold when requested + template: templates/deploy_valkey.yaml + set: + readinessProbe.enabled: true + asserts: + - exists: + path: spec.template.spec.containers[0].readinessProbe + - equal: + path: spec.template.spec.containers[0].readinessProbe.successThreshold + value: 1 + - equal: + path: spec.template.spec.containers[0].readinessProbe.exec.command + value: [ "valkey-cli", "ping" ] + + - it: omits a probe when it is disabled + template: templates/deploy_valkey.yaml + set: + startupProbe.enabled: false + asserts: + - notExists: + path: spec.template.spec.containers[0].startupProbe + - exists: + path: spec.template.spec.containers[0].livenessProbe + + - it: replaces the default handler and timing when customProbe is set + template: templates/deploy_valkey.yaml + set: + livenessProbe.customProbe: + tcpSocket: + port: 6379 + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.tcpSocket.port + value: 6379 + - notExists: + path: spec.template.spec.containers[0].livenessProbe.exec + - notExists: + path: spec.template.spec.containers[0].livenessProbe.periodSeconds + + - it: uses the TLS-aware ping command when TLS is enabled + template: templates/deploy_valkey.yaml + set: + tls.enabled: true + tls.caPublicKey: ca.crt + tls.existingSecret: valkey-tls + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.exec.command + value: [ "valkey-cli", "--cacert", "/tls/ca.crt", "--tls", "ping" ] diff --git a/charts/valkey/tests/init_config_test.yaml b/charts/valkey/tests/init_config_test.yaml new file mode 100644 index 0000000..0b1067d --- /dev/null +++ b/charts/valkey/tests/init_config_test.yaml @@ -0,0 +1,147 @@ +suite: init configuration +templates: + - templates/init_config.yaml +tests: + - it: should not configure ACL when auth disabled + set: + auth.enabled: false + asserts: + - isKind: + of: ConfigMap + - notMatchRegex: + path: data["init.sh"] + pattern: "aclfile" + + - it: should configure ACL file at /etc/valkey/users.acl with inline passwords + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: "aclfile /etc/valkey/users\\.acl" + - matchRegex: + path: data["init.sh"] + pattern: "mkdir -p /etc/valkey" + - matchRegex: + path: data["init.sh"] + pattern: "chmod 0400 /etc/valkey/users\\.acl" + - matchRegex: + path: data["init.sh"] + pattern: 'cat "/valkey-auth-secret/\$\{username\}-password"' + + - it: should configure ACL file with usersExistingSecret + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + admin: + permissions: "~* &* +@all" + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: "aclfile /etc/valkey/users\\.acl" + - matchRegex: + path: data["init.sh"] + pattern: 'cat "/valkey-users-secret/\$password_key"' + + - it: should append aclConfig to generated users + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + auth.aclConfig: | + user guest on nopass ~public:* +@read + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: "aclfile /etc/valkey/users\\.acl" + - matchRegex: + path: data["init.sh"] + pattern: "Appending custom ACL configuration" + - matchRegex: + path: data["init.sh"] + pattern: "/valkey-auth-secret/aclConfig" + + - it: should have error handling for missing passwords + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: "ERROR.*No password found" + + - it: should use aclConfig only + set: + auth.enabled: true + auth.aclConfig: | + user admin on >password ~* &* +@all + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: "aclfile /etc/valkey/users\\.acl" + - matchRegex: + path: data["init.sh"] + pattern: "/valkey-auth-secret/aclConfig" + + - it: should use passwordKey for replica authentication when configured + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "default" + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + passwordKey: "valkey-password" + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: 'REPL_PASSWORD=\$\(get_user_password "default" "valkey-password"\)' + + - it: should default to username for replica authentication when passwordKey not configured + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.replicationUser: "default" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["init.sh"] + pattern: 'REPL_PASSWORD=\$\(get_user_password "default" "default"\)' + + - it: should have correct metadata + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: valkey diff --git a/charts/valkey/tests/poddisruptionbudget_test.yaml b/charts/valkey/tests/poddisruptionbudget_test.yaml new file mode 100644 index 0000000..dd7c607 --- /dev/null +++ b/charts/valkey/tests/poddisruptionbudget_test.yaml @@ -0,0 +1,179 @@ +suite: poddisruptionbudget configuration +templates: + - templates/poddisruptionbudget.yaml +tests: + - it: should not create PDB when disabled + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should not create PDB when replica is disabled + set: + replica.enabled: false + podDisruptionBudget.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should create PDB when enabled with replica + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.name + value: RELEASE-NAME-valkey + - equal: + path: spec.maxUnavailable + value: 1 + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/name"] + value: valkey + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/instance"] + value: RELEASE-NAME + + - it: should use minAvailable when set + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.minAvailable: 2 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.minAvailable + value: 2 + - isNull: + path: spec.maxUnavailable + + - it: should use minAvailable over maxUnavailable when both set + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.minAvailable: 3 + podDisruptionBudget.maxUnavailable: 2 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.minAvailable + value: 3 + - isNull: + path: spec.maxUnavailable + + - it: should use maxUnavailable when minAvailable is null + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.minAvailable: null + podDisruptionBudget.maxUnavailable: 2 + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.maxUnavailable + value: 2 + - isNull: + path: spec.minAvailable + + - it: should support percentage values for minAvailable + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.minAvailable: "50%" + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.minAvailable + value: "50%" + - isNull: + path: spec.maxUnavailable + + - it: should support percentage values for maxUnavailable + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.minAvailable: null + podDisruptionBudget.maxUnavailable: "25%" + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.maxUnavailable + value: "25%" + - isNull: + path: spec.minAvailable + + - it: should include unhealthyPodEvictionPolicy when set + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.unhealthyPodEvictionPolicy: "IfHealthyBudget" + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: spec.unhealthyPodEvictionPolicy + value: "IfHealthyBudget" + + - it: should not include unhealthyPodEvictionPolicy when null + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + podDisruptionBudget.unhealthyPodEvictionPolicy: null + asserts: + - isKind: + of: PodDisruptionBudget + - isNull: + path: spec.unhealthyPodEvictionPolicy + + - it: should include standard labels + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: valkey + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + - equal: + path: metadata.labels["app.kubernetes.io/managed-by"] + value: Helm + - isNotNull: + path: metadata.labels["helm.sh/chart"] + + - it: should work with custom fullname override + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podDisruptionBudget.enabled: true + fullnameOverride: "my-custom-valkey" + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.name + value: "my-custom-valkey" diff --git a/charts/valkey/tests/secret_test.yaml b/charts/valkey/tests/secret_test.yaml new file mode 100644 index 0000000..fa06fec --- /dev/null +++ b/charts/valkey/tests/secret_test.yaml @@ -0,0 +1,135 @@ +suite: secret generation +templates: + - templates/secret.yaml +tests: + - it: should not render secret when auth disabled + set: + auth.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should not render secret when auth enabled with usersExistingSecret only + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + admin: + permissions: "~* &* +@all" + auth.aclConfig: "" + asserts: + - hasDocuments: + count: 0 + + - it: should render secret with aclConfig only + set: + auth.enabled: true + auth.aclConfig: "user admin on >password ~* &* +@all" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - equal: + path: metadata.name + value: RELEASE-NAME-valkey-auth + - equal: + path: type + value: Opaque + - exists: + path: data.aclConfig + + - it: should render secret with aclUsers and inline passwords + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + readonly: + permissions: "~* +@read" + password: "readonly-password" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - equal: + path: metadata.name + value: RELEASE-NAME-valkey-auth + - equal: + path: type + value: Opaque + - exists: + path: data["admin-password"] + - exists: + path: data["readonly-password"] + + - it: should encode passwords correctly + set: + auth.enabled: true + auth.aclUsers: + testuser: + permissions: "~* &* +@all" + password: "mypassword" + asserts: + - isKind: + of: Secret + - equal: + path: data["testuser-password"] + value: bXlwYXNzd29yZA== + + - it: should render secret with both aclUsers and aclConfig + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + auth.aclConfig: "user guest on nopass ~public:* +@read" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - exists: + path: data["admin-password"] + - exists: + path: data.aclConfig + + - it: should only include users with inline passwords in secret + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + readonly: + permissions: "~* +@read" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Secret + - exists: + path: data["admin-password"] + - notExists: + path: data["readonly-password"] + + - it: should have correct labels + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + asserts: + - isKind: + of: Secret + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: valkey + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME diff --git a/charts/valkey/tests/service_read_test.yaml b/charts/valkey/tests/service_read_test.yaml new file mode 100644 index 0000000..079b59f --- /dev/null +++ b/charts/valkey/tests/service_read_test.yaml @@ -0,0 +1,116 @@ +suite: replica read service configuration +templates: + - templates/service-read.yaml +tests: + - it: should have correct default service type when replica is enabled + set: + replica.enabled: true + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.type + value: ClusterIP + - it: should be able to set custom clusterIP + set: + replica.enabled: true + replica.service.clusterIP: "10.0.0.2" + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.clusterIP + value: "10.0.0.2" + - it: should have correct default ports + set: + replica.enabled: true + template: templates/service-read.yaml + asserts: + - contains: + path: spec.ports + content: + port: 6379 + targetPort: tcp + protocol: TCP + name: tcp + - it: should be able to set custom nodePort + set: + replica.enabled: true + replica.service.type: "NodePort" + replica.service.nodePort: 30081 + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.ports[0].nodePort + value: 30081 + - it: should be able to set custom appProtocol + set: + replica.enabled: true + replica.service.appProtocol: "my-protocol" + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.ports[0].appProtocol + value: "my-protocol" + - it: should be able to set custom loadBalancerClass + set: + replica.enabled: true + replica.service.loadBalancerClass: "custom-lb-class" + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.loadBalancerClass + value: "custom-lb-class" + - it: shouldn't have loadBalancerClass by default + set: + replica.enabled: true + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - notExists: + path: spec.loadBalancerClass + - it: should be able to set loadBalancerSourceRanges + set: + replica.enabled: true + replica.service.loadBalancerSourceRanges: ["10.0.0.1"] + template: templates/service-read.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.loadBalancerSourceRanges + value: ["10.0.0.1"] + - it: should have correct selector labels + set: + replica.enabled: true + template: templates/service-read.yaml + asserts: + - isSubset: + path: spec.selector + content: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: valkey + - it: should not render when replica is disabled + set: + replica.enabled: false + template: templates/service-read.yaml + asserts: + - hasDocuments: + count: 0 + - it: should not render when replica.service is disabled + set: + replica.enabled: true + replica.service.enabled: false + template: templates/service-read.yaml + asserts: + - hasDocuments: + count: 0 diff --git a/charts/valkey/tests/service_test.yaml b/charts/valkey/tests/service_test.yaml new file mode 100644 index 0000000..115c137 --- /dev/null +++ b/charts/valkey/tests/service_test.yaml @@ -0,0 +1,88 @@ +suite: service configuration +templates: + - templates/service.yaml +tests: + - it: should have correct default service type + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.type + value: ClusterIP + - it: should be able to set custom clusterIP + set: + service.clusterIP: "10.0.0.1" + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.clusterIP + value: "10.0.0.1" + - it: should have correct default ports + template: templates/service.yaml + asserts: + - contains: + path: spec.ports + content: + port: 6379 + targetPort: tcp + protocol: TCP + name: tcp + - it: should be able to set custom nodePort + set: + service.type: "NodePort" + service.nodePort: 30080 + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.ports[0].nodePort + value: 30080 + - it: should be able to set custom appProtocol + set: + service.appProtocol: "my-protocol" + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.ports[0].appProtocol + value: "my-protocol" + - it: should be able to set custom loadBalancerClass + set: + service.loadBalancerClass: "custom-lb-class" + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.loadBalancerClass + value: "custom-lb-class" + - it: shouldn't have loadBalancerClass by default + template: templates/service.yaml + asserts: + - isKind: + of: Service + - notExists: + path: spec.loadBalancerClass + - it: should be able to set loadBalancerSourceRanges + set: + service.loadBalancerSourceRanges: ["10.0.0.1"] + template: templates/service.yaml + asserts: + - isKind: + of: Service + - equal: + path: spec.loadBalancerSourceRanges + value: ["10.0.0.1"] + - it: should have correct selector labels + template: templates/service.yaml + asserts: + - isSubset: + path: spec.selector + content: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: valkey diff --git a/charts/valkey/tests/statefulset_test.yaml b/charts/valkey/tests/statefulset_test.yaml new file mode 100644 index 0000000..225b5b6 --- /dev/null +++ b/charts/valkey/tests/statefulset_test.yaml @@ -0,0 +1,441 @@ +suite: statefulset configuration +templates: + - templates/statefulset.yaml + - templates/init_config.yaml +tests: + - it: should fail when replica enabled but no size provided + set: + replica.enabled: true + replica.persistence.size: "" + template: templates/statefulset.yaml + asserts: + - failedTemplate: + errorPattern: "Replica mode requires persistent storage.*" + + - it: should succeed when replica enabled with size provided + set: + replica.enabled: true + replica.persistence.size: "5Gi" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.volumeClaimTemplates[0].spec.resources.requests.storage + value: "5Gi" + + - it: should succeed when replica enabled with custom storageClass + set: + replica.enabled: true + replica.persistence.size: "10Gi" + replica.persistence.storageClass: "fast-ssd" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.volumeClaimTemplates[0].spec.resources.requests.storage + value: "10Gi" + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: "fast-ssd" + + - it: should always have annotations block even without podAnnotations + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podAnnotations: null + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - exists: + path: spec.template.metadata.annotations + - exists: + path: spec.template.metadata.annotations["checksum/initconfig"] + + - it: should include podAnnotations when provided + set: + replica.enabled: true + replica.persistence.size: "5Gi" + podAnnotations: + prometheus.io/scrape: "true" + custom.annotation: "value" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.metadata.annotations["prometheus.io/scrape"] + value: "true" + - equal: + path: spec.template.metadata.annotations["custom.annotation"] + value: value + - exists: + path: spec.template.metadata.annotations["checksum/initconfig"] + + - it: should use image pull policy for containers, initContainers and metrics + set: + replica.enabled: true + replica.persistence.size: "5Gi" + image.pullPolicy: Always + metrics.enabled: true + metrics.exporter.image.pullPolicy: Always + template: templates/statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.containers[1].imagePullPolicy + value: Always + + - it: should not have imagePullSecrets when not configured + set: + replica.enabled: true + replica.persistence.size: "5Gi" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - notExists: + path: spec.template.spec.imagePullSecrets + + - it: should render imagePullSecrets from values.imagePullSecrets + set: + replica.enabled: true + replica.persistence.size: "5Gi" + imagePullSecrets: + - myregistrykey + - anotherregistrykey + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: myregistrykey + - name: anotherregistrykey + + - it: should render imagePullSecrets from global.imagePullSecrets + set: + replica.enabled: true + replica.persistence.size: "5Gi" + global.imagePullSecrets: + - globalregistrykey + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: globalregistrykey + + - it: should merge global and local imagePullSecrets + set: + replica.enabled: true + replica.persistence.size: "5Gi" + global.imagePullSecrets: + - globalregistrykey + imagePullSecrets: + - localregistrykey + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.imagePullSecrets + value: + - name: globalregistrykey + - name: localregistrykey + + - it: should include extraContainers when provided + set: + replica.enabled: true + replica.persistence.size: "5Gi" + extraContainers: + - name: nginx + image: registry.hub.docker.com/nginx/nginx:stable-alpine + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.containers[1].name + value: nginx + - it: should render extraVolumes + set: + replica.enabled: true + replica.persistence.size: "5Gi" + extraVolumes: + - name: extra-config + configMap: + name: my-config + - name: extra-secret + secret: + secretName: my-secret + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.volumes + content: + name: extra-config + configMap: + name: my-config + - contains: + path: spec.template.spec.volumes + content: + name: extra-secret + secret: + secretName: my-secret + + - it: should render extraVolumeMounts for main container + set: + replica.enabled: true + replica.persistence.size: "5Gi" + extraVolumeMounts: + - name: extra-config + mountPath: /extra-config + readOnly: true + - name: extra-data + mountPath: /extra/data + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-config + mountPath: /extra-config + readOnly: true + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-data + mountPath: /extra/data + + - it: should render both extraVolumes and extraVolumeMounts together + set: + replica.enabled: true + replica.persistence.size: "5Gi" + extraVolumes: + - name: extra-config + configMap: + name: my-config + extraVolumeMounts: + - name: extra-config + mountPath: /extra-config + readOnly: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.volumes + content: + name: extra-config + configMap: + name: my-config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra-config + mountPath: /extra-config + readOnly: true + + - it: should include persistentVolumeClaimRetentionPolicy when configured + set: + replica.enabled: true + replica.persistence.size: "5Gi" + replica.persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Delete + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.persistentVolumeClaimRetentionPolicy.whenDeleted + value: Retain + - equal: + path: spec.persistentVolumeClaimRetentionPolicy.whenScaled + value: Delete + + - it: should include workload annotations when provided + set: + replica.enabled: true + replica.persistence.size: "5Gi" + workloadAnnotations: + example.com/annotation: "test-value" + custom.annotation: "custom-value" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: metadata.annotations["example.com/annotation"] + value: "test-value" + - equal: + path: metadata.annotations["custom.annotation"] + value: "custom-value" + + - it: should render users-secret volume with templated usersExistingSecret + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret-{{ .Release.Name }}" + auth.aclUsers: + default: + permissions: "~* &* +@all" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-custom-secret-RELEASE-NAME + defaultMode: 0400 + + - it: should not have REDIS_PASSWORD env in metrics exporter when auth is disabled + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: false + metrics.enabled: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - notContains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + + - it: should set REDIS_PASSWORD from generated secret in metrics exporter when auth is enabled + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: true + auth.aclUsers: + default: + permissions: "~* &* +@all" + password: "default-password" + metrics.enabled: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-valkey-auth + key: default-password + + - it: should set REDIS_PASSWORD from existing secret in metrics exporter when usersExistingSecret is set + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + metrics.enabled: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret + key: default + + - it: should render templated usersExistingSecret in metrics exporter + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret-{{ .Release.Name }}" + auth.aclUsers: + default: + permissions: "~* &* +@all" + metrics.enabled: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret-RELEASE-NAME + key: default + + - it: should set REDIS_PASSWORD using custom passwordKey in metrics exporter when usersExistingSecret is set + set: + replica.enabled: true + replica.persistence.size: "5Gi" + auth.enabled: true + auth.usersExistingSecret: "my-custom-secret" + auth.aclUsers: + default: + permissions: "~* &* +@all" + passwordKey: "my-password-key" + metrics.enabled: true + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - contains: + path: spec.template.spec.containers[1].env + content: + name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: my-custom-secret + key: my-password-key + - it: should not set runtimeClassName when empty + set: + replica.enabled: true + replica.persistence.size: "5Gi" + runtimeClassName: "" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - notExists: + path: spec.template.spec.runtimeClassName + + - it: should set runtimeClassName when provided + set: + replica.enabled: true + replica.persistence.size: "5Gi" + runtimeClassName: "kata-containers" + template: templates/statefulset.yaml + asserts: + - isKind: + of: StatefulSet + - equal: + path: spec.template.spec.runtimeClassName + value: "kata-containers" diff --git a/charts/valkey/tests/test_pod_test.yaml b/charts/valkey/tests/test_pod_test.yaml new file mode 100644 index 0000000..017730b --- /dev/null +++ b/charts/valkey/tests/test_pod_test.yaml @@ -0,0 +1,119 @@ +suite: test pods +templates: + - templates/tests/auth.yaml +tests: + - it: should not render test pods when auth disabled + set: + auth.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should render generated user test pod with inline passwords + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Pod + - equal: + path: metadata.name + value: RELEASE-NAME-valkey-test-auth-generated + - equal: + path: metadata.annotations["helm.sh/hook"] + value: test + - equal: + path: spec.restartPolicy + value: Never + - contains: + path: spec.volumes + content: + name: valkey-auth + secret: + secretName: RELEASE-NAME-valkey-auth + + - it: should render existing secret test pod only + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret" + auth.aclUsers: + admin: + permissions: "~* &* +@all" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Pod + - equal: + path: metadata.name + value: RELEASE-NAME-valkey-test-auth-existing + - contains: + path: spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-secret + + - it: should render existing secret test pod with templated usersExistingSecret + set: + auth.enabled: true + auth.usersExistingSecret: "my-secret-{{ .Release.Name }}" + auth.aclUsers: + admin: + permissions: "~* &* +@all" + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Pod + - contains: + path: spec.volumes + content: + name: valkey-users-secret + secret: + secretName: my-secret-RELEASE-NAME + + - it: should not render test pods with aclConfig only + set: + auth.enabled: true + auth.aclConfig: "user admin on >password ~* &* +@all" + asserts: + - hasDocuments: + count: 0 + + - it: generated test pod should have correct command structure + set: + auth.enabled: true + auth.aclUsers: + testuser: + permissions: "~* &* +@all" + password: "test-password" + asserts: + - isKind: + of: Pod + - contains: + path: spec.containers[0].command + content: sh + - contains: + path: spec.containers[0].volumeMounts + content: + name: valkey-auth + mountPath: /valkey-auth + readOnly: true + + - it: test pods should have hook deletion policy + set: + auth.enabled: true + auth.aclUsers: + admin: + permissions: "~* &* +@all" + password: "admin-password" + asserts: + - equal: + path: metadata.annotations["helm.sh/hook-delete-policy"] + value: before-hook-creation,hook-succeeded diff --git a/charts/valkey/values.schema.json b/charts/valkey/values.schema.json new file mode 100644 index 0000000..9fb425f --- /dev/null +++ b/charts/valkey/values.schema.json @@ -0,0 +1,603 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "affinity": { + "type": "object" + }, + "auth": { + "type": "object", + "properties": { + "aclConfig": { + "type": "string" + }, + "aclUsers": { + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "usersExistingSecret": { + "type": "string" + } + } + }, + "clusterDomain": { + "type": "string" + }, + "commonLabels": { + "type": "object" + }, + "dataStorage": { + "type": "object", + "properties": { + "accessModes": { + "type": "array", + "items": { + "type": "string" + } + }, + "annotations": { + "type": "object" + }, + "className": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "hostPath": { + "type": "string" + }, + "keepPvc": { + "type": "boolean" + }, + "labels": { + "type": "object" + }, + "persistentVolumeClaimName": { + "type": "string" + }, + "requestedSize": { + "type": "string" + }, + "subPath": { + "type": "string" + }, + "volumeName": { + "type": "string" + } + } + }, + "deploymentStrategy": { + "type": "string", + "enum": [ + "RollingUpdate", + "Recreate" + ] + }, + "env": { + "type": "object" + }, + "extraInitContainers": { + "type": "array" + }, + "extraSecretValkeyConfigs": { + "type": "boolean" + }, + "extraValkeyConfigs": { + "type": "array" + }, + "extraValkeySecrets": { + "type": "array" + }, + "extraVolumeMounts": { + "type": "array" + }, + "extraVolumes": { + "type": "array" + }, + "fullnameOverride": { + "type": "string" + }, + "global": { + "type": "object", + "properties": { + "imagePullSecrets": { + "type": "array" + }, + "imageRegistry": { + "type": "string" + } + } + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "imagePullSecrets": { + "type": "array" + }, + "initResources": { + "type": "object" + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "exporter": { + "type": "object", + "properties": { + "args": { + "type": "array" + }, + "command": { + "type": "array" + }, + "extraEnvs": { + "type": "object" + }, + "extraExporterSecrets": { + "type": "array" + }, + "extraVolumeMounts": { + "type": "array" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "port": { + "type": "integer" + }, + "resources": { + "type": "object" + }, + "securityContext": { + "type": "object" + } + } + }, + "podMonitor": { + "type": "object", + "properties": { + "additionalLabels": { + "type": "object" + }, + "annotations": { + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "extraLabels": { + "type": "object" + }, + "honorLabels": { + "type": "boolean" + }, + "interval": { + "type": "string" + }, + "metricRelabelings": { + "type": "array" + }, + "podTargetLabels": { + "type": "array" + }, + "port": { + "type": "string" + }, + "relabelings": { + "type": "array" + }, + "sampleLimit": { + "type": "boolean" + }, + "scrapeTimeout": { + "type": "string" + }, + "targetLimit": { + "type": "boolean" + } + } + }, + "prometheusRule": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "extraAnnotations": { + "type": "object" + }, + "extraLabels": { + "type": "object" + }, + "rules": { + "type": "array" + } + } + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "appProtocol": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "extraLabels": { + "type": "object" + }, + "ports": { + "type": "object", + "properties": { + "http": { + "type": "integer" + } + } + }, + "type": { + "type": "string" + } + } + }, + "serviceMonitor": { + "type": "object", + "properties": { + "additionalLabels": { + "type": "object" + }, + "annotations": { + "type": "object" + }, + "enabled": { + "type": "boolean" + }, + "extraLabels": { + "type": "object" + }, + "honorLabels": { + "type": "boolean" + }, + "interval": { + "type": "string" + }, + "metricRelabelings": { + "type": "array" + }, + "podTargetLabels": { + "type": "array" + }, + "port": { + "type": "string" + }, + "relabelings": { + "type": "array" + }, + "sampleLimit": { + "type": "boolean" + }, + "scrapeTimeout": { + "type": "string" + }, + "targetLimit": { + "type": "boolean" + } + } + } + } + }, + "nameOverride": { + "type": "string" + }, + "networkPolicy": { + "type": "object" + }, + "nodeSelector": { + "type": "object" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "podSecurityContext": { + "type": "object", + "properties": { + "fsGroup": { + "type": "integer" + }, + "runAsGroup": { + "type": "integer" + }, + "runAsUser": { + "type": "integer" + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + } + } + }, + "priorityClassName": { + "type": "string" + }, + "runtimeClassName": { + "type": "string" + }, + "replica": { + "type": "object", + "properties": { + "disklessSync": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "minReplicasMaxLag": { + "type": "integer" + }, + "minReplicasToWrite": { + "type": "integer" + }, + "persistence": { + "type": "object", + "properties": { + "accessModes": { + "type": "array", + "items": { + "type": "string" + } + }, + "size": { + "type": "string" + }, + "storageClass": { + "type": "string" + } + } + }, + "persistentVolumeClaimRetentionPolicy": { + "type": "object" + }, + "replicas": { + "type": "integer" + }, + "replicationUser": { + "type": "string" + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "appProtocol": { + "type": "string" + }, + "clusterIP": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array" + }, + "nodePort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + } + }, + "resources": { + "type": "object" + }, + "securityContext": { + "type": "object", + "properties": { + "allowPrivilegeEscalation": { + "type": "boolean" + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "readOnlyRootFilesystem": { + "type": "boolean" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "integer" + } + } + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "appProtocol": { + "type": "string" + }, + "clusterIP": { + "type": "string" + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array" + }, + "nodePort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "annotations": { + "type": "object" + }, + "automount": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "tls": { + "type": "object", + "properties": { + "caPublicKey": { + "type": "string" + }, + "dhParamKey": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "existingSecret": { + "type": "string" + }, + "requireClientCertificate": { + "type": "boolean" + }, + "serverKey": { + "type": "string" + }, + "serverPublicKey": { + "type": "string" + } + } + }, + "tolerations": { + "type": "array" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "minAvailable": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^[0-9]+%$" + }, + { + "type": "null" + } + ] + }, + "maxUnavailable": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^[0-9]+%$" + }, + { + "type": "null" + } + ] + }, + "unhealthyPodEvictionPolicy": { + "type": ["string", "null"], + "enum": ["IfHealthyBudget", "AlwaysAllow", "", null] + } + } + }, + "valkeyConfig": { + "type": "string" + }, + "valkeyLogLevel": { + "type": "string" + }, + "workloadAnnotations": { + "type": "object" + } + } +} diff --git a/charts/valkey/values.yaml b/charts/valkey/values.yaml new file mode 100644 index 0000000..f4be0db --- /dev/null +++ b/charts/valkey/values.yaml @@ -0,0 +1,517 @@ +global: + # The global image registry (this will override the registry of all container images defined in this chart) + imageRegistry: "" + # The global image pull secrets (list of secret names) + imagePullSecrets: [] + +image: + # Image registry + registry: "docker.io" + # Valkey container image repository + repository: valkey/valkey + # Image pull policy (Always, IfNotPresent, Never) + pullPolicy: IfNotPresent + # Image tag (leave empty to use .Chart.AppVersion) + tag: "" + +# List of image pull secrets (for private registries) +imagePullSecrets: [] + +# Override the default name or full name of resources +nameOverride: "" +fullnameOverride: "" + +# Kubernetes cluster domain name +clusterDomain: cluster.local + +serviceAccount: + # Create a service account for Valkey + create: true + # Whether to automount the service account token + automount: false + # Annotations to add to the service account + annotations: {} + # Name of an existing service account to use (if create: false) + name: "" + +# Annotations and labels for the pods +podAnnotations: {} +podLabels: {} + +# Annotations for the Deployment/StatefulSet resource +workloadAnnotations: {} + +# Common labels to add to all resources (Deployment, Service, ConfigMap, etc.) +commonLabels: {} + +# Security context for the pod (applies to all containers) +podSecurityContext: + fsGroup: 1000 + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# Priority class name for pod scheduling (leave empty to use cluster's default) +priorityClassName: "" + +# RuntimeClassName for the pods, used to select the container runtime configuration +# (e.g. "gvisor", "kata-containers"). Leave empty to use the cluster's default runtime. +# See https://kubernetes.io/docs/concepts/containers/runtime-class/#usage +runtimeClassName: "" + +# Security context for the Valkey containers +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + +service: + # Type of Kubernetes service (ClusterIP, NodePort, LoadBalancer) + type: ClusterIP + # Port on which Valkey will be exposed + port: 6379 + annotations: {} + # NodePort value (if service.type is NodePort) + nodePort: 0 + # ClusterIP value + clusterIP: "" + # Class of a load balancer implementation + loadBalancerClass: "" + # If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer + # will be restricted to the specified client IPs. + # This field will be ignored if the cloud-provider does not support the feature. + loadBalancerSourceRanges: [] + # Application protocol + appProtocol: "" + +# Network policy to control traffic to the pods +# More info: https://kubernetes.io/docs/concepts/services-networking/network-policies/ +networkPolicy: {} + +# Resource limits/requests for the main Valkey container +resources: {} + # Example: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# Resource limits/requests for init containers +initResources: {} + # Example: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# Health probes for the main Valkey container. +# Each probe is gated on its own `enabled` flag and rendered by the +# `valkey.healthProbes` helper. The default handler is a `valkey-cli ping` exec +# (TLS-aware). Set `customProbe` to a full probe spec to replace the default +# handler and timing entirely (e.g. an httpGet or tcpSocket probe). +# The defaults below match the previous hardcoded behavior (Kubernetes probe +# defaults); readinessProbe is opt-in since the container had none before. +startupProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + customProbe: {} +livenessProbe: + enabled: true + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + customProbe: {} +readinessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + customProbe: {} + +# Additional init containers +extraInitContainers: [] + +# Additional containers +extraContainers: [] + +# Persistent storage configuration (standalone deployment only) +dataStorage: + # Enable persistent volume claim creation + enabled: false + + # Use existing PVC by name (skip dynamic provisioning if set) + persistentVolumeClaimName: "" + + # Subpath inside PVC to mount + subPath: "" + + # Name of the volume (referenced in deployment) + volumeName: "valkey-data" + + # Request size (e.g. 5Gi) for dynamically provisioned volume + requestedSize: "" + + # Name of the storage class to use + className: "" + + # Access modes for the PVC (e.g., ReadWriteOnce, ReadWriteMany) + accessModes: + - ReadWriteOnce + + # If true, keep the PVC on Helm uninstall + keepPvc: false + + # Optional annotations to add to the PVC + annotations: {} + + # Optional labels to add to the PVC + labels: {} + + # If hostPath is set then a hostPath DirectoryOrCreate volume is used + hostPath: "" + +# Mount additional secrets into the Valkey container +extraValkeySecrets: [] + +# Mount additional configMaps into the Valkey container +extraValkeyConfigs: [] + +# Mount extra secrets as volume to init container (deprecated, use extraValkeySecrets) +extraSecretValkeyConfigs: false + +# Mount additional emptyDir or hostPath volumes (advanced use) +extraVolumes: [] +# - name: hostpath-volume +# hostPath: +# path: /opt/valkey/hostpath-volume/ +# type: DirectoryOrCreate +extraVolumeMounts: [] +# - mountPath: /some/path/ +# name: hostpath-volume + +# Content for valkey.conf (will be mounted via ConfigMap) +valkeyConfig: "" + +auth: + # Enable ACL-based authentication + # IMPORTANT: When authentication is enabled, the 'default' user MUST be defined in either + # aclUsers or aclConfig. Without a default user, anyone can access the database without + # credentials, creating a security risk. + enabled: false + + # Use an existing secret for user passwords. Key defaults to username. + # Supports templating, e.g.: "users-{{ .Release.Name }}" + usersExistingSecret: "" + + # Map of users to create with ACL permissions. + # If usersExistingSecret is set, passwords from the secret take priority over inline passwords. + # NOTE: If using aclUsers, the 'default' user must be included here. + aclUsers: {} + # Example: + # default: + # permissions: "~* &* +@all" + # password: "secretpass" # Inline password (fallback if usersExistingSecret not set) + # passwordKey: "admin-pwd" # Key in usersExistingSecret (defaults to username) + # read-user: + # permissions: "~* -@all +@read +ping +info" + + # Inline ACL configuration that will be appended after generated users. + # NOTE: If using aclConfig, ensure the 'default' user is defined here. + aclConfig: "" + # Example: + # aclConfig: | + # user default on >secretpass ~* &* +@all + +# Replica configuration for master-replica replication mode +replica: + enabled: false + + # Number of replica instances (total pods = replicas + 1 master) + replicas: 2 + + # Username for replicas to authenticate to master, ignored if auth.enabled is false. + # IMPORTANT: When auth.enabled is true, this user MUST be defined in auth.aclUsers. + # The chart requires this to retrieve the password for replica authentication. + # The user must have appropriate replication permissions: +psync +replconf +ping + replicationUser: "default" + + # Replication settings + # Use diskless replication (sync directly from memory) vs disk-based + disklessSync: false + + # Write safety - require minimum number of healthy replicas to accept writes + # Set to 0 to disable this check, or 1+ to require minimum replicas before accepting writes + # This ensures data durability by requiring at least N replicas to be in sync + minReplicasToWrite: 0 + + # Maximum replication lag in seconds before a replica is considered unhealthy + minReplicasMaxLag: 10 + + # Read service configuration + service: + # Enable read service (load balances read traffic across all pods) + enabled: true + # Service type (ClusterIP, NodePort, LoadBalancer) + type: ClusterIP + # Port on which the read service will be exposed + port: 6379 + # Optional annotations for the read service + annotations: {} + # NodePort value (if service.type is NodePort) + nodePort: 0 + # ClusterIP value + clusterIP: "" + # Application protocol + appProtocol: "" + # Class of a load balancer implementation + loadBalancerClass: "" + # If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer + # will be restricted to the specified client IPs. + # This field will be ignored if the cloud-provider does not support the feature. + loadBalancerSourceRanges: [] + + # Persistence configuration (required for replicas) + persistence: + # Size of the PVC for each replica (required when replica.enabled is true) + size: "" + # Storage class name (empty = use default storage class) + storageClass: "" + # Access modes for the PVC + accessModes: + - ReadWriteOnce + + # PersistentVolumeClaim retention policy for StatefulSet + # Controls when PVCs are deleted (requires Kubernetes 1.23+) + # More info: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#persistentvolumeclaim-retention + persistentVolumeClaimRetentionPolicy: {} + +tls: + # Enable TLS + enabled: false + # Name of the Secret containing TLS keys (required) + existingSecret: "" + # Secret key name containing server public certificate + serverPublicKey: server.crt + # Secret key name containing server private key + serverKey: server.key + # Secret key name containing Certificate Authority public certificate + caPublicKey: ca.crt + # Secret key name containing DH parameters (optional) + dhParamKey: "" + # Require that clients authenticate with a certificate + requireClientCertificate: false + +# Node selector for pod assignment +nodeSelector: {} + +# Tolerations for pod assignment to tainted nodes +tolerations: [] + +# Affinity rules for pod scheduling +affinity: {} + +# Set Deployment strategy. See https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy +deploymentStrategy: RollingUpdate # @schema enum:[RollingUpdate,Recreate] + +# See https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints +topologySpreadConstraints: [] + +# PodDisruptionBudget configuration (only used in replicated mode) +# Helps keep enough replicas available during voluntary disruptions +podDisruptionBudget: + # Enable PodDisruptionBudget + enabled: false + # Minimum number of pods that must be available (takes precedence over maxUnavailable if both set) + minAvailable: null + # Maximum number of pods that can be unavailable during disruptions + maxUnavailable: 1 + # Unhealthy pod eviction policy (optional) + # Valid values: IfHealthyBudget, AlwaysAllow + unhealthyPodEvictionPolicy: "" + +# Valkey logging level: debug, verbose, notice, warning +valkeyLogLevel: "notice" +# Environment variables to inject into Valkey container + +env: {} + # Example: + # LOG_LEVEL: info + +metrics: + # Enable Prometheus exporter sidecar + enabled: false + # Exporter configuration + exporter: + # Command to run in the metrics exporter container (overrides args) + command: [] + # Arguments to pass to the metrics exporter container + args: [] + # Example: + # - --redis.addr=redis:6379 + # Port on which the metrics exporter will listen + port: 9121 + # Image configuration + image: + # Image registry + registry: ghcr.io + # Prometheus exporter container image repository + repository: oliver006/redis_exporter + # Image pull policy (Always, IfNotPresent, Never) + pullPolicy: IfNotPresent + # Image tag (leave empty to use latest) + tag: "v1.79.0" + resources: {} + # Example: + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + # Extra volume mounts for metrics exporter container + extraVolumeMounts: [] + # Additional secrets to mount for metrics exporter + extraExporterSecrets: [] + # Environment variables to inject into the metrics exporter container + extraEnvs: {} + # Example: + # LOG_LEVEL: info + securityContext: {} + # Example: + # runAsNonRoot: true + # runAsUser: 1000 + # runAsGroup: 1000 + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + + # Service configuration for the metrics exporter + service: + # Enable a separate service for the metrics exporter + enabled: true + # Service type (ClusterIP, NodePort, LoadBalancer) + type: ClusterIP + # Port on which the metrics exporter service will be exposed + ports: + http: 9121 + # Optional annotations for the metrics exporter service + annotations: {} + # Optional labels for the metrics exporter service + extraLabels: {} + # ServiceMonitor configuration for Prometheus Operator + # Application protocol + appProtocol: "" + serviceMonitor: + # Enable ServiceMonitor resource for scraping service metrics + enabled: false + # Port name or number to scrape metrics from + port: metrics + # Extra labels for the ServiceMonitor resource + extraLabels: {} + # Extra annotations for the ServiceMonitor resource + annotations: {} + # How often Prometheus should scrape metrics + interval: 30s + # Maximum duration allowed for a scrape request + scrapeTimeout: "" + # Relabeling rules applied before scraping metrics + relabelings: [] + # Relabeling rules applied before ingesting metrics + metricRelabelings: [] + # Set honorLabels to true to preserve original metric labels + honorLabels: false + # Extra labels to help Prometheus discover ServiceMonitor resources + additionalLabels: {} + # Pod labels to copy onto the generated metrics + podTargetLabels: [] + # Maximum number of samples to collect per Pod scrape + sampleLimit: false + # Maximum number of scrape targets allowed + targetLimit: false + podMonitor: + # Enable PodMonitor resource for scraping pod metrics + enabled: false + # Port name or number to scrape metrics from + port: metrics + # Extra labels for the ServiceMonitor resource + extraLabels: {} + # Extra annotations for the ServiceMonitor resource + annotations: {} + # Frequency for Prometheus to scrape pod metrics + interval: 30s + # Time limit for each scrape operation + scrapeTimeout: "" + # Relabeling rules to apply before scraping pod metrics + relabelings: [] + # Relabeling rules to apply before ingesting pod metrics + metricRelabelings: [] + # If true, keeps original labels from the pod metrics + honorLabels: false + # Additional labels for Prometheus to find PodMonitor resources + additionalLabels: {} + # Pod labels to attach to the metrics + podTargetLabels: [] + # Maximum samples to scrape from each Pod + sampleLimit: false + # Maximum number of pods to scrape + targetLimit: false + + # PrometheusRule configuration for alerting rules (used by kube-prometheus-stack) + prometheusRule: + # Enable creation of PrometheusRule resource + enabled: false + # Extra labels to add to the PrometheusRule resource + extraLabels: {} + # Extra annotations to add to the PrometheusRule resource + extraAnnotations: {} + # List of Prometheus alerting rules + rules: [] + # Example alerting rules: + # - alert: ValkeyDown + # annotations: + # summary: Valkey instance {{ "{{ $labels.instance }}" }} down + # description: Valkey instance {{ "{{ $labels.instance }}" }} is down. + # expr: | + # redis_up{service="{{ include "valkey.fullname" . }}-metrics"} == 0 + # for: 2m + # labels: + # severity: error + # - alert: ValkeyMemoryHigh + # annotations: + # summary: Valkey instance {{ "{{ $labels.instance }}" }} is using too much memory + # description: | + # Valkey instance {{ "{{ $labels.instance }}" }} is using {{ "{{ $value }}" }}% of its available memory. + # expr: | + # redis_memory_used_bytes{service="{{ include "valkey.fullname" . }}-metrics"} * 100 + # / + # redis_memory_max_bytes{service="{{ include "valkey.fullname" . }}-metrics"} + # > 90 <= 100 + # for: 2m + # labels: + # severity: error + # - alert: ValkeyKeyEviction + # annotations: + # summary: Valkey instance {{ "{{ $labels.instance }}" }} has evicted keys + # description: | + # Valkey instance {{ "{{ $labels.instance }}" }} has evicted {{ "{{ $value }}" }} keys in the last 5 minutes. + # expr: | + # increase(redis_evicted_keys_total{service="{{ include "valkey.fullname" . }}-metrics"}[5m]) > 0 + # for: 1s + # labels: + # severity: error