Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 51 additions & 31 deletions .github/workflows/federation-helm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
# SKIPPED WITH A `::warning::` ANNOTATION (not failed). All static-validation gates run
# UNCONDITIONALLY on every PR with zero external deps.
#
# LICENSE: the multi-sidecar (Pro) tiers are BEST-EFFORT, gated on TWO repo secrets
# together (skip-not-fail if either missing), on top of image availability: (1) secret
# SC4ES_PRO_TEST_JWT, (2) secret SC4ES_TEST_PUBLIC_KEY (injected as
# SOFTCLIENT4ES_LICENSE_PUBLIC_KEY so the verifier resolves the test kid OFFLINE). The
# published federation image must itself be Pro-capable (JWT-verifying SPI on classpath,
# Story 16.1 OQ-5) for the Pro JWT to verify — an OSS-only image falls back to Community.
# LICENSE: the multi-sidecar (Pro) tiers are BEST-EFFORT, gated on ONE repo secret
# (skip-not-fail if missing), on top of image availability: SC4ES_PRO_TEST_JWT. As of
# appVersion 0.3.0 the trust root is embedded in the image, so the JWT must be ISSUED BY
# THE SOFTCLIENT4ES LICENCE SERVER — a self-signed test JWT can no longer be made to
# verify by supplying its public key, and SC4ES_TEST_PUBLIC_KEY is gone. The published
# federation image must itself be Pro-capable (JWT-verifying SPI on classpath, Story 16.1
# OQ-5) for the Pro JWT to verify — an OSS-only image falls back to Community.
# single-cluster + per-ES-version + secret backends + uninstall + ALL static gates are
# license-free and run unconditionally (when the images are published).
name: Federation Helm Chart
Expand Down Expand Up @@ -63,7 +64,7 @@ env:
# public DockerHub tags at this version so the chart DEFAULTS resolve the kind-loaded images.
# MUST match Chart.yaml appVersion — stale on the 0.2.2 train, reconciled on 0.2.3,
# stale again on 0.2.4 (the chart bumped, this did not), reconciled on 0.2.5.
IMAGE_TAG: '0.2.5'
IMAGE_TAG: '0.3.0'
FED_IMAGE: softnetwork/softclient4es-federation
SIDECAR_IMAGE_PREFIX: softnetwork/softclient4es # softclient4es<N>-arrow-flight-sql
# A pre-baked ADBC client image avoids the smoke Job's runtime pip install (air-gap). When
Expand Down Expand Up @@ -138,6 +139,28 @@ jobs:
run: |
n=$(helm template fed "${CHART_DIR}" | grep -c '^kind: Secret' || true)
[ "$n" = "0" ] || { echo "::error::chart must NOT render a kind:Secret (got $n)"; exit 1; }
- name: assert the removed licence public-key value fails LOUDLY (appVersion 0.3.0)
run: |
# The trust root is embedded in the image, so SOFTCLIENT4ES_LICENSE_PUBLIC_KEY is
# ignored. A value that used to decide HOW a licence is verified must not decay into a
# silent no-op, so the chart aborts the render instead. Both halves are asserted: that
# it fails, and that the message still NAMES the value — an abort whose text drifted
# into something generic would leave an operator with no idea what to remove.
if out=$(helm template fed "${CHART_DIR}" --set license.publicKeySecretName=x 2>&1); then
echo "::error::chart rendered with license.publicKeySecretName set — the guard is gone"
exit 1
fi
case "$out" in
*license.publicKeySecretName*is\ no\ longer\ supported*) : ;;
*) echo "::error::render aborted, but not with the actionable guard message:"; echo "$out"; exit 1 ;;
esac
# And the variable must appear in NO rendered manifest, under any committed values.
for f in "${CHART_DIR}/tests/values"/*.yaml "${CHART_DIR}/examples"/*/values.yaml; do
[ -f "$f" ] || continue
if helm template fed "${CHART_DIR}" -f "$f" | grep -q 'SOFTCLIENT4ES_LICENSE_PUBLIC_KEY'; then
echo "::error::$f still renders SOFTCLIENT4ES_LICENSE_PUBLIC_KEY"; exit 1
fi
done
- name: heterogeneous-ready discriminator (16.4 A2b — only signal vs three-region)
run: |
# three-region and heterogeneous-ready goldens are BYTE-IDENTICAL by design (Helm
Expand Down Expand Up @@ -225,13 +248,13 @@ jobs:
- name: Skip multi-sidecar install when no Pro test license (FACT C + FACT F, best-effort tier)
id: lic
if: ${{ matrix.license && steps.guard.outputs.skip != 'true' }}
# The 3-sidecar tier needs BOTH: a Pro test JWT and its public verification key (so the
# verifier resolves the test kid OFFLINE). Missing EITHER → SKIP (annotation), never fail.
# (The published federation image must itself be Pro-capable — 16.1 OQ-5 — for the JWT to
# verify; an OSS-only image silently falls back to Community and CrashLoops at 3 clusters.)
# The 3-sidecar tier needs a licence-server-issued Pro JWT. Missing → SKIP (annotation),
# never fail. (The published federation image must itself be Pro-capable — 16.1 OQ-5 — for
# the JWT to verify; an OSS-only image silently falls back to Community and CrashLoops at
# 3 clusters.)
run: |
if [ -z "${{ secrets.SC4ES_PRO_TEST_JWT }}" ] || [ -z "${{ secrets.SC4ES_TEST_PUBLIC_KEY }}" ]; then
echo "::warning::missing SC4ES_PRO_TEST_JWT and/or SC4ES_TEST_PUBLIC_KEY — ${{ matrix.example }} (3 sidecars) needs a Pro JWT + its public key (FACT F #2); skipping (best-effort)"
if [ -z "${{ secrets.SC4ES_PRO_TEST_JWT }}" ]; then
echo "::warning::missing SC4ES_PRO_TEST_JWT — ${{ matrix.example }} (3 sidecars) needs a licence-server-issued Pro JWT; skipping (best-effort)"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up Helm
Expand All @@ -258,30 +281,28 @@ jobs:
- name: Deploy ES container(s) per sidecar (FACT D — reachable ES required)
if: ${{ steps.guard.outputs.skip != 'true' && steps.lic.outputs.skip != 'true' }}
run: ./.github/scripts/deploy-es.sh "${{ matrix.example }}" '${{ matrix.es }}'
- name: Create Pro license + public-key Secret (multi-sidecar only — FACT F #2)
- name: Create Pro license Secret (multi-sidecar only)
if: ${{ matrix.license && steps.guard.outputs.skip != 'true' && steps.lic.outputs.skip != 'true' }}
run: |
# ONE Secret carries both the JWT (license-key) and the public verification JWK
# (license-public-key → SOFTCLIENT4ES_LICENSE_PUBLIC_KEY, the air-gap path in
# LicenseKeyVerifier.loadPublicKey Step 3 — without it the test kid never resolves).
# JWT only. The verification key is embedded in the image as of appVersion 0.3.0, so
# there is no public-key half to supply any more.
kubectl create secret generic sc4es-license \
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}" \
--from-literal=license-public-key="${{ secrets.SC4ES_TEST_PUBLIC_KEY }}"
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}"
# The three-region example pins per-region license.secretName=sc4es-pro-license; also
# create that name so its bearer-auth sidecars + the federation share the test token.
kubectl create secret generic sc4es-pro-license \
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}" \
--from-literal=license-public-key="${{ secrets.SC4ES_TEST_PUBLIC_KEY }}" || true
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}" || true
- name: helm install
if: ${{ steps.guard.outputs.skip != 'true' && steps.lic.outputs.skip != 'true' }}
run: |
# The example values point sidecars[].elasticsearch.url at external https://*.example.com
# placeholders. deploy-es.sh stands up in-cluster ES Services; override each URL to the
# matching plaintext in-cluster Service. license/public-key are pointed at sc4es-license
# for the multi-sidecar tier.
# matching plaintext in-cluster Service. The licence Secret is pointed at sc4es-license
# for the multi-sidecar tier. NOTE: --set license.publicKeySecretName would now ABORT
# the render by design (the chart fails on it as of appVersion 0.3.0).
extra=""
if [ "${{ matrix.license }}" = "true" ]; then
extra="--set license.secretName=sc4es-license --set license.publicKeySecretName=sc4es-license"
extra="--set license.secretName=sc4es-license"
fi
# The three-region/heterogeneous examples set sidecars[].auth.method=bearer pointing at
# per-region sc4es-arrow-* Secrets that this CI does NOT create (the dedicated
Expand Down Expand Up @@ -447,12 +468,12 @@ jobs:
run: sudo sysctl -w vm.max_map_count=262144
- name: Skip if examples missing or no Pro test license (FACT C + FACT F)
id: guard
# Upgrade ends at 3 clusters → same Pro gate as install-topology: JWT secret AND
# public-key secret (+ the published image must be Pro-capable, 16.1 OQ-5).
# Upgrade ends at 3 clusters → same Pro gate as install-topology: the JWT secret
# (+ the published image must be Pro-capable, 16.1 OQ-5).
run: |
if [ ! -f "${CHART_DIR}/examples/single-cluster/values.yaml" ] || [ ! -f "${CHART_DIR}/examples/three-region/values.yaml" ] \
|| [ -z "${{ secrets.SC4ES_PRO_TEST_JWT }}" ] || [ -z "${{ secrets.SC4ES_TEST_PUBLIC_KEY }}" ]; then
echo "::warning::upgrade test needs both examples (16.4) + SC4ES_PRO_TEST_JWT + SC4ES_TEST_PUBLIC_KEY (and a Pro-capable image, FACT F); skipping"
|| [ -z "${{ secrets.SC4ES_PRO_TEST_JWT }}" ]; then
echo "::warning::upgrade test needs both examples (16.4) + SC4ES_PRO_TEST_JWT (and a Pro-capable image, FACT F); skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up Helm
Expand All @@ -478,8 +499,7 @@ jobs:
./.github/scripts/deploy-es.sh secrets '8'
for n in sc4es-license sc4es-pro-license; do
kubectl create secret generic "$n" \
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}" \
--from-literal=license-public-key="${{ secrets.SC4ES_TEST_PUBLIC_KEY }}" || true
--from-literal=license-key="${{ secrets.SC4ES_PRO_TEST_JWT }}" || true
done
helm install fed "${CHART_DIR}" -f "${CHART_DIR}/examples/single-cluster/values.yaml" \
${TEST_ADBC_IMAGE:+--set test.image=$TEST_ADBC_IMAGE} \
Expand All @@ -493,7 +513,7 @@ jobs:
# + catalog count, not the per-region bearer Secrets (which it does not create).
helm upgrade fed "${CHART_DIR}" -f "${CHART_DIR}/examples/three-region/values.yaml" \
${TEST_ADBC_IMAGE:+--set test.image=$TEST_ADBC_IMAGE} \
--set license.secretName=sc4es-license --set license.publicKeySecretName=sc4es-license \
--set license.secretName=sc4es-license \
--set sidecars[0].elasticsearch.url=http://es-us-east-1:9200 \
--set sidecars[1].elasticsearch.url=http://es-eu-west-1:9200 \
--set sidecars[2].elasticsearch.url=http://es-ap-south-1:9200 \
Expand Down
20 changes: 18 additions & 2 deletions softclient4es-federation/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,28 @@ type: application
# #141, a real health probe #146, and the server images ship a default
# logback.xml so they no longer boot at ROOT DEBUG #138), rolled into the
# federation + sidecar images.
version: 0.3.5
# 0.3.6: appVersion 0.3.0 — elasticsql 0.21.0 (sliced PIT row extraction #238:
# one reader per primary shard, merged into a single stream, with a new
# `elastic.scroll { size, max-slices }` HOCON surface and ELASTIC_SCROLL_*
# env overrides; row order of an un-ordered extraction now INTERLEAVES
# across slices, and a quota-capped result is an arbitrary subset)
# + extensions 0.3.0 + arrow 0.3.0 (the advertised-schema probe is
# bounded, so a LIMITed SELECT no longer hits Elasticsearch a second time
# softclient4es-arrow#163), rolled into the federation + sidecar images.
# BREAKING (chart): the licence trust root is now embedded in the image, so
# SOFTCLIENT4ES_LICENSE_PUBLIC_KEY is no longer consulted. `license.
# publicKeySecretName` / `publicKeyKey` are retained ONLY to abort the render
# with an actionable message — a value that used to decide HOW a licence is
# verified must not decay into a silent no-op. Remove them from your values.
# A licence issued by the SoftClient4ES licence server verifies with no extra
# configuration, air-gapped clusters included; a self-signed JWT can no longer
# be made to verify by supplying its public key.
version: 0.3.6
# The federation application/image version this chart deploys by default.
# Keep in sync with docker.io/softnetwork/softclient4es-federation:<tag>.
# Verified published on DockerHub for the federation image AND all four
# softclient4es{6,7,8,9}-arrow-flight-sql sidecar images.
appVersion: "0.2.5"
appVersion: "0.3.0"
keywords:
- elasticsearch
- flight-sql
Expand Down
23 changes: 11 additions & 12 deletions softclient4es-federation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ helm uninstall fed
| `federation.upgradeUrl` | `https://portal.softclient4es.com/pricing` | `FEDERATION_UPGRADE_URL`. |
| `telemetry.enabled` | `true` | `SOFTCLIENT4ES_TELEMETRY_ENABLED` daily-ping opt-out (`false` opts out). |
| `license.secretName` | `""` | Secret holding license/API key; empty = Community. |
| `license.publicKeySecretName` | `""` | Secret holding the Ed25519 public JWK for OFFLINE license verification → `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY`; empty = use JWKS fetch. |
| `license.publicKeyKey` | `license-public-key` | Data key within `license.publicKeySecretName`. |
| `license.publicKeySecretName` | `""` | **Removed in appVersion 0.3.0** — the trust root is embedded in the image. Setting it aborts the render. Leave empty. |
| `license.publicKeyKey` | `""` | **Removed in appVersion 0.3.0** — see above. Leave empty. |
| `service.type` | `ClusterIP` | Service type. |
| `service.port` | `32020` | `FEDERATION_PORT` (Flight SQL); the only port exposed by the Service. |
| `resources` | req `1Gi`/`500m`, lim `2Gi`/`1000m` | Container resource requests/limits. |
Expand Down Expand Up @@ -194,12 +194,11 @@ The test Pod defaults to `python:3.12-slim` and `pip install`s the ADBC driver a
with `--set test.image=<image>` (and `--set test.adbcVersion=<v>` to control the driver version
when the runtime install IS used). This is the same Job CI runs.

> **Offline license verification (`license.publicKeySecretName`).** When the federation must
> verify a license JWT WITHOUT reaching the license server's JWKS endpoint (air-gapped clusters,
> or a JWT whose `kid` is not in the prod JWKS), set `license.publicKeySecretName` to a Secret
> whose `license.publicKeyKey` data key holds the matching Ed25519 public JWK. It is mounted as
> `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY` (the air-gap path in the license verifier). Leave empty (the
> default) to use the normal JWKS fetch — it renders nothing, so the golden render is unaffected.
> **Offline license verification (appVersion 0.3.0 and later).** Nothing to configure. The
> licence trust root is embedded in the image, so a licence issued by the SoftClient4ES licence
> server verifies with no network access and no extra values — air-gapped clusters included.
> `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY` is no longer consulted, and `license.publicKeySecretName`
> now **aborts the render** rather than becoming a silent no-op. Remove it from your values.

## Secrets, TLS & Ingress

Expand Down Expand Up @@ -372,9 +371,9 @@ than fail — the static-validation gates run unconditionally on every PR with z

\* The multi-cluster (Pro) tiers run only when ALL of: (1) the federation image bundles the
JWT-verifying SPI (a **Pro-capable** image — an OSS-only image ships only the Community SPI and
cannot verify ANY Pro JWT), (2) the `SC4ES_PRO_TEST_JWT` repo secret, and (3) the
`SC4ES_TEST_PUBLIC_KEY` repo secret (injected as `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY` via
`license.publicKeySecretName` so the JWT verifies offline) are present. Otherwise they are
cannot verify ANY Pro JWT), and (2) the `SC4ES_PRO_TEST_JWT` repo secret, holding a JWT **issued
by the SoftClient4ES licence server** (since appVersion 0.3.0 a self-signed test JWT can no longer
be made to verify by supplying its public key) are present. Otherwise they are
**skipped with a CI annotation** (not a failure). A **single-cluster** federation is license-FREE
(Community `maxClusters=1`) and is always tested; the static three-region/heterogeneous golden
proves the mixed-version RENDER on every PR even when the live multi-cluster install is skipped.
Expand All @@ -397,7 +396,7 @@ failed. The golden gate renders with NO `--set image.tag`.
| `kubeconform` invalid resource | a manifest field renamed/typo (e.g. `replicaCount` vs `replicas`) | fix the template; `replicas` is the Deployment field |
| `heterogeneous-ready` discriminator fails | `three-region` and `heterogeneous-ready` overlays drifted (one copied over the other) | restore the commented R2b `duckdb-attach` preview in `heterogeneous-ready` (the goldens are byte-identical — this grep is the only signal) |
| federation pod CrashLoopBackOff (3 sidecars, license supplied but Community at runtime) | the image lacks the JWT SPI — it cannot verify the Pro JWT → falls back to Community → `maxClusters=1` exceeded | use a Pro-CAPABLE federation image (JWT SPI on classpath, 16.1 OQ-5); injecting a JWT into an OSS-only image does nothing |
| federation pod CrashLoopBackOff (`InvalidLicense: Unknown key ID: …`) | the JWT verification key didn't resolve (no JWKS entry for the kid AND no `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY`) | set `license.publicKeySecretName` → `SOFTCLIENT4ES_LICENSE_PUBLIC_KEY` (the offline verifier path), or ensure the license-server JWKS carries the kid |
| federation pod CrashLoopBackOff (`InvalidLicense: Unknown key ID: …`) | the JWT was not signed by a key this image trusts — as of appVersion 0.3.0 the trust root is embedded, so a self-signed or test-signed JWT can no longer be made to verify | use a licence issued by the SoftClient4ES licence server. Supplying a public key is no longer possible and `license.publicKeySecretName` now aborts the render |
| federation pod CrashLoopBackOff (3 sidecars, no license at all) | Community `maxClusters=1` exceeded | supply a Pro/Enterprise license (`license.secretName`) — by design |
| federation pod CrashLoopBackOff (`validate()` / `FlightCredentials`) | Secret-backed cred didn't arrive (wrong key / ESO sync lag) | check the Secret exists + keys match the contract table above; self-heals on next restart |
| federation NotReady, smoke connect-refused | a sidecar's backing ES is down/unreachable (all-or-nothing gRPC readiness) | ensure every sidecar's ES is reachable; or set `federation.probes.useGrpc=false` for partial availability |
Expand Down
Loading
Loading