diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 2098cc1..ce91d68 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -31,6 +31,8 @@ Apply conventional commit rules to determine the bump: Use the highest applicable bump. Parse the current version from `charts//Chart.yaml`. +**Keycloak chart exception**: The keycloak chart version is synced with the upstream Keycloak appVersion (e.g. `26.5.0`). Do not bump the chart version independently — it always matches `appVersion`. When a new upstream Keycloak version is released, bump both `version` and `appVersion` together to the new upstream version. + Present the proposed version bump to the user (current version -> new version) along with the commit list, and ask for confirmation before proceeding. The user may override the bump level. ## 3. Update Chart.yaml @@ -53,7 +55,7 @@ If the version bump includes a **minor** (or major) version change, run: make compat-matrix ``` -This tests the chart against the last 5 NetBird server minor versions and updates `docs/compatibility.md` with a new row for the new chart minor. The updated file will be included in the release commit. +This tests the chart against the last 5 NetBird server minor versions and updates `charts/netbird/docs/compatibility.md` with a new row for the new chart minor. The updated file will be included in the release commit. Skip this step for patch-only bumps — the existing row already covers the current chart minor. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b1b119..0e99523 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,38 @@ on: branches: [main] jobs: + # ── Detect which charts changed ────────────────────────────────────── + detect-changes: + name: Detect Changes + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + netbird: ${{ steps.filter.outputs.netbird }} + keycloak: ${{ steps.filter.outputs.keycloak }} + ci: ${{ steps.filter.outputs.ci }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect changed paths + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + netbird: + - 'charts/netbird/**' + - 'ci/scripts/netbird/**' + keycloak: + - 'charts/keycloak/**' + - 'ci/scripts/keycloak/**' + ci: + - '.github/workflows/ci.yaml' + - 'Makefile' + - 'dprint.json' + - '.helmfmt' + + # ── Format check (always runs) ────────────────────────────────────── format-check: name: Format Check runs-on: ubuntu-latest @@ -39,6 +71,7 @@ jobs: exit 1 fi + # ── Lint & unit test (always runs) ────────────────────────────────── lint-and-unit-test: name: Lint & Unit Test runs-on: ubuntu-latest @@ -70,10 +103,12 @@ jobs: fi done + # ── NetBird E2E tests (only when netbird chart or CI config changes) ─ e2e-sqlite: - name: E2E — SQLite + name: "E2E — NetBird: SQLite" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -89,7 +124,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (sqlite) - run: ci/scripts/e2e.sh sqlite + run: ci/scripts/netbird/e2e.sh sqlite - name: Show debug info on failure if: failure() @@ -104,9 +139,10 @@ jobs: kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true e2e-postgres: - name: E2E — PostgreSQL + name: "E2E — NetBird: PostgreSQL" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -122,7 +158,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (postgres) - run: ci/scripts/e2e.sh postgres + run: ci/scripts/netbird/e2e.sh postgres - name: Show debug info on failure if: failure() @@ -139,9 +175,10 @@ jobs: kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true e2e-mysql: - name: E2E — MySQL + name: "E2E — NetBird: MySQL" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -157,7 +194,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (mysql) - run: ci/scripts/e2e.sh mysql + run: ci/scripts/netbird/e2e.sh mysql - name: Show debug info on failure if: failure() @@ -174,9 +211,10 @@ jobs: kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true e2e-oidc-embedded: - name: E2E — OIDC (Embedded IdP) + name: "E2E — NetBird: OIDC (Embedded IdP)" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -192,7 +230,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (oidc-embedded) - run: ci/scripts/e2e-oidc.sh embedded + run: ci/scripts/netbird/e2e-oidc.sh embedded - name: Show debug info on failure if: failure() @@ -207,9 +245,10 @@ jobs: kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true e2e-oidc-keycloak: - name: E2E — OIDC (Keycloak) + name: "E2E — NetBird: OIDC (Keycloak)" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -225,7 +264,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (oidc-keycloak) - run: ci/scripts/e2e-oidc.sh keycloak + run: ci/scripts/netbird/e2e-oidc.sh keycloak - name: Show debug info on failure if: failure() @@ -242,9 +281,10 @@ jobs: kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true e2e-oidc-zitadel: - name: E2E — OIDC (Zitadel) + name: "E2E — NetBird: OIDC (Zitadel)" runs-on: ubuntu-latest - needs: lint-and-unit-test + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -260,7 +300,7 @@ jobs: cluster_name: helms-e2e - name: Run e2e test (oidc-zitadel) - run: ci/scripts/e2e-oidc.sh zitadel + run: ci/scripts/netbird/e2e-oidc.sh zitadel - name: Show debug info on failure if: failure() @@ -277,3 +317,104 @@ jobs: kubectl -n netbird-e2e logs deployment/zitadel-db --tail=50 || true echo "=== Events ===" kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true + + # ── Keycloak E2E tests (only when keycloak chart or CI config changes) ─ + e2e-keycloak-dev: + name: "E2E — Keycloak: Dev" + runs-on: ubuntu-latest + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.keycloak == 'true' || needs.detect-changes.outputs.ci == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v4.0.2 + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: helms-e2e + + - name: Run e2e test (keycloak dev) + run: ci/scripts/keycloak/e2e.sh dev + + - name: Show debug info on failure + if: failure() + run: | + echo "=== Pod status ===" + kubectl -n keycloak-e2e get pods -o wide || true + echo "=== Keycloak logs ===" + kubectl -n keycloak-e2e logs deployment/keycloak-e2e --tail=100 || true + echo "=== Events ===" + kubectl -n keycloak-e2e get events --sort-by='.lastTimestamp' || true + + e2e-keycloak-postgres: + name: "E2E — Keycloak: PostgreSQL" + runs-on: ubuntu-latest + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.keycloak == 'true' || needs.detect-changes.outputs.ci == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v4.0.2 + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: helms-e2e + + - name: Run e2e test (keycloak postgres) + run: ci/scripts/keycloak/e2e.sh postgres + + - name: Show debug info on failure + if: failure() + run: | + echo "=== Pod status ===" + kubectl -n keycloak-e2e get pods -o wide || true + echo "=== Keycloak logs ===" + kubectl -n keycloak-e2e logs deployment/keycloak-e2e --tail=100 || true + echo "=== PostgreSQL logs ===" + kubectl -n keycloak-e2e logs deployment/postgres --tail=50 || true + echo "=== Events ===" + kubectl -n keycloak-e2e get events --sort-by='.lastTimestamp' || true + + e2e-keycloak-replicas: + name: "E2E — Keycloak: Replicas" + runs-on: ubuntu-latest + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.keycloak == 'true' || needs.detect-changes.outputs.ci == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v4.0.2 + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: helms-e2e + + - name: Run e2e test (keycloak replicas) + run: ci/scripts/keycloak/e2e.sh replicas + + - name: Show debug info on failure + if: failure() + run: | + echo "=== Pod status ===" + kubectl -n keycloak-e2e get pods -o wide || true + echo "=== Keycloak logs ===" + kubectl -n keycloak-e2e logs deployment/keycloak-e2e --tail=100 || true + echo "=== PostgreSQL logs ===" + kubectl -n keycloak-e2e logs deployment/postgres --tail=50 || true + echo "=== Events ===" + kubectl -n keycloak-e2e get events --sort-by='.lastTimestamp' || true diff --git a/.upstream-monitor.yaml b/.upstream-monitor.yaml index c1db149..dc24c44 100644 --- a/.upstream-monitor.yaml +++ b/.upstream-monitor.yaml @@ -29,3 +29,15 @@ charts: targets: - file: Chart.yaml yaml_path: .appVersion + + - name: keycloak + path: charts/keycloak + sources: + - name: keycloak + github: keycloak/keycloak + strip_v_prefix: false + targets: + - file: Chart.yaml + yaml_path: .appVersion + - file: Chart.yaml + yaml_path: .version diff --git a/Makefile b/Makefile index 473d7d6..b4e229c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: lint unittest e2e e2e-sqlite e2e-postgres e2e-mysql e2e-oidc-keycloak e2e-oidc-zitadel e2e-setup e2e-teardown test compat-matrix +.PHONY: lint unittest e2e e2e-netbird e2e-sqlite e2e-postgres e2e-mysql e2e-oidc-keycloak e2e-oidc-zitadel e2e-keycloak e2e-keycloak-dev e2e-keycloak-postgres e2e-keycloak-replicas e2e-setup e2e-teardown test compat-matrix CHARTS := $(wildcard charts/*) @@ -26,34 +26,53 @@ e2e-setup: kind create cluster --name $(E2E_CLUSTER) --wait 60s 2>/dev/null || true kubectl cluster-info --context kind-$(E2E_CLUSTER) +# ── NetBird E2E ───────────────────────────────────────────────────────── e2e-sqlite: e2e-setup - ci/scripts/e2e.sh sqlite + ci/scripts/netbird/e2e.sh sqlite e2e-postgres: e2e-setup - ci/scripts/e2e.sh postgres + ci/scripts/netbird/e2e.sh postgres e2e-mysql: e2e-setup - ci/scripts/e2e.sh mysql + ci/scripts/netbird/e2e.sh mysql e2e-oidc-keycloak: e2e-setup - ci/scripts/e2e-oidc.sh keycloak + ci/scripts/netbird/e2e-oidc.sh keycloak e2e-oidc-zitadel: e2e-setup - ci/scripts/e2e-oidc.sh zitadel + ci/scripts/netbird/e2e-oidc.sh zitadel -e2e: e2e-setup - ci/scripts/e2e.sh sqlite - ci/scripts/e2e.sh postgres - ci/scripts/e2e.sh mysql - ci/scripts/e2e-oidc.sh keycloak - ci/scripts/e2e-oidc.sh zitadel +e2e-netbird: e2e-setup + ci/scripts/netbird/e2e.sh sqlite + ci/scripts/netbird/e2e.sh postgres + ci/scripts/netbird/e2e.sh mysql + ci/scripts/netbird/e2e-oidc.sh keycloak + ci/scripts/netbird/e2e-oidc.sh zitadel + +# ── Keycloak E2E ──────────────────────────────────────────────────────── +e2e-keycloak-dev: e2e-setup + ci/scripts/keycloak/e2e.sh dev + +e2e-keycloak-postgres: e2e-setup + ci/scripts/keycloak/e2e.sh postgres + +e2e-keycloak-replicas: e2e-setup + ci/scripts/keycloak/e2e.sh replicas + +e2e-keycloak: e2e-setup + ci/scripts/keycloak/e2e.sh dev + ci/scripts/keycloak/e2e.sh postgres + ci/scripts/keycloak/e2e.sh replicas + +# ── All E2E ───────────────────────────────────────────────────────────── +e2e: e2e-netbird e2e-keycloak e2e-teardown: kind delete cluster --name $(E2E_CLUSTER) 2>/dev/null || true # ── Compatibility Matrix ────────────────────────────────────────────── compat-matrix: e2e-setup - ci/scripts/compat-matrix.sh + ci/scripts/netbird/compat-matrix.sh # ── Run all tests ────────────────────────────────────────────────────── test: lint unittest diff --git a/charts/keycloak/.helmignore b/charts/keycloak/.helmignore new file mode 100644 index 0000000..83754b9 --- /dev/null +++ b/charts/keycloak/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +docs/ +tests/ +ci/ +README.md diff --git a/charts/keycloak/Chart.yaml b/charts/keycloak/Chart.yaml new file mode 100644 index 0000000..918678d --- /dev/null +++ b/charts/keycloak/Chart.yaml @@ -0,0 +1,34 @@ +apiVersion: v2 +name: keycloak +description: A Helm chart for deploying Keycloak IAM using the upstream quay.io/keycloak/keycloak image on Kubernetes +type: application +version: 26.5.0 +appVersion: "26.5.0" +keywords: + - keycloak + - iam + - identity + - authentication + - oauth2 + - oidc + - sso + - self-hosted + - kubernetes +home: https://www.keycloak.org +sources: + - https://github.com/keycloak/keycloak + - https://github.com/KitStream/helms +maintainers: + - name: KitStream + url: https://github.com/KitStream +icon: https://www.keycloak.org/resources/images/keycloak_icon_512px.svg +annotations: + artifacthub.io/license: Apache-2.0 + artifacthub.io/links: | + - name: Keycloak + url: https://www.keycloak.org + - name: Source + url: https://github.com/KitStream/helms + artifacthub.io/changes: | + - kind: added + description: Initial Keycloak chart with upstream image support diff --git a/charts/keycloak/README.md b/charts/keycloak/README.md new file mode 100644 index 0000000..204758b --- /dev/null +++ b/charts/keycloak/README.md @@ -0,0 +1,346 @@ +# Keycloak Helm Chart + +[![CI](https://github.com/KitStream/helms/actions/workflows/ci.yaml/badge.svg)](https://github.com/KitStream/helms/actions/workflows/ci.yaml) +[![Chart Version](https://img.shields.io/badge/chart-26.5.0-blue)](https://github.com/KitStream/helms/releases) +[![App Version](https://img.shields.io/badge/keycloak-26.5.0-green)](https://github.com/keycloak/keycloak) + +A Helm chart for deploying [Keycloak](https://www.keycloak.org) IAM using the upstream `quay.io/keycloak/keycloak` image on Kubernetes. + +## Overview + +This chart deploys Keycloak directly from the upstream container image with native `KC_*` environment variable configuration. It supports: + +- **Databases**: PostgreSQL, MySQL, MSSQL, or embedded H2 (dev mode) +- **Clustering**: JDBC-PING (default) or Kubernetes DNS-PING via headless service +- **Build optimization**: Optional init container for `kc.sh build` to speed up startup +- **Observability**: Health endpoints on a dedicated management port, Prometheus metrics, and optional ServiceMonitor + +## Prerequisites + +- Kubernetes 1.24+ +- Helm 3.x +- An external database for production use (PostgreSQL recommended) + +## Installation + +### From OCI Registry (recommended) + +```bash +helm install keycloak oci://ghcr.io/kitstream/helms/keycloak \ + --version 26.5.0 \ + -n keycloak --create-namespace \ + -f my-values.yaml +``` + +### From Source + +```bash +helm install keycloak ./charts/keycloak \ + -n keycloak --create-namespace \ + -f my-values.yaml +``` + +## Minimal Configuration Examples + +### Development (embedded H2) + +No external database required. **Not suitable for production.** + +```yaml +database: + type: dev + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password +``` + +### PostgreSQL (production) + +```yaml +hostname: keycloak.example.com +hostnameStrict: true + +database: + type: postgresql + host: postgres.database.svc.cluster.local + port: 5432 + user: keycloak + name: keycloak + password: + secretName: keycloak-db-password + secretKey: password + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password + +ingress: + enabled: true + className: nginx + hosts: + - host: keycloak.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: keycloak-tls + hosts: + - keycloak.example.com +``` + +### Multi-Replica with Kubernetes DNS-PING + +```yaml +replicaCount: 3 + +hostname: keycloak.example.com +hostnameStrict: true + +database: + type: postgresql + host: postgres.database.svc.cluster.local + port: 5432 + user: keycloak + name: keycloak + password: + secretName: keycloak-db-password + secretKey: password + +cache: + stack: kubernetes + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password +``` + +## Creating Secrets + +### Admin Password + +```bash +kubectl create secret generic keycloak-admin-password \ + --from-literal=password='YOUR_SECURE_PASSWORD' \ + -n keycloak +``` + +### Database Password + +```bash +kubectl create secret generic keycloak-db-password \ + --from-literal=password='YOUR_DB_PASSWORD' \ + -n keycloak +``` + +## Hostname Configuration + +Keycloak 26 requires explicit hostname configuration for production mode: + +- **`hostname`**: Set to your public Keycloak URL (e.g. `keycloak.example.com`) +- **`hostnameStrict`**: Set to `true` when `hostname` is configured to prevent dynamic hostname resolution from request headers. Defaults to `false` so Keycloak can start without a hostname (dev/testing). + +If `hostnameStrict` is `true` and no `hostname` is set, Keycloak will refuse to start. + +## Clustering + +The chart supports two cache stacks for multi-replica deployments: + +| Stack | Description | Default | +| ------------ | ------------------------------------------------------------------------ | ------- | +| `jdbc-ping` | Uses the configured database for node discovery. No extra config needed. | Yes | +| `kubernetes` | Uses DNS-PING via the headless service for JGroups discovery. | No | + +In dev mode (`database.type: dev`), clustering is disabled (`KC_CACHE=local`) since the embedded H2 database cannot be shared across replicas. + +## Build Optimization + +By default, Keycloak runs an auto-build at startup in production mode (`start`), which can take 2-5 minutes. To speed up startup, enable the build init container: + +```yaml +build: + enabled: true +``` + +This runs `kc.sh build` in an init container and passes `--optimized` to the main container, reducing startup time to seconds. + +## Values Reference + +### Global + +| Key | Type | Default | Description | +| ---------------------------- | ------ | ------- | -------------------------------- | +| `nameOverride` | string | `""` | Override the chart name | +| `fullnameOverride` | string | `""` | Fully override the resource name | +| `imagePullSecrets` | list | `[]` | Global image pull secrets | +| `serviceAccount.create` | bool | `true` | Create a ServiceAccount | +| `serviceAccount.annotations` | object | `{}` | ServiceAccount annotations | +| `serviceAccount.name` | string | `""` | ServiceAccount name override | + +### Image + +| Key | Type | Default | Description | +| ------------------ | ------ | ----------------------------- | --------------------------- | +| `image.repository` | string | `"quay.io/keycloak/keycloak"` | Container image repository | +| `image.tag` | string | `""` (appVersion) | Image tag | +| `image.pullPolicy` | string | `"IfNotPresent"` | Image pull policy | +| `replicaCount` | int | `1` | Number of Keycloak replicas | + +### Database + +| Key | Type | Default | Description | +| ------------------------------ | ------ | ------------ | ------------------------------------------------------- | +| `database.type` | string | `"dev"` | Database type (`postgresql`, `mysql`, `mssql`, `dev`) | +| `database.host` | string | `""` | Database hostname (required for external databases) | +| `database.port` | string | `""` | Database port (auto-defaults: 5432/3306/1433) | +| `database.name` | string | `"keycloak"` | Database name | +| `database.user` | string | `""` | Database username | +| `database.password.secretName` | string | `""` | Secret containing the database password | +| `database.password.secretKey` | string | `"password"` | Key in the Secret | +| `database.poolMinSize` | string | `""` | Connection pool minimum size | +| `database.poolInitialSize` | string | `""` | Connection pool initial size | +| `database.poolMaxSize` | string | `""` | Connection pool maximum size | +| `database.sslMode` | string | `""` | SSL mode for PostgreSQL (e.g. `verify-full`, `require`) | + +### Hostname & Proxy + +| Key | Type | Default | Description | +| ---------------- | ------ | ------- | ------------------------------------------------------------------------ | +| `hostname` | string | `""` | Public hostname (maps to `KC_HOSTNAME`) | +| `hostnameAdmin` | string | `""` | Separate admin console hostname (maps to `KC_HOSTNAME_ADMIN`) | +| `hostnameStrict` | bool | `false` | Disable dynamic hostname from request headers (set `true` with hostname) | +| `proxyHeaders` | string | `""` | Proxy header mode: `xforwarded` or `forwarded` | +| `httpEnabled` | bool | `true` | Enable HTTP listener (required for edge-terminated TLS) | + +### TLS + +| Key | Type | Default | Description | +| ---------------- | ------ | ------- | ------------------------------------------- | +| `tls.enabled` | bool | `false` | Enable TLS passthrough (mount cert and key) | +| `tls.secretName` | string | `""` | Secret containing `tls.crt` and `tls.key` | + +### Clustering + +| Key | Type | Default | Description | +| ------------- | ------ | ------------- | -------------------------------------------------- | +| `cache.stack` | string | `"jdbc-ping"` | Cache stack: `jdbc-ping` (default) or `kubernetes` | + +### Admin Credentials + +| Key | Type | Default | Description | +| --------------------------- | ------ | ------------ | ----------------------------------------- | +| `admin.username` | string | `""` | Admin username (maps to `KEYCLOAK_ADMIN`) | +| `admin.password.secretName` | string | `""` | Secret containing the admin password | +| `admin.password.secretKey` | string | `"password"` | Key in the Secret | + +### Observability + +| Key | Type | Default | Description | +| -------------------------------------- | ------ | -------- | ----------------------------------- | +| `healthEnabled` | bool | `true` | Enable health endpoints | +| `metrics.enabled` | bool | `true` | Enable Prometheus metrics | +| `metrics.serviceMonitor.enabled` | bool | `false` | Create a ServiceMonitor resource | +| `metrics.serviceMonitor.labels` | object | `{}` | Additional ServiceMonitor labels | +| `metrics.serviceMonitor.interval` | string | `""` | Scrape interval | +| `metrics.serviceMonitor.scrapeTimeout` | string | `""` | Scrape timeout | +| `logLevel` | string | `"info"` | Log level (`debug`, `info`, `warn`) | + +### Build Optimization + +| Key | Type | Default | Description | +| --------------- | ---- | ------- | ----------------------------------- | +| `build.enabled` | bool | `false` | Run `kc.sh build` in init container | + +### Extra Configuration + +| Key | Type | Default | Description | +| -------------------- | ------ | ------- | ------------------------------------------- | +| `extraEnvVars` | list | `[]` | Additional environment variables | +| `extraEnvVarsSecret` | string | `""` | Existing Secret to mount as env vars | +| `extraVolumes` | list | `[]` | Additional volumes | +| `extraVolumeMounts` | list | `[]` | Additional volume mounts | +| `features` | string | `""` | Comma-separated Keycloak features to enable | + +### Persistent Storage + +| Key | Type | Default | Description | +| -------------------------- | ------ | ------------------- | -------------------------------------- | +| `persistence.enabled` | bool | `false` | Enable PVC for custom themes/providers | +| `persistence.storageClass` | string | `""` | Storage class | +| `persistence.accessModes` | list | `["ReadWriteOnce"]` | PVC access modes | +| `persistence.size` | string | `"1Gi"` | Volume size | +| `persistence.annotations` | object | `{}` | PVC annotations | + +### Services + +| Key | Type | Default | Description | +| ----------------------------- | ------ | ------------- | ---------------------------------- | +| `service.type` | string | `"ClusterIP"` | Service type | +| `service.httpPort` | int | `8080` | HTTP port | +| `service.managementPort` | int | `9000` | Management port (health + metrics) | +| `service.annotations` | object | `{}` | Service annotations | +| `headlessService.jgroupsPort` | int | `7800` | JGroups clustering port | +| `headlessService.annotations` | object | `{}` | Headless service annotations | + +### Ingress + +| Key | Type | Default | Description | +| --------------------- | ------ | ------- | ----------------------- | +| `ingress.enabled` | bool | `false` | Create Ingress resource | +| `ingress.className` | string | `""` | Ingress class name | +| `ingress.annotations` | object | `{}` | Ingress annotations | +| `ingress.hosts` | list | `[]` | Ingress host rules | +| `ingress.tls` | list | `[]` | TLS configuration | + +### Probes + +| Key | Type | Default | Description | +| ---------------- | ------ | --------------------------------------------- | --------------- | +| `startupProbe` | object | HTTP GET `/health/started` on management:9000 | Startup probe | +| `livenessProbe` | object | HTTP GET `/health/live` on management:9000 | Liveness probe | +| `readinessProbe` | object | HTTP GET `/health/ready` on management:9000 | Readiness probe | + +### Pod Scheduling & Metadata + +| Key | Type | Default | Description | +| -------------------- | ------ | ------- | -------------------------------- | +| `resources` | object | `{}` | CPU/memory requests and limits | +| `nodeSelector` | object | `{}` | Node selector labels | +| `tolerations` | list | `[]` | Pod tolerations | +| `affinity` | object | `{}` | Pod affinity rules | +| `podAnnotations` | object | `{}` | Pod annotations | +| `podLabels` | object | `{}` | Additional pod labels | +| `podSecurityContext` | object | `{}` | Pod-level security context | +| `securityContext` | object | `{}` | Container-level security context | + +## Architecture + +``` +┌──────────────────────────────────────────────────────┐ +│ Ingress Controller │ +│ │ +│ / ────────────────────────► Keycloak Pod │ +│ ├─ :8080 HTTP │ +│ ├─ :9000 Management │ +│ │ (health + metrics) │ +│ └─ :7800 JGroups │ +│ (clustering) │ +└──────────────────────────────────────────────────────┘ + │ │ + Headless Service Main Service + (JGroups DNS-PING) (HTTP + Management) +``` + +## Upstream Source + +This chart deploys the upstream [Keycloak](https://github.com/keycloak/keycloak) image from `quay.io/keycloak/keycloak`. See the `sources` field in `Chart.yaml` for details. + +## License + +Apache License 2.0 — see [LICENSE](../../LICENSE). diff --git a/charts/keycloak/ci/e2e-values-postgres.yaml b/charts/keycloak/ci/e2e-values-postgres.yaml new file mode 100644 index 0000000..a3b059d --- /dev/null +++ b/charts/keycloak/ci/e2e-values-postgres.yaml @@ -0,0 +1,29 @@ +# E2E test values — PostgreSQL backend in a kind cluster. +# +# The e2e script deploys postgres via a simple Deployment + Service +# and creates the password secret before chart install. + +database: + type: postgresql + host: "postgres.keycloak-e2e.svc.cluster.local" + port: 5432 + user: keycloak + name: keycloak + password: + secretName: keycloak-db-password + secretKey: password + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password + +startupProbe: + httpGet: + path: /health/started + port: management + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 120 + timeoutSeconds: 3 diff --git a/charts/keycloak/ci/e2e-values-replicas.yaml b/charts/keycloak/ci/e2e-values-replicas.yaml new file mode 100644 index 0000000..9fd65ab --- /dev/null +++ b/charts/keycloak/ci/e2e-values-replicas.yaml @@ -0,0 +1,34 @@ +# E2E test values — multi-replica with kubernetes (dns-ping) cache stack. +# +# Tests that multiple Keycloak pods can form a cluster using the +# headless service for JGroups DNS discovery. + +replicaCount: 2 + +database: + type: postgresql + host: "postgres.keycloak-e2e.svc.cluster.local" + port: 5432 + user: keycloak + name: keycloak + password: + secretName: keycloak-db-password + secretKey: password + +cache: + stack: kubernetes + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password + +startupProbe: + httpGet: + path: /health/started + port: management + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 120 + timeoutSeconds: 3 diff --git a/charts/keycloak/ci/e2e-values.yaml b/charts/keycloak/ci/e2e-values.yaml new file mode 100644 index 0000000..1c0aac6 --- /dev/null +++ b/charts/keycloak/ci/e2e-values.yaml @@ -0,0 +1,24 @@ +# E2E test values — dev mode (embedded H2) in a kind cluster. +# +# This configuration: +# - Uses the embedded H2 dev database (no external DB) +# - Sets admin credentials for API testing +# - Increases startup probe tolerance for slow CI environments + +database: + type: dev + +admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password + +startupProbe: + httpGet: + path: /health/started + port: management + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 120 + timeoutSeconds: 3 diff --git a/charts/keycloak/templates/NOTES.txt b/charts/keycloak/templates/NOTES.txt new file mode 100644 index 0000000..2603683 --- /dev/null +++ b/charts/keycloak/templates/NOTES.txt @@ -0,0 +1,127 @@ +Keycloak has been deployed successfully! + +Components: + HTTP Service: {{ include "keycloak.fullname" . }}:{{ .Values.service.httpPort }} + Management Service: {{ include "keycloak.fullname" . }}:{{ .Values.service.managementPort }} (health + metrics) + Headless Service: {{ include "keycloak.fullname" . }}-headless:{{ .Values.headlessService.jgroupsPort }} (JGroups clustering) + +{{- if .Values.ingress.enabled }} + + Ingress: + {{- range .Values.ingress.hosts }} + https://{{ .host }}{{ (index .paths 0).path }} + {{- end }} +{{- end }} + + Database: {{ .Values.database.type }} + Cache Stack: {{ .Values.cache.stack }} + Replicas: {{ .Values.replicaCount }} + +───────────────────────────────────────────────────────────────────────── +Admin Credentials: +{{- if and .Values.admin.username .Values.admin.password.secretName }} + + The initial admin user is created automatically on first startup. + + Username: {{ .Values.admin.username }} + Password: stored in Kubernetes Secret "{{ .Values.admin.password.secretName }}" (key: {{ .Values.admin.password.secretKey }}) + + To retrieve the admin password: + + kubectl get secret {{ .Values.admin.password.secretName }} -n {{ .Release.Namespace }} \ + -o jsonpath='{.data.{{ .Values.admin.password.secretKey }}}' | base64 -d + + To set up admin credentials, create a Secret before installing the chart: + + kubectl create secret generic keycloak-admin-password \ + --from-literal=password='YOUR_SECURE_PASSWORD' \ + -n {{ .Release.Namespace }} + + Then reference it in your values.yaml: + + admin: + username: admin + password: + secretName: keycloak-admin-password + secretKey: password + + IMPORTANT: The admin user is only created on the first startup of a + fresh Keycloak instance. If the database already contains users, the + KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD environment variables are + ignored. To reset credentials on an existing instance, use the + Keycloak CLI (kcadm.sh) or the Admin REST API. +{{- else }} + + WARNING: No admin credentials are configured. Keycloak will start + without an initial admin user. To create one, set these values: + + admin: + username: admin + password: + secretName: + secretKey: password + + The Secret must exist in the release namespace and contain the + admin password. Create it with: + + kubectl create secret generic keycloak-admin-password \ + --from-literal=password='YOUR_SECURE_PASSWORD' \ + -n {{ .Release.Namespace }} +{{- end }} + +───────────────────────────────────────────────────────────────────────── +Accessing the Admin Console: +{{- if .Values.ingress.enabled }} + {{- range .Values.ingress.hosts }} + + Open https://{{ .host }}/admin in your browser. + {{- end }} +{{- else }} + + Port-forward the HTTP service to access the admin console locally: + + kubectl port-forward svc/{{ include "keycloak.fullname" . }} 8080:{{ .Values.service.httpPort }} -n {{ .Release.Namespace }} + + Then open http://localhost:8080/admin in your browser. +{{- end }} + +───────────────────────────────────────────────────────────────────────── +REST API Quick Start: + + 1. Obtain an admin access token: + + TOKEN=$(curl -s -X POST \ + http://localhost:8080/realms/master/protocol/openid-connect/token \ + -d "client_id=admin-cli" \ + -d "username={{ .Values.admin.username | default "admin" }}" \ + -d "password=" \ + -d "grant_type=password" | jq -r '.access_token') + + 2. Use the token to call the Admin REST API: + + curl -H "Authorization: Bearer $TOKEN" \ + http://localhost:8080/admin/realms + +{{- if eq .Values.database.type "dev" }} + +───────────────────────────────────────────────────────────────────────── +WARNING: Development Database + + You are using the embedded H2 development database (database.type=dev). + This is NOT suitable for production use: + - Data is not persisted across pod restarts + - Clustering is not supported with H2 + - Performance is significantly lower than a dedicated database + + For production deployments, configure an external database: + + database: + type: postgresql # or: mysql, mssql + host: your-db-host.example.com + port: 5432 + name: keycloak + user: keycloak + password: + secretName: keycloak-db-password + secretKey: password +{{- end }} diff --git a/charts/keycloak/templates/_helpers.tpl b/charts/keycloak/templates/_helpers.tpl new file mode 100644 index 0000000..b016692 --- /dev/null +++ b/charts/keycloak/templates/_helpers.tpl @@ -0,0 +1,165 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "keycloak.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "keycloak.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 }} + +{{/* +Chart label +*/}} +{{- define "keycloak.chart" -}} + {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "keycloak.labels" -}} +helm.sh/chart: {{ include "keycloak.chart" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "keycloak.selectorLabels" -}} +app.kubernetes.io/name: {{ include "keycloak.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +All labels (common + selector) +*/}} +{{- define "keycloak.allLabels" -}} +{{ include "keycloak.labels" . }} +{{ include "keycloak.selectorLabels" . }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "keycloak.serviceAccountName" -}} + {{- if .Values.serviceAccount.create }} +{{- default (include "keycloak.fullname" .) .Values.serviceAccount.name }} + {{- else }} +{{- default "default" .Values.serviceAccount.name }} + {{- end }} +{{- end }} + +{{/* +Headless service FQDN for dns-ping JGroups discovery +*/}} +{{- define "keycloak.headlessServiceFQDN" -}} + {{- printf "%s-headless.%s.svc.cluster.local" (include "keycloak.fullname" .) .Release.Namespace }} +{{- end }} + +{{/* ===== Database helpers ===== */}} + +{{/* +keycloak.database.isExternal — true when database.type is not dev. +*/}} +{{- define "keycloak.database.isExternal" -}} +{{- ne .Values.database.type "dev" -}} +{{- end }} + +{{/* +keycloak.database.vendor — maps database.type to KC_DB value. +*/}} +{{- define "keycloak.database.vendor" -}} + {{- if eq .Values.database.type "postgresql" -}}postgres + {{- else if eq .Values.database.type "mysql" -}}mysql + {{- else if eq .Values.database.type "mssql" -}}mssql + {{- else -}}dev-file + {{- end -}} +{{- end }} + +{{/* +keycloak.database.port — resolves the effective database port. +*/}} +{{- define "keycloak.database.port" -}} + {{- if .Values.database.port -}} +{{- .Values.database.port -}} + {{- else if eq .Values.database.type "postgresql" -}}5432 + {{- else if eq .Values.database.type "mysql" -}}3306 + {{- else if eq .Values.database.type "mssql" -}}1433 + {{- else -}}0 + {{- end -}} +{{- end }} + +{{/* +keycloak.envConfigData — renders the ConfigMap data block for KC_* environment +variables. Used both in the ConfigMap template and as a checksum source for +rolling deployment updates. +*/}} +{{- define "keycloak.envConfigData" -}} +KC_HEALTH_ENABLED: {{ .Values.healthEnabled | quote }} +KC_METRICS_ENABLED: {{ .Values.metrics.enabled | quote }} +KC_LOG_LEVEL: {{ .Values.logLevel | quote }} +KC_HTTP_ENABLED: {{ .Values.httpEnabled | quote }} +{{- if .Values.hostname }} +KC_HOSTNAME: {{ .Values.hostname | quote }} +{{- end }} +KC_HOSTNAME_STRICT: {{ .Values.hostnameStrict | quote }} +{{- if .Values.hostnameAdmin }} +KC_HOSTNAME_ADMIN: {{ .Values.hostnameAdmin | quote }} +{{- end }} +{{- if .Values.proxyHeaders }} +KC_PROXY_HEADERS: {{ .Values.proxyHeaders | quote }} +{{- end }} +{{- if .Values.features }} +KC_FEATURES: {{ .Values.features | quote }} +{{- end }} +{{- if .Values.tls.enabled }} +KC_HTTPS_CERTIFICATE_FILE: "/opt/keycloak/conf/tls/tls.crt" +KC_HTTPS_CERTIFICATE_KEY_FILE: "/opt/keycloak/conf/tls/tls.key" +{{- end }} +{{- if eq (include "keycloak.database.isExternal" .) "true" }} +KC_DB: {{ include "keycloak.database.vendor" . | quote }} +KC_DB_URL_HOST: {{ .Values.database.host | quote }} +KC_DB_URL_PORT: {{ include "keycloak.database.port" . | quote }} +KC_DB_URL_DATABASE: {{ .Values.database.name | quote }} +KC_DB_USERNAME: {{ .Values.database.user | quote }} + {{- if .Values.database.poolMinSize }} +KC_DB_POOL_MIN_SIZE: {{ .Values.database.poolMinSize | quote }} + {{- end }} + {{- if .Values.database.poolInitialSize }} +KC_DB_POOL_INITIAL_SIZE: {{ .Values.database.poolInitialSize | quote }} + {{- end }} + {{- if .Values.database.poolMaxSize }} +KC_DB_POOL_MAX_SIZE: {{ .Values.database.poolMaxSize | quote }} + {{- end }} + {{- if .Values.database.sslMode }} + {{- if eq .Values.database.type "postgresql" }} +KC_DB_URL_PROPERTIES: {{ printf "?sslmode=%s" .Values.database.sslMode | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- if eq .Values.database.type "dev" }} +KC_CACHE: "local" +{{- else }} +KC_CACHE: "ispn" +KC_CACHE_STACK: {{ .Values.cache.stack | quote }} +{{- if eq .Values.cache.stack "kubernetes" }} +KC_CACHE_CONFIG_FILE: "cache-ispn.xml" +JAVA_OPTS_APPEND: {{ printf "-Djgroups.dns.query=%s" (include "keycloak.headlessServiceFQDN" .) | quote }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/keycloak/templates/configmap.yaml b/charts/keycloak/templates/configmap.yaml new file mode 100644 index 0000000..85d33a8 --- /dev/null +++ b/charts/keycloak/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "keycloak.fullname" . }}-env + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} +data: + {{- include "keycloak.envConfigData" . | nindent 2 }} diff --git a/charts/keycloak/templates/deployment.yaml b/charts/keycloak/templates/deployment.yaml new file mode 100644 index 0000000..6515413 --- /dev/null +++ b/charts/keycloak/templates/deployment.yaml @@ -0,0 +1,193 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "keycloak.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "keycloak.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/env: {{ include "keycloak.envConfigData" . | sha256sum }} +{{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} +{{- end }} + labels: + {{- include "keycloak.selectorLabels" . | nindent 8 }} +{{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} +{{- end }} + spec: + serviceAccountName: {{ include "keycloak.serviceAccountName" . }} +{{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- if .Values.build.enabled }} + initContainers: + - name: build + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/opt/keycloak/bin/kc.sh"] + args: ["build"] + envFrom: + - configMapRef: + name: {{ include "keycloak.fullname" . }}-env + env: +{{- if eq (include "keycloak.database.isExternal" .) "true" }} + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.password.secretName }} + key: {{ .Values.database.password.secretKey }} +{{- end }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + volumeMounts: + - name: build-cache + mountPath: /opt/keycloak/lib/quarkus +{{- end }} + containers: + - name: keycloak + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: +{{- if eq .Values.database.type "dev" }} + - start-dev +{{- else }} + - start + {{- if .Values.build.enabled }} + - --optimized + {{- end }} +{{- end }} + envFrom: + - configMapRef: + name: {{ include "keycloak.fullname" . }}-env +{{- if .Values.extraEnvVarsSecret }} + - secretRef: + name: {{ .Values.extraEnvVarsSecret }} +{{- end }} +{{- $hasAdmin := and .Values.admin.username .Values.admin.password.secretName }} +{{- $hasExternalDB := eq (include "keycloak.database.isExternal" .) "true" }} +{{- if or $hasAdmin $hasExternalDB .Values.extraEnvVars }} + env: + {{- if $hasAdmin }} + - name: KEYCLOAK_ADMIN + value: {{ .Values.admin.username | quote }} + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.admin.password.secretName }} + key: {{ .Values.admin.password.secretKey }} + {{- end }} + {{- if $hasExternalDB }} + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.password.secretName }} + key: {{ .Values.database.password.secretKey }} + {{- end }} + {{- with .Values.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: management + containerPort: 9000 + protocol: TCP + - name: jgroups + containerPort: 7800 + protocol: TCP +{{- if .Values.tls.enabled }} + - name: https + containerPort: 8443 + protocol: TCP +{{- end }} +{{- if or .Values.build.enabled .Values.tls.enabled .Values.persistence.enabled .Values.extraVolumeMounts }} + volumeMounts: + {{- if .Values.build.enabled }} + - name: build-cache + mountPath: /opt/keycloak/lib/quarkus + readOnly: true + {{- end }} + {{- if .Values.tls.enabled }} + - name: tls-certs + mountPath: /opt/keycloak/conf/tls + readOnly: true + {{- end }} + {{- if .Values.persistence.enabled }} + - name: providers + mountPath: /opt/keycloak/providers + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} +{{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} +{{- end }} +{{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} +{{- end }} +{{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} +{{- end }} +{{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} +{{- end }} +{{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} +{{- end }} +{{- if or .Values.build.enabled .Values.tls.enabled .Values.persistence.enabled .Values.extraVolumes }} + volumes: + {{- if .Values.build.enabled }} + - name: build-cache + emptyDir: {} + {{- end }} + {{- if .Values.tls.enabled }} + - name: tls-certs + secret: + secretName: {{ .Values.tls.secretName }} + {{- end }} + {{- if .Values.persistence.enabled }} + - name: providers + persistentVolumeClaim: + claimName: {{ include "keycloak.fullname" . }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} +{{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} +{{- end }} diff --git a/charts/keycloak/templates/headless-service.yaml b/charts/keycloak/templates/headless-service.yaml new file mode 100644 index 0000000..0c2d544 --- /dev/null +++ b/charts/keycloak/templates/headless-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "keycloak.fullname" . }}-headless + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.headlessService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: jgroups + port: {{ .Values.headlessService.jgroupsPort }} + targetPort: jgroups + protocol: TCP + selector: + {{- include "keycloak.selectorLabels" . | nindent 4 }} diff --git a/charts/keycloak/templates/ingress.yaml b/charts/keycloak/templates/ingress.yaml new file mode 100644 index 0000000..a1d593e --- /dev/null +++ b/charts/keycloak/templates/ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "keycloak.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - secretName: {{ .secretName }} + hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ include "keycloak.fullname" $ }} + port: + name: http + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/keycloak/templates/pvc.yaml b/charts/keycloak/templates/pvc.yaml new file mode 100644 index 0000000..e07205a --- /dev/null +++ b/charts/keycloak/templates/pvc.yaml @@ -0,0 +1,22 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "keycloak.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- toYaml .Values.persistence.accessModes | nindent 4 }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} diff --git a/charts/keycloak/templates/service.yaml b/charts/keycloak/templates/service.yaml new file mode 100644 index 0000000..f0b7faf --- /dev/null +++ b/charts/keycloak/templates/service.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "keycloak.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.httpPort }} + targetPort: http + protocol: TCP + - name: management + port: {{ .Values.service.managementPort }} + targetPort: management + protocol: TCP + selector: + {{- include "keycloak.selectorLabels" . | nindent 4 }} diff --git a/charts/keycloak/templates/serviceaccount.yaml b/charts/keycloak/templates/serviceaccount.yaml new file mode 100644 index 0000000..c1d7c40 --- /dev/null +++ b/charts/keycloak/templates/serviceaccount.yaml @@ -0,0 +1,17 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "keycloak.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.imagePullSecrets }} +imagePullSecrets: + {{- toYaml . | nindent 2 }} + {{- end }} +{{- end }} diff --git a/charts/keycloak/templates/servicemonitor.yaml b/charts/keycloak/templates/servicemonitor.yaml new file mode 100644 index 0000000..42187fb --- /dev/null +++ b/charts/keycloak/templates/servicemonitor.yaml @@ -0,0 +1,25 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "keycloak.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "keycloak.selectorLabels" . | nindent 6 }} + endpoints: + - port: management + path: /metrics + {{- with .Values.metrics.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} +{{- end }} diff --git a/charts/keycloak/templates/tests/test-connection.yaml b/charts/keycloak/templates/tests/test-connection.yaml new file mode 100644 index 0000000..eab9554 --- /dev/null +++ b/charts/keycloak/templates/tests/test-connection.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "keycloak.fullname" . }}-test" + namespace: {{ .Release.Namespace }} + labels: + {{- include "keycloak.allLabels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: test + image: busybox:1.37 + command: ['sh', '-c'] + args: + - | + set -e + FAIL=0 + + echo "==> Testing Keycloak health endpoint..." + RESPONSE=$(wget -qO- --timeout=10 http://{{ include "keycloak.fullname" . }}:{{ .Values.service.managementPort }}/health/ready 2>&1) || { + echo "FAIL: could not reach health endpoint" + FAIL=1 + } + + if [ "$FAIL" -eq 0 ]; then + echo "Response: $RESPONSE" + case "$RESPONSE" in + *UP*) + echo "PASS: Keycloak health check returned UP" + ;; + *) + echo "FAIL: unexpected health response: $RESPONSE" + FAIL=1 + ;; + esac + fi + + echo "" + if [ "$FAIL" -ne 0 ]; then + echo "Some tests FAILED" + exit 1 + fi + echo "All tests PASSED" diff --git a/charts/keycloak/tests/configmap_test.yaml b/charts/keycloak/tests/configmap_test.yaml new file mode 100644 index 0000000..4a7b037 --- /dev/null +++ b/charts/keycloak/tests/configmap_test.yaml @@ -0,0 +1,263 @@ +suite: configmap tests +templates: + - templates/configmap.yaml +tests: + - it: should render a ConfigMap + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ConfigMap + + - it: should include health and metrics by default + asserts: + - equal: + path: data.KC_HEALTH_ENABLED + value: "true" + - equal: + path: data.KC_METRICS_ENABLED + value: "true" + + - it: should set log level + set: + logLevel: "debug" + asserts: + - equal: + path: data.KC_LOG_LEVEL + value: "debug" + + - it: should set hostname when configured + set: + hostname: keycloak.example.com + asserts: + - equal: + path: data.KC_HOSTNAME + value: "keycloak.example.com" + + - it: should not set hostname when empty + asserts: + - notExists: + path: data.KC_HOSTNAME + + - it: should set admin hostname when configured + set: + hostnameAdmin: admin.example.com + asserts: + - equal: + path: data.KC_HOSTNAME_ADMIN + value: "admin.example.com" + + - it: should default hostname strict to false + asserts: + - equal: + path: data.KC_HOSTNAME_STRICT + value: "false" + + - it: should set hostname strict to true when configured + set: + hostname: keycloak.example.com + hostnameStrict: true + asserts: + - equal: + path: data.KC_HOSTNAME_STRICT + value: "true" + + - it: should set proxy headers when configured + set: + proxyHeaders: xforwarded + asserts: + - equal: + path: data.KC_PROXY_HEADERS + value: "xforwarded" + + - it: should not set proxy headers when empty + asserts: + - notExists: + path: data.KC_PROXY_HEADERS + + - it: should set features when configured + set: + features: "token-exchange,admin-fine-grained-authz" + asserts: + - equal: + path: data.KC_FEATURES + value: "token-exchange,admin-fine-grained-authz" + + - it: should set TLS certificate paths when enabled + set: + tls.enabled: true + tls.secretName: kc-tls + asserts: + - equal: + path: data.KC_HTTPS_CERTIFICATE_FILE + value: "/opt/keycloak/conf/tls/tls.crt" + - equal: + path: data.KC_HTTPS_CERTIFICATE_KEY_FILE + value: "/opt/keycloak/conf/tls/tls.key" + + - it: should not set TLS paths when disabled + asserts: + - notExists: + path: data.KC_HTTPS_CERTIFICATE_FILE + + # ── Database ────────────────────────────────────────────────────────── + + - it: should not set DB vars for dev database + asserts: + - notExists: + path: data.KC_DB + + - it: should set PostgreSQL database vars + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + asserts: + - equal: + path: data.KC_DB + value: "postgres" + - equal: + path: data.KC_DB_URL_HOST + value: "pg.example.com" + - equal: + path: data.KC_DB_URL_PORT + value: "5432" + - equal: + path: data.KC_DB_URL_DATABASE + value: "keycloak" + - equal: + path: data.KC_DB_USERNAME + value: "keycloak" + + - it: should set MySQL database vars + set: + database.type: mysql + database.host: mysql.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: mysql-secret + asserts: + - equal: + path: data.KC_DB + value: "mysql" + - equal: + path: data.KC_DB_URL_PORT + value: "3306" + + - it: should set MSSQL database vars + set: + database.type: mssql + database.host: mssql.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: mssql-secret + asserts: + - equal: + path: data.KC_DB + value: "mssql" + - equal: + path: data.KC_DB_URL_PORT + value: "1433" + + - it: should use custom database port + set: + database.type: postgresql + database.host: pg.example.com + database.port: 5433 + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + asserts: + - equal: + path: data.KC_DB_URL_PORT + value: "5433" + + - it: should set connection pool sizes when configured + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + database.poolMinSize: "5" + database.poolInitialSize: "10" + database.poolMaxSize: "50" + asserts: + - equal: + path: data.KC_DB_POOL_MIN_SIZE + value: "5" + - equal: + path: data.KC_DB_POOL_INITIAL_SIZE + value: "10" + - equal: + path: data.KC_DB_POOL_MAX_SIZE + value: "50" + + - it: should set PostgreSQL SSL mode + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + database.sslMode: "verify-full" + asserts: + - equal: + path: data.KC_DB_URL_PROPERTIES + value: "?sslmode=verify-full" + + # ── Clustering ──────────────────────────────────────────────────────── + + - it: should use local cache in dev mode + asserts: + - equal: + path: data.KC_CACHE + value: "local" + - notExists: + path: data.KC_CACHE_STACK + + - it: should set jdbc-ping cache stack with external database + set: + database.type: postgresql + database.host: db.example.com + database.user: keycloak + database.password.secretName: db-secret + asserts: + - equal: + path: data.KC_CACHE + value: "ispn" + - equal: + path: data.KC_CACHE_STACK + value: "jdbc-ping" + + - it: should configure kubernetes cache stack with dns query + set: + database.type: postgresql + database.host: db.example.com + database.user: keycloak + database.password.secretName: db-secret + cache.stack: kubernetes + asserts: + - equal: + path: data.KC_CACHE_STACK + value: "kubernetes" + - equal: + path: data.KC_CACHE_CONFIG_FILE + value: "cache-ispn.xml" + - equal: + path: data.JAVA_OPTS_APPEND + value: "-Djgroups.dns.query=RELEASE-NAME-keycloak-headless.NAMESPACE.svc.cluster.local" + + - it: should not set dns query for jdbc-ping + set: + database.type: postgresql + database.host: db.example.com + database.user: keycloak + database.password.secretName: db-secret + asserts: + - notExists: + path: data.JAVA_OPTS_APPEND + - notExists: + path: data.KC_CACHE_CONFIG_FILE diff --git a/charts/keycloak/tests/deployment_test.yaml b/charts/keycloak/tests/deployment_test.yaml new file mode 100644 index 0000000..a845b45 --- /dev/null +++ b/charts/keycloak/tests/deployment_test.yaml @@ -0,0 +1,476 @@ +suite: deployment tests +templates: + - templates/deployment.yaml +tests: + - it: should render a Deployment + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Deployment + + - it: should use default image from appVersion + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: "quay.io/keycloak/keycloak:26.5.0" + + - it: should use custom image tag when set + set: + image.tag: "25.0.0" + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: "quay.io/keycloak/keycloak:25.0.0" + + - it: should use custom image repository + set: + image.repository: my-registry/keycloak + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: "my-registry/keycloak:26.5.0" + + - it: should set replica count + set: + replicaCount: 3 + asserts: + - equal: + path: spec.replicas + value: 3 + + - it: should include selector labels + asserts: + - isSubset: + path: spec.selector.matchLabels + content: + app.kubernetes.io/name: keycloak + app.kubernetes.io/instance: RELEASE-NAME + + - it: should include pod labels + asserts: + - isSubset: + path: spec.template.metadata.labels + content: + app.kubernetes.io/name: keycloak + app.kubernetes.io/instance: RELEASE-NAME + + - it: should add custom pod annotations + set: + podAnnotations: + prometheus.io/scrape: "true" + asserts: + - isSubset: + path: spec.template.metadata.annotations + content: + prometheus.io/scrape: "true" + + - it: should add custom pod labels + set: + podLabels: + custom-label: custom-value + asserts: + - isSubset: + path: spec.template.metadata.labels + content: + custom-label: custom-value + + - it: should include env checksum annotation + asserts: + - exists: + path: spec.template.metadata.annotations.checksum/env + + # ── Ports ────────────────────────────────────────────────────────────── + + - it: should expose http, management, and jgroups ports + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + name: http + containerPort: 8080 + protocol: TCP + - contains: + path: spec.template.spec.containers[0].ports + content: + name: management + containerPort: 9000 + protocol: TCP + - contains: + path: spec.template.spec.containers[0].ports + content: + name: jgroups + containerPort: 7800 + protocol: TCP + + - it: should expose https port when TLS is enabled + set: + tls.enabled: true + tls.secretName: kc-tls + asserts: + - contains: + path: spec.template.spec.containers[0].ports + content: + name: https + containerPort: 8443 + protocol: TCP + + - it: should not expose https port when TLS is disabled + asserts: + - notContains: + path: spec.template.spec.containers[0].ports + content: + name: https + any: true + + # ── Args ─────────────────────────────────────────────────────────────── + + - it: should use start-dev for dev database + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: start-dev + + - it: should use start for external database + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: start + - notContains: + path: spec.template.spec.containers[0].args + content: start-dev + + - it: should add --optimized flag when build is enabled + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + build.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --optimized + + # ── Environment Variables ────────────────────────────────────────────── + + - it: should mount env configmap via envFrom + asserts: + - contains: + path: spec.template.spec.containers[0].envFrom + content: + configMapRef: + name: RELEASE-NAME-keycloak-env + + - it: should inject admin credentials when configured + set: + admin.username: admin + admin.password.secretName: admin-secret + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: KEYCLOAK_ADMIN + value: "admin" + - contains: + path: spec.template.spec.containers[0].env + content: + name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: admin-secret + key: password + + - it: should not inject admin credentials when not configured + asserts: + - notExists: + path: spec.template.spec.containers[0].env + + - it: should inject DB_PASSWORD for external database + set: + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + database.password.secretKey: dbpass + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: pg-secret + key: dbpass + + - it: should not inject DB_PASSWORD for dev database + asserts: + - notExists: + path: spec.template.spec.containers[0].env + + - it: should mount extraEnvVarsSecret when set + set: + extraEnvVarsSecret: my-extra-secret + asserts: + - contains: + path: spec.template.spec.containers[0].envFrom + content: + secretRef: + name: my-extra-secret + + - it: should include extra env vars + set: + extraEnvVars: + - name: MY_VAR + value: my-value + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: MY_VAR + value: my-value + + # ── Build Init Container ────────────────────────────────────────────── + + - it: should not have init containers by default + asserts: + - notExists: + path: spec.template.spec.initContainers + + - it: should add build init container when enabled + set: + build.enabled: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: build + - equal: + path: spec.template.spec.initContainers[0].args[0] + value: build + + - it: should use same image for build init container + set: + build.enabled: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: "quay.io/keycloak/keycloak:26.5.0" + + - it: should set hardened securityContext on build init container + set: + build.enabled: true + asserts: + - equal: + path: spec.template.spec.initContainers[0].securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.initContainers[0].securityContext.allowPrivilegeEscalation + value: false + + - it: should inject DB_PASSWORD into build init container for external database + set: + build.enabled: true + database.type: postgresql + database.host: pg.example.com + database.user: keycloak + database.name: keycloak + database.password.secretName: pg-secret + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: pg-secret + key: password + + # ── Volumes ──────────────────────────────────────────────────────────── + + - it: should mount TLS certs when enabled + set: + tls.enabled: true + tls.secretName: kc-tls + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: tls-certs + mountPath: /opt/keycloak/conf/tls + readOnly: true + - contains: + path: spec.template.spec.volumes + content: + name: tls-certs + secret: + secretName: kc-tls + + - it: should mount PVC when persistence is enabled + set: + persistence.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: providers + mountPath: /opt/keycloak/providers + - contains: + path: spec.template.spec.volumes + content: + name: providers + persistentVolumeClaim: + claimName: RELEASE-NAME-keycloak + + - it: should mount extra volumes + set: + extraVolumes: + - name: my-theme + configMap: + name: my-theme-cm + extraVolumeMounts: + - name: my-theme + mountPath: /opt/keycloak/themes/my-theme + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: my-theme + configMap: + name: my-theme-cm + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: my-theme + mountPath: /opt/keycloak/themes/my-theme + + # ── Probes ───────────────────────────────────────────────────────────── + + - it: should configure startup probe on management port + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /health/started + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.port + value: management + + - it: should configure liveness probe on management port + asserts: + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /health/live + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.port + value: management + + - it: should configure readiness probe on management port + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /health/ready + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.port + value: management + + # ── Scheduling ───────────────────────────────────────────────────────── + + - it: should set resource limits when specified + set: + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + asserts: + - equal: + path: spec.template.spec.containers[0].resources + value: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + + - it: should set node selector + set: + nodeSelector: + disktype: ssd + asserts: + - equal: + path: spec.template.spec.nodeSelector + value: + disktype: ssd + + - it: should set tolerations + set: + tolerations: + - key: "dedicated" + operator: "Equal" + value: "keycloak" + effect: "NoSchedule" + asserts: + - contains: + path: spec.template.spec.tolerations + content: + key: "dedicated" + operator: "Equal" + value: "keycloak" + effect: "NoSchedule" + + - it: should set affinity + set: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: keycloak + topologyKey: kubernetes.io/hostname + asserts: + - exists: + path: spec.template.spec.affinity.podAntiAffinity + + - it: should set pod security context + set: + podSecurityContext: + runAsUser: 1000 + fsGroup: 2000 + asserts: + - equal: + path: spec.template.spec.securityContext + value: + runAsUser: 1000 + fsGroup: 2000 + + - it: should set container security context + set: + securityContext: + readOnlyRootFilesystem: true + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext + value: + readOnlyRootFilesystem: true + + - it: should set imagePullSecrets + set: + imagePullSecrets: + - name: my-registry + asserts: + - contains: + path: spec.template.spec.imagePullSecrets + content: + name: my-registry diff --git a/charts/keycloak/tests/headless-service_test.yaml b/charts/keycloak/tests/headless-service_test.yaml new file mode 100644 index 0000000..fc4b308 --- /dev/null +++ b/charts/keycloak/tests/headless-service_test.yaml @@ -0,0 +1,49 @@ +suite: headless service tests +templates: + - templates/headless-service.yaml +tests: + - it: should render a headless Service + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Service + - equal: + path: spec.clusterIP + value: None + + - it: should expose jgroups port + asserts: + - contains: + path: spec.ports + content: + name: jgroups + port: 7800 + targetPort: jgroups + protocol: TCP + + - it: should use custom jgroups port + set: + headlessService.jgroupsPort: 7801 + asserts: + - contains: + path: spec.ports + content: + name: jgroups + port: 7801 + targetPort: jgroups + protocol: TCP + + - it: should include selector labels + asserts: + - isSubset: + path: spec.selector + content: + app.kubernetes.io/name: keycloak + app.kubernetes.io/instance: RELEASE-NAME + + - it: should have -headless suffix in name + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-keycloak-headless diff --git a/charts/keycloak/tests/ingress_test.yaml b/charts/keycloak/tests/ingress_test.yaml new file mode 100644 index 0000000..7137e4f --- /dev/null +++ b/charts/keycloak/tests/ingress_test.yaml @@ -0,0 +1,102 @@ +suite: ingress tests +templates: + - templates/ingress.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render when enabled + set: + ingress.enabled: true + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + pathType: Prefix + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Ingress + + - it: should set ingress class name + set: + ingress.enabled: true + ingress.className: nginx + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + asserts: + - equal: + path: spec.ingressClassName + value: nginx + + - it: should set annotations + set: + ingress.enabled: true + ingress.annotations: + cert-manager.io/cluster-issuer: letsencrypt + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + asserts: + - isSubset: + path: metadata.annotations + content: + cert-manager.io/cluster-issuer: letsencrypt + + - it: should set host and paths + set: + ingress.enabled: true + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + pathType: Prefix + - path: /admin + pathType: Prefix + asserts: + - equal: + path: spec.rules[0].host + value: keycloak.example.com + - equal: + path: spec.rules[0].http.paths[0].path + value: / + - equal: + path: spec.rules[0].http.paths[1].path + value: /admin + + - it: should default pathType to Prefix + set: + ingress.enabled: true + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + asserts: + - equal: + path: spec.rules[0].http.paths[0].pathType + value: Prefix + + - it: should set TLS + set: + ingress.enabled: true + ingress.hosts: + - host: keycloak.example.com + paths: + - path: / + ingress.tls: + - secretName: keycloak-tls + hosts: + - keycloak.example.com + asserts: + - equal: + path: spec.tls[0].secretName + value: keycloak-tls + - contains: + path: spec.tls[0].hosts + content: keycloak.example.com diff --git a/charts/keycloak/tests/pvc_test.yaml b/charts/keycloak/tests/pvc_test.yaml new file mode 100644 index 0000000..3b8c1e9 --- /dev/null +++ b/charts/keycloak/tests/pvc_test.yaml @@ -0,0 +1,57 @@ +suite: pvc tests +templates: + - templates/pvc.yaml +tests: + - it: should not render by default + asserts: + - hasDocuments: + count: 0 + + - it: should render when persistence is enabled + set: + persistence.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: PersistentVolumeClaim + + - it: should set default access mode and size + set: + persistence.enabled: true + asserts: + - contains: + path: spec.accessModes + content: ReadWriteOnce + - equal: + path: spec.resources.requests.storage + value: 1Gi + + - it: should use custom storage class + set: + persistence.enabled: true + persistence.storageClass: fast-ssd + asserts: + - equal: + path: spec.storageClassName + value: fast-ssd + + - it: should set custom size + set: + persistence.enabled: true + persistence.size: 10Gi + asserts: + - equal: + path: spec.resources.requests.storage + value: 10Gi + + - it: should add annotations + set: + persistence.enabled: true + persistence.annotations: + volume.beta.kubernetes.io/storage-class: "standard" + asserts: + - isSubset: + path: metadata.annotations + content: + volume.beta.kubernetes.io/storage-class: "standard" diff --git a/charts/keycloak/tests/service_test.yaml b/charts/keycloak/tests/service_test.yaml new file mode 100644 index 0000000..12f3cf3 --- /dev/null +++ b/charts/keycloak/tests/service_test.yaml @@ -0,0 +1,79 @@ +suite: service tests +templates: + - templates/service.yaml +tests: + - it: should render a Service + asserts: + - hasDocuments: + count: 1 + - isKind: + of: Service + + - it: should use ClusterIP type by default + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: should expose http and management ports + asserts: + - contains: + path: spec.ports + content: + name: http + port: 8080 + targetPort: http + protocol: TCP + - contains: + path: spec.ports + content: + name: management + port: 9000 + targetPort: management + protocol: TCP + + - it: should use custom service type + set: + service.type: NodePort + asserts: + - equal: + path: spec.type + value: NodePort + + - it: should use custom ports + set: + service.httpPort: 8081 + service.managementPort: 9001 + asserts: + - contains: + path: spec.ports + content: + name: http + port: 8081 + targetPort: http + protocol: TCP + - contains: + path: spec.ports + content: + name: management + port: 9001 + targetPort: management + protocol: TCP + + - it: should include selector labels + asserts: + - isSubset: + path: spec.selector + content: + app.kubernetes.io/name: keycloak + app.kubernetes.io/instance: RELEASE-NAME + + - it: should add annotations when set + set: + service.annotations: + service.beta.kubernetes.io/aws-load-balancer-type: nlb + asserts: + - isSubset: + path: metadata.annotations + content: + service.beta.kubernetes.io/aws-load-balancer-type: nlb diff --git a/charts/keycloak/tests/serviceaccount_test.yaml b/charts/keycloak/tests/serviceaccount_test.yaml new file mode 100644 index 0000000..b658eb3 --- /dev/null +++ b/charts/keycloak/tests/serviceaccount_test.yaml @@ -0,0 +1,57 @@ +suite: serviceaccount tests +templates: + - templates/serviceaccount.yaml +tests: + - it: should create a ServiceAccount by default + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ServiceAccount + - equal: + path: metadata.name + value: RELEASE-NAME-keycloak + + - it: should not create a ServiceAccount when disabled + set: + serviceAccount.create: false + asserts: + - hasDocuments: + count: 0 + + - it: should use custom name when set + set: + serviceAccount.name: my-sa + asserts: + - equal: + path: metadata.name + value: my-sa + + - it: should add annotations + set: + serviceAccount.annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::role/my-role + asserts: + - isSubset: + path: metadata.annotations + content: + eks.amazonaws.com/role-arn: arn:aws:iam::role/my-role + + - it: should include imagePullSecrets from global level + set: + imagePullSecrets: + - name: global-registry + asserts: + - contains: + path: imagePullSecrets + content: + name: global-registry + + - it: should include common labels + asserts: + - isSubset: + path: metadata.labels + content: + helm.sh/chart: keycloak-26.5.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/version: "26.5.0" diff --git a/charts/keycloak/tests/servicemonitor_test.yaml b/charts/keycloak/tests/servicemonitor_test.yaml new file mode 100644 index 0000000..e9a1d28 --- /dev/null +++ b/charts/keycloak/tests/servicemonitor_test.yaml @@ -0,0 +1,63 @@ +suite: servicemonitor tests +templates: + - templates/servicemonitor.yaml +tests: + - it: should not render by default + asserts: + - hasDocuments: + count: 0 + + - it: should not render when only metrics are enabled + set: + metrics.enabled: true + asserts: + - hasDocuments: + count: 0 + + - it: should render when serviceMonitor is enabled + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ServiceMonitor + + - it: should target management port + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + asserts: + - equal: + path: spec.endpoints[0].port + value: management + - equal: + path: spec.endpoints[0].path + value: /metrics + + - it: should add custom labels + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.labels: + release: prometheus + asserts: + - isSubset: + path: metadata.labels + content: + release: prometheus + + - it: should set interval and scrapeTimeout + set: + metrics.enabled: true + metrics.serviceMonitor.enabled: true + metrics.serviceMonitor.interval: 30s + metrics.serviceMonitor.scrapeTimeout: 10s + asserts: + - equal: + path: spec.endpoints[0].interval + value: 30s + - equal: + path: spec.endpoints[0].scrapeTimeout + value: 10s diff --git a/charts/keycloak/values.yaml b/charts/keycloak/values.yaml new file mode 100644 index 0000000..4797fda --- /dev/null +++ b/charts/keycloak/values.yaml @@ -0,0 +1,235 @@ +# -- Override the chart name +nameOverride: "" +# -- Override the full resource name +fullnameOverride: "" + +# -- Global image pull secrets applied to all pods +imagePullSecrets: [] + +# ── Service Account ────────────────────────────────────────────────────── +serviceAccount: + # -- Create a dedicated ServiceAccount + create: true + # -- Annotations for the ServiceAccount (e.g. IAM role bindings) + annotations: {} + # -- Override the ServiceAccount name (defaults to fullname) + name: "" + +# ── Image ──────────────────────────────────────────────────────────────── +image: + # -- Keycloak container image repository + repository: quay.io/keycloak/keycloak + # -- Image tag (defaults to Chart appVersion) + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + +# ── Replica Count ──────────────────────────────────────────────────────── +# -- Number of Keycloak replicas +replicaCount: 1 + +# ── Database ───────────────────────────────────────────────────────────── +database: + # -- Database type: "postgresql", "mysql", "mssql", or "dev" (embedded H2) + type: "dev" + # -- Database hostname (required for postgresql/mysql/mssql) + host: "" + # -- Database port (auto-defaults: 5432 for postgresql, 3306 for mysql, 1433 for mssql) + port: "" + # -- Database name + name: "keycloak" + # -- Database username + user: "" + # -- Database password via Kubernetes Secret reference + password: + # -- Name of the Secret containing the database password + secretName: "" + # -- Key within the Secret + secretKey: "password" + # -- Connection pool minimum size + poolMinSize: "" + # -- Connection pool initial size + poolInitialSize: "" + # -- Connection pool maximum size + poolMaxSize: "" + # -- SSL/TLS mode for database connection (e.g. "verify-full", "require", "disable") + sslMode: "" + +# ── Hostname ───────────────────────────────────────────────────────────── +# -- Public hostname for Keycloak (maps to KC_HOSTNAME) +hostname: "" +# -- Separate admin console hostname (maps to KC_HOSTNAME_ADMIN) +hostnameAdmin: "" +# -- Enforce strict hostname checking (maps to KC_HOSTNAME_STRICT). Set to true when hostname is configured. +hostnameStrict: false + +# ── Proxy / TLS ────────────────────────────────────────────────────────── +# -- Proxy header mode: "xforwarded" or "forwarded" (maps to KC_PROXY_HEADERS) +proxyHeaders: "" +# -- Enable HTTP listener (maps to KC_HTTP_ENABLED); required for edge-terminated TLS +httpEnabled: true +# -- TLS passthrough configuration +tls: + # -- Enable TLS passthrough (mounts certificate and key) + enabled: false + # -- Name of the Secret containing tls.crt and tls.key + secretName: "" + +# ── Clustering ─────────────────────────────────────────────────────────── +cache: + # -- Cache stack: "jdbc-ping" (default, uses the database) or "kubernetes" (dns-ping via headless service) + stack: "jdbc-ping" + +# ── Health ─────────────────────────────────────────────────────────────── +# -- Enable Keycloak health endpoints (KC_HEALTH_ENABLED) +healthEnabled: true + +# ── Observability ──────────────────────────────────────────────────────── +metrics: + # -- Enable Prometheus metrics endpoint on /metrics (KC_METRICS_ENABLED) + enabled: true + # -- Deploy a ServiceMonitor for Prometheus Operator + serviceMonitor: + # -- Create the ServiceMonitor resource + enabled: false + # -- Additional labels for the ServiceMonitor + labels: {} + # -- Scrape interval + interval: "" + # -- Scrape timeout + scrapeTimeout: "" + +# ── Logging ────────────────────────────────────────────────────────────── +# -- Keycloak log level (maps to KC_LOG_LEVEL) +logLevel: "info" + +# ── Features ───────────────────────────────────────────────────────────── +# -- Comma-separated list of Keycloak features to enable (maps to KC_FEATURES) +features: "" + +# ── Admin Credentials ──────────────────────────────────────────────────── +admin: + # -- Admin username (maps to KEYCLOAK_ADMIN). Leave empty to skip initial admin creation + username: "" + # -- Admin password via Kubernetes Secret reference + password: + # -- Name of the Secret containing the admin password + secretName: "" + # -- Key within the Secret + secretKey: "password" + +# ── Build Optimization ────────────────────────────────────────────────── +build: + # -- Run `kc.sh build` in an init container for faster startup + enabled: false + +# ── Extra Configuration ───────────────────────────────────────────────── +# -- Additional environment variables as a list of {name, value} or {name, valueFrom} objects +extraEnvVars: [] + +# -- Name of an existing Secret to mount as additional environment variables +extraEnvVarsSecret: "" + +# -- Additional volumes to add to the pod +extraVolumes: [] + +# -- Additional volume mounts to add to the Keycloak container +extraVolumeMounts: [] + +# ── Persistent Storage ────────────────────────────────────────────────── +persistence: + # -- Enable persistent volume for custom themes/providers + enabled: false + # -- Storage class name + storageClass: "" + # -- Access modes + accessModes: + - ReadWriteOnce + # -- Volume size + size: 1Gi + # -- Annotations for the PVC + annotations: {} + +# ── Service ────────────────────────────────────────────────────────────── +service: + # -- Service type + type: ClusterIP + # -- HTTP port + httpPort: 8080 + # -- Management port (health + metrics) + managementPort: 9000 + # -- Service annotations + annotations: {} + +# ── Headless Service (clustering) ──────────────────────────────────────── +headlessService: + # -- JGroups port for cluster communication + jgroupsPort: 7800 + # -- Annotations for the headless service + annotations: {} + +# ── Ingress ────────────────────────────────────────────────────────────── +ingress: + # -- Enable Ingress resource + enabled: false + # -- Ingress class name + className: "" + # -- Ingress annotations + annotations: {} + # -- Ingress hosts + hosts: [] + # - host: keycloak.example.com + # paths: + # - path: / + # pathType: Prefix + # -- TLS configuration + tls: [] + # - secretName: keycloak-tls + # hosts: + # - keycloak.example.com + +# ── Probes ─────────────────────────────────────────────────────────────── +startupProbe: + httpGet: + path: /health/started + port: management + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 60 + timeoutSeconds: 3 + +livenessProbe: + httpGet: + path: /health/live + port: management + initialDelaySeconds: 0 + periodSeconds: 15 + failureThreshold: 3 + timeoutSeconds: 3 + +readinessProbe: + httpGet: + path: /health/ready + port: management + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 3 + +# ── Pod Scheduling & Metadata ──────────────────────────────────────────── +# -- Resource requests and limits +resources: {} +# -- Node selector +nodeSelector: {} +# -- Tolerations +tolerations: [] +# -- Affinity rules +affinity: {} +# -- Pod annotations +podAnnotations: {} +# -- Pod labels +podLabels: {} +# -- Pod-level security context +podSecurityContext: {} +# -- Container-level security context +securityContext: {} diff --git a/charts/netbird/.helmignore b/charts/netbird/.helmignore new file mode 100644 index 0000000..83754b9 --- /dev/null +++ b/charts/netbird/.helmignore @@ -0,0 +1,5 @@ +# Patterns to ignore when building packages. +docs/ +tests/ +ci/ +README.md diff --git a/docs/compatibility.md b/charts/netbird/docs/compatibility.md similarity index 100% rename from docs/compatibility.md rename to charts/netbird/docs/compatibility.md diff --git a/ci/scripts/keycloak/e2e.sh b/ci/scripts/keycloak/e2e.sh new file mode 100755 index 0000000..e437881 --- /dev/null +++ b/ci/scripts/keycloak/e2e.sh @@ -0,0 +1,327 @@ +#!/usr/bin/env bash +# +# E2E test runner for the keycloak Helm chart. +# +# Usage: +# ci/scripts/keycloak/e2e.sh +# +# Scenarios: +# dev — embedded H2 dev mode (single replica, no external DB) +# postgres — PostgreSQL backend (single replica) +# replicas — multi-replica with kubernetes (dns-ping) cache stack +# +set -euo pipefail + +SCENARIO="${1:-dev}" +RELEASE="keycloak-e2e" +NAMESPACE="keycloak-e2e" +CHART="charts/keycloak" +TIMEOUT="10m" +ADMIN_USER="admin" +ADMIN_PASSWORD="e2e-test-password-123" + +log() { echo "==> $*"; } +fail() { echo "FAIL: $*" >&2; exit 1; } + +# ── Debug & Cleanup functions ────────────────────────────────────────── +dump_debug() { + echo "=== Pod status ===" + kubectl -n "$NAMESPACE" get pods -o wide 2>/dev/null || true + echo "=== Pod describe ===" + kubectl -n "$NAMESPACE" describe pods 2>/dev/null || true + echo "=== Keycloak logs ===" + kubectl -n "$NAMESPACE" logs deployment/"$RELEASE" --all-containers --tail=200 2>/dev/null || true + echo "=== Events ===" + kubectl -n "$NAMESPACE" get events --sort-by='.lastTimestamp' 2>/dev/null || true +} +cleanup() { + local exit_code=$? + if [ "$exit_code" -ne 0 ]; then + log "Test failed — dumping debug info..." + dump_debug + fi + log "Cleaning up..." + helm uninstall "$RELEASE" -n "$NAMESPACE" --ignore-not-found 2>/dev/null || true + kubectl delete namespace "$NAMESPACE" --ignore-not-found 2>/dev/null || true +} +trap cleanup EXIT + +# ── Create namespace ─────────────────────────────────────────────────── +log "Creating namespace $NAMESPACE..." +kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + +# ── Create admin password secret ───────────────────────────────────── +log "Creating admin password secret..." +kubectl -n "$NAMESPACE" create secret generic keycloak-admin-password \ + --from-literal=password="$ADMIN_PASSWORD" + +# ── Deploy PostgreSQL (if needed) ──────────────────────────────────── +deploy_postgres() { + log "Deploying PostgreSQL..." + kubectl -n "$NAMESPACE" apply -f - <<'EOF' +apiVersion: v1 +kind: Secret +metadata: + name: postgres-credentials +type: Opaque +stringData: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: "test-db-password" +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + envFrom: + - secretRef: + name: postgres-credentials + ports: + - containerPort: 5432 + readinessProbe: + exec: + command: ["pg_isready", "-U", "keycloak"] + initialDelaySeconds: 5 + periodSeconds: 3 +EOF + + log "Waiting for PostgreSQL to be ready..." + kubectl -n "$NAMESPACE" rollout status deployment/postgres --timeout=120s + + # Create the password secret for keycloak + kubectl -n "$NAMESPACE" create secret generic keycloak-db-password \ + --from-literal=password='test-db-password' +} + +# ── Scenario dispatch ──────────────────────────────────────────────── +EXTRA_SETS=() +case "$SCENARIO" in + dev) + log "Using dev mode — no external database" + VALUES_FILE="$CHART/ci/e2e-values.yaml" + ;; + postgres) + deploy_postgres + VALUES_FILE="$CHART/ci/e2e-values-postgres.yaml" + ;; + replicas) + deploy_postgres + VALUES_FILE="$CHART/ci/e2e-values-replicas.yaml" + ;; + *) + fail "Unknown scenario: $SCENARIO (expected: dev, postgres, replicas)" + ;; +esac + +# ── Install keycloak chart ─────────────────────────────────────────── +log "Installing keycloak chart with $SCENARIO scenario..." +if ! helm install "$RELEASE" "$CHART" \ + -n "$NAMESPACE" \ + -f "$VALUES_FILE" \ + "${EXTRA_SETS[@]}" \ + --timeout "$TIMEOUT"; then + log "Helm install failed — dumping logs..." + kubectl -n "$NAMESPACE" logs deployment/"$RELEASE" --all-containers --tail=100 2>/dev/null || true + fail "Helm install failed" +fi + +# ── Verify rollout ─────────────────────────────────────────────────── +log "Verifying deployment..." +kubectl -n "$NAMESPACE" rollout status deployment/"$RELEASE" --timeout=600s + +log "Pod status:" +kubectl -n "$NAMESPACE" get pods -o wide + +# ── Run helm test ──────────────────────────────────────────────────── +log "Running helm test..." +helm test "$RELEASE" -n "$NAMESPACE" --timeout 5m + +# ── Verify REST API access ─────────────────────────────────────────── +log "Verifying Keycloak REST API..." +SVC_URL="http://$RELEASE.$NAMESPACE.svc.cluster.local" +MGMT_URL="$SVC_URL:9000" +HTTP_URL="$SVC_URL:8080" + +kubectl -n "$NAMESPACE" run api-test --image=alpine:3.20 --restart=Never \ + --env="HTTP_URL=$HTTP_URL" \ + --env="MGMT_URL=$MGMT_URL" \ + --env="ADMIN_USER=$ADMIN_USER" \ + --env="ADMIN_PASSWORD=$ADMIN_PASSWORD" \ + --command -- sh -c ' + apk add --no-cache curl jq >/dev/null 2>&1 + sleep 3 + + echo "==> Test 1: Health endpoint returns UP..." + HEALTH=$(curl -sf "$MGMT_URL/health/ready" 2>/dev/null || echo "") + if echo "$HEALTH" | grep -q "UP"; then + echo "PASS: Health check returned UP" + else + echo "FAIL: Health check did not return UP" + echo "Response: $HEALTH" + exit 1 + fi + + echo "" + echo "==> Test 2: Metrics endpoint is accessible..." + METRICS=$(curl -sf "$MGMT_URL/metrics" 2>/dev/null | head -5 || echo "") + if [ -n "$METRICS" ]; then + echo "PASS: Metrics endpoint returned data" + else + echo "FAIL: Metrics endpoint returned empty response" + exit 1 + fi + + echo "" + echo "==> Test 3: OIDC discovery endpoint..." + OIDC=$(curl -sf "$HTTP_URL/realms/master/.well-known/openid-configuration" 2>/dev/null || echo "") + ISSUER=$(echo "$OIDC" | jq -r ".issuer // empty" 2>/dev/null || echo "") + if [ -n "$ISSUER" ]; then + echo "PASS: OIDC discovery returned issuer: $ISSUER" + else + echo "FAIL: OIDC discovery did not return issuer" + echo "Response: $(echo "$OIDC" | head -c 300)" + exit 1 + fi + + echo "" + echo "==> Test 4: Obtain admin access token..." + TOKEN_RESP=$(curl -sf -X POST \ + "$HTTP_URL/realms/master/protocol/openid-connect/token" \ + -d "client_id=admin-cli" \ + -d "username=$ADMIN_USER" \ + -d "password=$ADMIN_PASSWORD" \ + -d "grant_type=password" 2>/dev/null || echo "") + ACCESS_TOKEN=$(echo "$TOKEN_RESP" | jq -r ".access_token // empty" 2>/dev/null || echo "") + if [ -z "$ACCESS_TOKEN" ]; then + echo "FAIL: Could not obtain admin access token" + echo "Response: $(echo "$TOKEN_RESP" | head -c 500)" + exit 1 + fi + echo "PASS: Admin token obtained" + + echo "" + echo "==> Test 5: Admin REST API — list realms..." + REALMS=$(curl -sf -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$HTTP_URL/admin/realms" 2>/dev/null || echo "") + REALM_COUNT=$(echo "$REALMS" | jq "length" 2>/dev/null || echo "0") + if [ "$REALM_COUNT" -ge 1 ]; then + echo "PASS: Admin API returned $REALM_COUNT realm(s)" + else + echo "FAIL: Admin API returned no realms" + echo "Response: $(echo "$REALMS" | head -c 500)" + exit 1 + fi + + echo "" + echo "==> Test 6: Admin REST API — create a test realm..." + CREATE_RESP=$(curl -s -o /tmp/create-body -w "%{http_code}" -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + "$HTTP_URL/admin/realms" \ + -d "{\"realm\":\"e2e-test\",\"enabled\":true}") + if [ "$CREATE_RESP" = "201" ]; then + echo "PASS: Created e2e-test realm (HTTP 201)" + else + echo "FAIL: Could not create realm (HTTP $CREATE_RESP)" + cat /tmp/create-body | head -c 300 + exit 1 + fi + + echo "" + echo "==> Test 7: Admin REST API — create a client in test realm..." + CLIENT_RESP=$(curl -s -o /tmp/client-body -w "%{http_code}" -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + "$HTTP_URL/admin/realms/e2e-test/clients" \ + -d "{\"clientId\":\"e2e-client\",\"publicClient\":true,\"directAccessGrantsEnabled\":true}") + if [ "$CLIENT_RESP" = "201" ]; then + echo "PASS: Created e2e-client (HTTP 201)" + else + echo "FAIL: Could not create client (HTTP $CLIENT_RESP)" + cat /tmp/client-body | head -c 300 + exit 1 + fi + + echo "" + echo "==> Test 8: Admin REST API — create a user in test realm..." + USER_RESP=$(curl -s -o /tmp/user-body -w "%{http_code}" -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + "$HTTP_URL/admin/realms/e2e-test/users" \ + -d "{\"username\":\"testuser\",\"enabled\":true,\"credentials\":[{\"type\":\"password\",\"value\":\"testpass\",\"temporary\":false}]}") + if [ "$USER_RESP" = "201" ]; then + echo "PASS: Created testuser (HTTP 201)" + else + echo "FAIL: Could not create user (HTTP $USER_RESP)" + cat /tmp/user-body | head -c 300 + exit 1 + fi + + echo "" + echo "All API tests PASSED" + exit 0 + ' + +log "Waiting for API test pod..." +kubectl -n "$NAMESPACE" wait --for=condition=Ready pod/api-test --timeout=60s 2>/dev/null || true +kubectl -n "$NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded pod/api-test --timeout=180s || { + log "API test pod logs:" + kubectl -n "$NAMESPACE" logs api-test || true + log "Keycloak pod logs (last 50 lines):" + kubectl -n "$NAMESPACE" logs deployment/"$RELEASE" --tail=50 || true + fail "API tests failed" +} +log "API test pod logs:" +kubectl -n "$NAMESPACE" logs api-test || true + +# ── Multi-replica verification ─────────────────────────────────────── +if [ "$SCENARIO" = "replicas" ]; then + log "Verifying multi-replica deployment..." + READY_REPLICAS=$(kubectl -n "$NAMESPACE" get deployment "$RELEASE" \ + -o jsonpath='{.status.readyReplicas}') + DESIRED_REPLICAS=$(kubectl -n "$NAMESPACE" get deployment "$RELEASE" \ + -o jsonpath='{.spec.replicas}') + log "Replicas: $READY_REPLICAS/$DESIRED_REPLICAS ready" + if [ "$READY_REPLICAS" != "$DESIRED_REPLICAS" ]; then + fail "Not all replicas are ready: $READY_REPLICAS/$DESIRED_REPLICAS" + fi + log "PASS: All $DESIRED_REPLICAS replicas are ready" + + # Verify that each pod is healthy individually + log "Checking health of each replica..." + PODS=$(kubectl -n "$NAMESPACE" get pods -l app.kubernetes.io/name=keycloak -o jsonpath='{.items[*].metadata.name}') + for POD in $PODS; do + POD_IP=$(kubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{.status.podIP}') + kubectl -n "$NAMESPACE" run "health-check-${POD##*-}" --image=alpine:3.20 --restart=Never --rm -i \ + --command -- sh -c " + wget -q -O - --timeout=10 http://$POD_IP:9000/health/ready 2>/dev/null | grep -q UP && echo 'PASS: $POD is healthy' || { echo 'FAIL: $POD is not healthy'; exit 1; } + " || fail "Health check failed for pod $POD" + done + log "PASS: All replicas are individually healthy" +fi + +log "E2E test with $SCENARIO scenario PASSED!" diff --git a/ci/scripts/compat-matrix.sh b/ci/scripts/netbird/compat-matrix.sh similarity index 98% rename from ci/scripts/compat-matrix.sh rename to ci/scripts/netbird/compat-matrix.sh index 5926801..997e3ae 100755 --- a/ci/scripts/compat-matrix.sh +++ b/ci/scripts/netbird/compat-matrix.sh @@ -2,18 +2,18 @@ # # Compatibility matrix: test the current chart against recent NetBird server versions. # -# Produces/updates docs/compatibility.md with a 2D table (chart minors × server minors). +# Produces/updates charts/netbird/docs/compatibility.md with a 2D table (chart minors × server minors). # Each run fills the current chart version's row; older rows are preserved. # # Usage: -# ci/scripts/compat-matrix.sh +# ci/scripts/netbird/compat-matrix.sh # # Requires: helm, kubectl, gh, yq (or sed/grep for Chart.yaml parsing) # set -uo pipefail CHART="charts/netbird" -COMPAT_FILE="docs/compatibility.md" +COMPAT_FILE="charts/netbird/docs/compatibility.md" NUM_MINORS=5 log() { echo "==> $*"; } diff --git a/ci/scripts/e2e-oidc.sh b/ci/scripts/netbird/e2e-oidc.sh similarity index 99% rename from ci/scripts/e2e-oidc.sh rename to ci/scripts/netbird/e2e-oidc.sh index d7dcbac..c56a09e 100755 --- a/ci/scripts/e2e-oidc.sh +++ b/ci/scripts/netbird/e2e-oidc.sh @@ -3,7 +3,7 @@ # E2E test runner for the netbird Helm chart — OIDC integration. # # Usage: -# ci/scripts/e2e-oidc.sh +# ci/scripts/netbird/e2e-oidc.sh # # Providers: # keycloak — Keycloak deployed in-cluster (quay.io/keycloak/keycloak:26.0) diff --git a/ci/scripts/e2e.sh b/ci/scripts/netbird/e2e.sh similarity index 99% rename from ci/scripts/e2e.sh rename to ci/scripts/netbird/e2e.sh index 60b1e73..02a9fc9 100755 --- a/ci/scripts/e2e.sh +++ b/ci/scripts/netbird/e2e.sh @@ -3,7 +3,7 @@ # E2E test runner for the netbird Helm chart (with PAT seeding). # # Usage: -# ci/scripts/e2e.sh +# ci/scripts/netbird/e2e.sh # # Backends: # sqlite — default SQLite storage (no external DB)