diff --git a/.github/scripts/check-docker-images.sh b/.github/scripts/check-docker-images.sh index 15ac6f9195..41c8911246 100644 --- a/.github/scripts/check-docker-images.sh +++ b/.github/scripts/check-docker-images.sh @@ -30,6 +30,28 @@ for pair in \ HC=$(docker image inspect --format '{{json .Config.Healthcheck}}' "$IMAGE_ID") [[ "$HC" != null ]] || { echo "Missing HEALTHCHECK: $dockerfile"; exit 1; } + # Helm reuses these images for tool containers instead of pulling curlimages/curl. + # Check the same non-root/read-only tool environment as the Server test hook. + if [[ "$dockerfile" == hugegraph-store/* || "$dockerfile" == hugegraph-server/* ]]; then + HELM_TOOLS=(curl tr date sleep) + if [[ "$dockerfile" == hugegraph-server/* ]]; then + HELM_TOOLS+=(getent base64 awk grep sort tail) + fi + docker run --rm --pull never --read-only --user 100:101 --entrypoint sh "$IMAGE_ID" -c ' + for tool in "$@"; do + command -v "$tool" >/dev/null || { + echo "Missing Helm tool: $tool" >&2 + exit 1 + } + case "$tool" in + getent) getent ahosts localhost >/dev/null ;; + base64) test "$(printf Helm | base64 | tr -d "\n")" = SGVsbQ== ;; + esac + done + curl --version >/dev/null + ' helm-tools "${HELM_TOOLS[@]}" + fi + JAVA_VERSION=$(docker run --rm --pull never --entrypoint java "$IMAGE_ID" -version 2>&1) echo "$JAVA_VERSION" grep -Eq 'version "17\.' <<< "$JAVA_VERSION" || { diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 6e425a3dca..a24e6a242d 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -46,6 +46,7 @@ jobs: helm lint helm/hugegraph helm lint helm/hugegraph -f helm/hugegraph/values-single.yaml helm lint helm/hugegraph -f helm/hugegraph/values-cluster.yaml + helm lint helm/hugegraph -f helm/hugegraph/values-strict.yaml - name: packaged chart regression run: python3 -m unittest discover -s helm -p 'test_helm_package.py' -v @@ -61,13 +62,13 @@ jobs: - name: helm template run: | - for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml" "-f helm/hugegraph/values-strict.yaml"; do # shellcheck disable=SC2086 # $preset intentionally splits into flags helm template ci helm/hugegraph $preset > /dev/null done # Positive coverage for every hubble wrapper branch: pd mode with # auth, direct mode with a custom port, and a TLS-less ingress with - # the explicit opt-in. + # ordinary HTTP configuration, without acknowledgement flags. helm template ci helm/hugegraph \ --set hubble.enabled=true \ --set server.auth.enabled=true \ @@ -130,6 +131,18 @@ jobs: assert ann(srv, 'checksum/auth') == h('ci-admin|password|ci-auth-token|token_secret|' + h(val('ci-admin', 'password')) + '|' + h(val('ci-auth-token', 'token_secret'))) print('install-time checksums match the generated Secret values') + services = {d['metadata']['name']: d['spec'] for d in docs if d['kind'] == 'Service'} + assert services['ci-hugegraph-server']['type'] == 'NodePort' + assert services['ci-hugegraph-hubble']['type'] == 'NodePort' + assert services['ci-hugegraph-pd-client']['type'] == 'ClusterIP' + for name in ('ci-hugegraph-server', 'ci-hugegraph-hubble'): + assert 'nodePort' not in services[name]['ports'][0] + sts = [d for d in docs if d['kind'] == 'StatefulSet'] + assert sum(int(d['spec']['replicas']) * len(d['spec'].get('volumeClaimTemplates', [])) + for d in sts) + sum(d['kind'] == 'PersistentVolumeClaim' for d in docs) == 7 + assert not any(d['kind'] == 'NetworkPolicy' for d in docs) + assert not any('curlimages/' in str(d) for d in docs) + print('easy defaults: three-member groups, seven volumes and direct HTTP NodePorts') PY - name: reject invalid values @@ -202,29 +215,33 @@ jobs: must_fail "${A[@]}" \ --set hubble.persistence.enabled=true \ --set hubble.persistence.size="" - must_fail "${A[@]}" --set hubble.service.nodePort=30080 - # A NodePort or LoadBalancer Hubble Service publishes the plain-HTTP - # UI directly; it needs the same explicit acknowledgement as PD. - must_fail "${A[@]}" --set hubble.service.type=NodePort - must_fail "${A[@]}" --set hubble.service.type=LoadBalancer - helm template ci helm/hugegraph "${A[@]}" \ - --set hubble.service.type=NodePort \ - --set hubble.service.allowInsecureExposure=true > /dev/null + must_fail "${A[@]}" --set hubble.service.type=ClusterIP --set hubble.service.nodePort=30080 + # HTTP is supported directly in both modes. Component choices take + # precedence over mode defaults and require no acknowledgement. + for mode in easy strict; do + for component in pd server hubble; do + for kind in NodePort LoadBalancer; do + helm template ci helm/hugegraph --set "mode=$mode" \ + --set "${component}.service.type=$kind" > /dev/null + done + done + helm template ci helm/hugegraph --set "mode=$mode" \ + --set server.ingress.enabled=true \ + --set hubble.ingress.enabled=true > /dev/null + done must_fail "${A[@]}" --set hubble.mode=bogus must_fail "${A[@]}" --set hubble.image.tag="" - must_fail "${A[@]}" --set hubble.ingress.enabled=true - # A NodePort or LoadBalancer Server Service publishes the plain-HTTP - # API, credentials and JWTs included; same acknowledgement. - must_fail --set server.service.type=NodePort - must_fail --set server.service.type=LoadBalancer - helm template ci helm/hugegraph \ - --set server.service.type=LoadBalancer \ - --set server.service.allowInsecureExposure=true > /dev/null - # A TLS-less Server Ingress needs the explicit plain-HTTP opt-in - must_fail --set server.ingress.enabled=true + # When UI is explicitly disabled, stale credentials may remain unused. + helm template ci helm/hugegraph --set hubble.enabled=false \ + --set server.auth.enabled=false \ + --set server.auth.admin.password=unused-secret \ + --set server.auth.token.value=unused-token-that-is-at-least-32-bytes > /dev/null + # Respect custom API-server NodePort ranges rather than assuming 30000+. helm template ci helm/hugegraph \ - --set server.ingress.enabled=true \ - --set server.ingress.allowPlainHttp=true > /dev/null + --set server.service.nodePort=8802 \ + --set hubble.service.nodePort=8808 > /dev/null + must_fail --set server.service.nodePort=65536 + must_fail --set server.probes.startup.failureThreshold=1 # Kubernetes refuses a StatefulSet that carries rollingUpdate # options with updateStrategy OnDelete; the schema rejects the # combination up front (a schema abort, so it lives here). @@ -409,7 +426,7 @@ jobs: run_hook "127.0.0.1 127.0.0.2 127.0.0.3 ::1 ::2 ::3" # The auth-off render must carry no credentials. if helm template ci helm/hugegraph -s templates/tests/test-connection.yaml \ - --set server.auth.enabled=false | grep -q 'Authorization'; then + --set hubble.enabled=false --set server.auth.enabled=false | grep -q 'Authorization'; then echo "the auth-off hook render must not carry credentials" >&2 exit 1 fi @@ -421,7 +438,7 @@ jobs: curl -sSLo /tmp/kc.tar.gz https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz echo "95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73 /tmp/kc.tar.gz" | sha256sum -c - tar -xzf /tmp/kc.tar.gz -C /tmp - for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml"; do + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml" "-f helm/hugegraph/values-strict.yaml"; do # shellcheck disable=SC2086 # $preset intentionally splits into flags helm template ci helm/hugegraph $preset | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 done @@ -450,10 +467,11 @@ jobs: # regressed five times, so it is guarded here rather than by review. # `-f` is NOT equivalent: it merges over the new defaults, whereas # --reuse-values discards them, so the fixture must become values.yaml. - cp -R helm/hugegraph /tmp/legacy - cp helm/hugegraph/testdata/values-pre-hardening.yaml /tmp/legacy/values.yaml - helm template legacy /tmp/legacy > /dev/null - helm template legacy /tmp/legacy --is-upgrade > /dev/null + LEGACY_ROOT=$(mktemp -d) + cp -R helm/hugegraph "$LEGACY_ROOT/chart" + cp helm/hugegraph/testdata/values-pre-hardening.yaml "$LEGACY_ROOT/chart/values.yaml" + helm template legacy "$LEGACY_ROOT/chart" > /dev/null + helm template legacy "$LEGACY_ROOT/chart" --is-upgrade > /dev/null - name: helm package run: helm package helm/hugegraph -d /tmp/chart diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml index deb1859d20..e603f08a5d 100644 --- a/helm/hugegraph/Chart.yaml +++ b/helm/hugegraph/Chart.yaml @@ -19,7 +19,7 @@ apiVersion: v2 name: hugegraph description: Helm chart for Apache HugeGraph HStore cluster (PD + Store + Server) type: application -version: 0.1.1 +version: 0.2.0 # TODO: set appVersion to "1.8.0" when the release images are published. appVersion: "latest" kubeVersion: ">=1.23.0-0" diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index ae74b35a43..5284603071 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -1,1183 +1,197 @@ # HugeGraph HStore Helm Chart -[Apache HugeGraph](https://hugegraph.apache.org/) - an open source, distributed graph database. +Deploys **3 PD + 3 Store + 3 Server and Hubble** by default, with authentication +and persistent storage enabled. Requires **Helm 3 and Kubernetes 1.23+**. -## Documentation +## Install and open Hubble -This chart deploys a distributed HugeGraph cluster - PD, Store, and Server - on -Kubernetes. For HugeGraph itself see . - -Two docs-site pages accompany this README: -[deploying with Helm](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm/) -and -[operating on Kubernetes](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/). -The operations page carries the walkthroughs this README links to below. - -Note that this chart requires Helm 3. `--reset-then-reuse-values`, referenced -under Upgrading, requires Helm 3.14 or later. - -### Packaging - -`helm package helm/hugegraph` includes the runtime templates, value presets and -schema, but excludes source diagrams and contributor test fixtures. Helm also -stores chart files in its release Secret, which Kubernetes limits to 1 MiB; -documentation images must stay outside that payload. The diagram below links -to the source repository so it remains available when reading a packaged chart. - -CI checks the packaged archive, renders all three presets from it, and keeps -the compressed chart below 512 KiB to leave room for values and rendered -resources in the release. This package budget is a regression guard, not a -replacement for validating a real Helm installation with the intended values. - -## Prerequisites Details - -* Kubernetes 1.23+ (the chart renders `autoscaling/v2` and `policy/v1`) -* PV support on the underlying infrastructure: a default StorageClass, or an - explicit `storageClassName` for PD and Store -* Sufficient memory for nine JVM processes in the default topology. - Insufficient memory causes OOM kills that surface as silent Raft failures - rather than as clear errors. - -## Chart Details - -| Component | Workload | Purpose | -|---|---|---| -| PD | StatefulSet + PVC | Placement driver; Raft group tracking Stores and partitions | -| Store | StatefulSet + PVC | Graph data storage (HStore) | -| Server | Deployment | Gremlin and REST query layer | -| Hubble | Deployment + optional PVC | Web UI, off by default; enable with `hubble.enabled` | - -![HugeGraph HStore architecture and Kubernetes workload types](../../.github/images/architecture.png) - -A distributed HugeGraph cluster has a startup contract that this chart encodes -so operators do not have to: - -- **Server does not run `init-store`.** The chart injects - `HG_SERVER_INIT_STORE_ENABLED=false`; nothing serializes Server replicas, - so without the gate every replica would initialize the same backend - concurrently. The chart creates no init Job and does not set - `HG_SERVER_SKIP_INIT`. -- **Every Server uses PD for graph metadata.** The startup wrapper always writes - `usePD=true` and the chart-derived `pd.peers` into - `rest-server.properties`, so all Server replicas share the graph catalog - through PD. It also registers each Server Pod IP with PD for in-cluster - discovery (`server.advertiseUrl` replaces that with one shared URL). This is - required for distributed HStore and does not make a local RocksDB backend - shared across replicas. -- **Store waits for PD** in an init container before starting: a majority of - the PD peers must answer `store.waitPath`. The default `/v1/ready` stays 503 - until a raft leader exists, so that majority is a quorum and not merely a set - of live listeners. -- **One PD REST secret, three readers.** PD checks the Basic-auth password of - every management call against `auth.secret-key` and refuses to start - without one. The chart keeps that value in a release-pd-auth Secret - (or `pd.auth.existingSecret`) and hands it to PD as `HG_PD_AUTH_SECRET_KEY`, - to the Server storage wait as `PD_AUTH_PASSWORD`, and to Hubble as - `operations.pd.password`; a `checksum/pd-auth` annotation rolls all three - when the Secret changes. -- **The Server startup probe guarantees at least 450 seconds, and the - image start command fits inside it.** The guaranteed budget is - `(failureThreshold - 1) * periodSeconds`, because kubelet may run the - first probe immediately; the chart raises a lower configured threshold to - that floor, and sets `HG_SERVER_STARTUP_TIMEOUT_S` to the guaranteed - budget minus the 300-second storage wait the entrypoint runs first, so - the image's 120-second default cannot self-kill a Server that is still - starting and the start command never outlives the probe. The variable is - chart-managed; change the probe, not `server.extraEnv`. -- **The wrapper writes `auth.admin_pa` from the auth Secret.** With - `init_store.enabled=false` the admin credential is created on the PD startup - path from `auth.admin_pa`, which applies only when the admin is first - created: changing the Secret later does not rotate a live cluster's - password, and only makes `helm test` disagree with the live credential. To - rotate, change the password through the Server API - (`PUT /graphspaces/DEFAULT/auth/users/admin` with `{"user_password": "..."}`), - set the Secret to the same value, and - `kubectl -n rollout restart deployment/-server` so - every replica's auth cache drops the old password at once (the caches - otherwise expire per replica over minutes). The Secret value lands in - `rest-server.properties` (mode 600) and must be printable ASCII with no - spaces, colons or backslashes; the wrapper refuses to start if it is not. - The rotation caveats are also on the - [deployment page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm/#4-authentication-and-secrets). - -## Installing the Chart - -Before installing, confirm `kubectl` points at the intended cluster and that -it can provision volumes. The default topology needs 3 PD and 3 Store PVCs, -and PVCs stuck `Pending` for want of a StorageClass are the most common -first-run failure: +Provide a default StorageClass or prepare matching PVs. The default installation +needs **7 volumes**: 3 × 10 GiB for PD, 3 × 50 GiB for Store, and 1 × 1 GiB +for Hubble. The chart creates PVCs; it does not create a storage provisioner. ```bash kubectl config current-context -kubectl get nodes kubectl get storageclass -``` - -```bash -helm install hugegraph ./helm/hugegraph --namespace hugegraph \ - --create-namespace --wait --timeout 15m -``` - -This deploys 3 PD + 3 Store + 3 Server, preserves the image's automatic JVM -sizing, and sets no resource requests or limits. Set resources before -production use. +helm install hugegraph ./helm/hugegraph -n hugegraph \ + --create-namespace --wait --timeout 15m -The command examples in this document assume the release is named -`hugegraph`. With a different release name, substitute the release-prefixed -resource names (`kubectl get svc,secret -n ` lists them): -workloads and Services are named `-*` (the release name itself -when it already contains `hugegraph`, `-hugegraph` otherwise, or -`fullnameOverride`), while the kept Secrets always use the release name: -`-admin`, `-auth-token`, and `-pd-auth`. +# Find assigned ports and a reachable node IP +kubectl -n hugegraph get svc hugegraph-server hugegraph-hubble +kubectl get nodes -o wide -**Authentication is enabled by default.** The chart creates a kept Secret -named `-admin` (for example `hugegraph-admin`) with a random -password unless `server.auth.admin.existingSecret` points at a pre-created Secret. -To manage the credential yourself, create the Secret before installing and set -`server.auth.admin.existingSecret`. It always takes priority, and the chart does -not overwrite or manage that Secret: - -```bash -kubectl -n hugegraph create secret generic my-hugegraph-admin \ - --from-literal=password='CHANGE_ME' -``` - -Then add `--set-string server.auth.admin.existingSecret=my-hugegraph-admin` to -the install command. The Secret must contain a `password` key of printable -ASCII with no spaces, colons or backslashes (the image stores a space with a -backslash before it, so a password with a space creates the account under a -different password than the Secret holds; the schema rejects these on inline -values, and the Server wrapper refuses them in a bring-your-own Secret at Pod -start). The JWT signing key uses the same shape -under `server.auth.token` (`value`, `existingSecret`, `autoGenerate`), and -its value must be at least 32 bytes. -Read the password and exercise the API: - -```bash -PASSWORD="$(kubectl get secret -n hugegraph hugegraph-admin \ - -o jsonpath='{.data.password}' | base64 --decode)" -kubectl port-forward -n hugegraph svc/hugegraph-server 8080:8080 -curl --user "admin:${PASSWORD}" http://127.0.0.1:8080/versions -``` - -**Hubble is not installed by default.** Enable the optional UI after install: - -```bash -helm upgrade hugegraph ./helm/hugegraph --namespace hugegraph \ - --reuse-values --set hubble.enabled=true --wait --timeout 15m +# Read the initial admin password +kubectl -n hugegraph get secret hugegraph-admin \ + -o jsonpath='{.data.password}' | base64 --decode ``` -`--reuse-values` keeps the release's existing overrides (presets, images, -resources, Secrets); without it the upgrade rebuilds the release from chart -defaults. Auth is already on, so that single flag is enough. Login uses the same admin -credential from the chart-managed (or BYO) Secret. +Open **`http://:`** and log in as `admin` with that +password. Server API uses `http://:`. +HTTP works directly; a hostname, certificate or extra acknowledgement is not +required. PD remains internal. -The default anti-affinity for `pd`, `store`, and `server` is `preferred` -(Server always was; Hubble has no anti-affinity knob because it is -single-replica by design), so the chart schedules even on clusters with -fewer nodes than replicas. Production should pin `pd.antiAffinity` and -`store.antiAffinity` to `required`, as `values-cluster.yaml` does, so one -node failure cannot take out the PD quorum or co-locate shard replicas; see -Scheduling below. - -A fresh install seeds PD with a partition shard count of 3 when -`store.replicas` is at least 3, and 1 otherwise, instead of the image -default of 1. The seed applies at first bootstrap only; see Partition -Sharding below. - -The component image tags and `appVersion` stay at `latest` until the 1.8.0 -release images are published. Empty component tags inherit `appVersion`; -explicit image tags still override it. For production, pin the image tags (or -digests) and switch the component pull policies to `IfNotPresent`. - -Verify the release: +If node access is unavailable, use port-forward: ```bash -helm test hugegraph --namespace hugegraph --logs --timeout 5m +kubectl -n hugegraph port-forward svc/hugegraph-hubble 8088:8088 +# Open http://127.0.0.1:8088 ``` -`helm --wait` checks Kubernetes readiness; the hook checks Server Gremlin. -With Hubble enabled, also verify a fresh login and graph query after installation -or recovery. Hubble health probes and the Server hook do not verify UI access. - -Besides the Service checks, the test resolves a headless Server Service and -requires an authenticated, graph-bound Gremlin answer from every Server Pod -(at least the replica floor), because a Server that started during a PD roll -can pass readiness and serve REST while every Gremlin call on it fails (see -Troubleshooting, "Could not rebind"). Transient failures are retried for -150 seconds before the test fails and names the failing Pod IPs. Large HPA -fleets may need `helm test --timeout` above the default. - -### Values Presets - -| File | Purpose | -|---|---| -| `values.yaml` | Default 3+3+3 topology with preferred anti-affinity, authentication on, and Hubble off | -| `values-single.yaml` | Single-node 1+1+1 example with authentication on | -| `values-cluster.yaml` | Production 3+3+3 starting point with JVM/resources, PD/Store PDBs, required anti-affinity for PD and Store, and NetworkPolicy on; authentication on, Hubble still opt-in | - -`values-cluster.yaml` is a production starting point, not a capacity -guarantee. Recalculate capacity for the graph size, traffic, failure budget, -node topology, and storage class before production use. - -### Local Kubernetes (Kind / minikube) - -Only needed when there is no cluster yet, or to test locally built images. -Build the three images, load them into the cluster, and override their tags -and pull policies. The override is required, not optional: this chart -defaults `pullPolicy: Always`, so without `Never` the kubelet tries to pull -your local tag from Docker Hub and fails even though the image is loaded. - -```bash -kind create cluster --name hg - -docker build -f hugegraph-pd/Dockerfile -t hugegraph/pd:local . -docker build -f hugegraph-store/Dockerfile -t hugegraph/store:local . -docker build -f hugegraph-server/Dockerfile-hstore -t hugegraph/server:local . +Commands assume release/namespace `hugegraph`. For another release, use the names +shown by `kubectl get svc,secret -n `. Default workloads use +`-`; generated Secrets use `-admin`, +`-auth-token`, and `-pd-auth`. -kind load docker-image hugegraph/pd:local hugegraph/store:local \ - hugegraph/server:local --name hg -# minikube: minikube image load +## Common configurations -helm upgrade --install hugegraph ./helm/hugegraph \ - --namespace hugegraph --create-namespace \ - -f helm/hugegraph/values-single.yaml \ - --set pd.image.tag=local --set pd.image.pullPolicy=Never \ - --set store.image.tag=local --set store.image.pullPolicy=Never \ - --set server.image.tag=local --set server.image.pullPolicy=Never -``` +### API only -Server uses `Dockerfile-hstore` so the image's default backend is HStore. -Skipping the load step fails the Pods with `ErrImageNeverPull`; do not retag -Docker Hub images as `local`. - -## Upgrading the Chart +Hubble is optional. Current Hubble login requires Server authentication. ```bash -helm upgrade hugegraph ./helm/hugegraph --namespace hugegraph --reuse-values -``` - -Any upgrade that changes a Pod template rolls that workload once. - -A release created before the exposure gates existed can hit them on its -next upgrade, `--reuse-values` included: a non-ClusterIP `pd.service.type` -now needs `pd.service.allowInsecureExposure=true`, a non-ClusterIP -`server.service.type` needs `server.service.allowInsecureExposure=true`, a -non-ClusterIP `hubble.service.type` needs -`hubble.service.allowInsecureExposure=true`, and a TLS-less Server Ingress -needs `server.ingress.allowPlainHttp=true`. -The render error names the value to set. - -PD and Store storage sizes live in the StatefulSet `volumeClaimTemplates`, -which Kubernetes forbids changing, so an upgrade with a new size is -rejected in full. To grow storage on a StorageClass that supports volume -expansion: patch each PVC's `spec.resources.requests.storage`, wait for -the resize to finish, recreate the StatefulSet object without touching -Pods (`kubectl delete statefulset --cascade=orphan`), then upgrade -with the matching value. - -Two cases are worth knowing about in advance: - -- **PD** restarts one pod at a time whenever its Pod template changes, which - includes adopting the `-Draft.ip-whitelist.enabled=false` setting described - under Limitations. For a maintenance-window upgrade, set - `pd.updateStrategy.type=OnDelete` and restart the pods yourself. -- **Store** rolling updates advance on `/v1/health`, which reports the - listener, not shard recovery: the controller can replace the next Store - while the previous one is still rejoining its shard groups. - `values-cluster.yaml` therefore sets `store.updateStrategy.type=OnDelete` - (set it yourself on any other production values), and Store Pods are - deleted one at a time, checking between deletions. OnDelete only stops - automatic advancement; it is not by itself a safety proof, and manual - deletion without the checks below carries the same risk. - - `Up` in PD is not that check: PD marks a Store `Up` at registration, - before anything is restored, and a stopped Store stays `Up` in every - shard group until its keep-alive entry expires (300 s on current images). - Start with the Pod (`kubectl -n wait --for=condition=Ready - pod/-store- --timeout=10m`), then check shard - membership and leadership per group, read from the PD leader: - - ```bash - # PD leader, then its shard groups (see Disaster Recovery for the port-forward) - curl -s -u "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/shardGroups | jq ' - .shardGroups[] | {id: (.id // 0), - shards: [.shards[] | {storeId, role}], - leaders: [.shards[] | select(.role=="Leader")] | length}' - ``` - - Delete the next Store only when the replaced Pod is `Ready`, its Store id - shows a fresh `lastHeartBeat` in `/v1/stores`, and every group reports its - full shard count with exactly one `Leader`. Leave a margin after the - membership check, keep `store.pdb.minAvailable` at `replicas - 1` so an - accidental second eviction is refused, and treat a group that is short a - shard or has no leader as a stop. What the membership record does not - prove, and the closer per-group check on the Store's own REST port, are on - the - [operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#6-rolling-store-images-safely). -- **A no-change upgrade rolls nothing.** The `checksum/auth` and - `checksum/pd-auth` annotations hash the Secret name, key and a revision - input chosen by credential source: the digest of the value for an inline - value or a chart-generated Secret (the install renders the value it is - about to write, every later render reads the live Secret, so the two - agree), and the live `resourceVersion` for an operator-supplied - `existingSecret`. Rotating a credential rolls the Pods that read it - exactly once: PD, Server and Hubble together for the PD REST Secret, - Server for the admin password or JWT key. An out-of-band `kubectl` edit - of a Secret applies on the next `helm upgrade`. Earlier chart revisions - hashed the generated Secrets' `resourceVersion`, which the install could - not see, so the first no-change upgrade rolled PD and Server together; - releases installed with those revisions roll once more on their first - upgrade to this one, as the revision input changes. -- **A Server that starts while PD is rolling can come up without its Gremlin - binding** and then passes readiness and serves REST while every Gremlin - request on it fails with `Could not rebind [graph]`, for the life of the - Pod ([apache/hugegraph#3228](https://github.com/apache/hugegraph/issues/3228): - the Server's startup load never offers the graph to Gremlin again after - one failed attempt). The chart no longer rolls PD and Server together on - a no-change upgrade, which was the common trigger; any upgrade that does - roll both (a PD REST Secret rotation, an image change on both) can still - hit it. Run `helm test` after such an upgrade: it checks Gremlin on every - Server Pod and names a Pod that fails; delete that Pod and the replacement - binds normally once PD is stable (see Troubleshooting). -- **Dropping an inline credential back to the chart-managed Secret rolls - nothing**: the inline digest and the generated-Secret digest are the same - function of the same value. - -Every optional field stays optional, so a release created by an earlier -revision continues to render under `--reuse-values`. That flag keeps the old -values as the complete base, so such a release does **not** pick up new -values defaults (the hardened `securityContext`, ServiceAccounts, -`terminationGracePeriodSeconds`); use `-f` with your own values, or -`--reset-then-reuse-values`, to adopt them. Template-derived settings -**are** applied either way, because they are computed at render time from -whatever values are in effect. - -Upgrading an existing release to this chart version rolls the PD StatefulSet -once: PD Pods now always carry a `JAVA_OPTS` environment variable with the -chart-derived partition properties, where previous versions set the variable -only when `pd.javaOpts` was non-empty. - -The `pd.antiAffinity` and `store.antiAffinity` defaults changed from -`required` to `preferred` in this version. `--reuse-values` keeps the old -effective value, but installs that relied on the old `required` default -while supplying their own values files must now pin `antiAffinity: required` -explicitly. - -### Settings by lifecycle - -Not every value is an ordinary override. The table says when each kind -can change and what a rollback does to it. - -| Kind | Values | Can change | Rollback | -|---|---|---|---| -| Install-time identity | `nameOverride`, `fullnameOverride`, `pd.ports.raft`, `store.ports.raft`, `pd.storage.*`, `store.storage.*`, `persistentVolumeClaimRetentionPolicy` | Never on an initialized release. The chart refuses the override and raft port changes against the live StatefulSet; Kubernetes refuses the `volumeClaimTemplates` change (see above for growing storage). Changing one means a fresh install and a data migration | Must not cross a change in any of these | -| Bootstrap-only | `pd.partition.defaultShardCount`, `pd.partition.storeMaxShardCount`, `server.auth.admin.*` (the password is applied at first account creation only), chart-generated credentials | The upgrade renders, but an initialized cluster ignores the new value; change it through PD's config API or the Server auth API instead (Partition Sharding, Limitations) | Reverts the rendered value only; the cluster keeps what it has | -| Routinely mutable | Images, `resources`, `javaOpts`, probes, scheduling, `podAnnotations`, `podLabels`, `extraEnv`, `terminationGracePeriodSeconds`, Services and Ingresses, NetworkPolicy, `server.replicas`, `server.hpa.*`, `server.auth.token`, Hubble | Any upgrade; the affected workload rolls once; joint PD/Server image changes also require the verification below | Re-renders the earlier values; the same joint-rollout verification still applies | -| Coordinated credential rotation | `pd.auth` | Rolls PD, Server and PD-mode Hubble together. On affected Server images this can leave a Ready Pod without Gremlin bindings. Run `helm test --logs` after the rollout and use the documented recovery if it fails; do not treat this as an unattended routine change | Rolls the same components again and carries the same verification/recovery requirement | -| Maintenance-only | `pd.replicas`, `store.replicas` (shrink, and PD growth), Store image rolls under `OnDelete`, Store retirement, the balance tasks | Only through the documented procedure (Scaling, Upgrading, Disaster Recovery); the chart refuses the shortcut it can see | Never: a rollback across a replica change scales the StatefulSet without the procedure | - -### Rollback +# Authenticated API, without the UI +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set hubble.enabled=false -`helm rollback`, and the automatic rollback a failed `--atomic` upgrade -performs, reapply the stored manifest of an earlier revision without -rendering the chart. None of the render-time guards run: the membership -guards, the identity guards and the exposure acknowledgements are all -bypassed. A rollback to a revision with a different `pd.replicas` or -`store.replicas` scales the live StatefulSet straight to that count, which -removes Pods from raft groups and shard groups that still list them, and -can drop PD below quorum or a shard below majority; `OnDelete`, retained -PVCs and PodDisruptionBudgets do not make that safe. Rollback across a -membership or identity change is therefore unsupported, and the chart adds -no template guard for it (a template is not rendered during a rollback). - -To return to an earlier chart version or image, run a forward upgrade with -the earlier chart (or the earlier image tags) and the current topology -values, so the guards run and the live membership is preserved. Keep -`--atomic` off for upgrades that change `pd.replicas` or `store.replicas`. - -After any rollback, compare the live StatefulSets with the membership PD -holds before doing anything else. A replica count that differs from the -member or registered-Store count means the rollback moved the topology; -stop and follow Scaling. Then run `helm test`: - -```bash -kubectl -n get statefulset -l app.kubernetes.io/instance= \ - -o custom-columns='NAME:.metadata.name,REPLICAS:.spec.replicas,READY:.status.readyReplicas' -# PD members and registered Stores, through the PD port-forward from -# Disaster Recovery (the credential is required). -curl -su "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/members | jq '.data.numOfService' -curl -su "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/stores | jq '.data.numOfNormalService' -helm test -n +# Also disable API authentication, if wanted +# Add: --set server.auth.enabled=false ``` -### Template-only pipelines (GitOps) - -The chart-generated credentials (`pd.auth.autoGenerate`, -`server.auth.admin.autoGenerate` and `server.auth.token.autoGenerate`, all -on by default) are produced at render time: the template looks the live -Secret up and keeps its value, and generates a new random one only when no -Secret exists. A renderer without cluster access (`helm template`, Argo CD, -Flux in template mode) never sees the live Secret, so every render produces -a new credential: the Secret and the rollout checksum change on each sync, -every Pod that reads the credential rolls on every sync, and the admin -account created at first start no longer matches the Secret. The chart -cannot tell a template-only render from an install, so it cannot refuse -this; the install notes repeat the warning whenever a generated credential -is in use. +Inactive credential values may remain in the values file when auth is disabled. -For such a pipeline, manage the three credentials outside the chart: create -the Secrets first (a sealed or external Secret works) and set -`pd.auth.existingSecret`, `server.auth.admin.existingSecret` and -`server.auth.token.existingSecret`. Inline `value` and `password` settings -sourced from a secret store are stable too, but they land in the rendered -manifest. +### Internal image registry -## Uninstalling the Chart +Mirror the four component images (`hugegraph/pd`, `hugegraph/store`, +`hugegraph/server`, `hugegraph/hubble`) at the configured tags or digests: ```bash -helm uninstall hugegraph --namespace hugegraph +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set global.imageRegistry=registry.internal.example.com ``` -Helm does not remove PersistentVolumeClaims created by StatefulSets. Delete -them explicitly, and only when the data is no longer needed. - -The chart-managed authentication Secret is kept on uninstall and reused by a -later install of the same release name. Do not delete it unless you intend to -manage the password separately. `helm template` and client-side dry runs cannot -read an existing Secret, so the password they generate is only a render-time -placeholder; a live install or upgrade reuses the existing Secret when Helm has -permission to read it. - -## Configuration - -The following table lists the configurable parameters of the chart and their -default values. - -### Global - -| Parameter | Description | Default | -|---|---|---| -| `nameOverride` | Override the chart name in generated resource names | `""` | -| `fullnameOverride` | Override the full generated resource name | `""` | -| `imagePullSecrets` | Secrets used to pull the PD, Store, and Server images | `[]` | - -### PD - -| Parameter | Description | Default | -|---|---|---| -| `pd.replicas` | PD StatefulSet replicas. Maximum `99` | `3` | -| `pd.image.repository` | PD image repository | `hugegraph/pd` | -| `pd.image.tag` | PD image tag; pin it (or a digest) for production | `latest` | -| `pd.image.digest` | Optional immutable digest such as `sha256:...`; when set it takes priority over the tag | `""` | -| `pd.image.pullPolicy` | PD image pull policy | `Always` | -| `pd.javaOpts` | Extra JVM flags, rendered after the chart-derived `-D` properties below so an explicit duplicate here wins. The image's automatic heap sizing is preserved unless heap flags are set | `""` | -| `pd.raftIpWhitelistEnabled` | Enable PD's raft peer IP whitelist. Off in-cluster because PD resolves peers once at boot; requires a PD image carrying the upstream switch | `false` | -| `pd.raftRpcTimeoutMs` | Raft RPC timeout (`-Draft.rpc-timeout`). Bounds the wait on a vanished leader, so it bounds leader elections: the image default of 10000 was measured leaderless for about a minute, 3000 elects in seconds. Empty preserves the image default | `3000` | -| `pd.partition.defaultShardCount` | Shard replicas per partition, seeded into PD's persisted config at first bootstrap only; inert on an initialized cluster (see Partition Sharding). Empty derives 3 when `store.replicas` is at least 3, else 1. An explicit value must be odd and must not exceed `store.replicas` | `""` | -| `pd.partition.storeMaxShardCount` | Maximum shards per Store, seeded at first bootstrap only. Also fixes the initial partition count, `store.replicas x storeMaxShardCount / shardCount` (see Partition Sharding). Empty preserves the image default of `12` | `""` | -| `pd.ports.grpc` | PD gRPC port | `8686` | -| `pd.ports.rest` | PD REST port, also used by probes | `8620` | -| `pd.ports.raft` | PD Raft port. Install-time identity: part of every PD's persisted raft address, refused on an initialized release (see Settings by lifecycle) | `8610` | -| `pd.dataPath` | PD data directory inside the container | `/hugegraph-pd/pd_data` | -| `pd.storage.size` | PD PersistentVolumeClaim size. Applies at install; see Upgrading for the resize procedure | `10Gi` | -| `pd.storage.storageClassName` | Empty uses the cluster default StorageClass | `""` | -| `pd.resources` | PD container resources. Set these for production | `{}` | -| `pd.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | -| `pd.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | -| `pd.antiAffinity` | One of `required`, `preferred`, `disabled`. `preferred` schedules on clusters with fewer nodes than replicas; production should use `required` so one node failure cannot take out the PD quorum | `preferred` | -| `pd.nodeSelector` | Node selector for pd Pods | `{}` | -| `pd.tolerations` | Tolerations for pd Pods | `[]` | -| `pd.affinity` | Raw affinity; overrides `pd.antiAffinity` when set | `{}` | -| `pd.topologySpreadConstraints` | Topology spread constraints for pd Pods | `[]` | -| `pd.priorityClassName` | PriorityClass for pd Pods | `""` | -| `pd.podAnnotations` | Extra annotations on pd Pods | `{}` | -| `pd.podLabels` | Extra labels on pd Pods. The `app.kubernetes.io/name`, `instance` and `component` keys are chart-managed and rejected | `{}` | -| `pd.extraEnv` | Extra environment variables for the PD container | `[]` | -| `pd.terminationGracePeriodSeconds` | Shutdown grace period | `300` | -| `pd.serviceAccount.create` | Create a ServiceAccount for pd | `true` | -| `pd.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | -| `pd.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | -| `pd.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | -| `pd.pdb.enabled` | Create a PodDisruptionBudget for PD | `true` | -| `pd.pdb.minAvailable` | Must be strictly less than `pd.replicas`. No PDB is rendered when `pd.replicas` is 1 | `2` | -| `pd.readinessPath` | Path the PD readinessProbe hits. `/v1/ready` is quorum-aware and returns 503 without a raft leader | `/v1/ready` | -| `pd.livenessPath` | Path the PD startup and liveness probes hit. Empty derives it from `pd.replicas`: `/v1/health` above one replica, `/v1/ready` at one | `""` | -| `pd.auth.value` | Plaintext PD REST secret (`auth.secret-key`). Prefer `existingSecret` in shared clusters. Printable ASCII, no backslashes, no leading or trailing space (a properties read trims it) | `""` | -| `pd.auth.existingSecret` | Pre-created Secret holding the PD REST secret under `pd.auth.key`. Wins over `value` and `autoGenerate`; the chart does not manage it. Its value must meet the same constraint as `pd.auth.value`: printable ASCII, no backslashes, no leading or trailing space | `""` | -| `pd.auth.key` | Key inside the PD REST Secret | `secret-key` | -| `pd.auth.autoGenerate` | Create and keep a random release-pd-auth Secret when `value` and `existingSecret` are empty | `true` | -| `pd.probes.*.periodSeconds` | Probe interval | see `values.yaml` | -| `pd.probes.*.failureThreshold` | Probe failure threshold | see `values.yaml` | -| `pd.probes.*.timeoutSeconds` | Probe timeout. Defaults to `5` on readiness/liveness; Kubernetes would otherwise apply `1` | `5` | -| `pd.probes.*.initialDelaySeconds` | Optional probe start delay | unset | -| `pd.probes.*.successThreshold` | Optional probe success threshold | unset | - -### Store - -| Parameter | Description | Default | -|---|---|---| -| `store.replicas` | Store StatefulSet replicas. Maximum `99` | `3` | -| `store.image.repository` | Store image repository | `hugegraph/store` | -| `store.image.tag` | Store image tag; pin it (or a digest) for production | `latest` | -| `store.image.digest` | Optional immutable digest such as `sha256:...`; when set it takes priority over the tag | `""` | -| `store.image.pullPolicy` | Store image pull policy | `Always` | -| `store.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | -| `store.ports.grpc` | Store gRPC port | `8500` | -| `store.ports.raft` | Store Raft port. Install-time identity: part of every Store's persisted shard-group address, refused on an initialized release (see Settings by lifecycle) | `8510` | -| `store.ports.rest` | Store REST port | `8520` | -| `store.dataPath` | Store data directory | `/hugegraph-store/storage` | -| `store.storage.size` | Store PersistentVolumeClaim size. Applies at install; see Upgrading for the resize procedure | `50Gi` | -| `store.storage.storageClassName` | Empty uses the cluster default StorageClass | `""` | -| `store.resources` | Store container resources. Set these for production | `{}` | -| `store.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | -| `store.securityContext` | Container-level securityContext; also applied to the PD wait init container. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | -| `store.waitPath` | Path the init container polls on each PD peer; a majority must answer 2xx. `/v1/ready` counts quorum members, not merely live listeners. Use one of PD's unauthenticated paths (`/v1/ready`, `/v1/health`); the init container sends no credential | `/v1/ready` | -| `store.waitTimeoutSeconds` | Bound on the PD wait before the init container fails | `900` | -| `store.antiAffinity` | One of `required`, `preferred`, `disabled`. `preferred` schedules on clusters with fewer nodes than replicas; production should use `required` so one node failure cannot co-locate shard replicas | `preferred` | -| `store.nodeSelector` | Node selector for store Pods | `{}` | -| `store.tolerations` | Tolerations for store Pods | `[]` | -| `store.affinity` | Raw affinity; overrides `store.antiAffinity` when set | `{}` | -| `store.topologySpreadConstraints` | Topology spread constraints for store Pods | `[]` | -| `store.priorityClassName` | PriorityClass for store Pods | `""` | -| `store.podAnnotations` | Extra annotations on store Pods | `{}` | -| `store.podLabels` | Extra labels on store Pods | `{}` | -| `store.extraEnv` | Extra environment variables for the Store container | `[]` | -| `store.terminationGracePeriodSeconds` | Shutdown grace period | `300` | -| `store.serviceAccount.create` | Create a ServiceAccount for store | `true` | -| `store.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | -| `store.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | -| `store.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | -| `store.pdb.enabled` | Create a PodDisruptionBudget for Store | `true` | -| `store.pdb.minAvailable` | Must be strictly less than `store.replicas` and at least `store.replicas - 1`, so voluntary evictions cannot remove two copies of one shard at once. No PDB is rendered when `store.replicas` is 1 | `2` | -| `store.waitImage` | Image for the PD wait init container | `curlimages/curl:8.5.0` | -| `store.waitResources` | Resources for the init container | `{}` | -| `store.probes.*` | Same probe keys as PD | see `values.yaml` | - -### Server - -| Parameter | Description | Default | -|---|---|---| -| `server.replicas` | Server Deployment replicas. Ignored when `server.hpa.enabled` | `3` | -| `server.image.repository` | Server image repository | `hugegraph/server` | -| `server.image.tag` | Server image tag; pin it (or a digest) for production | `latest` | -| `server.image.digest` | Optional immutable digest such as `sha256:...`; when set it takes priority over the tag | `""` | -| `server.image.pullPolicy` | Server image pull policy | `Always` | -| `server.javaOpts` | Empty preserves the image's automatic JVM sizing | `""` | -| `server.port` | Server REST port, container port, and Service port | `8080` | -| `server.readinessPath` | Path the Server readinessProbe hits. Set `/readiness` once the Server image serves it (apache/hugegraph#3212); it answers 503 while the Server cannot serve graph traffic. Startup and liveness stay on `/versions` | `/versions` | -| `server.backend` | Storage backend | `hstore` | -| `server.resources` | Server resources. `requests.cpu` is required when HPA is enabled | `{}` | -| `server.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | -| `server.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | -| `server.pdb.enabled` | Create a PodDisruptionBudget for Server. Off by default: Server holds no quorum. `values-cluster.yaml` enables it so a node drain cannot evict every Server at once | `false` | -| `server.pdb.minAvailable` | Must be less than `server.hpa.minReplicas` when HPA is enabled, otherwise less than `server.replicas` | `2` | -| `server.antiAffinity` | One of `required`, `preferred`, `disabled`. Defaults to `preferred` rather than `required` because HPA may scale Server past the node count; set `required` when replicas always stay below it | `preferred` | -| `server.nodeSelector` | Node selector for server Pods | `{}` | -| `server.tolerations` | Tolerations for server Pods | `[]` | -| `server.affinity` | Raw affinity; overrides `server.antiAffinity` when set | `{}` | -| `server.topologySpreadConstraints` | Topology spread constraints for server Pods | `[]` | -| `server.priorityClassName` | PriorityClass for server Pods | `""` | -| `server.podAnnotations` | Extra annotations on server Pods | `{}` | -| `server.podLabels` | Extra labels on server Pods | `{}` | -| `server.extraEnv` | Extra environment variables for the Server container | `[]` | -| `server.terminationGracePeriodSeconds` | Shutdown grace period | `60` | -| `server.serviceAccount.create` | Create a ServiceAccount for server | `true` | -| `server.serviceAccount.name` | Use an existing ServiceAccount instead | `""` | -| `server.serviceAccount.annotations` | Annotations on the created ServiceAccount | `{}` | -| `server.serviceAccount.automountServiceAccountToken` | Mount an API token. The chart makes no API calls | `false` | -| `server.waitImage` | Image for the Helm test hook | `curlimages/curl:8.5.0` | -| `server.testResources` | Resources for the Helm test hook container | requests `25m`/`32Mi`, limits `250m`/`64Mi` | -| `server.initStoreEnabled` | Must remain `false` for distributed HStore | `false` | -| `server.auth.enabled` | Enable admin authentication | `true` | -| `server.auth.admin.password` | Optional inline admin password; prefer a Secret in shared clusters. Printable ASCII, no spaces, no backslashes, no colons; an `existingSecret` value is held to the same contract when the Pod starts (see Validation) | `""` | -| `server.auth.admin.existingSecret` | Pre-created Secret name (key defaults to `password`); takes priority | `""` | -| `server.auth.admin.key` | Key inside the admin password Secret | `password` | -| `server.auth.admin.autoGenerate` | Create and keep a random release-admin Secret when password and existingSecret are empty | `true` | -| `server.auth.token.value` | Optional inline JWT signing key, minimum 32 bytes; prefer a Secret in shared clusters | `""` | -| `server.auth.token.existingSecret` | Pre-created Secret for the JWT signing key (`auth.token_secret`) | `""` | -| `server.auth.token.key` | Key inside the JWT signing Secret | `token_secret` | -| `server.auth.token.autoGenerate` | Create and keep a random release-auth-token Secret when value and existingSecret are empty | `true` | -| `server.ingress.enabled` | Create an Ingress for the Server Service | `false` | -| `server.ingress.className` | IngressClass name | `""` | -| `server.ingress.annotations` | Ingress annotations (cert-manager, nginx, ALB) | `{}` | -| `server.advertiseUrl` | Absolute Server URL registered with PD (`server.urls_to_pd`). Empty registers each Server Pod IP for in-cluster discovery | `""` | -| `server.service.type` | Server Service type. A non-ClusterIP type requires `server.service.allowInsecureExposure` | `ClusterIP` | -| `server.service.allowInsecureExposure` | Acknowledgement that a NodePort or LoadBalancer Service publishes the plain-HTTP Server API, so Basic-auth credentials and JWTs cross the network in cleartext; prefer a TLS Ingress, or restrict reachability by other means first | `false` | -| `server.service.annotations` | Server Service annotations | `{}` | -| `server.ingress.hosts` | Ingress hosts and paths | see `values.yaml` | -| `server.ingress.tls` | Ingress TLS configuration. Empty is refused unless `allowPlainHttp` opts in: the Server carries Basic-auth credentials and JWTs | `[]` | -| `server.ingress.allowPlainHttp` | Explicit opt-in to a TLS-less Server Ingress on a trusted network | unset | -| `server.hpa.enabled` | Create a HorizontalPodAutoscaler | `false` | -| `server.hpa.minReplicas` | HPA minimum replicas | `3` | -| `server.hpa.maxReplicas` | HPA maximum replicas | `10` | -| `server.hpa.targetCPUUtilizationPercentage` | HPA CPU utilization target | `70` | -| `server.probes.startup.failureThreshold` | Raised automatically so the guaranteed budget, `(failureThreshold - 1) * periodSeconds` (the first probe can fail immediately), is at least 450s | `91` | -| `server.probes.startup.periodSeconds` | Startup probe interval | `5` | -| `server.probes.*` | Same optional probe keys as PD | see `values.yaml` | - -When `server.hpa.enabled` is `true` the Deployment omits `spec.replicas`, so a -Helm upgrade does not overwrite the autoscaler's live replica count. Enabling -utilization-based HPA requires a strictly positive -`server.resources.requests.cpu`. - - -### Reaching Hubble (pick one path) +A component repository with an explicit registry takes priority over the global +prefix. Set `imagePullSecrets` if the registry needs credentials. +Store's wait init container and Helm test reuse the Store and Server images, +which already contain `curl`; no separate curl image is needed by default. +See the [complete registry example](examples/values-internal-registry.yaml). -Most people should stop at **1**. Use **2** only if Hubble must run outside -the cluster. Use **3** only if that outside Hubble must discover Server through -PD. Store Operations metrics from outside the cluster are out of scope here. +### Storage -#### 1. In-cluster Hubble (recommended) - -Set `hubble.enabled=true` (off by default so API-only clusters stay lean); -the chart wires PD/Server for you. Open the UI with one port-forward, then -open `http://127.0.0.1:8088` and log in with the chart admin password: +Use a named dynamic StorageClass when the cluster has no suitable default: ```bash -kubectl -n port-forward svc/-hubble 8088:8088 +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set pd.storage.storageClassName=my-csi \ + --set store.storage.storageClassName=my-csi \ + --set hubble.persistence.storageClassName=my-csi ``` -For a shared environment, expose Hubble through `hubble.ingress` with -`tls` instead of port-forward. A NodePort or LoadBalancer -`hubble.service.type` publishes the plain-HTTP UI directly and is refused -unless `hubble.service.allowInsecureExposure=true` acknowledges it. - -#### 2 and 3. Outside Hubble (direct Server URL, or PD discovery) - -A non-ClusterIP PD Service requires `pd.service.allowInsecureExposure=true` -(PD gRPC has no authentication; restrict who can reach it first), a -non-ClusterIP Server Service requires -`server.service.allowInsecureExposure=true` (plain HTTP; prefer a TLS -`server.ingress`), and a set -`server.advertiseUrl` registers that one URL with PD for every discovery -client, an in-cluster Hubble included. The walkthrough for both paths is on -the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#9-running-hubble-outside-the-cluster). - -| Parameter | Description | Default | -|---|---|---| -| `server.advertiseUrl` | Absolute Server URL registered with PD for discovery clients. Empty registers each Server Pod IP for in-cluster discovery | `""` | -| `pd.service.type` | PD client Service type (`ClusterIP`, `NodePort`, `LoadBalancer`). A non-ClusterIP type requires `pd.service.allowInsecureExposure` | `ClusterIP` | -| `pd.service.allowInsecureExposure` | Acknowledgement that a non-ClusterIP PD Service exposes the unauthenticated gRPC port; restrict reachability by other means first | `false` | -| `pd.service.annotations` | Annotations on the PD client Service | `{}` | -| `pd.service.restNodePort` | Optional fixed NodePort for PD REST; requires NodePort/LoadBalancer | unset | -| `pd.service.grpcNodePort` | Optional fixed NodePort for PD gRPC; requires NodePort/LoadBalancer | unset | - -### Hubble (optional UI) - -How to open Hubble (in-cluster vs outside) is under -[Reaching Hubble](#reaching-hubble-pick-one-path) above. This section covers -chart wiring and parameters. +Empty `storageClassName` inherits the cluster default. `"-"` explicitly selects +no StorageClass. Neither creates a provisioner. For local disks, prepare host +directories and PVs first; the [seven-volume local example](examples/README.md#storage) +includes node affinity, capacities and scheduling requirements. -`hubble.enabled=true` deploys [HugeGraph Hubble](https://hugegraph.apache.org/docs/quickstart/toolchain/hugegraph-hubble/), -the web UI; login uses the chart admin credential. `hubble.mode` selects -the wiring. The default `pd` mode points Hubble at the PD gRPC peers, the -PD client Service REST port, and the Store REST endpoints, so the cluster -operations view works without manual wiring (the Server's own PD -registration supplies a resolvable Server address); `direct` mode hands -Hubble only `server.direct_url` on the Server client Service, with no PD -discovery and no operations view. Everything else in -`hugegraph-hubble.properties` keeps the image default. The Store metrics -allow-list is computed from `store.replicas` at render time, so scale Store -with `helm upgrade`, not `kubectl scale`, or the list goes stale until the -next upgrade. +For a **new temporary installation**, add `--set persistence.enabled=false` +to use `emptyDir`. Component storage flags override that global setting. +PVC failures never cause an automatic fallback. Do not switch initialized +PD/Store data between persistent and temporary storage. -Hubble is one replica by design: it keeps UI connection metadata, including -any graph credentials entered in the UI, in an embedded per-instance H2 -database. Enable `hubble.persistence` to keep that metadata across Pod -replacement; the chart then redirects the H2 location into the mounted -volume through Spring's environment binding. The Deployment uses the -`Recreate` strategy so two Hubble instances never attach the same database. -The PVC is kept on `helm uninstall` (delete it explicitly to discard the -stored metadata), `size` and `storageClassName` apply at install time only, -and a non-root `podSecurityContext` needs a matching `fsGroup` so H2 can -write the volume. +## Modes and entry points -**Current Hubble images still require `server.auth`**: the UI login -authenticates against the cluster, so the chart refuses to render -`hubble.enabled=true` with `server.auth.enabled=false` unless -`hubble.allowWithoutServerAuth=true` overrides it for images whose login -does not need cluster authentication. +`mode: easy` is the default. `mode: strict` changes Server/Hubble to ClusterIP +and enables NetworkPolicy; authentication, Hubble and persistence stay enabled. +Use `--set mode=strict` or `-f helm/hugegraph/values-strict.yaml`. -**Hubble serves plain HTTP.** Reach it through a port-forward or an -HTTPS-terminating Ingress, never directly from an untrusted network; an -Ingress without `tls` is rejected unless `hubble.ingress.allowPlainHttp=true` -opts in. +**Explicit component value > global value > mode default.** An explicit `false` +is preserved; null component values inherit. The global settings are +`persistence.enabled` and `global.imageRegistry`. No mode requires HTTPS. -| Parameter | Description | Default | +| Entry | Access | When to use it | |---|---|---| -| `hubble.enabled` | Deploy the Hubble UI. Requires `server.auth` (see below) | `false` | -| `hubble.mode` | `pd` discovers the cluster through PD and enables the operations view; `direct` talks to the Server client Service only | `pd` | -| `hubble.allowWithoutServerAuth` | Renders Hubble without `server.auth`, for future images whose login does not require cluster authentication | `false` | -| `hubble.image.repository` | Hubble image repository | `hugegraph/hubble` | -| `hubble.image.tag` | Hubble image tag; pin it (or a digest) for production | `latest` | -| `hubble.image.digest` | Optional immutable digest such as `sha256:...`; when set it takes priority over the tag | `""` | -| `hubble.image.pullPolicy` | Hubble image pull policy | `Always` | -| `hubble.port` | Hubble HTTP port, container port, and Service port | `8088` | -| `hubble.persistence.enabled` | Persist UI connection metadata in a PVC | `false` | -| `hubble.persistence.size` | PVC size | `1Gi` | -| `hubble.persistence.storageClassName` | Empty uses the cluster default StorageClass | `""` | -| `hubble.resources` | Hubble resources | `{}` | -| `hubble.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | -| `hubble.securityContext` | Container-level securityContext, hardened like the other components | see `values.yaml` | -| `hubble.service.type` | Hubble Service type. A non-ClusterIP type requires `hubble.service.allowInsecureExposure` | `ClusterIP` | -| `hubble.service.allowInsecureExposure` | Acknowledgement that a NodePort or LoadBalancer Service publishes the plain-HTTP Hubble UI, which has no login of its own; restrict reachability by other means first | `false` | -| `hubble.service.annotations` | Hubble Service annotations | `{}` | -| `hubble.service.nodePort` | Requires a `NodePort` or `LoadBalancer` Service type | unset | -| `hubble.ingress.*` | Same Ingress keys as `server.ingress.*`, including `allowPlainHttp` | `enabled: false` | -| `hubble.serviceAccount.*` | Same ServiceAccount keys as the other components | `create: true` | -| `hubble.nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` | Scheduling controls | unset | -| `hubble.priorityClassName` | PriorityClass for the hubble Pod | `""` | -| `hubble.podAnnotations` / `hubble.podLabels` | Extra Pod metadata | `{}` | -| `hubble.extraEnv` | Extra environment variables for the hubble container | `[]` | -| `hubble.terminationGracePeriodSeconds` | Shutdown grace period | `30` | -| `hubble.probes.*` | Same probe keys as PD; startup and readiness check `/actuator/health`, liveness is a TCP check | see `values.yaml` | +| NodePort | Reachable node IP + assigned port | Direct access without another controller; easy default | +| ClusterIP | Internal Service IP/DNS | In-cluster clients, an existing gateway or port-forward | +| LoadBalancer | Infrastructure-assigned address | A load balancer controller is available | +| Ingress | HTTP routes through a controller | Shared HTTP routing; hostname and TLS are optional | -Specify each parameter with `--set`, or supply a YAML file with `-f`: +ClusterIP is an internal virtual service address, not a node IP. An ordinary +laptop usually cannot reach it directly. With default `externalTrafficPolicy: +Cluster`, NodePort can forward through a node that does not host the target Pod. -```bash -helm install hugegraph ./helm/hugegraph --set server.replicas=5 -``` - -`values.schema.json` and template helpers reject invalid input at render time, -before anything reaches the cluster: - -- Unknown keys and wrong types are rejected. -- `server.initStoreEnabled` must remain `false` for a distributed deployment. -- With authentication enabled, either `server.auth.admin.existingSecret` must - name a Secret containing the configured key (default `password`), or - `server.auth.admin.password` must be set, or `server.auth.admin.autoGenerate` - must be true. The same shape applies to `server.auth.token` (`existingSecret` - / `value` / `autoGenerate`). With authentication disabled, - `admin.existingSecret`, `admin.password`, `token.existingSecret`, and - `token.value` must be empty, so a configured but inactive Secret reference - cannot be overlooked. A missing Secret fails when Kubernetes configures the - container; an empty `password` fails in the Server startup wrapper. -- `server.hpa.minReplicas` must not exceed `maxReplicas`, and enabling - utilization-based HPA requires a strictly positive - `server.resources.requests.cpu`. -- `pdb.minAvailable` must be less than the matching `replicas`, so a - PodDisruptionBudget cannot permanently block node drains. -- `pd.pdb.minAvailable` must also be at least the PD Raft majority, - `floor(replicas/2)+1`, so the budget cannot permit evictions that drop PD - below quorum. With 2 PD replicas no valid budget exists (the majority is - the whole membership); disable the PD PDB or use an odd replica count. -- `extraEnv` must not set chart-managed variable names (for example - `HG_SERVER_INIT_STORE_ENABLED` or the PD/Store identity and topology - variables): entries render after the chart-owned variables and the last - duplicate wins, so an override would silently bypass a validated contract. - `JAVA_OPTS` and `JAVA_OPTIONS` are reserved for the same reason: the - component start scripts skip automatic heap sizing and drop the chart's - `JAVA_OPTS` flags entirely when `JAVA_OPTIONS` arrives preset. -- `pd.replicas` and `store.replicas` are capped at 99. -- `pd.partition.defaultShardCount` and `pd.partition.storeMaxShardCount` - must each be empty or a positive integer. An explicit shard count must - also be odd (PD's config API rejects even values, and PD clamps 2 to 1) - and must not exceed `store.replicas`, past which PD would silently clamp - it to the live store count. -- `hubble.port` must be a valid port, `hubble.persistence.size` must be - non-empty, and `hubble.service.nodePort` requires a `NodePort` or - `LoadBalancer` Service type. -- `hubble.image` needs a tag or a digest (the chart `appVersion` tracks the - Server release, not Hubble), and an Ingress without `tls` is rejected for - Server and Hubble alike unless the matching `ingress.allowPlainHttp=true` - opts in. -- `hubble.enabled` without `server.auth.enabled` is rejected unless - `hubble.allowWithoutServerAuth=true`, and - `securityContext.readOnlyRootFilesystem=true` is rejected for Server and - Hubble alike because each one's wrapper rewrites its properties file - inside the image at startup and the chart mounts no writable volume there. -- `store.pdb.minAvailable` must be at least `store.replicas - 1`, so - voluntary evictions cannot remove two copies of one shard at once. -- `podLabels` may not override the chart-managed `app.kubernetes.io/name`, - `instance` or `component` keys on any workload. -- `podAnnotations` may not set keys under the chart-owned `checksum/` - prefix on any workload: user annotations render after the chart's own - and the last duplicate key wins, so a fixed value would pin the checksum - and stop Secret or config rotation from rolling the pods. -- `updateStrategy.rollingUpdate` options are rejected together with - `updateStrategy.type: OnDelete` for PD and Store: Kubernetes refuses such - a StatefulSet at apply time, which would otherwise surface mid-upgrade. -- `updateStrategy.rollingUpdate.maxUnavailable` accepts only the integer - `1`: a larger value or a percentage lets the controller disrupt two - members of a three-member raft group or shard at once, and a - PodDisruptionBudget does not constrain controller rollouts. -- With `networkPolicy.enabled`, an in-cluster Hubble in `pd` mode combined - with `server.advertiseUrl` requires `networkPolicy.hubble.extraEgress`: - PD hands Hubble the advertised external URL, which Hubble's egress - policy would otherwise block. -- In `pd` mode the Hubble wrapper refuses a PD REST secret that is not - printable ASCII, contains backslashes, or starts or ends with - whitespace, the same constraint the schema puts on inline values; an - `existingSecret` is only seen at container start. The ASCII check is - byte-wise under the C locale, so the image locale cannot widen it. -- The Server wrapper refuses an admin password that is not printable - ASCII or contains spaces, backslashes or colons, and the schema puts the - same pattern on `server.auth.admin.password`, so - an inline value fails at render and an `existingSecret` fails at Pod - start with a message naming the Secret. The Server decodes Basic-auth - credentials as ASCII and splits them on every colon, so a non-ASCII - password would be applied to the admin account and then answer 401 to - every request made with it, and a password with a colon would answer - 400 (measured on `:latest`, 2026-10-03). The image entrypoint writes the - password into `rest-server.properties` with a backslash before each space, - and the Server keeps that backslash, so a password with a space would be - applied with the backslashes and answer 401 to the Secret value (measured - on `:latest`, 2026-10-03). This guard is temporary: it goes once the - Server decodes Basic auth as UTF-8 and splits on the first colon only, and - the entrypoint writes spaces so they read back unchanged (Server side: - apache/hugegraph#3284, once the image this chart deploys includes that - fix; the TODO at `encode_prop_value` is in apache/hugegraph#3260). -- A non-ClusterIP `pd.service.type` requires - `pd.service.allowInsecureExposure=true`, a non-ClusterIP - `server.service.type` requires `server.service.allowInsecureExposure=true` - (the Server serves plain HTTP, so Basic-auth credentials and JWTs travel - in cleartext), and a non-ClusterIP `hubble.service.type` requires - `hubble.service.allowInsecureExposure=true`: Hubble serves plain HTTP with - no login of its own. -- With `networkPolicy.enabled`, exposing PD, Server or Hubble (a NodePort - or LoadBalancer Service, a Server or Hubble Ingress, or a set - `server.advertiseUrl`) requires a non-empty - `networkPolicy..extraIngress` naming who may connect. -- An upgrade may not shrink `pd.replicas` or `store.replicas` below the - live StatefulSet, or grow `pd.replicas`; see Scaling for the manual - procedure. -- An upgrade may not change `pd.ports.raft` or `store.ports.raft`, or - rename the PD or Store StatefulSet through `nameOverride` or - `fullnameOverride`, once the release is initialized: the raft address - `...svc:` is the identity the - persisted membership records, and a Pod restarted under a new one cannot - rejoin its group. The guard reads the live StatefulSet, so a fresh - install chooses freely and `helm template` never fails on it. - -### NetworkPolicy +To fix a NodePort, set `server.service.nodePort` or `hubble.service.nodePort`. +Omission or `0` allocates automatically. Kubernetes validates the actual cluster +range; the chart does not force `30000–32767`. -`networkPolicy.enabled` renders one NetworkPolicy per component (PD, Store, -Server, and Hubble when enabled). Each one isolates its own Pods in both -directions and lists the traffic they need, so the policies take effect in any -apply order. It is off in `values.yaml`, because the base values cannot know -who your clients are, and on in `values-cluster.yaml`. +A [hostless HTTP Ingress example](examples/values-http-ingress.yaml) is available. +With NetworkPolicy enabled, add native rules for actual clients and controllers; +empty external rule lists are valid but can block access. Explicit Service and +policy values override either mode. Legacy exposure acknowledgement fields are +accepted for compatibility and no longer gate rendering. -It only works when the cluster's network plugin enforces NetworkPolicy (kind -v0.25 or later, k3s, Calico, Cilium). Other plugins accept the objects and -enforce nothing. To check, run a Pod without chart labels in another -namespace and `curl` the PD client Service on the REST port: it must time out. +## Common parameters -With it on, the release admits only its own traffic plus DNS; anything -else, the Ingress controller and NodePort or LoadBalancer clients included, -must be listed in `networkPolicy..extraIngress`, and exposing -PD, Server or Hubble (or setting `server.advertiseUrl`) with an empty -`extraIngress` fails the render instead of opening the port. The -admitted-traffic matrix, the egress notes, and worked `extraIngress` -examples with the per-plugin NodePort client addresses are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#5-networkpolicy). +Defaults below describe a fresh easy-mode install. Supply `--set` or a YAML file +with `-f`; see [values.yaml](values.yaml) for all fields and their comments. -| Parameter | Description | Default | +| Parameter | Meaning | Default | |---|---|---| -| `networkPolicy.enabled` | Render the policies | `false` | -| `networkPolicy..extraIngress` | Extra NetworkPolicy ingress rules, appended as written | `[]` | -| `networkPolicy.hubble.extraEgress` | Extra egress rules for Hubble's optional outside endpoints (`es.urls`, `prometheus.url`) | `[]` | - -## Deep Dive - -### Cluster Health - -Component ports are in the configuration tables above; a stalled component -is ended by its liveness probe within about a minute. The connection -commands and the health walkthrough are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#2-ports-and-health). - -### Scheduling - -Every component exposes the full set of scheduling controls, and -`antiAffinity` renders the hostname anti-affinity preset described under -Installing. Examples and the preferred-versus-required trade are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#3-scheduling). - -### Partition Sharding - -**The shard-count seed applies at first bootstrap only**: changing -`pd.partition.defaultShardCount`, or scaling `store.replicas` across the -derivation boundary, has no effect on an initialized cluster. Change a -running cluster through PD's own config API (odd values only, at most the -live store count), then trigger `GET /v1/task/patrolPartitions`. The -derivation, the initial partition count, and the constraints are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#4-partition-sharding). - -### Disaster Recovery - -Recovery is operator-triggered on current builds: PD's own patrol only -marks silent Stores `Offline`, and there is **no automatic -re-replication**. The task endpoints execute locally on the PD that -receives them, and a follower answers with an empty success while doing -nothing, so identify the leader first and port-forward that Pod (the -forward runs in the foreground; use a second terminal for the curls). The -credential is required; PD answers 401 without it: - -```bash -kubectl port-forward -n hugegraph svc/hugegraph-pd-client 8620:8620 -PD_SECRET="$(kubectl -n hugegraph get secret hugegraph-pd-auth \ - -o jsonpath='{.data.secret-key}' | base64 --decode)" -# Read .data.pdLeader.raftUrl; its host names the leader Pod. -curl -su "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/members -# Stop the Service forward, then forward the leader Pod instead. -kubectl port-forward -n hugegraph pod/ 8620:8620 -# Reconcile shard groups and process tombstoned Stores. -curl -u "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/task/patrolPartitions -# Spread Raft leaders, then partition data. -curl -u "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/task/balanceLeaders -curl -u "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/task/balancePartitions -``` - -Read `/v1/members` again after the tasks: if leadership moved mid-sequence, -the later tasks ran on a follower and did nothing. Wait at least 180 s -before rerunning `balanceLeaders` after a `balancePartitions` call; the -refusal shapes, when to run which task, and telling a real run from a -no-op or a follower answer are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#7-disaster-recovery). - -**Replace a Store Pod, keep its PVC**: the Store id lives in the data path, -and the Pod comes back under the same id. A Store rebuilt with an empty PVC -registers under a **new Store ID** at the unchanged address, and recovers -in place only on images carrying -[apache/hugegraph#3234](https://github.com/apache/hugegraph/pull/3234) -(merged 2026-09-24); on every earlier image, including all published -release images, it does not. On post-#3234 images, retire the old ID on -the PD leader: - -```bash -# The old ID is the row at the replaced Pod's address that is not the -# newly registered one. -curl -su "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/stores -curl -u "hg:${PD_SECRET}" -X POST -H 'Content-Type: application/json' \ - -d '{"storeState":"Tombstone"}' http://127.0.0.1:8620/v1/store/ -curl -u "hg:${PD_SECRET}" http://127.0.0.1:8620/v1/task/patrolPartitions -# Verify: every group at full shard count with one leader, the old ID in -# no group, and the replaced Store answering 200 on :8520/v1/partition/. -curl -u "hg:${PD_SECRET}" -X DELETE http://127.0.0.1:8620/v1/store/ -``` - -On earlier images the same retirement runs and does not repair the groups, -and nothing in the health surface shows the loss: treat a genuinely lost -volume there as a degraded cluster and expect to rebuild rather than to -recover in place. Both measurements and the full walkthrough are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#7-disaster-recovery). - -Periodic leader balancing is tracked in -[apache/hugegraph#3135](https://github.com/apache/hugegraph/issues/3135); -disaster-recovery metrics are tracked in -[apache/hugegraph#3136](https://github.com/apache/hugegraph/issues/3136). - ---- - -### Scaling - -Server scales through `server.replicas`, or by enabling `server.hpa` (the -Deployment then omits `spec.replicas`, so a Helm upgrade does not overwrite -the autoscaler). PD and Store are capped at 99 replicas. Stage a rollout -with `kubectl scale statefulset -store --replicas=0` and scale -back up when ready; the Servers wait, not-ready, until Stores register, and -the next `helm upgrade` restores the values topology. - -**Changing PD or Store replicas on a live release is not a values change.** -Raft and shard membership are persisted, and Pods alone do not reconfigure -them; the chart rejects both directions for PD and a shrink for Store by -reading the live StatefulSet (a client-side `--dry-run` does not show the -guard). Treat a PD replica change as unsupported and install the PD count -you intend to keep. The PD membership background and the Store -drain-then-scale procedure (Tombstone the leaving ids, wait for the groups, -then scale) are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#8-scaling). -Deleting the PVCs of removed ordinals is separate and permanent; do it only -after the groups no longer list the retired ids. - -## Troubleshooting - -### Store Pods Stuck in `Init:0/1` - -The Store init container waits for a majority of PD peers to answer -`store.waitPath`. Check PD first: - -```bash -kubectl -n get pods -l app.kubernetes.io/component=pd -kubectl -n logs -c wait-for-pd -``` - -The wait is bounded by `store.waitTimeoutSeconds` (default 900). On timeout the -init container exits with a message naming the peers it polled, so the failure -appears in `kubectl describe pod` instead of hanging silently. - -### PersistentVolumeClaims Stay `Pending` - -No default StorageClass, or the provisioner is unhealthy: - -```bash -kubectl get sc -kubectl -n get pvc -l app.kubernetes.io/instance= -kubectl -n get pods -``` - -### Server Ready but Queries Fail - -The Server readiness probe uses `/versions`, which can report ready before the -graph is fully able to serve index-backed queries. Confirm the graph is live -(the server image does not ship `curl`, so probe through a port-forward): - -```bash -kubectl port-forward -n hugegraph svc/hugegraph-server 8080:8080 -curl -s --user "admin:${PASSWORD}" http://127.0.0.1:8080/graphs -``` - -### Queries Fail with "Could not rebind" Right After Creating a Graph - -Two causes. Right after `CreateGraph`, the creating Server is consistent at -HTTP 200 ([#3138](https://github.com/apache/hugegraph/pull/3138)), but the -other replicas converge independently for a short window, and a Gremlin -query routed to a not-yet-converged replica fails with a 400 such as -`Could not rebind [g]`: retry with backoff, use sticky routing for -create-then-verify flows, or poll `/graphs` on each replica before opening -query traffic (cluster-wide readiness is tracked in -[#3137](https://github.com/apache/hugegraph/issues/3137)). - -The second cause does not close on its own: a Server Pod that started while -PD was rolling serves REST and passes readiness while every Gremlin call on -it fails, for the life of the Pod. Its `hugegraph-server.log` names it: - -``` -Graph [DEFAULT-hugegraph] configured at [...] could not be instantiated and -will not be available in Gremlin Server -``` - -Check Gremlin on each Server Pod after any upgrade that rolled PD: a -port-forward to the Pod, then `POST /gremlin` with the admin credential read -into `PASSWORD` as in Installing the Chart (authentication is on by default, so -the call answers 401 without it): - -```bash -kubectl port-forward -n hugegraph pod/ 8080:8080 -curl -s --compressed -u "admin:${PASSWORD}" -H 'Content-Type: application/json' \ - -X POST http://127.0.0.1:8080/gremlin \ - -d '{"gremlin":"graph.traversal().V().limit(1).count()","aliases":{"graph":"DEFAULT-hugegraph"}}' -``` - -A healthy Pod answers with `result.data`; delete a Pod that answers -`Could not rebind`, and its replacement binds normally once PD is stable. -`helm test` runs this same graph-bound query against every Server Pod -through the headless Service, so a stuck replica now fails the release -check instead of hiding behind the load balancer. The measurements behind -both causes are on the -[operations page](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/#10-when-gremlin-fails-with-could-not-rebind). - -### Pods OOM Killed or Restarting - -The default `values.yaml` sets **no** resource requests or limits and preserves -the image's automatic JVM sizing. Set resources explicitly before production -use; see `values-cluster.yaml`. - -```bash -kubectl get pods -o wide -kubectl -n describe pod | grep -A5 "Last State" -``` - -`kubectl -n logs --previous` shows the previous container's -output, including launcher and bootstrap errors that stopped the Server. The -chart does not mount a volume at `/hugegraph-server/logs`, so JVM crash logs and -heap dumps the Server writes there are lost when its container restarts; see -[Server logs and crash files](../../docker/README.md#server-logs-and-crash-files). - -### Release Name Too Long - -Helm itself rejects release names longer than 53 characters, before this chart -renders anything: - -```text -invalid release name ... the length must not be longer than 53 -``` - -Within that limit the chart is safe: resource names reserve their suffix and -StatefulSet ordinal before truncation, so every generated Service and Pod name -stays inside the 63-character DNS label limit, and PD/Store identities do not -shift when replicas change. Use `fullnameOverride` to shorten generated names -independently of the release name. - ---- - -## Limitations - -- On images predating - [apache/hugegraph#3234](https://github.com/apache/hugegraph/pull/3234) - (merged 2026-09-24, in no release yet), a Store cannot be recovered in - place after its volume is lost: the replacement keeps the Pod's DNS raft - address, PD's reallocation adds a peer the raft group already has, the - group keeps the old Store id, and no health surface reports it. Images - built from `master` at or after `dbb6663a` repair this through the - documented retirement; see Disaster Recovery for both measurements. -- A Server Pod that starts while PD is rolling can lose its Gremlin binding - for the life of the Pod while passing readiness and serving REST; delete - that Pod. See Troubleshooting, "Could not rebind". -- The default values set no container resources, so every pod is - BestEffort and each JVM sizes its heap against total node memory. Fine on - a single node; on a multi-node cluster the heaps oversubscribe the nodes - and pods abort. Use `values-cluster.yaml`, or set your own `resources`, - for any multi-node deployment. -- The Store's memory ceiling is not its heap: the image pins - `rocksdb.total_memory_size` at 32 GB of native caches outside the JVM, - and the chart cannot lower it (the entrypoint rebuilds its Spring config - and no file is mounted). The cluster preset requests 5Gi and limits at - 8Gi per Store for this reason; a 4Gi limit was OOM-killed after about - 1 GB of data. Scale both numbers with the data size; the full breakdown - is in the `values-cluster.yaml` comment. -- PD's raft IP whitelist resolves peer hostnames once at startup, which - under Kubernetes can block peers whose pod IPs were unpublished at that - moment or change later. The chart disables the whitelist in-cluster via - the upstream `raft.ip-whitelist.enabled` switch; enable - `networkPolicy.enabled` (on in `values-cluster.yaml`) so only PD Pods - reach the raft port. `pd.raftIpWhitelistEnabled=true` restores the image - default and its one-shot resolution races at the operator's own risk. -- PD's `/v1/health` answers 200 as soon as the REST listener is up and - cannot see a lost quorum. With more than one PD the chart uses it for - startup and liveness on purpose (a follower that merely lost its leader - is not restarted) and puts readiness and the Store wait on `/v1/ready`. - A single PD derives startup and liveness to `/v1/ready` instead: one - that steps down for good, as after a failed raft snapshot on a full disk - ([apache/hugegraph#3222](https://github.com/apache/hugegraph/issues/3222)), - would answer `/v1/health` forever; `pd.livenessPath` overrides. -- A PD that cannot open its RocksDB store at startup (for example while - another process still holds the store's `LOCK` file) neither retries nor - exits: `/v1/ready` answers 503 with `STATE_UNINITIALIZED` while - `/v1/health` answers 200, as observed outside Kubernetes in - [apache/hugegraph#3226](https://github.com/apache/hugegraph/issues/3226). - Under the chart such a Pod is not Ready; with more than one PD its - startup and liveness probes use `/v1/health`, so the kubelet does not - restart it, and with a single PD they use `/v1/ready`, so it does. How - the state arises in a cluster, and whether a restart or Pod deletion - clears it, is untested. Deleting the `LOCK` files is not a fix. -- Server discovery is a lease: a replaced Server can stay in PD's list for - up to 45 seconds after it stops, so Hubble's cluster view may briefly - show a stale address. Application traffic is unaffected, because it - reaches Servers through the Service, which drops the Pod immediately. -- No TLS, backups, Operator, multi-cluster support, automatic leader - transfer, or a complete monitoring stack. Store recovery is manual on - current builds (see Disaster Recovery); periodic balancing and shard-sync - metrics are upstream feature work. -- After [#3138](https://github.com/apache/hugegraph/pull/3138), the creating - Server is consistent at HTTP 200; other replicas may still lag for a short - window (see Troubleshooting: "Could not rebind"; - [#3137](https://github.com/apache/hugegraph/issues/3137) stays open). -- The published images run as root, so `runAsNonRoot` and - `readOnlyRootFilesystem` are not chart defaults; the container - `securityContext` is still hardened (`allowPrivilegeEscalation: false`, - `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault`) and fully - configurable per component. -- `values-cluster.yaml` is a starting point, not a capacity guarantee. -- Authentication is on by default. The auth Secret sets the admin password - only at first creation via `auth.admin_pa`; the chart cannot rotate an - existing cluster's admin password. -- Every Server replica must share one JWT signing key. The chart injects - `HG_SERVER_AUTH_TOKEN_SECRET` from `server.auth.token` - (chart-managed by default) so Hubble login stays stable behind a - multi-replica Service. -- Hubble is single-replica, serves plain HTTP, and requires `server.auth` - for its login to complete; the persistence and H2 constraints are in the - Hubble section above. +| `mode` | Entry and policy defaults | `easy` | +| `pd.replicas` / `store.replicas` / `server.replicas` | Component replicas | `3` / `3` / `3` | +| `hubble.enabled` | Install the UI | `true` | +| `server.auth.enabled` | Server authentication | `true` | +| `server.service.type` / `hubble.service.type` | Explicit entry type; null inherits mode | resolved `NodePort` | +| `pd.service.type` | PD client entry type | `ClusterIP` | +| `*.service.nodePort` | Fixed Server/Hubble port; omitted/0 allocates automatically | unset | +| `global.imageRegistry` | Prefix image repositories without an explicit registry | `""` | +| `*.image.repository` / `tag` / `digest` | Component image; digest takes priority | see values | +| `persistence.enabled` | Global persistent storage | `true` | +| `pd.storage.enabled` / `store.storage.enabled` / `hubble.persistence.enabled` | Explicit component storage override | `null` (inherits) | +| `*.storageClassName` | Cluster default when empty; `"-"` means no class | `""` | +| `pd.storage.size` / `store.storage.size` / `hubble.persistence.size` | Volume capacities | `10Gi` / `50Gi` / `1Gi` | +| `server.storageWaitTimeoutSeconds` | Storage-ready deadline | `300` | +| `networkPolicy.enabled` | Explicit policy override; null inherits mode | resolved `false` | + +
+Advanced settings and presets + +- [Operations](docs/operations.md): credentials, probes, upgrades, membership, + scheduling, policies and troubleshooting. +- [Deployment examples](examples/README.md): private registry, local volumes, + fixed GitOps credentials, external Secrets, HTTP Ingress and API-only usage. +- `values-single.yaml`: 1 PD + 1 Store + 1 Server, with Hubble and authentication. +- `values-cluster.yaml`: required PD/Store anti-affinity, resources and controlled + Store updates. NetworkPolicy inherits mode; the preset is a capacity starting point. +- All client Services support native `externalTrafficPolicy`, `loadBalancerClass`, + `loadBalancerIP`, `loadBalancerSourceRanges`, `externalIPs` and + `allocateLoadBalancerNodePorts`, subject to cluster/controller support. +- Resources, JVM options, scheduling, probes, HPA/PDB, metadata and external + Hubble discovery are configured under their components in `values.yaml`. + Default images preserve automatic JVM sizing; set resources for your capacity. + +
+ +## Upgrade and verify + +```bash +helm get values hugegraph -n hugegraph --all +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --wait --timeout 15m +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +`--reuse-values` preserves existing UI, entry, storage and credential settings. +An easy-mode upgrade that inherits Server/Hubble entry types prints a short +NodePort-default notice in NOTES; explicitly configured entry types are preserved. +To adopt new defaults, use `--reset-values` with a reviewed values file retaining +all settings your release needs. Older explicit helper images also remain under +value reuse; clear `store.waitImage` and `server.waitImage` to reuse component images. + +The admin Secret is an **initial credential**. Changing it alone does not rotate +an existing account; follow [credential rotation](docs/operations.md#credentials). +The chart checks persisted Raft identities and storage/membership changes during +upgrades; those changes require a coordinated procedure. + +`helm --wait` checks readiness; `helm test` checks graph-bound Gremlin on each +Server Pod. Also verify a fresh Hubble login and graph query. Templates and +linting alone do not prove live volume, network or application behavior. + +Website guides: [Helm deployment](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm/) +and [Kubernetes operations](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/). diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md new file mode 100644 index 0000000000..cb7f2e8048 --- /dev/null +++ b/helm/hugegraph/docs/operations.md @@ -0,0 +1,306 @@ +# Helm operations + +Start with [installation and common settings](../README.md). +[values.yaml](../values.yaml) documents every available parameter; this page +covers settings that need coordination or diagnosis. + +## Credentials + +Normal Helm installations generate/reuse three kept Secrets: + +| Default Secret | Data key | Readers | +|---|---|---| +| `-admin` | `password` | Server, Helm test | +| `-auth-token` | `token_secret` | All Server replicas | +| `-pd-auth` | `secret-key` | PD, Server storage wait, Hubble | + +`existingSecret` takes priority over an inline value, which takes priority over +auto-generation. Existing Secrets are not managed by the chart. Override their +key names through the corresponding `key` fields. + +The admin password must be printable ASCII without spaces, colons or +backslashes. The PD REST secret must be printable ASCII without surrounding +whitespace or backslashes. JWT signing keys must be at least 32 bytes. +These constraints match the current image's authentication/property encoding. + +The admin Secret supplies the **initial** password when the account is created. +To rotate an existing account, first call the Server API with the old credential: + +```bash +curl --user 'admin:' \ + -X PUT -H 'Content-Type: application/json' \ + -d '{"user_password":""}' \ + "http:///graphspaces/DEFAULT/auth/users/admin" +``` + +Do not replace password placeholders with real credentials in recorded shell +commands: they can enter history or briefly appear in process arguments. +For interactive calls, `curl --user admin` (or `--user hg` for PD) prompts for +the password. Variable-based examples avoid literal passwords in shell history. + +
+Optional credential files for shared terminals or repeated requests + +Create a curl config and password-update JSON with an editor or secret manager, +using owner-only permissions (`600`). The config contains +`user = "admin:"`; the JSON contains the `user_password` body above. +Then use `--config /path/to/server.curl` and +`--data-binary @/path/to/password-update.json` instead of inline credentials. +For PD commands, use a separate config containing `user = "hg:"` +and replace `--user "hg:${PD_SECRET}"` with `--config /path/to/pd.curl`. +Do not log or commit these files; remove them when no longer needed. + +
+ +Update the Secret and its source values to the same new password, then restart +all Server replicas to clear cached credentials: + +```bash +kubectl -n hugegraph rollout restart deployment/hugegraph-server +``` + +Use your secret manager rather than storing real passwords in public files. +GitOps needs fixed encrypted values or external Secrets, because offline +renderers cannot reuse a live generated Secret through `lookup`. +[Both complete examples](../examples/README.md#gitops-credentials) include all +three credential sources. Changing PD credentials rolls PD, Server and Hubble; +verify Gremlin and UI login after that coordinated rollout. + +### PD REST authentication + +PD REST uses HTTP Basic auth with a service username: `hg`, `store`, `hubble` +or `vermeer`. Server's `admin` account is not a PD REST account and receives 401. +All four service names use the shared PD REST password, not the admin password. +For the default release/Secret, query a reachable PD REST address (port 8620): + +```bash +PD_SECRET=$(kubectl -n hugegraph get secret hugegraph-pd-auth \ + -o jsonpath='{.data.secret-key}' | base64 --decode) +curl --user "hg:${PD_SECRET}" "http://:8620/v1/stores" +``` + +With `pd.auth.existingSecret` or a custom `pd.auth.key`, read the actual Secret +and key instead. See the [PD REST API reference](../../../hugegraph-pd/docs/api-reference.md#rest-api) +for probe exceptions and API details. Management tasks below must target the PD leader. + +## Storage and upgrades + +Inspect saved values before upgrading. `--reuse-values` preserves old effective +settings, including disabled Hubble, ClusterIP and explicit helper images. +Adopt new defaults only with reviewed values and `--reset-values`. + +PD/Store PVC size and StorageClass live in StatefulSet claim templates and are +not routine mutable settings. For capacity growth, the StorageClass must support +expansion: resize each PVC, wait for completion, then coordinate an orphaned +StatefulSet recreation with matching claim-template values. Preserve Pod/claim +names and persisted data; do not recreate claims as a resize workaround. + +PD/Store storage-mode changes are rejected after initialization. Hubble uses +an embedded H2 database with connection metadata and saved graph credentials; +a PVC and `Recreate` deployment strategy preserve it across Pod replacement. +Disabling Hubble or uninstalling does not delete kept claims/Secrets automatically. +Changing Hubble storage requires a separate reviewed metadata migration. + +An older Hubble Deployment may have no `data` volume: its H2 records live in +its container filesystem. The new persistent default is a storage change too, +so a live Helm upgrade rejects it rather than silently creating an empty database. +Keep `hubble.persistence.enabled=false` with `--reuse-values` or an explicit +GitOps value to preserve the previous storage mode. Move any records you need +to retain through a reviewed metadata migration before enabling persistence; +retaining the temporary mode does not make those records survive Pod replacement. +Export or back up required metadata before any rollout of that old Pod, including +an upgrade that retains `hubble.persistence.enabled=false`. +Offline `helm template` and GitOps rendering cannot inspect the existing Deployment +through `lookup`, so this live guard cannot enforce the migration requirement there. +Export and explicitly pin the existing storage flags before a GitOps sync; a +successful offline render does not establish a safe migration. + +Local PVs bind to nodes and cannot move their files automatically on node loss. +See [all seven local PVs](../examples/README.md#storage) for directory, capacity, +claim binding and scheduling requirements. `Retain` preserves files on claim +deletion; reclamation/recovery needs explicit inspection. + +## Startup and probes + +Store's init container waits for a majority of PD peers on `store.waitPath` +(default `/v1/ready`) for up to `store.waitTimeoutSeconds` (default 900). +PD readiness requires a Raft leader; `/v1/health` only confirms a live listener. +Store's own readiness does not prove every shard has recovered. + +Server waits for storage for `server.storageWaitTimeoutSeconds` (default 300), +passed as `WAIT_STORAGE_TIMEOUT_S`. An unspecified startup threshold is derived +from that wait plus a 150-second startup allowance. Explicit thresholds are +preserved and must leave at least 120 seconds beyond the storage wait. +The probe budget is `initialDelaySeconds + (failureThreshold - 1) * periodSeconds`; +without an initial delay, the first probe can run immediately. Contradictory budgets fail rendering with +the relevant parameter names; `HG_SERVER_STARTUP_TIMEOUT_S` is chart-managed. +Legacy reused values retain their earlier probe-budget compatibility behavior. + +`store.waitImage` and `server.waitImage` default empty and reuse the Store/Server +images, respectively. Those images already include `/bin/sh` and `curl`. +Explicit helper overrides remain supported for custom images. To clear an older +override without resetting other release settings: + +```bash +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --set-string store.waitImage= --set-string server.waitImage= +``` + +## Topology and scheduling + +The startup wrapper sets `usePD=true` and the chart PD peers so all Servers +share graph metadata. Distributed Servers keep `initStoreEnabled=false` and do +not concurrently run `init-store`. Each Server registers its Pod address with +PD unless `server.advertiseUrl` specifies one shared discovery address. + +At first bootstrap, the chart seeds three shard replicas with at least three +Stores, or one otherwise. PD persists those settings. Chart changes after +initialization do not rewrite the partition metadata; use the PD configuration +API and coordinate partition patrol for existing groups. + +PD Raft addresses, Store Raft addresses and StatefulSet names are persisted +identities. Do not change those hostnames/ports as an ordinary upgrade. +PD membership changes and Store shrink require explicit member/shard operations; +the chart rejects unsupported changes against initialized live StatefulSets. +Fresh installs do not perform the upgrade-only StatefulSet query. + +For Store shrink, first confirm the remaining Stores can hold the configured +shard replica count and data. Retire the leaving Store IDs through PD and run +partition patrol on the PD leader. Wait until no shard group references a retired ID and +all remaining groups have their full membership and one leader. Only then +scale the live StatefulSet and upgrade with the matching `store.replicas`; +PVC removal is a separate step after retirement. Keep the installed PD count +unless its persisted voting configuration is reconfigured outside Helm; a +fresh release with data migration is the alternative. The +[PD management API](../../../hugegraph-pd/docs/api-reference.md) documents the +Store state and task endpoints. + +Default preferred anti-affinity permits fewer nodes than replicas. The cluster +preset requires separate nodes for PD/Store; explicit affinity overrides presets. +Local PV node affinity must agree with scheduling rules. PDBs protect voluntary +eviction, not controller-driven updates. PD needs a majority available; Store +needs enough shard copies to remain available. + +Server replicas can be changed routinely; CPU-utilization HPA requires positive +CPU requests. Store growth still needs volume and shard planning. Use chart +updates for Store scale changes so Hubble's endpoint list is updated too. + +## Rolling Store updates and recovery + +Use `store.updateStrategy.type=OnDelete` for a controlled rollout. Replace one +Store Pod at a time. Before replacing another, confirm the previous Pod is +Ready, its Store heartbeat is fresh, and every shard group reports full +membership and exactly one leader from the PD leader's view. +PD `Up` status alone does not prove shard recovery; stop on missing members or +leaders. The [website operations guide](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/) +explains the rollout and recovery constraints. +A single Store replacement can still cause request errors or timeouts while Raft +leadership and client routes converge. Observe application errors as well as +shard recovery before continuing; do not blindly retry writes. + +### PD patrol and leader balance + +Partition patrol repairs shard membership; it does not balance shard leaders. +After a Store replacement, leader counts can remain uneven because leader +balancing is not scheduled automatically. Use the existing manual task when +all Stores have fresh heartbeats and every group has full, healthy membership. + +Find the PD leader through authenticated `/v1/members` (`data.pdLeader.restUrl`). +Forward that Pod in a separate terminal, not a randomly selected Service peer: + +```bash +kubectl -n hugegraph port-forward pod/ 8620:8620 +``` + +In the terminal holding `PD_SECRET` from the credentials section, check that +`/v1/ready` reports `isLeader: true`, then inspect the current group and Store state: + +```bash +PD_REST=http://127.0.0.1:8620 +curl "${PD_REST}/v1/ready" +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/partitions" +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/stores" +``` + +After an intentional member retirement or shard-count change, patrol on that +leader if needed; it is not a leader-balancing command: + +```bash +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/task/patrolPartitions" +``` + +Wait for membership recovery and split/move tasks to finish before requesting +leader balance separately: + +```bash +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/task/balanceLeaders" +``` + +A busy task returns body `status: 1001`; inspect the result and wait for current +maintenance to finish rather than looping patrol/balance calls. A follower can +return an empty success, and leader transfers are asynchronous: HTTP 200 or a +returned transfer plan does not prove completion. Recheck `/v1/partitions` and +`/v1/stores` for actual leaders, full membership and fresh heartbeats before +continuing maintenance. + +Preserve volumes and recorded member identities during recovery. Deleting a +RocksDB lock file or assigning an empty volume does not recover shard data. +The chart does not provide backup or automatic disk-loss recovery; keep backups +outside the data PVs and validate recovery separately. + +## Networking and Hubble + +Easy mode leaves NetworkPolicy off; strict enables it. Explicit values win. +Policies require a network plugin that enforces them. Generated rules allow +component communication and DNS; admit external clients/controllers with native +`extraIngress`, and external addresses with appropriate `extraEgress`. +Selecting strict mode and then overriding a Service to NodePort/LoadBalancer, +setting `externalIPs`, or enabling Ingress does not automatically admit outside traffic. NOTES names +exposed components with no external ingress rule and points to the matching +`networkPolicy..extraIngress` setting; template rendering remains valid. +For Hubble PD discovery via an external `server.advertiseUrl`, also configure +Hubble's `extraEgress` for that destination. +Empty external rule lists are valid and can block clients; `{}` is a valid +allow-all rule. Consider NodePort source NAT when choosing source ranges. + +Client Services accept native Kubernetes traffic and load-balancer fields under +`pd.service`, `server.service` and `hubble.service`. LoadBalancer-specific fields +need that Service type and a compatible controller. Hostless HTTP Ingress is +supported; install an Ingress controller separately. TLS is optional. + +In-chart Hubble is wired to internal PD/Server endpoints. `hubble.mode=direct` +uses only the Server Service and has no PD operations view. External Hubble +needs reachable Server addresses; PD discovery additionally needs reachable PD +gRPC and a suitable `server.advertiseUrl`. That override affects every discovery +client, including in-cluster Hubble. Current Hubble login requires Server auth; +use `hubble.enabled=false` for an unauthenticated API-only deployment. + +API response compression follows `Accept-Encoding`. Clients that support gzip +can request it explicitly or use `curl --compressed`; requests with `identity` +or without a compression header receive plain responses. Quality factors `q=0.` +and `q=1.` are equivalent to `q=0` and `q=1`. Explicit gzip exclusion overrides +`*`; a higher `identity` quality selects a plain response. +Older images can still force gzip; use a Server image built from current source. +Updating chart values alone does not change an existing image's behavior. + +## Troubleshooting + +| Symptom | First checks | +|---|---| +| PVC Pending | StorageClass/provisioner, matching PVs, PVC and Pod events; initial `WaitForFirstConsumer` Pending can be normal | +| Mount failure | Host directory, node affinity and image UID/GID permissions | +| ImagePullBackOff | Pod events, registry reachability, image tags/digests and `imagePullSecrets` | +| `HG_PD_AUTH_SECRET_KEY` missing | PD Pod Secret reference and key; chart injects the shared PD REST password automatically | +| Server storage wait timeout | Ready PD majority, Store registration, network policies and `server.storageWaitTimeoutSeconds` | +| NodePort/Ingress unreachable | Assigned port, reachable node/controller address, firewall and NetworkPolicy rules | +| REST works but Gremlin reports `Could not rebind` | Run `helm test`; after PD stabilizes, replace the failing Server Pod and retry | + +```bash +kubectl -n hugegraph get pods,pvc,svc +kubectl -n hugegraph describe pod +kubectl -n hugegraph logs +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +Never print credential values in diagnostic logs. Ready and `/versions` do not +prove graph binding or fresh UI login; verify graph queries and a new login too. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md new file mode 100644 index 0000000000..1f748d70d7 --- /dev/null +++ b/helm/hugegraph/examples/README.md @@ -0,0 +1,140 @@ +# HugeGraph deployment examples + +These files are examples, not automatically installed resources. Replace registry, +node names, paths, release/namespace and credentials for your cluster before use. +Commands below are deployment instructions; authoring them does not deploy anything. + +## Storage + +The default topology needs **seven volumes**: 3 × 10 GiB for PD, 3 × 50 GiB for +Store, and 1 × 1 GiB for Hubble. With a default dynamic StorageClass, ordinary +`helm install` requests them automatically. For a named dynamic StorageClass: + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set pd.storage.storageClassName=my-csi \ + --set store.storage.storageClassName=my-csi \ + --set hubble.persistence.storageClassName=my-csi +``` + +An empty chart `storageClassName` inherits the cluster default; `"-"` renders +an explicit empty Kubernetes StorageClass and disables default-class assignment. +Neither creates a provisioner. PVCs remain Pending without matching PVs. + +### Static local volumes + +[local-pvs.yaml](local-pvs.yaml) contains a `kubernetes.io/no-provisioner` +StorageClass with `WaitForFirstConsumer` and all seven prebound PVs. It assumes +release/namespace `hugegraph` and hostname labels `worker-1`, `worker-2`, `worker-3`. +Change both `claimRef` and nodes if those names differ. StatefulSet claim names +are `---`; Hubble uses +`-hubble-data`. + +On each indicated host, create these directories on disks with sufficient free +space and make them writable by the UID/GID of the chosen images (published +images currently run as root). Existing unrelated data must not be reused. + +| Node | Directories | Required capacities | +|---|---|---| +| worker-1 | `/var/lib/hugegraph/pd-0`, `/var/lib/hugegraph/store-0`, `/var/lib/hugegraph/hubble` | 10 GiB + 50 GiB + 1 GiB | +| worker-2 | `/var/lib/hugegraph/pd-1`, `/var/lib/hugegraph/store-1` | 10 GiB + 50 GiB | +| worker-3 | `/var/lib/hugegraph/pd-2`, `/var/lib/hugegraph/store-2` | 10 GiB + 50 GiB | + +PV capacity is a binding declaration, not a directory quota; arrange filesystem +capacity separately. Check labels with `kubectl get nodes -L kubernetes.io/hostname`, +then apply the reviewed resources: + +```bash +kubectl create namespace hugegraph +kubectl apply -f helm/hugegraph/examples/local-pvs.yaml +helm install hugegraph ./helm/hugegraph -n hugegraph \ + -f helm/hugegraph/examples/values-local-storage.yaml --wait --timeout 15m +``` + +`WaitForFirstConsumer` may leave a PVC Pending until a Pod can be scheduled; +local PV node affinity and workload affinity/nodeSelector must allow the same +node. Local volumes survive Pod replacement on that node, but cannot move their +files to another node on failure. `Retain` keeps data on claim deletion; +recovery/reuse requires operator inspection. + +For a fresh temporary installation, `--set persistence.enabled=false` uses +`emptyDir`. Component `pd.storage.enabled`, `store.storage.enabled` and +`hubble.persistence.enabled` override the global choice. A failed PVC never +falls back to temporary storage. Changing an initialized cluster's storage mode +requires a separate data migration/recovery procedure. + +## Internal images + +[values-internal-registry.yaml](values-internal-registry.yaml) mirrors the default +four images: `hugegraph/pd:latest`, `hugegraph/store:latest`, +`hugegraph/server:latest`, and `hugegraph/hubble:latest`. Tags follow the chart's +current version policy; pin your own published tags/digests. + +`global.imageRegistry` prefixes repositories without an explicit registry. +A repository such as `other-registry.example.com/team/pd` overrides that prefix. +Registry credentials remain `imagePullSecrets`. + +The PD wait init container reuses the Store image and Helm test reuses the +Server image. Both already include `curl`; the chart selects a shell command +rather than the component entrypoint. No extra `curlimages/curl` image or runtime +package installation is needed by default. `store.waitImage` and `server.waitImage` +remain explicit overrides for custom images. Store wait overrides need +`/bin/sh`, `curl`, `tr`, `date` and `sleep`; Server test overrides additionally +need `base64`, `getent`, `awk`, `sort`, `grep` and `tail`. Reused component images +also inherit the component pull policy. Older values may retain a curl image +under `--reuse-values`; clear those two fields to restore inheritance. + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + -f helm/hugegraph/examples/values-internal-registry.yaml --wait --timeout 15m +``` + +## GitOps credentials + +Use [values-gitops-fixed.yaml](values-gitops-fixed.yaml) with encrypted values, +or [values-gitops-existing-secrets.yaml](values-gitops-existing-secrets.yaml) with +a secret controller. Replace sample credentials before use. For the latter, +provision these three Secret data keys before the workloads start: + +| Secret | Key | Purpose | +|---|---|---| +| `hugegraph-pd-credentials` | `secret-key` | Shared PD REST Basic-auth password | +| `hugegraph-admin-credentials` | `password` | Initial Server admin password | +| `hugegraph-token-credentials` | `secret` | Shared JWT signing key, at least 32 bytes | + +The admin password must be printable ASCII without spaces, colons or backslashes. +PD credentials must be printable ASCII without surrounding whitespace or +backslashes. `existingSecret` takes priority over fixed values and is not managed +by the chart. Ordinary Helm installations generate/reuse credentials; repeated +offline renders cannot look up live generated Secrets and otherwise generate new +random values. Changing the admin Secret alone does not change an already-created +account; follow the [account password and Secret synchronization steps](../docs/operations.md#credentials). + +Before moving an existing release to a declarative GitOps renderer, export and +pin its effective values; offline rendering cannot apply Helm's `--reuse-values`. +Keep the existing `server.service.type`, `hubble.service.type`, `hubble.enabled` +and storage flags explicitly, as well as fixed/external credentials. Selecting +`mode: strict` keeps inherited Services internal, but also enables NetworkPolicy; +set `networkPolicy.enabled` explicitly to preserve the installed behavior. +A legacy Hubble with no mounted data volume still has metadata in its container +filesystem: keep `hubble.persistence.enabled: false` until a reviewed migration +moves that metadata to a PVC. Export or back up any records you need before +upgrading: keeping the temporary mode does not retain the old container's H2 +files when its Pod is replaced. Do not use a default reset as a migration. + +## Entry points and UI + +[values-http-ingress.yaml](values-http-ingress.yaml) uses an already installed +nginx controller for hostless HTTP Server Ingress. Both modes allow HTTP; +certificates, hostnames and acknowledgement fields are optional. +NodePort needs reachable node IPs and firewall permissions; query the actual port +with `kubectl -n hugegraph get svc`, then use `http://:`. +ClusterIP stays inside the cluster; use a gateway or port-forward for external +access. LoadBalancer needs your infrastructure's controller. + +[values-api-only.yaml](values-api-only.yaml) disables Hubble. Keep auth for an +authenticated API; disable `server.auth.enabled` as well for an unauthenticated +API. Current Hubble login still needs Server auth. Strict mode defaults to internal +Services and NetworkPolicy, but explicit parameters override defaults. With +NetworkPolicy enabled, add native `extraIngress` rules for actual clients; `{}` +is a valid allow-all rule. diff --git a/helm/hugegraph/examples/local-pvs.yaml b/helm/hugegraph/examples/local-pvs.yaml new file mode 100644 index 0000000000..72721ea9f0 --- /dev/null +++ b/helm/hugegraph/examples/local-pvs.yaml @@ -0,0 +1,193 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: hugegraph-local +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Retain +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-0 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-0 + local: + path: /var/lib/hugegraph/pd-0 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-1 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-1 + local: + path: /var/lib/hugegraph/pd-1 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-2] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-2 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-2 + local: + path: /var/lib/hugegraph/pd-2 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-3] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-0 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-0 + local: + path: /var/lib/hugegraph/store-0 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-1 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-1 + local: + path: /var/lib/hugegraph/store-1 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-2] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-2 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-2 + local: + path: /var/lib/hugegraph/store-2 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-3] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-hubble +spec: + capacity: + storage: 1Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: hugegraph-hubble-data + local: + path: /var/lib/hugegraph/hubble + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] diff --git a/helm/hugegraph/examples/values-api-only.yaml b/helm/hugegraph/examples/values-api-only.yaml new file mode 100644 index 0000000000..9e6c4c5216 --- /dev/null +++ b/helm/hugegraph/examples/values-api-only.yaml @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Current Hubble login needs Server auth; disable UI for API-only. +hubble: + enabled: false +# Optional unauthenticated API: +# server: +# auth: +# enabled: false diff --git a/helm/hugegraph/examples/values-gitops-existing-secrets.yaml b/helm/hugegraph/examples/values-gitops-existing-secrets.yaml new file mode 100644 index 0000000000..8b5d65d544 --- /dev/null +++ b/helm/hugegraph/examples/values-gitops-existing-secrets.yaml @@ -0,0 +1,34 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Provision these Secrets before workloads through your secret controller. +pd: + auth: + existingSecret: hugegraph-pd-credentials + key: secret-key + autoGenerate: false +server: + auth: + admin: + existingSecret: hugegraph-admin-credentials + key: password + autoGenerate: false + token: + existingSecret: hugegraph-token-credentials + key: secret + autoGenerate: false diff --git a/helm/hugegraph/examples/values-gitops-fixed.yaml b/helm/hugegraph/examples/values-gitops-fixed.yaml new file mode 100644 index 0000000000..b4c5294054 --- /dev/null +++ b/helm/hugegraph/examples/values-gitops-fixed.yaml @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Example only: replace with unique credentials in encrypted values. +pd: + auth: + value: REPLACE_WITH_PD_REST_SECRET + autoGenerate: false +server: + auth: + admin: + password: REPLACE_WITH_INITIAL_ADMIN_PASSWORD + autoGenerate: false + token: + value: REPLACE_WITH_UNIQUE_JWT_SIGNING_KEY_AT_LEAST_32_BYTES + autoGenerate: false diff --git a/helm/hugegraph/examples/values-http-ingress.yaml b/helm/hugegraph/examples/values-http-ingress.yaml new file mode 100644 index 0000000000..f459bb2866 --- /dev/null +++ b/helm/hugegraph/examples/values-http-ingress.yaml @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Requires an installed Ingress controller; host/TLS are optional. +server: + service: + type: ClusterIP + ingress: + enabled: true + className: nginx + hosts: + - paths: + - path: / + pathType: Prefix + tls: [] +hubble: + service: + type: ClusterIP + ingress: + enabled: false +# Use separate hosts/paths to expose Hubble through the same controller. diff --git a/helm/hugegraph/examples/values-internal-registry.yaml b/helm/hugegraph/examples/values-internal-registry.yaml new file mode 100644 index 0000000000..65c6301f34 --- /dev/null +++ b/helm/hugegraph/examples/values-internal-registry.yaml @@ -0,0 +1,32 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Mirror hugegraph/pd:latest, hugegraph/store:latest, +# hugegraph/server:latest, hugegraph/hubble:latest at your private registry. +global: + imageRegistry: registry.internal.example.com +store: + waitImage: "" +server: + waitImage: "" +# Qualified component repository overrides the global prefix: +# pd: +# image: +# repository: other-registry.example.com/team/pd +# imagePullSecrets: +# - name: registry-credentials diff --git a/helm/hugegraph/examples/values-local-storage.yaml b/helm/hugegraph/examples/values-local-storage.yaml new file mode 100644 index 0000000000..a6a31a9f7e --- /dev/null +++ b/helm/hugegraph/examples/values-local-storage.yaml @@ -0,0 +1,29 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Apply local-pvs.yaml after preparing the host directories. +# Release/namespace must match the example PV claimRef: hugegraph. +pd: + storage: + storageClassName: hugegraph-local +store: + storage: + storageClassName: hugegraph-local +hubble: + persistence: + storageClassName: hugegraph-local diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 70f9eb31b5..10892ba8ef 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -14,8 +14,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */}} -{{- $hubbleEnabled := get (get .Values "hubble" | default dict) "enabled" | default false }} -{{ .Release.Name }} installed in namespace {{ .Release.Namespace }}. +{{ include "hugegraph.resolveValues" . }} +{{- $hubbleEnabled := .Values.hubble.enabled }} +{{ .Release.Name }} installed in namespace {{ .Release.Namespace }} ({{ .Values.mode | default "legacy" }} mode). PD {{ .Values.pd.replicas }} replica(s) Store {{ .Values.store.replicas }} replica(s) @@ -23,125 +24,144 @@ limitations under the License. {{- if $hubbleEnabled }} Hubble 1 replica (UI) {{- end }} +{{- $inheritedEntries := list }} +{{- range $component := list "server" "hubble" }} +{{- $cfg := get $.Values $component }} +{{- if and (or (ne $component "hubble") $hubbleEnabled) (get $cfg "__serviceTypeInherited") (eq $cfg.service.type "NodePort") }} +{{- $inheritedEntries = append $inheritedEntries ($component | title) }} +{{- end }} +{{- end }} +{{- if and .Release.IsUpgrade (eq (.Values.mode | default "legacy") "easy") $inheritedEntries }} + +Upgrade notice: chart 0.2.0 changed entry defaults. This easy-mode upgrade inherits NodePort for {{ join ", " $inheritedEntries }}. +For releases installed before 0.2.0, review helm/hugegraph/docs/operations.md#storage-and-upgrades. +Use --reuse-values to retain previous settings, or explicitly set the desired Service types and Hubble setting. +{{- end }} -Watch the cluster come up: +Watch startup (run each command in a separate terminal): kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} -w + kubectl get pvc -n {{ .Release.Namespace }} -w +{{- $claims := 0 }} +{{- if .Values.pd.storage.enabled }}{{ $claims = add $claims (int .Values.pd.replicas) }}{{ end }} +{{- if .Values.store.storage.enabled }}{{ $claims = add $claims (int .Values.store.replicas) }}{{ end }} +{{- if and $hubbleEnabled .Values.hubble.persistence.enabled }}{{ $claims = add $claims 1 }}{{ end }} +{{- if gt (int $claims) 0 }} + +This configuration needs {{ $claims }} persistent volume claim(s). A dynamic +StorageClass or matching pre-created PVs must be available; see helm/hugegraph/examples/README.md#storage. +{{- end }} -Store Pods stay in Init until a majority of PD peers answer store.waitPath -(/v1/ready by default, which stays 503 until a raft leader exists, so the -majority counted is a quorum and not merely a set of live listeners). +Store waits for a PD quorum before startup. If a Pod remains in Init, inspect +its init-container logs and PD readiness; see helm/hugegraph/docs/operations.md#troubleshooting. + +Access: +{{- range $component := list "server" "hubble" "pd" }} +{{- if or (ne $component "hubble") $hubbleEnabled }} +{{- $cfg := get $.Values $component }} +{{- $name := include (printf "hugegraph.%s.name" $component) $ }} +{{- if eq $component "pd" }}{{ $name = include "hugegraph.pd.clientName" $ }}{{ end }} +{{- $portName := "http" }} +{{- $port := get $cfg "port" }} +{{- if eq $component "pd" }}{{ $portName = "rest" }}{{ $port = $cfg.ports.rest }}{{ end }} +{{- $svc := $cfg.service }} + + {{ $component | title }}: +{{- if eq $svc.type "NodePort" }} + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o jsonpath='{.spec.ports[?(@.name=="{{ $portName }}")].nodePort}{"\n"}' + kubectl get nodes -o wide + Open http://:{{ if eq $component "server" }}/versions{{ end }} +{{- else if eq $svc.type "LoadBalancer" }} + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o wide + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o jsonpath='{.status.loadBalancer.ingress}{"\n"}' + Once an address is assigned, use http://:{{ $port }}{{ if eq $component "server" }}/versions{{ end }} +{{- else }} + In-cluster address: http://{{ $name }}.{{ $.Release.Namespace }}.svc:{{ $port }} + ClusterIP is reachable inside the cluster; use port-forward below from your workstation. +{{- end }} +{{- $ingress := get $cfg "ingress" | default dict }} +{{- if get $ingress "enabled" }} + Ingress address/status: kubectl get ingress -n {{ $.Release.Namespace }} {{ $name }} -o wide +{{- $tlsHosts := list }} +{{- range (get $ingress "tls" | default list) }} +{{- range (.hosts | default list) }}{{ $tlsHosts = append $tlsHosts . }}{{ end }} +{{- end }} +{{- range (get $ingress "hosts" | default (list (dict "host" ""))) }} +{{- $host := .host | default "" }} +{{- $scheme := "http" }} +{{- if or (has $host $tlsHosts) (and (empty $host) (not (empty (get $ingress "tls")))) }}{{ $scheme = "https" }}{{ end }} + {{ $scheme }}://{{ $host | default "" }}{{ (first (.paths | default (list (dict "path" "/")))).path | default "/" }} +{{- end }} +{{- end }} + Port-forward: kubectl port-forward -n {{ $.Release.Namespace }} svc/{{ $name }} {{ $port }}:{{ $port }} + Local address: http://127.0.0.1:{{ $port }} +{{- end }} +{{- end }} -PD's management REST API (Disaster Recovery calls in the README) takes the -release's PD secret as the Basic-auth password: +Credentials: PD_SECRET="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.pd.authSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.pd.authSecretKey" . | replace "." "\\." }}}' | base64 --decode)" +{{- if .Values.server.auth.enabled }} + PASSWORD="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.server.authSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.server.authSecretKey" . | replace "." "\\." }}}' | base64 --decode)" + curl --user "admin:${PASSWORD}" http:///versions -Verify the release: - - helm test {{ .Release.Name }} --namespace {{ .Release.Namespace }} --logs --timeout 5m - -Kubernetes Ready and helm --wait cover component probes only. -{{- if $hubbleEnabled }} -The hook verifies Server Gremlin, not Hubble login: also confirm a fresh Hubble -login and graph query after installation or recovery. -{{- end }} - -Run it again after every upgrade that rolls PD and Server together: a -Server that started while PD was rolling can pass readiness with a dead -Gremlin binding (README, Troubleshooting), and the test checks every Server -Pod for exactly that. -{{- $pdAuth := get .Values.pd "auth" | default dict }} -{{- $srvAuth := get .Values.server "auth" | default dict }} -{{- $adminCfg := get $srvAuth "admin" | default dict }} -{{- $tokenCfg := get $srvAuth "token" | default dict }} +The admin Secret contains the initial password. Changing it does not reset an +existing account; see helm/hugegraph/docs/operations.md#credentials for account password changes and Secret synchronization. +{{- else }} + Server authentication is disabled. +{{- end }} +{{- if and $hubbleEnabled (not .Values.server.auth.enabled) }} + Current Hubble images require Server authentication to complete login. + Enable server.auth.enabled or set hubble.enabled=false for API-only deployment. +{{- end }} +{{- if and $hubbleEnabled (not .Values.hubble.persistence.enabled) }} + Hubble connection metadata uses temporary storage and is lost on Pod replacement. +{{- end }} +{{- $pdAuth := .Values.pd.auth }} +{{- $srvAuth := .Values.server.auth }} {{- $generated := list }} -{{- if and (not (get $pdAuth "existingSecret" | default "")) (not (get $pdAuth "value" | default "")) }}{{ $generated = append $generated "pd.auth" }}{{ end }} -{{- if and (get $srvAuth "enabled" | default false) (not (get $adminCfg "existingSecret" | default "")) (not (get $adminCfg "password" | default "")) }}{{ $generated = append $generated "server.auth.admin" }}{{ end }} -{{- if and (get $srvAuth "enabled" | default false) (not (get $tokenCfg "existingSecret" | default "")) (not (get $tokenCfg "value" | default "")) }}{{ $generated = append $generated "server.auth.token" }}{{ end }} +{{- if and (not $pdAuth.existingSecret) (not $pdAuth.value) }}{{ $generated = append $generated "pd.auth" }}{{ end }} +{{- if and $srvAuth.enabled (not $srvAuth.admin.existingSecret) (not $srvAuth.admin.password) }}{{ $generated = append $generated "server.auth.admin" }}{{ end }} +{{- if and $srvAuth.enabled (not $srvAuth.token.existingSecret) (not $srvAuth.token.value) }}{{ $generated = append $generated "server.auth.token" }}{{ end }} {{- if $generated }} Chart-generated credentials are in use: {{ join ", " $generated }}. -They are stable only while Helm renders against the cluster. A template-only -pipeline (helm template, Argo CD, Flux in template mode) cannot see the live -Secret and generates a new value on every render, so every sync changes the -Secret, rolls the Pods that read it, and leaves the admin account created at -first start out of step with the Secret. For such a pipeline, pre-create the -Secrets and set the existingSecret values (README, Template-only pipelines). +For GitOps/template-only pipelines use fixed credentials or existingSecret; +see helm/hugegraph/examples/README.md#gitops-credentials. {{- end }} - -Reach the Server API: - - kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hugegraph.server.name" . }} {{ .Values.server.port }}:{{ .Values.server.port }} -{{- if .Values.server.auth.enabled }} - PASSWORD="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.server.authSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.server.authSecretKey" . | replace "." "\\." }}}' | base64 --decode)" - curl --user "admin:${PASSWORD}" http://127.0.0.1:{{ .Values.server.port }}/versions -{{- else }} - curl http://127.0.0.1:{{ .Values.server.port }}/versions -{{- end }} -{{- if $hubbleEnabled }} - -Reach the Hubble UI: - - kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hugegraph.hubble.name" . }} {{ .Values.hubble.port }}:{{ .Values.hubble.port }} - - then open http://127.0.0.1:{{ .Values.hubble.port }} - -{{- if not .Values.server.auth.enabled }} - -Server authentication is disabled, so the Hubble login cannot complete on -this release: current Hubble images authenticate against the cluster. -Enable server.auth to use Hubble. +{{- if .Values.networkPolicy.enabled }} + +NetworkPolicy is enabled. Add application or monitoring clients through +networkPolicy..extraIngress; enforcement depends on the cluster network plugin. +{{- range $component := list "server" "hubble" "pd" }} +{{- if or (ne $component "hubble") $hubbleEnabled }} +{{- $cfg := get $.Values $component }} +{{- $ingress := get $cfg "ingress" | default dict }} +{{- $exposed := or (has $cfg.service.type (list "NodePort" "LoadBalancer")) (get $ingress "enabled" | default false) (not (empty (get $cfg.service "externalIPs"))) }} +{{- $policy := get $.Values.networkPolicy $component | default dict }} +{{- if and $exposed (empty (get $policy "extraIngress")) }} + {{ $component | title }} has an external entry, but NetworkPolicy has no external ingress rule. + Add networkPolicy.{{ $component }}.extraIngress for the client/controller, or disable networkPolicy.enabled. + An explicit [{}] ingress rule allows all sources and ports; choose the rule for your network. {{- end }} - -Hubble serves plain HTTP. Reach it through port-forward or an -HTTPS-terminating Ingress; never expose it directly to an untrusted network. -{{- if not .Values.hubble.persistence.enabled }} -Hubble persistence is disabled: UI connection metadata is lost when the -Hubble Pod is replaced. Graph data is unaffected. {{- end }} {{- end }} -{{- if not .Values.server.auth.enabled }} - -Authentication is disabled. Do not expose this release to untrusted networks. +{{- $hubblePolicy := get $.Values.networkPolicy "hubble" | default dict }} +{{- if and $.Values.server.advertiseUrl (empty (get $hubblePolicy "extraEgress")) $hubbleEnabled (eq $.Values.hubble.mode "pd") }} + For an external server.advertiseUrl, also add networkPolicy.hubble.extraEgress for that address. {{- end }} -{{- if (get (get .Values "networkPolicy" | default dict) "enabled") }} - -NetworkPolicy is on: only this release's components, Hubble and the helm test -Pod reach each other. List other clients (applications, Prometheus, Vermeer) -in networkPolicy..extraIngress; see README, NetworkPolicy. The -policies are only enforced by a network plugin that implements NetworkPolicy. {{- end }} -{{- $advertiseUrl := trim (default "" .Values.server.advertiseUrl) }} -{{- if $advertiseUrl }} - -Outside PD discovery: Server registers this URL with PD: - - {{ $advertiseUrl }} +{{- if .Values.server.advertiseUrl }} -Keep server.auth.enabled=true so this registration stays active. Expose the -PD client Service (pd.service.type=NodePort or LoadBalancer) and point a -standalone Hubble at that PD endpoint with pd.enabled=true. Graph UI works -via PD discovery; Store Operations metrics are unchanged and still use -in-cluster Store URLs unless configured separately. +Server registers {{ .Values.server.advertiseUrl }} with PD for external discovery. +A standalone Hubble also needs a reachable PD endpoint; see helm/hugegraph/docs/operations.md#networking-and-hubble. {{- end }} -{{- if and (gt (int .Values.pd.replicas) 1) (ne (get .Values.pd "antiAffinity" | default "") "required") (empty (get .Values.pd "affinity")) }} +Verify all Server Gremlin endpoints: -pd.antiAffinity is not "required", so the scheduler may co-locate PD quorum -members on one node and a single node loss can take down the PD quorum. The -PD PodDisruptionBudget only limits voluntary disruption (drains, evictions), -not node failure. On clusters with at least {{ .Values.pd.replicas }} nodes, -set pd.antiAffinity=required; see values-cluster.yaml. -{{- end }} -{{- if or (empty .Values.pd.resources) (empty .Values.store.resources) (empty .Values.server.resources) }} - -One or more components have no resource requests or limits. Set them before -production use; see values-cluster.yaml. -{{- end }} + helm test {{ .Release.Name }} --namespace {{ .Release.Namespace }} --logs --timeout 5m -{{- if and (empty .Values.pd.resources) (empty .Values.store.resources) (empty .Values.server.resources) }} -WARNING: no resources are set, so every pod is BestEffort and each JVM sizes its -heap against total node memory. On a multi-node cluster this oversubscribes the -nodes and pods may abort. Use values-cluster.yaml or set resources explicitly. +Run this after upgrades too. Ready probes alone do not verify Gremlin binding. +{{- if $hubbleEnabled }} +Also verify a fresh Hubble login and graph query; the hook checks Server, not UI login. {{- end }} diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index edf747252d..911a1bd84e 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -15,6 +15,81 @@ # limitations under the License. # +{{/* Resolve inherited defaults once. Never use default/or for explicit booleans. */}} +{{- define "hugegraph.resolveValues" -}} +{{- if not (get .Values "__hugegraphResolved") -}} +{{- $_ := set .Values "__hugegraphResolved" true -}} +{{- $legacy := not (hasKey .Values "mode") -}} +{{- if not (hasKey .Values "hubble") -}} +{{- $_ := set .Values "hubble" (dict "enabled" false "service" dict "persistence" dict) -}} +{{- end -}} +{{- $strict := eq (.Values.mode | default "easy") "strict" -}} +{{- $serviceType := ternary "ClusterIP" "NodePort" (or $strict $legacy) -}} +{{- range $comp := list "server" "hubble" -}} +{{- $component := get $.Values $comp -}} +{{- $svc := get $component "service" | default dict -}} +{{- $_ := set $component "service" $svc -}} +{{- $typeInherited := or (not (hasKey $svc "type")) (eq (get $svc "type" | toString) "") -}} +{{- $_ := set $component "__serviceTypeInherited" $typeInherited -}} +{{- if $typeInherited -}}{{- $_ := set $svc "type" $serviceType -}}{{- end -}} +{{- end -}} +{{- $np := .Values.networkPolicy | default dict -}} +{{- $_ := set .Values "networkPolicy" $np -}} +{{- if or (not (hasKey $np "enabled")) (eq (get $np "enabled" | toString) "") -}}{{- $_ := set $np "enabled" $strict -}}{{- end -}} +{{- $persistent := true -}} +{{- if hasKey (.Values.persistence | default dict) "enabled" -}}{{- $persistent = .Values.persistence.enabled -}}{{- end -}} +{{- range $comp := list "pd" "store" "hubble" -}} +{{- $component := get $.Values $comp -}} +{{- $key := ternary "persistence" "storage" (eq $comp "hubble") -}} +{{- $storage := get $component $key | default dict -}} +{{- $_ := set $component $key $storage -}} +{{- if or (not (hasKey $storage "enabled")) (eq (get $storage "enabled" | toString) "") -}}{{- $_ := set $storage "enabled" $persistent -}}{{- end -}} +{{- end -}} +{{- $registry := trimSuffix "/" (get (.Values.global | default dict) "imageRegistry" | default "") -}} +{{- if contains "://" $registry -}}{{- fail "global.imageRegistry must be a registry host/path without http:// or https://" -}}{{- end -}} +{{- if $registry -}} +{{- range $comp := list "pd" "store" "server" "hubble" -}} +{{- $image := get (get $.Values $comp) "image" | default dict -}} +{{- if $image -}} +{{- $_ := set $image "repository" (include "hugegraph.registryImage" (dict "reference" $image.repository "registry" $registry)) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- range $comp := list "store" "server" -}} +{{- $values := get $.Values $comp -}} +{{- $override := get $values "waitImage" | default "" -}} +{{- $componentImage := include "hugegraph.image" (dict "image" $values.image "appVersion" $.Chart.AppVersion) -}} +{{- if $override -}} +{{- $_ := set $values "waitImage" (include "hugegraph.registryImage" (dict "reference" $override "registry" $registry)) -}} +{{- else -}} +{{- $_ := set $values "waitImage" $componentImage -}} +{{- end -}} +{{- $_ := set $values "__waitImageInherited" (eq $values.waitImage $componentImage) -}} +{{- end -}} +{{- $server := .Values.server -}} +{{- if not (hasKey $server "storageWaitTimeoutSeconds") -}}{{- $_ := set $server "storageWaitTimeoutSeconds" 300 -}}{{- end -}} +{{- $startup := $server.probes.startup -}} +{{- if $legacy -}} +{{- $period := int $startup.periodSeconds -}} +{{- $budget := add (int $server.storageWaitTimeoutSeconds) 150 -}} +{{- $_ := set $startup "failureThreshold" (max (int $startup.failureThreshold) (add (div (add (sub $budget 1) $period) $period) 1)) -}} +{{- end -}} +{{- if or (not (hasKey $startup "failureThreshold")) (eq (get $startup "failureThreshold" | toString) "") -}} +{{- $period := int $startup.periodSeconds -}} +{{- $budget := add (int $server.storageWaitTimeoutSeconds) 150 -}} +{{- $_ := set $startup "failureThreshold" (add (div (add (sub $budget 1) $period) $period) 1) -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* A qualified component reference overrides the global registry. */}} +{{- define "hugegraph.registryImage" -}} +{{- $reference := .reference -}} +{{- $first := first (splitList "/" $reference) -}} +{{- $qualified := and (contains "/" $reference) (or (contains "." $first) (contains ":" $first) (eq $first "localhost")) -}} +{{- if and .registry (not $qualified) -}}{{- printf "%s/%s" .registry $reference -}}{{- else -}}{{- $reference -}}{{- end -}} +{{- end }} + {{/* Expand the name of the chart. */}} @@ -573,18 +648,12 @@ leader election waits on; empty preserves the image default. {{- end }} {{/* -Keep the startup probe alive for the 300-second storage wait, the Server's -start command, and process overhead, on a conservative timeline: kubelet may -run the first probe immediately, so the guaranteed alive time is -(failureThreshold - 1) * periodSeconds, not the full product. The minimum -below keeps that guaranteed time at 450 seconds or more. Older stored values -remain accepted, but their rendered threshold is raised to this floor. +Unspecified probes reserve storageWaitTimeoutSeconds plus 150 seconds. +An explicitly configured threshold is preserved and checked by validateValues. +Kubelet may probe immediately: the budget is (failureThreshold - 1) * periodSeconds. */}} {{- define "hugegraph.server.startupFailureThreshold" -}} -{{- $period := int .Values.server.probes.startup.periodSeconds -}} -{{- $configured := int .Values.server.probes.startup.failureThreshold -}} -{{- $minimum := add (div (add 449 $period) $period) 1 -}} -{{- max $configured $minimum -}} +{{- .Values.server.probes.startup.failureThreshold -}} {{- end }} {{/* @@ -593,16 +662,20 @@ HG_SERVER_STARTUP_TIMEOUT_S. The image defaults that to 120 seconds, which is shorter than the storage wait alone, so a Server still coming up kills itself before Kubernetes has given up on it. The value therefore tracks the startup probe's guaranteed alive time, (failureThreshold - 1) * periodSeconds, -because the first probe can fail immediately, minus the 300-second storage +because the first probe can fail immediately, minus the configured storage wait the entrypoint runs before the start command, so the start command and kubelet give up together instead of the image outliving the probe. Floored at the image's own 120-second default, and capped at the entrypoint's 86400 maximum rather than rendered into a Pod that refuses to start. */}} +{{- define "hugegraph.server.startupProbeBudget" -}} +{{- $startup := .Values.server.probes.startup -}} +{{- add (int ($startup.initialDelaySeconds | default 0)) (mul (sub (int $startup.failureThreshold) 1) (int $startup.periodSeconds)) -}} +{{- end }} + {{- define "hugegraph.server.startupTimeoutSeconds" -}} -{{- $period := int .Values.server.probes.startup.periodSeconds -}} -{{- $threshold := include "hugegraph.server.startupFailureThreshold" . | int -}} -{{- min 86400 (max 120 (sub (mul (sub $threshold 1) $period) 300)) -}} +{{- $budget := include "hugegraph.server.startupProbeBudget" . | int -}} +{{- min 86400 (max 120 (sub $budget (int .Values.server.storageWaitTimeoutSeconds))) -}} {{- end }} {{/* @@ -653,6 +726,11 @@ The minimum Server replica count that a PDB must remain valid against. Cross-field validation that JSON Schema draft-07 cannot express. */}} {{- define "hugegraph.validateValues" -}} +{{- include "hugegraph.resolveValues" . -}} +{{- $startupBudget := include "hugegraph.server.startupProbeBudget" . | int -}} +{{- if lt $startupBudget (add (int .Values.server.storageWaitTimeoutSeconds) 120) -}} +{{- fail "server.probes.startup.failureThreshold and periodSeconds must leave at least server.storageWaitTimeoutSeconds + 120 seconds to wait for storage and start Server; increase the probe budget or reduce server.storageWaitTimeoutSeconds" -}} +{{- end -}} {{- range $comp := list "pd" "store" "server" "hubble" -}} {{- $compLabels := get (get $.Values $comp | default dict) "podLabels" | default dict -}} {{- range $reserved := list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component" -}} @@ -694,15 +772,25 @@ and skip it. An operator who has completed the documented manual scale-down procedure has already scaled the live StatefulSet, so desired equals live and the upgrade passes. */}} +{{- if .Release.IsUpgrade -}} {{- range $comp := list "pd" "store" -}} {{- $stsName := "" -}} {{- if eq $comp "pd" -}}{{- $stsName = include "hugegraph.pd.name" $ -}}{{- else -}}{{- $stsName = include "hugegraph.store.name" $ -}}{{- end -}} {{- $live := lookup "apps/v1" "StatefulSet" $.Release.Namespace $stsName -}} {{- if $live -}} +{{- $wasPersistent := false -}} +{{- range $claim := get (get $live "spec") "volumeClaimTemplates" | default list -}} +{{- if eq (dig "metadata" "name" "" $claim) (printf "%s-data" $comp) -}}{{- $wasPersistent = true -}}{{- end -}} +{{- end -}} +{{- if ne $wasPersistent (get (get (get $.Values $comp) "storage") "enabled") -}} +{{- fail (printf "%s.storage.enabled cannot change on an initialized release: StatefulSet volumeClaimTemplates are immutable and changing storage can discard data. Use a fresh release and migrate data" $comp) -}} +{{- end -}} + {{- $liveReplicas := int (dig "spec" "replicas" 0 $live) -}} {{- $desired := int (get (get $.Values $comp) "replicas") -}} {{- if and (gt $liveReplicas 0) (lt $desired $liveReplicas) -}} -{{- fail (printf "%s.replicas cannot shrink from %d to %d through a helm upgrade: raft and shard membership are persisted, and removing Pods does not reconfigure them. Follow the manual scale-down procedure in the README (Scaling), which ends by scaling the live StatefulSet; the upgrade passes once the live replicas match the value" $comp $liveReplicas $desired) -}} +{{- $procedure := ternary "Reconfigure the persisted PD voting membership before scaling the live StatefulSet" "Retire leaving Store IDs and migrate their shards before scaling the live StatefulSet" (eq $comp "pd") -}} +{{- fail (printf "%s.replicas cannot shrink from %d to %d through a helm upgrade: persisted membership is not changed by removing Pods. %s; see docs/operations.md (Topology and scheduling). The upgrade passes once live replicas match the value" $comp $liveReplicas $desired $procedure) -}} {{- end -}} {{/* PD raft membership is the persisted voting configuration, and the peer list @@ -715,7 +803,7 @@ from no REST route, so the chart cannot perform it and does not pretend to. Growing Store is ordinary scale-out and stays allowed. */}} {{- if and (eq $comp "pd") (gt $liveReplicas 0) (gt $desired $liveReplicas) -}} -{{- fail (printf "pd.replicas cannot grow from %d to %d through a helm upgrade: the rendered peer list reaches raft only as the initial configuration, so new Pods would start without joining the voting configuration. Change the persisted membership through PD first, then scale the live StatefulSet, then upgrade with the matching value; the README (Scaling) has the procedure and its limits. A fresh install at any replica count is unaffected" $liveReplicas $desired) -}} +{{- fail (printf "pd.replicas cannot grow from %d to %d through a helm upgrade: the rendered peer list reaches raft only as the initial configuration, so new Pods would start without joining the voting configuration. Change the persisted membership through PD first, then scale the live StatefulSet, then upgrade with the matching value; see docs/operations.md (Topology and scheduling) for the supported lifecycle. A fresh install at any replica count is unaffected" $liveReplicas $desired) -}} {{- end -}} {{/* Raft identity. Every PD and Store names itself to its peers as @@ -763,6 +851,22 @@ the instance and component labels instead, which do not depend on the name. {{- end -}} {{- end -}} {{- end -}} +{{- $hubbleLive := lookup "apps/v1" "Deployment" .Release.Namespace (include "hugegraph.hubble.name" .) -}} +{{- if and $hubbleLive .Values.hubble.enabled -}} +{{- $wasPersistent := false -}} +{{- $hasDataVolume := false -}} +{{- range $volume := dig "spec" "template" "spec" "volumes" list $hubbleLive -}} +{{- if eq ($volume.name | default "") "data" -}}{{- $hasDataVolume = true -}}{{- end -}} +{{- if and (eq ($volume.name | default "") "data") (hasKey $volume "persistentVolumeClaim") -}}{{- $wasPersistent = true -}}{{- end -}} +{{- end -}} +{{- if ne $wasPersistent .Values.hubble.persistence.enabled -}} +{{- if not $hasDataVolume -}} +{{- fail "hubble.persistence.enabled cannot change on an initialized release: legacy Hubble has no data volume and its UI records are in the container filesystem. Keep hubble.persistence.enabled=false with --reuse-values or an explicit GitOps value; enabling persistence requires a reviewed metadata migration first" -}} +{{- end -}} +{{- fail "hubble.persistence.enabled cannot change on an initialized release: changing storage can discard UI records. Migrate the data into a fresh deployment first" -}} +{{- end -}} +{{- end -}} +{{- end -}} {{/* Only validate minAvailable where a PDB is actually rendered. The pd/store PDB templates require replicas > 1, so a single-replica release never creates one @@ -811,9 +915,6 @@ keys for releases stored before the values existed. {{- if and (get $svc "nodePort") (not (has (get $svc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} {{- fail "server.service.nodePort requires server.service.type to be NodePort or LoadBalancer" -}} {{- end -}} -{{- if and (ne (get $svc "type" | default "ClusterIP") "ClusterIP") (not (get $svc "allowInsecureExposure" | default false)) -}} -{{- fail "server.service.type NodePort or LoadBalancer publishes the plain-HTTP Server API outside the cluster, so Basic-auth credentials and JWTs cross the network in cleartext; keep ClusterIP behind a port-forward or an HTTPS-terminating Ingress (server.ingress.tls), or set server.service.allowInsecureExposure=true once reachability is restricted by other means (NetworkPolicy, load balancer allowlist, firewall)" -}} -{{- end -}} {{- $advertiseUrl := trim (default "" .Values.server.advertiseUrl) -}} {{- if and $advertiseUrl (not (or (hasPrefix "http://" $advertiseUrl) (hasPrefix "https://" $advertiseUrl))) -}} {{- fail "server.advertiseUrl must be an absolute http:// or https:// URL when set" -}} @@ -823,18 +924,12 @@ keys for releases stored before the values existed. {{- if and (or (get $pdSvc "restNodePort") (get $pdSvc "grpcNodePort")) (not (has $pdSvcType (list "NodePort" "LoadBalancer"))) -}} {{- fail "pd.service.restNodePort and pd.service.grpcNodePort require pd.service.type to be NodePort or LoadBalancer" -}} {{- end -}} -{{- if and (ne $pdSvcType "ClusterIP") (not (get $pdSvc "allowInsecureExposure" | default false)) -}} -{{- fail "pd.service.type NodePort or LoadBalancer exposes PD's unauthenticated gRPC port outside the cluster, raft membership RPCs included; keep ClusterIP, or set pd.service.allowInsecureExposure=true once reachability is restricted by other means (NetworkPolicy, load balancer allowlist, firewall)" -}} -{{- end -}} {{- $serverPdb := get .Values.server "pdb" | default dict -}} {{- $serverReplicaFloor := include "hugegraph.server.replicaFloor" . | int -}} {{- if and (get $serverPdb "enabled" | default false) (gt $serverReplicaFloor 1) (ge (int (get $serverPdb "minAvailable" | default 1)) $serverReplicaFloor) -}} {{- fail "server.pdb.minAvailable must be less than the active Server replica floor (server.hpa.minReplicas when HPA is enabled, otherwise server.replicas), otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} {{- end -}} {{- $serverIngress := get .Values.server "ingress" | default dict -}} -{{- if and (get $serverIngress "enabled" | default false) (empty (get $serverIngress "tls")) (not (get $serverIngress "allowPlainHttp" | default false)) -}} -{{- fail "server.ingress.enabled without tls publishes Basic-auth credentials and JWTs over plain HTTP; configure server.ingress.tls, or set server.ingress.allowPlainHttp=true to accept that on a trusted network" -}} -{{- end -}} {{- if get (get .Values.server "securityContext" | default dict) "readOnlyRootFilesystem" | default false -}} {{- fail "server.securityContext.readOnlyRootFilesystem=true breaks Server: its wrapper rewrites conf/rest-server.properties inside the image at startup and the chart mounts no writable volume there" -}} {{- end -}} @@ -851,7 +946,7 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- $reservedEnv := dict "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "HG_PD_AUTH_SECRET_KEY" "JAVA_OPTS" "JAVA_OPTIONS") "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") - "server" (list "POD_IP" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_STARTUP_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") + "server" (list "POD_IP" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_STARTUP_TIMEOUT_S" "WAIT_STORAGE_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_PD_PASSWORD" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} {{- range $component, $reserved := $reservedEnv -}} {{- $componentValues := get $.Values $component | default dict -}} @@ -870,9 +965,6 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- if and (get $hubbleSvc "nodePort") (not (has (get $hubbleSvc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} {{- fail "hubble.service.nodePort requires hubble.service.type to be NodePort or LoadBalancer" -}} {{- end -}} -{{- if and (ne (get $hubbleSvc "type" | default "ClusterIP") "ClusterIP") (not (get $hubbleSvc "allowInsecureExposure" | default false)) -}} -{{- fail "hubble.service.type NodePort or LoadBalancer publishes the plain-HTTP Hubble UI outside the cluster, with no login of its own and the graph credentials typed into it; keep ClusterIP behind a port-forward or an HTTPS-terminating Ingress, or set hubble.service.allowInsecureExposure=true once reachability is restricted by other means (NetworkPolicy, load balancer allowlist, firewall)" -}} -{{- end -}} {{- $hubbleImage := get $hubble "image" | default dict -}} {{- if and (eq (trim (get $hubbleImage "tag" | default "")) "") (eq (trim (get $hubbleImage "digest" | default "")) "") -}} {{- fail "hubble.image needs a tag or a digest: the chart appVersion tracks the Server release, not Hubble, so there is no meaningful fallback" -}} @@ -881,9 +973,6 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- fail "hubble.securityContext.readOnlyRootFilesystem=true breaks Hubble: its wrapper writes conf/hugegraph-hubble.properties inside the image at startup and the chart mounts no writable volume there" -}} {{- end -}} {{- $hubbleIngress := get $hubble "ingress" | default dict -}} -{{- if and (get $hubbleIngress "enabled" | default false) (empty (get $hubbleIngress "tls")) (not (get $hubbleIngress "allowPlainHttp" | default false)) -}} -{{- fail "hubble.ingress.enabled without tls publishes the plain-HTTP, unauthenticated Hubble UI; configure hubble.ingress.tls, or set hubble.ingress.allowPlainHttp=true to accept that on a trusted network" -}} -{{- end -}} {{- end -}} {{- $pdAuth := get .Values.pd "auth" | default dict -}} {{/* A values set with no pd.auth block at all (a release stored before the @@ -905,27 +994,7 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- fail "server.auth.token requires existingSecret, value, or autoGenerate=true when auth is enabled" -}} {{- end -}} {{- end -}} -{{/* NetworkPolicy exposure check runs last, so an exposure the other checks - refuse (allowInsecureExposure, Ingress TLS) is reported first. */}} -{{- $networkPolicy := get .Values "networkPolicy" | default dict -}} -{{- if get $networkPolicy "enabled" -}} -{{- $exposed := dict - "pd" (ne $pdSvcType "ClusterIP") - "server" (or (ne (get $svc "type" | default "ClusterIP") "ClusterIP") (get $serverIngress "enabled" | default false) (ne $advertiseUrl "")) - "hubble" (and (get $hubble "enabled" | default false) (or (ne (get (get $hubble "service" | default dict) "type" | default "ClusterIP") "ClusterIP") (get (get $hubble "ingress" | default dict) "enabled" | default false))) -}} -{{- range $comp := list "pd" "server" "hubble" -}} -{{- if and (get $exposed $comp) (empty (get (get $networkPolicy $comp | default dict) "extraIngress")) -}} -{{- fail (printf "networkPolicy.enabled admits nothing from outside the release, so the %s exposure (NodePort/LoadBalancer Service, Ingress%s) is unreachable; list its callers in networkPolicy.%s.extraIngress, for example the Ingress controller's namespace or a client CIDR" $comp (ternary ", server.advertiseUrl" "" (eq $comp "server")) $comp) -}} -{{- end -}} -{{- end -}} -{{/* An in-cluster Hubble in pd mode receives whatever Server URL PD hands - out. With server.advertiseUrl set, that is the advertised external URL, - and Hubble's egress policy only admits traffic to Server Pods, so the - discovered URL would be unreachable from Hubble. */}} -{{- if and (get $hubble "enabled" | default false) (eq (get $hubble "mode" | default "pd") "pd") (ne $advertiseUrl "") (empty (get (get $networkPolicy "hubble" | default dict) "extraEgress")) -}} -{{- fail "networkPolicy.enabled restricts Hubble egress to the release's own Pods, but server.advertiseUrl makes PD hand Hubble that external URL for discovery; allow Hubble to reach it in networkPolicy.hubble.extraEgress, or leave server.advertiseUrl empty for in-cluster discovery" -}} -{{- end -}} -{{- end -}} + {{- end }} {{/* diff --git a/helm/hugegraph/templates/hubble-deployment.yaml b/helm/hugegraph/templates/hubble-deployment.yaml index 2a2873b8e8..bd83736de2 100644 --- a/helm/hugegraph/templates/hubble-deployment.yaml +++ b/helm/hugegraph/templates/hubble-deployment.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if get (get .Values "hubble" | default dict) "enabled" | default false }} {{- include "hugegraph.validateValues" . }} {{- $customPort := ne (int .Values.hubble.port) 8088 }} @@ -270,19 +271,15 @@ spec: - name: HG_HUBBLE_SERVER_URL value: {{ include "hugegraph.server.clientUrl" . | quote }} {{- end }} - {{- if .Values.hubble.persistence.enabled }} # Hubble's H2 location is fixed to ./db inside the image classpath # config; Spring Boot's environment binding is the supported way to # point it into the mounted volume. - name: SPRING_DATASOURCE_URL value: "jdbc:h2:file:/hubble-data/db;DB_CLOSE_ON_EXIT=FALSE" - {{- end }} {{- with .Values.hubble.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} - {{- if .Values.hubble.persistence.enabled }} volumeMounts: - name: data mountPath: /hubble-data - {{- end }} startupProbe: httpGet: path: /actuator/health @@ -310,10 +307,12 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} - {{- if .Values.hubble.persistence.enabled }} volumes: - name: data + {{- if .Values.hubble.persistence.enabled }} persistentVolumeClaim: claimName: {{ include "hugegraph.hubble.dataName" . }} - {{- end }} + {{- else }} + emptyDir: {} + {{- end }} {{- end }} diff --git a/helm/hugegraph/templates/hubble-ingress.yaml b/helm/hugegraph/templates/hubble-ingress.yaml index 845730619d..9b69bbdf94 100644 --- a/helm/hugegraph/templates/hubble-ingress.yaml +++ b/helm/hugegraph/templates/hubble-ingress.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $hubble := get .Values "hubble" | default dict }} {{- if and (get $hubble "enabled" | default false) .Values.hubble.ingress.enabled }} apiVersion: networking.k8s.io/v1 @@ -37,11 +38,13 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} rules: - {{- range .Values.hubble.ingress.hosts }} - - host: {{ .host | quote }} + {{- range (.Values.hubble.ingress.hosts | default (list (dict "paths" (list (dict "path" "/" "pathType" "Prefix"))))) }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} http: paths: - {{- range .paths }} + {{- range (.paths | default (list (dict "path" "/" "pathType" "Prefix"))) }} - path: {{ .path | quote }} pathType: {{ .pathType }} backend: diff --git a/helm/hugegraph/templates/hubble-pvc.yaml b/helm/hugegraph/templates/hubble-pvc.yaml index 8143fab844..a1396a6918 100644 --- a/helm/hugegraph/templates/hubble-pvc.yaml +++ b/helm/hugegraph/templates/hubble-pvc.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $hubble := get .Values "hubble" | default dict }} {{- if and (get $hubble "enabled" | default false) .Values.hubble.persistence.enabled }} apiVersion: v1 @@ -35,7 +36,9 @@ spec: resources: requests: storage: {{ .Values.hubble.persistence.size }} - {{- if .Values.hubble.persistence.storageClassName }} + {{- if eq .Values.hubble.persistence.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.hubble.persistence.storageClassName }} storageClassName: {{ .Values.hubble.persistence.storageClassName | quote }} {{- end }} {{- end }} diff --git a/helm/hugegraph/templates/hubble-service.yaml b/helm/hugegraph/templates/hubble-service.yaml index 53eed8ec4a..a6210f5a72 100644 --- a/helm/hugegraph/templates/hubble-service.yaml +++ b/helm/hugegraph/templates/hubble-service.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if get (get .Values "hubble" | default dict) "enabled" | default false }} apiVersion: v1 kind: Service @@ -30,6 +31,16 @@ metadata: {{- end }} spec: type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} selector: {{- include "hugegraph.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: hubble diff --git a/helm/hugegraph/templates/networkpolicy.yaml b/helm/hugegraph/templates/networkpolicy.yaml index 9b20c0afb1..e8638d6d74 100644 --- a/helm/hugegraph/templates/networkpolicy.yaml +++ b/helm/hugegraph/templates/networkpolicy.yaml @@ -15,15 +15,15 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- /* One NetworkPolicy per component. Each object isolates its own Pods in both directions and carries all of their allows, so no object ever denies a Pod before that Pod's allows exist, whatever order the objects are applied in. Peers match the release and component labels; ports are the values the containers bind. The helm test Pod is selected by no policy, so its egress -stays open and the Server policy admits it. Nothing outside the release is -admitted unless .extraIngress names it; exposing a component -without such rules fails in validateValues instead of opening its ports. +stays open and the Server policy admits it. Outside clients require native extraIngress rules. An exposed Service or +Ingress does not automatically change these rules. */}} {{- $np := get .Values "networkPolicy" | default dict }} {{- if get $np "enabled" }} diff --git a/helm/hugegraph/templates/pd-auth-secret.yaml b/helm/hugegraph/templates/pd-auth-secret.yaml index 66a484bdf5..a699447c49 100644 --- a/helm/hugegraph/templates/pd-auth-secret.yaml +++ b/helm/hugegraph/templates/pd-auth-secret.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $auth := get .Values.pd "auth" | default dict -}} {{- $autoGen := ternary (get $auth "autoGenerate") true (hasKey $auth "autoGenerate") -}} {{- if and (not (get $auth "existingSecret" | default "")) (or (get $auth "value" | default "") $autoGen) }} diff --git a/helm/hugegraph/templates/pd-pdb.yaml b/helm/hugegraph/templates/pd-pdb.yaml index a0d93a098e..492fad2ca1 100644 --- a/helm/hugegraph/templates/pd-pdb.yaml +++ b/helm/hugegraph/templates/pd-pdb.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) }} apiVersion: policy/v1 kind: PodDisruptionBudget diff --git a/helm/hugegraph/templates/pd-service-client.yaml b/helm/hugegraph/templates/pd-service-client.yaml index a25108b511..f9d1373836 100644 --- a/helm/hugegraph/templates/pd-service-client.yaml +++ b/helm/hugegraph/templates/pd-service-client.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $svc := get .Values.pd "service" | default dict }} apiVersion: v1 kind: Service @@ -29,6 +30,16 @@ metadata: {{- end }} spec: type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} selector: {{- include "hugegraph.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: pd diff --git a/helm/hugegraph/templates/pd-service-headless.yaml b/helm/hugegraph/templates/pd-service-headless.yaml index 3b724f9dcf..5a42745811 100644 --- a/helm/hugegraph/templates/pd-service-headless.yaml +++ b/helm/hugegraph/templates/pd-service-headless.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} apiVersion: v1 kind: Service metadata: diff --git a/helm/hugegraph/templates/pd-statefulset.yaml b/helm/hugegraph/templates/pd-statefulset.yaml index 070945b208..0546070ac8 100644 --- a/helm/hugegraph/templates/pd-statefulset.yaml +++ b/helm/hugegraph/templates/pd-statefulset.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -30,10 +31,12 @@ spec: updateStrategy: {{- toYaml . | nindent 4 }} {{- end }} + {{- if .Values.pd.storage.enabled }} {{- with .Values.pd.persistentVolumeClaimRetentionPolicy }} persistentVolumeClaimRetentionPolicy: {{- toYaml . | nindent 4 }} {{- end }} + {{- end }} selector: matchLabels: {{- include "hugegraph.selectorLabels" . | nindent 6 }} @@ -159,14 +162,23 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} + {{- if not .Values.pd.storage.enabled }} + volumes: + - name: pd-data + emptyDir: {} + {{- end }} + {{- if .Values.pd.storage.enabled }} volumeClaimTemplates: - metadata: name: pd-data spec: accessModes: ["ReadWriteOnce"] - {{- if .Values.pd.storage.storageClassName }} + {{- if eq .Values.pd.storage.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.pd.storage.storageClassName }} storageClassName: {{ .Values.pd.storage.storageClassName | quote }} {{- end }} resources: requests: storage: {{ .Values.pd.storage.size }} + {{- end }} diff --git a/helm/hugegraph/templates/server-auth-token-secret.yaml b/helm/hugegraph/templates/server-auth-token-secret.yaml index 717d4fc23b..340d04571d 100644 --- a/helm/hugegraph/templates/server-auth-token-secret.yaml +++ b/helm/hugegraph/templates/server-auth-token-secret.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $auth := get .Values.server "auth" | default dict -}} {{- $token := get $auth "token" | default dict -}} {{- if and (get $auth "enabled" | default false) (not (get $token "existingSecret" | default "")) (or (get $token "value" | default "") (get $token "autoGenerate" | default false)) }} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml index 48d24ebaf9..fb51578da0 100644 --- a/helm/hugegraph/templates/server-deployment.yaml +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- include "hugegraph.validateValues" . }} {{- $customPort := ne (int .Values.server.port) 8080 }} {{/* @@ -276,6 +277,8 @@ spec: # The image would otherwise give the start command 120 seconds, # less than the storage wait, and kill a Server that is still # coming up. Track the startup probe's own budget instead. + - name: WAIT_STORAGE_TIMEOUT_S + value: {{ .Values.server.storageWaitTimeoutSeconds | quote }} - name: HG_SERVER_STARTUP_TIMEOUT_S value: {{ include "hugegraph.server.startupTimeoutSeconds" . | quote }} - name: HG_SERVER_URLS_TO_PD diff --git a/helm/hugegraph/templates/server-hpa.yaml b/helm/hugegraph/templates/server-hpa.yaml index 40e95c0d99..0ab64c3d62 100644 --- a/helm/hugegraph/templates/server-hpa.yaml +++ b/helm/hugegraph/templates/server-hpa.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if .Values.server.hpa.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler diff --git a/helm/hugegraph/templates/server-ingress.yaml b/helm/hugegraph/templates/server-ingress.yaml index ad473fde73..2f88becd4f 100644 --- a/helm/hugegraph/templates/server-ingress.yaml +++ b/helm/hugegraph/templates/server-ingress.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if .Values.server.ingress.enabled }} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -36,11 +37,13 @@ spec: {{- toYaml . | nindent 4 }} {{- end }} rules: - {{- range .Values.server.ingress.hosts }} - - host: {{ .host | quote }} + {{- range (.Values.server.ingress.hosts | default (list (dict "paths" (list (dict "path" "/" "pathType" "Prefix"))))) }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} http: paths: - {{- range .paths }} + {{- range (.paths | default (list (dict "path" "/" "pathType" "Prefix"))) }} - path: {{ .path | quote }} pathType: {{ .pathType }} backend: diff --git a/helm/hugegraph/templates/server-pdb.yaml b/helm/hugegraph/templates/server-pdb.yaml index 7fb4826aec..0fdd176d3a 100644 --- a/helm/hugegraph/templates/server-pdb.yaml +++ b/helm/hugegraph/templates/server-pdb.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $pdb := get .Values.server "pdb" | default dict }} {{- $replicaFloor := include "hugegraph.server.replicaFloor" . | int }} {{- if and (get $pdb "enabled" | default false) (gt $replicaFloor 1) }} diff --git a/helm/hugegraph/templates/server-secret.yaml b/helm/hugegraph/templates/server-secret.yaml index 4d132224a6..6b16c661de 100644 --- a/helm/hugegraph/templates/server-secret.yaml +++ b/helm/hugegraph/templates/server-secret.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- $auth := get .Values.server "auth" | default dict -}} {{- $admin := get $auth "admin" | default dict -}} {{- if and (get $auth "enabled" | default false) (not (get $admin "existingSecret" | default "")) (or (get $admin "password" | default "") (get $admin "autoGenerate" | default false)) }} diff --git a/helm/hugegraph/templates/server-service-headless.yaml b/helm/hugegraph/templates/server-service-headless.yaml index f93d62de3f..6f46957514 100644 --- a/helm/hugegraph/templates/server-service-headless.yaml +++ b/helm/hugegraph/templates/server-service-headless.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} # Headless Service enumerating the individual Server Pods. The helm test # hook resolves it to run its graph-bound Gremlin check against every Pod # (a broken-binding Pod passes readiness, so it appears here), and the diff --git a/helm/hugegraph/templates/server-service.yaml b/helm/hugegraph/templates/server-service.yaml index b4053dc4ba..f183946e86 100644 --- a/helm/hugegraph/templates/server-service.yaml +++ b/helm/hugegraph/templates/server-service.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} apiVersion: v1 kind: Service metadata: @@ -29,6 +30,16 @@ metadata: {{- end }} spec: type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} selector: {{- include "hugegraph.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: server diff --git a/helm/hugegraph/templates/serviceaccount.yaml b/helm/hugegraph/templates/serviceaccount.yaml index 0dbf4c302c..cb8e08139a 100644 --- a/helm/hugegraph/templates/serviceaccount.yaml +++ b/helm/hugegraph/templates/serviceaccount.yaml @@ -14,6 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -}} +{{ include "hugegraph.resolveValues" . }} {{- $components := list "pd" "store" "server" }} {{- if get (get .Values "hubble" | default dict) "enabled" | default false }} {{- $components = append $components "hubble" }} diff --git a/helm/hugegraph/templates/store-pdb.yaml b/helm/hugegraph/templates/store-pdb.yaml index 9fb1e536da..1b70b7f02e 100644 --- a/helm/hugegraph/templates/store-pdb.yaml +++ b/helm/hugegraph/templates/store-pdb.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} {{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) }} apiVersion: policy/v1 kind: PodDisruptionBudget diff --git a/helm/hugegraph/templates/store-service-headless.yaml b/helm/hugegraph/templates/store-service-headless.yaml index 4c82023b5d..4bf790155a 100644 --- a/helm/hugegraph/templates/store-service-headless.yaml +++ b/helm/hugegraph/templates/store-service-headless.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} apiVersion: v1 kind: Service metadata: diff --git a/helm/hugegraph/templates/store-statefulset.yaml b/helm/hugegraph/templates/store-statefulset.yaml index 69dc03d746..81c143ee8a 100644 --- a/helm/hugegraph/templates/store-statefulset.yaml +++ b/helm/hugegraph/templates/store-statefulset.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -30,10 +31,12 @@ spec: updateStrategy: {{- toYaml . | nindent 4 }} {{- end }} + {{- if .Values.store.storage.enabled }} {{- with .Values.store.persistentVolumeClaimRetentionPolicy }} persistentVolumeClaimRetentionPolicy: {{- toYaml . | nindent 4 }} {{- end }} + {{- end }} selector: matchLabels: {{- include "hugegraph.selectorLabels" . | nindent 6 }} @@ -86,6 +89,9 @@ spec: initContainers: - name: wait-for-pd image: {{ .Values.store.waitImage | quote }} + {{- if .Values.store.__waitImageInherited }} + imagePullPolicy: {{ .Values.store.image.pullPolicy }} + {{- end }} {{- with .Values.store.securityContext }} securityContext: {{- toYaml . | nindent 12 }} @@ -189,14 +195,23 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} + {{- if not .Values.store.storage.enabled }} + volumes: + - name: store-data + emptyDir: {} + {{- end }} + {{- if .Values.store.storage.enabled }} volumeClaimTemplates: - metadata: name: store-data spec: accessModes: ["ReadWriteOnce"] - {{- if .Values.store.storage.storageClassName }} + {{- if eq .Values.store.storage.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.store.storage.storageClassName }} storageClassName: {{ .Values.store.storage.storageClassName | quote }} {{- end }} resources: requests: storage: {{ .Values.store.storage.size }} + {{- end }} diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 7d71d323ba..b683b0d4c3 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -15,6 +15,7 @@ # limitations under the License. # +{{ include "hugegraph.resolveValues" . }} # Official Helm chart test hook — only runs with: helm test # Not an install-time hook. Safe with --wait (no init Job in no-init design). apiVersion: v1 @@ -44,6 +45,9 @@ spec: containers: - name: curl image: {{ .Values.server.waitImage | quote }} + {{- if .Values.server.__waitImageInherited }} + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + {{- end }} securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap b/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap index ca8ea46112..ccd33aaa05 100644 --- a/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap +++ b/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap @@ -26,7 +26,7 @@ matches the reviewed cluster plus Hubble render: app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hugegraph app.kubernetes.io/version: latest - helm.sh/chart: hugegraph-0.1.1 + helm.sh/chart: hugegraph-0.2.0 name: RELEASE-NAME-hugegraph-pd spec: egress: @@ -97,7 +97,7 @@ matches the reviewed cluster plus Hubble render: app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hugegraph app.kubernetes.io/version: latest - helm.sh/chart: hugegraph-0.1.1 + helm.sh/chart: hugegraph-0.2.0 name: RELEASE-NAME-hugegraph-store spec: egress: @@ -174,7 +174,7 @@ matches the reviewed cluster plus Hubble render: app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hugegraph app.kubernetes.io/version: latest - helm.sh/chart: hugegraph-0.1.1 + helm.sh/chart: hugegraph-0.2.0 name: RELEASE-NAME-hugegraph-server spec: egress: @@ -238,7 +238,7 @@ matches the reviewed cluster plus Hubble render: app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: hugegraph app.kubernetes.io/version: latest - helm.sh/chart: hugegraph-0.1.1 + helm.sh/chart: hugegraph-0.2.0 name: RELEASE-NAME-hugegraph-hubble spec: egress: diff --git a/helm/hugegraph/tests/credential_contract_test.yaml b/helm/hugegraph/tests/credential_contract_test.yaml index 67c9d1442c..5fbddb830b 100644 --- a/helm/hugegraph/tests/credential_contract_test.yaml +++ b/helm/hugegraph/tests/credential_contract_test.yaml @@ -53,6 +53,7 @@ tests: - it: skips the admin password check when authentication is disabled template: server-deployment.yaml set: + hubble.enabled: false server.auth.enabled: false asserts: - notMatchRegex: @@ -103,3 +104,30 @@ tests: - matchRegex: path: spec.containers[0].command[2] pattern: "grep -v ':'" + + - it: ignores unused credential encoding when Server authentication is disabled + set: + hubble.enabled: false + server.auth.enabled: false + server.auth.admin.password: "unused password: value" + server.auth.token.value: unused + template: server-deployment.yaml + asserts: + - isKind: + of: Deployment + - it: existing Secrets take priority over unused inline credentials + set: + server.auth.admin.existingSecret: provided-admin + server.auth.admin.password: "unused password: value" + server.auth.token.existingSecret: provided-token + server.auth.token.value: unused + template: server-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: PASSWORD + valueFrom: + secretKeyRef: + name: provided-admin + key: password diff --git a/helm/hugegraph/tests/easy_modes_test.yaml b/helm/hugegraph/tests/easy_modes_test.yaml new file mode 100644 index 0000000000..172495d6d5 --- /dev/null +++ b/helm/hugegraph/tests/easy_modes_test.yaml @@ -0,0 +1,179 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +suite: easy and strict defaults and explicit overrides +templates: + - server-service.yaml + - hubble-service.yaml + - pd-service-client.yaml + - networkpolicy.yaml + - store-statefulset.yaml + - server-deployment.yaml + - tests/test-connection.yaml +tests: + - it: defaults to NodePort API and UI with internal PD + asserts: + - equal: + path: spec.type + value: NodePort + template: server-service.yaml + - equal: + path: spec.type + value: NodePort + template: hubble-service.yaml + - equal: + path: spec.type + value: ClusterIP + template: pd-service-client.yaml + - hasDocuments: + count: 0 + template: networkpolicy.yaml + - it: strict defaults are internal and isolated + set: + mode: strict + asserts: + - equal: + path: spec.type + value: ClusterIP + template: server-service.yaml + - equal: + path: spec.type + value: ClusterIP + template: hubble-service.yaml + - hasDocuments: + count: 4 + template: networkpolicy.yaml + - it: explicit false and NodePort override strict mode without acknowledgement + set: + mode: strict + networkPolicy.enabled: false + server.service.type: NodePort + hubble.enabled: false + asserts: + - equal: + path: spec.type + value: NodePort + template: server-service.yaml + - hasDocuments: + count: 0 + template: networkpolicy.yaml + - hasDocuments: + count: 0 + template: hubble-service.yaml + - it: supports cluster-specific fixed ports and zero automatic allocation + set: + server.service.nodePort: 8802 + hubble.service.nodePort: 8808 + pd.service.type: NodePort + pd.service.restNodePort: 0 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 8802 + template: server-service.yaml + - equal: + path: spec.ports[0].nodePort + value: 8808 + template: hubble-service.yaml + - it: reuses existing business images for tool containers by default + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: hugegraph/store:latest + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: hugegraph/server:latest + template: tests/test-connection.yaml + - equal: + path: spec.template.spec.initContainers[0].command[0] + value: sh + template: store-statefulset.yaml + - equal: + path: spec.containers[0].command[0] + value: sh + template: tests/test-connection.yaml + - it: mirrors component and inherited tool images without changing digest pins + set: + global.imageRegistry: registry.internal/project + store.image.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal/project/hugegraph/store@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + template: store-statefulset.yaml + - equal: + path: spec.template.spec.initContainers[0].image + value: registry.internal/project/hugegraph/store@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: registry.internal/project/hugegraph/server:latest + template: tests/test-connection.yaml + - it: qualified individual images override the mirror + set: + global.imageRegistry: registry.internal + store.image.repository: other.internal/store + server.waitImage: tools.internal/http-check:1 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: other.internal/store:latest + template: store-statefulset.yaml + - equal: + path: spec.template.spec.initContainers[0].image + value: other.internal/store:latest + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: tools.internal/http-check:1 + template: tests/test-connection.yaml + - it: adapts unspecified startup probes to the configured storage wait + set: + server.storageWaitTimeoutSeconds: 600 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 151 + template: server-deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: WAIT_STORAGE_TIMEOUT_S + value: "600" + template: server-deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "150" + template: server-deployment.yaml + + - it: reuses the component pull policy for offline tool containers + set: + store.image.pullPolicy: Never + server.image.pullPolicy: IfNotPresent + asserts: + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: Never + template: store-statefulset.yaml + - equal: + path: spec.containers[0].imagePullPolicy + value: IfNotPresent + template: tests/test-connection.yaml diff --git a/helm/hugegraph/tests/hubble_exposure_test.yaml b/helm/hugegraph/tests/hubble_exposure_test.yaml index 17109cc507..d42d5a54c8 100644 --- a/helm/hugegraph/tests/hubble_exposure_test.yaml +++ b/helm/hugegraph/tests/hubble_exposure_test.yaml @@ -15,54 +15,58 @@ # limitations under the License. # -# A non-ClusterIP Hubble Service publishes the plain-HTTP UI directly, the -# same class of exposure the PD Service and the TLS-less Ingresses already -# gate behind an explicit acknowledgement. -suite: Hubble Service exposure gate +suite: Hubble Service exposure and mode defaults templates: - - server-deployment.yaml - hubble-service.yaml tests: - # validateValues is included from the Server Deployment, so the - # rejections render that template. - - it: rejects a NodePort Hubble Service without the acknowledgement - template: server-deployment.yaml - set: - hubble.enabled: true - hubble.service.type: NodePort + - it: defaults to an automatic NodePort in easy mode asserts: - - failedTemplate: - errorPattern: "set hubble.service.allowInsecureExposure=true" + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort - - it: rejects a LoadBalancer Hubble Service without the acknowledgement - template: server-deployment.yaml + - it: defaults to ClusterIP in strict mode set: - hubble.enabled: true - hubble.service.type: LoadBalancer + mode: strict asserts: - - failedTemplate: - errorPattern: "set hubble.service.allowInsecureExposure=true" + - equal: + path: spec.type + value: ClusterIP - - it: renders a NodePort Hubble Service once acknowledged - template: hubble-service.yaml + - it: honors an explicit NodePort in strict mode without acknowledgement set: - hubble.enabled: true + mode: strict hubble.service.type: NodePort - hubble.service.allowInsecureExposure: true - hubble.service.nodePort: 30088 + hubble.service.nodePort: 8808 asserts: - equal: path: spec.type value: NodePort - equal: path: spec.ports[0].nodePort - value: 30088 + value: 8808 - - it: leaves the default ClusterIP Service alone - template: hubble-service.yaml + - it: renders LoadBalancer without acknowledgement set: - hubble.enabled: true + hubble.service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + + - it: honors an explicit ClusterIP in easy mode + set: + hubble.service.type: ClusterIP asserts: - equal: path: spec.type value: ClusterIP + + - it: treats zero NodePort as automatic allocation + set: + hubble.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort diff --git a/helm/hugegraph/tests/identity_guard_selector_test.yaml b/helm/hugegraph/tests/identity_guard_selector_test.yaml index ebe5bd8669..8c11add741 100644 --- a/helm/hugegraph/tests/identity_guard_selector_test.yaml +++ b/helm/hugegraph/tests/identity_guard_selector_test.yaml @@ -46,6 +46,9 @@ kubernetesProvider: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/component: pd spec: + volumeClaimTemplates: + - metadata: + name: pd-data replicas: "3" selector: matchLabels: @@ -54,6 +57,8 @@ kubernetesProvider: app.kubernetes.io/component: pd tests: - it: refuses the selector label change with the reason + release: + upgrade: true set: nameOverride: NAME asserts: @@ -61,6 +66,8 @@ tests: errorPattern: "changes the app.kubernetes.io/name selector label of the pd StatefulSet RELEASE-NAME-pd from RELEASE to NAME" - it: lets the same override through when the selector already matches + release: + upgrade: true set: nameOverride: RELEASE asserts: diff --git a/helm/hugegraph/tests/identity_guard_test.yaml b/helm/hugegraph/tests/identity_guard_test.yaml index a635310275..88e1c41a19 100644 --- a/helm/hugegraph/tests/identity_guard_test.yaml +++ b/helm/hugegraph/tests/identity_guard_test.yaml @@ -45,6 +45,9 @@ kubernetesProvider: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/component: pd spec: + volumeClaimTemplates: + - metadata: + name: pd-data replicas: "3" selector: matchLabels: @@ -71,6 +74,9 @@ kubernetesProvider: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/component: store spec: + volumeClaimTemplates: + - metadata: + name: store-data replicas: "3" selector: matchLabels: @@ -90,6 +96,8 @@ kubernetesProvider: containerPort: "8520" tests: - it: lets an unchanged upgrade through + release: + upgrade: true template: server-deployment.yaml asserts: - equal: @@ -97,6 +105,8 @@ tests: value: 3 - it: refuses a PD raft port change on an initialized release + release: + upgrade: true template: server-deployment.yaml set: pd.ports.raft: 8611 @@ -105,6 +115,8 @@ tests: errorPattern: "pd.ports.raft cannot change from 8610 to 8611 on an initialized release" - it: refuses a Store raft port change on an initialized release + release: + upgrade: true template: server-deployment.yaml set: store.ports.raft: 8511 @@ -113,6 +125,8 @@ tests: errorPattern: "store.ports.raft cannot change from 8510 to 8511 on an initialized release" - it: leaves the other ports routinely mutable + release: + upgrade: true template: pd-statefulset.yaml set: pd.ports.rest: 8621 @@ -123,6 +137,8 @@ tests: value: 8621 - it: refuses a rename through fullnameOverride on an initialized release + release: + upgrade: true template: server-deployment.yaml set: fullnameOverride: renamed @@ -131,6 +147,8 @@ tests: errorPattern: "already owns the pd StatefulSet RELEASE-NAME-hugegraph-pd, but the values now name it renamed-pd" - it: refuses a rename through nameOverride on an initialized release + release: + upgrade: true template: server-deployment.yaml set: nameOverride: graph @@ -139,6 +157,8 @@ tests: errorPattern: "already owns the (pd|store) StatefulSet RELEASE-NAME-hugegraph-(pd|store), but the values now name it RELEASE-NAME-graph-(pd|store)" - it: refuses a Store replica shrink against the live StatefulSet + release: + upgrade: true template: server-deployment.yaml set: store.replicas: 2 @@ -148,6 +168,8 @@ tests: errorPattern: "store.replicas cannot shrink from 3 to 2 through a helm upgrade" - it: refuses PD growth against the live StatefulSet + release: + upgrade: true template: server-deployment.yaml set: pd.replicas: 5 @@ -155,3 +177,17 @@ tests: asserts: - failedTemplate: errorPattern: "pd.replicas cannot grow from 3 to 5 through a helm upgrade" + + - it: ignores existing StatefulSet identity and membership on a fresh install + template: server-deployment.yaml + release: + upgrade: false + set: + fullnameOverride: fresh + pd.replicas: 1 + pd.ports.raft: 8611 + store.ports.raft: 8511 + asserts: + - equal: + path: metadata.name + value: fresh-server diff --git a/helm/hugegraph/tests/image_digest_test.yaml b/helm/hugegraph/tests/image_digest_test.yaml index aa2237692c..6f69ef7ddc 100644 --- a/helm/hugegraph/tests/image_digest_test.yaml +++ b/helm/hugegraph/tests/image_digest_test.yaml @@ -133,3 +133,125 @@ tests: - matchRegex: path: spec.template.spec.containers[0].image pattern: "^hugegraph/server@sha256:30f99c6b" + + - it: applies the global private registry to pd + template: pd-statefulset.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/pd:latest + + - it: lets a fully qualified pd repository override the global registry + template: pd-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + pd.image.repository: another.internal/team/pd + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/pd:latest + + - it: applies the global private registry to store + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/store:latest + + - it: lets a fully qualified store repository override the global registry + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.image.repository: another.internal/team/store + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/store:latest + + - it: applies the global private registry to server + template: server-deployment.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/server:latest + + - it: lets a fully qualified server repository override the global registry + template: server-deployment.yaml + set: + global.imageRegistry: registry.internal/mirror + server.image.repository: another.internal/team/server + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/server:latest + + - it: applies the global private registry to hubble + template: hubble-deployment.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/hubble:latest + + - it: lets a fully qualified hubble repository override the global registry + template: hubble-deployment.yaml + set: + global.imageRegistry: registry.internal/mirror + hubble.image.repository: another.internal/team/hubble + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/hubble:latest + + - it: reuses the Store image for PD readiness waiting by default + template: store-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: hugegraph/store:latest + + - it: reuses the private Store image including its digest for readiness waiting + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.image.digest: sha256:458d77a18542e8a2f7980b075f2fda32026d582200a8983921354398467ff860 + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: registry.internal/mirror/hugegraph/store@sha256:458d77a18542e8a2f7980b075f2fda32026d582200a8983921354398467ff860 + + - it: reuses the private Server image for the test hook without a curl image + template: tests/test-connection.yaml + set: + global.imageRegistry: registry.internal/mirror + asserts: + - equal: + path: spec.containers[0].image + value: registry.internal/mirror/hugegraph/server:latest + + - it: honors an explicit qualified wait image over the component and global registry + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.waitImage: tools.internal/readiness:v2 + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: tools.internal/readiness:v2 + + - it: applies the global registry to an explicitly chosen unqualified test image + template: tests/test-connection.yaml + set: + global.imageRegistry: registry.internal/mirror + server.waitImage: tools/check:v2 + asserts: + - equal: + path: spec.containers[0].image + value: registry.internal/mirror/tools/check:v2 diff --git a/helm/hugegraph/tests/native_network_options_test.yaml b/helm/hugegraph/tests/native_network_options_test.yaml new file mode 100644 index 0000000000..157c7d16b3 --- /dev/null +++ b/helm/hugegraph/tests/native_network_options_test.yaml @@ -0,0 +1,152 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Native Service options and catch-all Ingress +templates: + - server-service.yaml + - hubble-service.yaml + - pd-service-client.yaml + - server-ingress.yaml + - hubble-ingress.yaml +tests: + - it: defaults to automatic NodePorts for Server and Hubble + template: server-service.yaml + asserts: + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort + - it: enables the Hubble NodePort by default + template: hubble-service.yaml + asserts: + - equal: + path: spec.type + value: NodePort + - it: leaves PD reachable only inside the cluster by default + template: pd-service-client.yaml + asserts: + - equal: + path: spec.type + value: ClusterIP + - it: renders a valid custom NodePort below the standard cluster range + template: server-service.yaml + set: + server.service.nodePort: 8802 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 8802 + - it: omits zero NodePort for automatic allocation + template: hubble-service.yaml + set: + hubble.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort + - it: preserves explicit native load balancer settings including false + template: server-service.yaml + set: + server.service.type: LoadBalancer + server.service.externalTrafficPolicy: Local + server.service.loadBalancerClass: internal.example/controller + server.service.loadBalancerIP: 10.1.2.3 + server.service.loadBalancerSourceRanges: [10.0.0.0/8] + server.service.externalIPs: [10.1.2.4] + server.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.loadBalancerClass + value: internal.example/controller + - equal: + path: spec.loadBalancerIP + value: 10.1.2.3 + - equal: + path: spec.loadBalancerSourceRanges + value: [10.0.0.0/8] + - equal: + path: spec.externalIPs + value: [10.1.2.4] + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: applies native load balancer settings to Hubble + template: hubble-service.yaml + set: + hubble.service.type: LoadBalancer + hubble.service.externalTrafficPolicy: Local + hubble.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: applies native load balancer settings to PD + template: pd-service-client.yaml + set: + pd.service.type: LoadBalancer + pd.service.externalTrafficPolicy: Local + pd.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: renders hostless Server ingress using native catch-all semantics + template: server-ingress.yaml + set: + server.ingress.enabled: true + server.ingress.hosts: + - paths: + - path: / + pathType: Prefix + asserts: + - notExists: + path: spec.rules[0].host + - equal: + path: spec.rules[0].http.paths[0].path + value: / + - it: renders an empty host as a Hubble catch-all ingress + template: hubble-ingress.yaml + set: + hubble.ingress.enabled: true + hubble.ingress.hosts: + - host: "" + paths: + - path: / + pathType: Prefix + asserts: + - notExists: + path: spec.rules[0].host + - it: supplies a catch-all rule when ingress hosts are omitted + template: server-ingress.yaml + set: + server.ingress.enabled: true + server.ingress.hosts: [] + asserts: + - notExists: + path: spec.rules[0].host + - equal: + path: spec.rules[0].http.paths[0].backend.service.name + value: RELEASE-NAME-hugegraph-server diff --git a/helm/hugegraph/tests/networkpolicy_test.yaml b/helm/hugegraph/tests/networkpolicy_test.yaml index bd2f36a296..7f64a3ab4b 100644 --- a/helm/hugegraph/tests/networkpolicy_test.yaml +++ b/helm/hugegraph/tests/networkpolicy_test.yaml @@ -19,6 +19,21 @@ suite: NetworkPolicy templates: - networkpolicy.yaml tests: + - it: enables NetworkPolicy by strict mode defaults + set: + mode: strict + asserts: + - hasDocuments: + count: 4 + + - it: honors explicit false over the strict NetworkPolicy default + set: + mode: strict + networkPolicy.enabled: false + asserts: + - hasDocuments: + count: 0 + - it: renders nothing with the base values asserts: - hasDocuments: @@ -31,14 +46,12 @@ tests: - hasDocuments: count: 0 - - it: renders pd, store and server policies on the cluster preset + - it: renders no policy on the ordinary cluster preset values: - ../values-cluster.yaml asserts: - hasDocuments: - count: 3 - - isKind: - of: NetworkPolicy + count: 0 - it: renders pd, store and server policies on the single preset when enabled values: @@ -47,12 +60,13 @@ tests: networkPolicy.enabled: true asserts: - hasDocuments: - count: 3 + count: 4 - it: adds a Hubble policy when Hubble is enabled values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true asserts: - hasDocuments: @@ -62,6 +76,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true documentSelector: path: metadata.name @@ -138,6 +153,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true documentSelector: path: metadata.name @@ -217,6 +233,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true documentSelector: path: metadata.name @@ -277,6 +294,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true documentSelector: path: metadata.name @@ -328,6 +346,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true hubble.mode: direct asserts: @@ -365,6 +384,9 @@ tests: - it: leaves every Hubble peer out when Hubble is disabled values: - ../values-cluster.yaml + set: + hubble.enabled: false + networkPolicy.enabled: true asserts: - hasDocuments: count: 3 @@ -411,6 +433,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true pd.ports.raft: 9610 store.ports.rest: 9520 server.port: 9080 @@ -448,6 +471,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true pd.service.type: NodePort pd.service.allowInsecureExposure: true networkPolicy.pd.extraIngress: @@ -480,6 +504,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true server.service.type: LoadBalancer server.service.allowInsecureExposure: true networkPolicy.server.extraIngress: @@ -503,6 +528,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true server.ingress.enabled: true server.ingress.allowPlainHttp: true networkPolicy.server.extraIngress: @@ -542,6 +568,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true server.advertiseUrl: http://hg.example.com:8080 networkPolicy.server.extraIngress: - from: @@ -562,6 +589,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true hubble.service.type: NodePort hubble.service.allowInsecureExposure: true @@ -588,6 +616,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true pd.service.type: LoadBalancer pd.service.allowInsecureExposure: true @@ -642,6 +671,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true asserts: - hasDocuments: @@ -655,6 +685,8 @@ tests: values: - ../values-cluster.yaml set: + hubble.enabled: false + networkPolicy.enabled: true nameOverride: graphdb fullnameOverride: x asserts: @@ -686,6 +718,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true asserts: - matchRegex: @@ -702,6 +735,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true networkPolicy.pd.extraIngress: - from: - namespaceSelector: @@ -733,6 +767,8 @@ tests: values: - ../values-cluster.yaml set: + hubble.enabled: false + networkPolicy.enabled: true networkPolicy.hubble.extraIngress: - from: - namespaceSelector: @@ -757,6 +793,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true server.advertiseUrl: " " documentSelector: path: metadata.name @@ -770,6 +807,8 @@ tests: values: - ../values-cluster.yaml set: + hubble.enabled: false + networkPolicy.enabled: true networkPolicy.pd: null asserts: - hasDocuments: @@ -779,6 +818,7 @@ tests: values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true asserts: - hasDocuments: @@ -792,7 +832,7 @@ tests: documentIndex: 0 - equal: path: metadata.labels["helm.sh/chart"] - value: hugegraph-0.1.1 + value: hugegraph-0.2.0 documentIndex: 0 - equal: path: metadata.labels["app.kubernetes.io/version"] @@ -800,7 +840,7 @@ tests: documentIndex: 1 - equal: path: metadata.labels["helm.sh/chart"] - value: hugegraph-0.1.1 + value: hugegraph-0.2.0 documentIndex: 1 - equal: path: metadata.labels["app.kubernetes.io/version"] @@ -808,7 +848,7 @@ tests: documentIndex: 2 - equal: path: metadata.labels["helm.sh/chart"] - value: hugegraph-0.1.1 + value: hugegraph-0.2.0 documentIndex: 2 - equal: path: metadata.labels["app.kubernetes.io/version"] @@ -816,13 +856,14 @@ tests: documentIndex: 3 - equal: path: metadata.labels["helm.sh/chart"] - value: hugegraph-0.1.1 + value: hugegraph-0.2.0 documentIndex: 3 - it: appends extra rules verbatim values: - ../values-cluster.yaml set: + networkPolicy.enabled: true hubble.enabled: true networkPolicy.store.extraIngress: - from: @@ -915,45 +956,36 @@ tests: - failedTemplate: errorPattern: "networkPolicy[./]enabled" - - it: rejects an extraIngress rule without from + - it: accepts Kubernetes-valid an extraIngress rule without from set: networkPolicy.server.extraIngress: - ports: - port: 8080 asserts: - - failedTemplate: - errorPattern: "(missing property 'from'|from is required)" - - - it: rejects an extraIngress rule with an empty from + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an extraIngress rule with an empty from set: networkPolicy.server.extraIngress: - from: [] asserts: - - failedTemplate: - errorPattern: "(minItems|at least 1 items)" - - - it: rejects an empty extraIngress rule + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an empty extraIngress rule set: networkPolicy.pd.extraIngress: - {} asserts: - - failedTemplate: - errorPattern: "(missing property 'from'|from is required)" - - - it: rejects an empty peer + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an empty peer set: networkPolicy.pd.extraIngress: - from: - {} asserts: - - failedTemplate: - errorPattern: "(minProperties|at least 1 properties)" - - - it: rejects a Hubble egress rule without to + - notFailedTemplate: {} + - it: accepts Kubernetes-valid a Hubble egress rule without to set: networkPolicy.hubble.extraEgress: - ports: - port: 9200 asserts: - - failedTemplate: - errorPattern: "(missing property 'to'|to is required)" + - notFailedTemplate: {} diff --git a/helm/hugegraph/tests/notes_test.yaml b/helm/hugegraph/tests/notes_test.yaml index 80d7c290c1..a610fa7fa3 100644 --- a/helm/hugegraph/tests/notes_test.yaml +++ b/helm/hugegraph/tests/notes_test.yaml @@ -22,12 +22,12 @@ suite: Install notes templates: - NOTES.txt tests: - - it: says a template-only render rolls the Pods on every sync + - it: directs template-only pipelines to deterministic credential sources asserts: - matchRegexRaw: pattern: "Chart-generated credentials are in use: pd.auth, server.auth.admin, server.auth.token" - matchRegexRaw: - pattern: "every sync changes the\\s+Secret, rolls the Pods that read it" + pattern: "For GitOps/template-only pipelines use fixed credentials or existingSecret" - notMatchRegexRaw: pattern: "without rolling" diff --git a/helm/hugegraph/tests/persistence_hubble_test.yaml b/helm/hugegraph/tests/persistence_hubble_test.yaml new file mode 100644 index 0000000000..63f3850e42 --- /dev/null +++ b/helm/hugegraph/tests/persistence_hubble_test.yaml @@ -0,0 +1,111 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Hubble persistence inheritance and temporary data mount +tests: + - it: creates the seventh default persistent volume after three PD and three Store claims + template: hubble-pvc.yaml + asserts: + - hasDocuments: + count: 1 + - isKind: + of: PersistentVolumeClaim + + - it: defaults to the persistent data mount with Recreate rollout + template: hubble-deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 1 + - equal: + path: spec.strategy.type + value: Recreate + - exists: + path: spec.template.spec.volumes[0].persistentVolumeClaim + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /hubble-data + + - it: global temporary storage omits the Hubble PVC + template: hubble-pvc.yaml + set: + persistence.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: temporary storage uses the same datasource path on emptyDir + template: hubble-deployment.yaml + set: + persistence.enabled: false + asserts: + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + - notExists: + path: spec.template.spec.volumes[0].persistentVolumeClaim + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /hubble-data + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_DATASOURCE_URL + value: jdbc:h2:file:/hubble-data/db;DB_CLOSE_ON_EXIT=FALSE + + - it: explicit Hubble persistence wins over global temporary storage + template: hubble-pvc.yaml + set: + persistence.enabled: false + hubble.persistence.enabled: true + asserts: + - hasDocuments: + count: 1 + + - it: explicit Hubble temporary storage wins over global persistence + template: hubble-pvc.yaml + set: + persistence.enabled: true + hubble.persistence.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: disabled UI never creates a PVC + template: hubble-pvc.yaml + set: + hubble.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: empty storageClass inherits the Kubernetes default + template: hubble-pvc.yaml + set: + hubble.persistence.storageClassName: "" + asserts: + - notExists: + path: spec.storageClassName + + - it: dash storageClass explicitly disables the Kubernetes default + template: hubble-pvc.yaml + set: + hubble.persistence.storageClassName: "-" + asserts: + - equal: + path: spec.storageClassName + value: "" diff --git a/helm/hugegraph/tests/persistence_statefulset_test.yaml b/helm/hugegraph/tests/persistence_statefulset_test.yaml new file mode 100644 index 0000000000..850faeaa30 --- /dev/null +++ b/helm/hugegraph/tests/persistence_statefulset_test.yaml @@ -0,0 +1,116 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: PD and Store persistence inheritance +templates: + - pd-statefulset.yaml + - store-statefulset.yaml +tests: + - it: defaults to three persistent replicas per stateful component + asserts: + - equal: + path: spec.replicas + value: 3 + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + - notExists: + path: spec.template.spec.volumes + + - it: global temporary storage mounts emptyDir without PVC retention settings + set: + persistence.enabled: false + asserts: + - notExists: + path: spec.volumeClaimTemplates + - notExists: + path: spec.persistentVolumeClaimRetentionPolicy + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + + - it: explicit component persistence wins over global temporary storage + set: + persistence.enabled: false + pd.storage.enabled: true + store.storage.enabled: true + asserts: + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + - notExists: + path: spec.template.spec.volumes + + - it: explicit component temporary storage wins over global persistence + set: + persistence.enabled: true + pd.storage.enabled: false + store.storage.enabled: false + asserts: + - notExists: + path: spec.volumeClaimTemplates + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + + - it: empty storageClass inherits the Kubernetes default + set: + pd.storage.storageClassName: "" + store.storage.storageClassName: "" + asserts: + - notExists: + path: spec.volumeClaimTemplates[0].spec.storageClassName + + - it: dash storageClass explicitly disables the Kubernetes default + set: + pd.storage.storageClassName: "-" + store.storage.storageClassName: "-" + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: "" + + - it: named storageClass is preserved + set: + pd.storage.storageClassName: internal-local + store.storage.storageClassName: internal-local + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: internal-local + + - it: PD override leaves Store persistence inherited independently + template: store-statefulset.yaml + set: + persistence.enabled: true + pd.storage.enabled: false + asserts: + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + + - it: Store override leaves PD persistence inherited independently + template: pd-statefulset.yaml + set: + persistence.enabled: false + store.storage.enabled: true + asserts: + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + - notExists: + path: spec.volumeClaimTemplates diff --git a/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml b/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml new file mode 100644 index 0000000000..16a3c4182d --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml @@ -0,0 +1,100 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Legacy Hubble storage upgrade guard +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + containers: + - name: hubble +tests: + - it: explains how to preserve the volume-less legacy storage mode + release: + upgrade: true + asserts: + - failedTemplate: + errorPattern: "legacy Hubble has no data volume.*hubble.persistence.enabled=false.*--reuse-values" + + - it: accepts an explicit legacy temporary override despite global persistence + release: + upgrade: true + set: + hubble.persistence.enabled: false + asserts: + - isKind: + of: Deployment + + - it: checks an explicit persistent override even when the global flag is false + release: + upgrade: true + set: + persistence.enabled: false + pd.storage.enabled: true + store.storage.enabled: true + hubble.persistence.enabled: true + asserts: + - failedTemplate: + errorPattern: "legacy Hubble has no data volume" + + - it: does not apply the legacy live guard to fresh installations + release: + upgrade: false + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml new file mode 100644 index 0000000000..378ae5705c --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml @@ -0,0 +1,115 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Persistence upgrade guards from persistent storage +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + volumes: + - name: data + persistentVolumeClaim: + claimName: RELEASE-NAME-hugegraph-hubble-data +tests: + - it: rejects changing pd away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + pd.storage.enabled: false + asserts: + - failedTemplate: + errorPattern: "pd.storage.enabled cannot change on an initialized release" + + - it: rejects changing store away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + store.storage.enabled: false + asserts: + - failedTemplate: + errorPattern: "store.storage.enabled cannot change on an initialized release" + + - it: rejects changing hubble away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + hubble.persistence.enabled: false + asserts: + - failedTemplate: + errorPattern: "hubble.persistence.enabled cannot change on an initialized release" + + - it: accepts an unchanged persistent storage upgrade + release: + upgrade: true + set: + persistence.enabled: true + asserts: + - isKind: + of: Deployment + + - it: fresh installation skips live storage migration checks + release: + upgrade: false + set: + persistence.enabled: false + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml b/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml new file mode 100644 index 0000000000..a1242a25a5 --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml @@ -0,0 +1,108 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Persistence upgrade guards from temporary storage +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + volumes: + - name: data + emptyDir: {} +tests: + - it: rejects changing pd away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + pd.storage.enabled: true + asserts: + - failedTemplate: + errorPattern: "pd.storage.enabled cannot change on an initialized release" + + - it: rejects changing store away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + store.storage.enabled: true + asserts: + - failedTemplate: + errorPattern: "store.storage.enabled cannot change on an initialized release" + + - it: rejects changing hubble away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + hubble.persistence.enabled: true + asserts: + - failedTemplate: + errorPattern: "hubble.persistence.enabled cannot change on an initialized release" + + - it: accepts an unchanged temporary storage upgrade + release: + upgrade: true + set: + persistence.enabled: false + asserts: + - isKind: + of: Deployment + + - it: fresh installation skips live storage migration checks + release: + upgrade: false + set: + persistence.enabled: true + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/practical_notes_test.yaml b/helm/hugegraph/tests/practical_notes_test.yaml new file mode 100644 index 0000000000..5ed67fd888 --- /dev/null +++ b/helm/hugegraph/tests/practical_notes_test.yaml @@ -0,0 +1,133 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Practical access notes +templates: + - NOTES.txt +tests: + - it: watches one resource type per command and links current operations docs + asserts: + - matchRegexRaw: + pattern: "kubectl get pods -n NAMESPACE -l app.kubernetes.io/instance=RELEASE-NAME -w" + - matchRegexRaw: + pattern: "kubectl get pvc -n NAMESPACE -w" + - notMatchRegexRaw: + pattern: "kubectl get pods,pvc.*-w" + - notMatchRegexRaw: + pattern: "see README" + - matchRegexRaw: + pattern: "helm/hugegraph/docs/operations.md#credentials" + - it: reports automatic NodePort discovery and seven claims + asserts: + - matchRegexRaw: + pattern: "needs 7 persistent volume claim" + - matchRegexRaw: + pattern: "kubectl get nodes -o wide" + - matchRegexRaw: + pattern: "http://:" + - notMatchRegexRaw: + pattern: "never expose" + - it: explains ClusterIP for strict mode + set: + mode: strict + asserts: + - matchRegexRaw: + pattern: "ClusterIP is reachable inside the cluster" + - it: shows LoadBalancer status discovery + set: + server.service.type: LoadBalancer + asserts: + - matchRegexRaw: + pattern: "status.loadBalancer.ingress" + - it: reports zero Hubble persistence after disabling the UI + set: + hubble.enabled: false + asserts: + - matchRegexRaw: + pattern: "needs 6 persistent volume claim" + - notMatchRegexRaw: + pattern: "Hubble 1 replica" + - it: uses HTTP for an ingress without TLS + set: + server.ingress.enabled: true + server.ingress.hosts: [{host: internal.example, paths: [{path: /, pathType: Prefix}]}] + asserts: + - matchRegexRaw: + pattern: "http://internal.example/" + - notMatchRegexRaw: + pattern: "https://internal.example/" + - it: uses HTTPS for the configured TLS host + set: + server.ingress.enabled: true + server.ingress.hosts: [{host: internal.example, paths: [{path: /, pathType: Prefix}]}] + server.ingress.tls: [{hosts: [internal.example], secretName: example-tls}] + asserts: + - matchRegexRaw: + pattern: "https://internal.example/" + - it: names a strict NodePort entry lacking an external policy rule without failing + set: + mode: strict + server.service.type: NodePort + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + - matchRegexRaw: + pattern: "Add networkPolicy.server.extraIngress" + + - it: names a strict hostless Ingress lacking an external policy rule + set: + mode: strict + server.ingress.enabled: true + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + + - it: accepts native allow-all rules without the missing-rule hint + set: + mode: strict + server.service.type: NodePort + server.ingress.enabled: true + networkPolicy.server.extraIngress: [{}] + asserts: + - notMatchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + + - it: does not show policy advice when an explicit override disables policies + set: + mode: strict + server.service.type: NodePort + networkPolicy.enabled: false + asserts: + - notMatchRegexRaw: + pattern: "NetworkPolicy has no external ingress rule" + + - it: names the outside discovery egress setting for strict Hubble + set: + mode: strict + server.advertiseUrl: http://external.example:8080 + asserts: + - matchRegexRaw: + pattern: "also add networkPolicy.hubble.extraEgress" + - it: names a strict ClusterIP with externalIPs lacking an external policy rule + set: + mode: strict + server.service.externalIPs: [192.0.2.10] + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + - matchRegexRaw: + pattern: "allows all sources and ports" diff --git a/helm/hugegraph/tests/server_exposure_test.yaml b/helm/hugegraph/tests/server_exposure_test.yaml index 8563ff01ed..61bd755edf 100644 --- a/helm/hugegraph/tests/server_exposure_test.yaml +++ b/helm/hugegraph/tests/server_exposure_test.yaml @@ -15,59 +15,58 @@ # limitations under the License. # -# A non-ClusterIP Server Service publishes the plain-HTTP Server API, and -# with it Basic-auth credentials and JWTs in cleartext, the same class of -# exposure the PD and Hubble Services and the TLS-less Ingresses already gate -# behind an explicit acknowledgement. -suite: Server Service exposure gate +suite: Server Service exposure and mode defaults templates: - - server-deployment.yaml - server-service.yaml tests: - # validateValues is included from the Server Deployment, so the - # rejections render that template. - - it: rejects a NodePort Server Service without the acknowledgement - template: server-deployment.yaml - set: - server.service.type: NodePort + - it: defaults to an automatic NodePort in easy mode asserts: - - failedTemplate: - errorPattern: "set server.service.allowInsecureExposure=true" - - - it: rejects a LoadBalancer Server Service without the acknowledgement - template: server-deployment.yaml - set: - server.service.type: LoadBalancer - asserts: - - failedTemplate: - errorPattern: "set server.service.allowInsecureExposure=true" + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort - - it: rejects it with authentication off too - template: server-deployment.yaml + - it: defaults to ClusterIP in strict mode set: - server.auth.enabled: false - server.service.type: NodePort + mode: strict asserts: - - failedTemplate: - errorPattern: "set server.service.allowInsecureExposure=true" + - equal: + path: spec.type + value: ClusterIP - - it: renders a NodePort Server Service once acknowledged - template: server-service.yaml + - it: honors an explicit NodePort in strict mode without acknowledgement set: + mode: strict server.service.type: NodePort - server.service.allowInsecureExposure: true - server.service.nodePort: 30080 + server.service.nodePort: 8802 asserts: - equal: path: spec.type value: NodePort - equal: path: spec.ports[0].nodePort - value: 30080 + value: 8802 + + - it: renders LoadBalancer without acknowledgement + set: + server.service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer - - it: keeps the default ClusterIP Service without the acknowledgement - template: server-service.yaml + - it: honors an explicit ClusterIP in easy mode + set: + server.service.type: ClusterIP asserts: - equal: path: spec.type value: ClusterIP + + - it: treats zero NodePort as automatic allocation + set: + server.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort diff --git a/helm/hugegraph/tests/server_startup_timeout_test.yaml b/helm/hugegraph/tests/server_startup_timeout_test.yaml index 7746336c48..e7cafde287 100644 --- a/helm/hugegraph/tests/server_startup_timeout_test.yaml +++ b/helm/hugegraph/tests/server_startup_timeout_test.yaml @@ -15,12 +15,17 @@ # limitations under the License. # -suite: Server startup timeout tracks the startup probe budget +suite: Server startup timeout and explicit probe budgets templates: - server-deployment.yaml tests: - - it: passes the derived 150 second start budget to the image + - it: derives a 450 second probe budget for the default 300 second storage wait asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: WAIT_STORAGE_TIMEOUT_S + value: "300" - contains: path: spec.template.spec.containers[0].env content: @@ -29,99 +34,73 @@ tests: - equal: path: spec.template.spec.containers[0].startupProbe.failureThreshold value: 91 - - equal: - path: spec.template.spec.containers[0].startupProbe.periodSeconds - value: 5 - - it: follows a raised startup probe budget on the guaranteed timeline + - it: coordinates an unspecified probe threshold with an extended storage wait set: - server.probes.startup.failureThreshold: 200 - server.probes.startup.periodSeconds: 10 + server.storageWaitTimeoutSeconds: 600 asserts: - contains: path: spec.template.spec.containers[0].env content: - name: HG_SERVER_STARTUP_TIMEOUT_S - value: "1690" + name: WAIT_STORAGE_TIMEOUT_S + value: "600" - equal: path: spec.template.spec.containers[0].startupProbe.failureThreshold - value: 200 + value: 151 - - it: follows the 450 second probe floor when a lower failureThreshold is configured + - it: retains an explicit sufficient startup threshold set: - server.probes.startup.failureThreshold: 1 - server.probes.startup.periodSeconds: 5 + server.probes.startup.failureThreshold: 200 + server.probes.startup.periodSeconds: 10 asserts: - contains: path: spec.template.spec.containers[0].env content: name: HG_SERVER_STARTUP_TIMEOUT_S - value: "150" + value: "1690" - equal: path: spec.template.spec.containers[0].startupProbe.failureThreshold - value: 91 + value: 200 - - it: holds the floor when the period is large and the threshold low + - it: refuses a conflicting explicit threshold instead of silently increasing it set: server.probes.startup.failureThreshold: 1 - server.probes.startup.periodSeconds: 300 + server.probes.startup.periodSeconds: 5 asserts: - # First probe can fail at t=0, so 3 failures guarantee (3-1)*300=600s - # alive; the image gets 600-300=300s, inside the guaranteed window. - - equal: - path: spec.template.spec.containers[0].startupProbe.failureThreshold - value: 3 - - contains: - path: spec.template.spec.containers[0].env - content: - name: HG_SERVER_STARTUP_TIMEOUT_S - value: "300" + - failedTemplate: + errorPattern: "server.probes.startup" - - it: needs two failures even when one period covers the whole floor + - it: accepts the minimum 120 second Server startup budget after storage waiting set: - server.probes.startup.failureThreshold: 1 - server.probes.startup.periodSeconds: 450 + server.probes.startup.failureThreshold: 85 + server.probes.startup.periodSeconds: 5 asserts: - equal: path: spec.template.spec.containers[0].startupProbe.failureThreshold - value: 2 + value: 85 - contains: path: spec.template.spec.containers[0].env content: name: HG_SERVER_STARTUP_TIMEOUT_S - value: "150" + value: "120" - - it: rounds the floor up when the period does not divide it + - it: derives and rounds the unspecified threshold for a large probe period set: - server.probes.startup.failureThreshold: 1 server.probes.startup.periodSeconds: 449 asserts: - equal: path: spec.template.spec.containers[0].startupProbe.failureThreshold value: 3 - - contains: - path: spec.template.spec.containers[0].env - content: - name: HG_SERVER_STARTUP_TIMEOUT_S - value: "598" - - it: leaves an initialDelay to lengthen the timeline without shrinking budgets + - it: keeps an explicitly configured initial delay set: server.probes.startup.initialDelaySeconds: 600 asserts: - equal: path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds value: 600 - - equal: - path: spec.template.spec.containers[0].startupProbe.failureThreshold - value: 91 - - contains: - path: spec.template.spec.containers[0].env - content: - name: HG_SERVER_STARTUP_TIMEOUT_S - value: "150" - - it: caps at the entrypoint's 86400 second maximum + - it: caps the image start budget at the entrypoint maximum without rewriting the threshold set: server.probes.startup.failureThreshold: 100000 server.probes.startup.periodSeconds: 10 @@ -131,8 +110,11 @@ tests: content: name: HG_SERVER_STARTUP_TIMEOUT_S value: "86400" + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 100000 - - it: refuses an extraEnv override that would silently diverge from the probe + - it: refuses an extraEnv override that diverges from the startup probe set: server.extraEnv: - name: HG_SERVER_STARTUP_TIMEOUT_S @@ -140,3 +122,26 @@ tests: asserts: - failedTemplate: errorMessage: server.extraEnv must not set the chart-managed variable HG_SERVER_STARTUP_TIMEOUT_S + + - it: refuses a storage wait override through chart-owned environment variables + set: + server.extraEnv: + - name: WAIT_STORAGE_TIMEOUT_S + value: "60" + asserts: + - failedTemplate: + errorPattern: "server.extraEnv must not set the chart-managed variable WAIT_STORAGE_TIMEOUT_S" + + - it: accepts an explicit delayed first probe with a sufficient total budget + set: + server.probes.startup.initialDelaySeconds: 600 + server.probes.startup.failureThreshold: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 1 + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "300" diff --git a/helm/hugegraph/tests/test_hook_gremlin_test.yaml b/helm/hugegraph/tests/test_hook_gremlin_test.yaml index acd41bf25b..c35ef51de8 100644 --- a/helm/hugegraph/tests/test_hook_gremlin_test.yaml +++ b/helm/hugegraph/tests/test_hook_gremlin_test.yaml @@ -74,6 +74,7 @@ tests: - it: sends no credentials when authentication is disabled template: tests/test-connection.yaml set: + hubble.enabled: false server.auth.enabled: false asserts: - notMatchRegex: diff --git a/helm/hugegraph/tests/upgrade_entry_notice_test.yaml b/helm/hugegraph/tests/upgrade_entry_notice_test.yaml new file mode 100644 index 0000000000..aeb5b31556 --- /dev/null +++ b/helm/hugegraph/tests/upgrade_entry_notice_test.yaml @@ -0,0 +1,90 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: Upgrade entry default notice +templates: + - NOTES.txt +tests: + - it: reports both inherited NodePort entries during an easy upgrade + release: + upgrade: true + asserts: + - matchRegexRaw: + pattern: "Upgrade notice: chart 0.2.0 changed entry defaults" + - matchRegexRaw: + pattern: "inherits NodePort for Server, Hubble" + - matchRegexRaw: + pattern: "docs/operations.md#storage-and-upgrades" + + - it: keeps fresh-install notes free of an upgrade notice + release: + upgrade: false + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: omits the disabled Hubble entry from the upgrade notice + release: + upgrade: true + set: + hubble.enabled: false + asserts: + - matchRegexRaw: + pattern: "inherits NodePort for Server\\." + - notMatchRegexRaw: + pattern: "inherits NodePort for Server, Hubble" + + - it: only names the inherited Hubble entry with an explicit internal Server + release: + upgrade: true + set: + server.service.type: ClusterIP + asserts: + - matchRegexRaw: + pattern: "inherits NodePort for Hubble" + - notMatchRegexRaw: + pattern: "inherits NodePort for Server" + + - it: accepts explicit NodePort entries without the inherited-default notice + release: + upgrade: true + set: + server.service.type: NodePort + hubble.service.type: NodePort + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: preserves explicit easy-mode internal entries without the notice + release: + upgrade: true + set: + server.service.type: ClusterIP + hubble.service.type: ClusterIP + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: does not report an easy-default change for a strict-mode upgrade + release: + upgrade: true + set: + mode: strict + server.service.type: NodePort + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" diff --git a/helm/hugegraph/tests/validate_values_test.yaml b/helm/hugegraph/tests/validate_values_test.yaml index d5b7a30ff1..7f83a4779d 100644 --- a/helm/hugegraph/tests/validate_values_test.yaml +++ b/helm/hugegraph/tests/validate_values_test.yaml @@ -56,113 +56,89 @@ tests: - failedTemplate: errorPattern: "must not set the chart-managed variable JAVA_OPTIONS" - - it: rejects a PD NodePort under NetworkPolicy without pd extraIngress + - it: accepts a PD NodePort under NetworkPolicy without pd extraIngress set: networkPolicy.enabled: true pd.service.type: NodePort pd.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the pd exposure" - - - it: rejects a PD LoadBalancer under NetworkPolicy without pd extraIngress + - notFailedTemplate: {} + - it: accepts a PD LoadBalancer under NetworkPolicy without pd extraIngress set: networkPolicy.enabled: true pd.service.type: LoadBalancer pd.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the pd exposure" - - - it: rejects a Server NodePort under NetworkPolicy without server extraIngress + - notFailedTemplate: {} + - it: accepts a Server NodePort under NetworkPolicy without server extraIngress set: networkPolicy.enabled: true server.service.type: NodePort server.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the server exposure" - - - it: rejects a Server LoadBalancer under NetworkPolicy without server extraIngress + - notFailedTemplate: {} + - it: accepts a Server LoadBalancer under NetworkPolicy without server extraIngress set: networkPolicy.enabled: true server.service.type: LoadBalancer server.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the server exposure" - - - it: rejects a Server Ingress under NetworkPolicy without server extraIngress + - notFailedTemplate: {} + - it: accepts a Server Ingress under NetworkPolicy without server extraIngress set: networkPolicy.enabled: true server.ingress.enabled: true server.ingress.allowPlainHttp: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the server exposure" - - - it: rejects a Server advertiseUrl under NetworkPolicy without server extraIngress + - notFailedTemplate: {} + - it: accepts a Server advertiseUrl under NetworkPolicy without server extraIngress set: networkPolicy.enabled: true server.advertiseUrl: http://hg.example.com:8080 asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the server exposure" - - - it: rejects a Hubble NodePort under NetworkPolicy without hubble extraIngress + - notFailedTemplate: {} + - it: accepts a Hubble NodePort under NetworkPolicy without hubble extraIngress set: networkPolicy.enabled: true hubble.enabled: true hubble.service.type: NodePort hubble.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the hubble exposure" - - - it: rejects a Hubble LoadBalancer under NetworkPolicy without hubble extraIngress + - notFailedTemplate: {} + - it: accepts a Hubble LoadBalancer under NetworkPolicy without hubble extraIngress set: networkPolicy.enabled: true hubble.enabled: true hubble.service.type: LoadBalancer hubble.service.allowInsecureExposure: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the hubble exposure" - - - it: rejects a Hubble Ingress under NetworkPolicy without hubble extraIngress + - notFailedTemplate: {} + - it: accepts a Hubble Ingress under NetworkPolicy without hubble extraIngress set: networkPolicy.enabled: true hubble.enabled: true hubble.ingress.enabled: true hubble.ingress.allowPlainHttp: true asserts: - - failedTemplate: - errorPattern: "admits nothing from outside the release, so the hubble exposure" - - - it: names advertiseUrl in the Server exposure message only + - notFailedTemplate: {} + - it: accepts an advertised Server URL under explicit NetworkPolicy set: networkPolicy.enabled: true server.advertiseUrl: http://hg.example.com:8080 asserts: - - failedTemplate: - errorPattern: "server exposure \\(NodePort/LoadBalancer Service, Ingress, server.advertiseUrl\\)" - - - it: reports the Server acknowledgement before the NetworkPolicy exposure check + - notFailedTemplate: {} + - it: accepts Server exposure without an acknowledgement or source confirmation set: networkPolicy.enabled: true server.service.type: NodePort asserts: - - failedTemplate: - errorPattern: "server.service.allowInsecureExposure=true" - - - it: reports the PD acknowledgement before the NetworkPolicy exposure check + - notFailedTemplate: {} + - it: accepts PD exposure without an acknowledgement or source confirmation set: networkPolicy.enabled: true pd.service.type: NodePort asserts: - - failedTemplate: - errorPattern: "pd.service.allowInsecureExposure=true" - + - notFailedTemplate: {} - it: leaves exposure alone while NetworkPolicy is off set: server.service.type: LoadBalancer @@ -188,14 +164,12 @@ tests: - failedTemplate: errorPattern: "hubble.enabled requires server.auth" - - it: rejects a plain-HTTP Hubble Ingress + - it: accepts a plain-HTTP Hubble Ingress set: hubble.ingress.enabled: true hubble.enabled: true asserts: - - failedTemplate: - errorPattern: "publishes the plain-HTTP, unauthenticated Hubble UI" - + - notFailedTemplate: {} - it: rejects an empty Hubble image tag set: hubble.image.tag: "" @@ -219,20 +193,16 @@ tests: - failedTemplate: errorPattern: "store.podLabels must not set app.kubernetes.io/component" - - it: rejects a TLS-less Server Ingress without the plain-HTTP opt-in + - it: accepts a TLS-less Server Ingress without the plain-HTTP opt-in set: server.ingress.enabled: true asserts: - - failedTemplate: - errorPattern: "server.ingress.enabled without tls" - - - it: rejects a non-ClusterIP PD Service without the exposure acknowledgement + - notFailedTemplate: {} + - it: accepts a non-ClusterIP PD Service without the exposure acknowledgement set: pd.service.type: NodePort asserts: - - failedTemplate: - errorPattern: "pd.service.type NodePort or LoadBalancer exposes" - + - notFailedTemplate: {} - it: rejects a read-only root filesystem on Hubble set: hubble.enabled: true @@ -264,7 +234,7 @@ tests: - failedTemplate: errorPattern: "pd.podAnnotations must not set checksum/pd-auth" - - it: rejects an advertised Server URL that Hubble's egress policy would block + - it: accepts an advertised Server URL that Hubble's egress policy would block set: networkPolicy.enabled: true hubble.enabled: true @@ -274,9 +244,7 @@ tests: - ipBlock: cidr: 10.0.0.0/8 asserts: - - failedTemplate: - errorPattern: "networkPolicy.hubble.extraEgress" - + - notFailedTemplate: {} - it: accepts an advertised Server URL once Hubble egress names it set: networkPolicy.enabled: true diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml index dd6bfdcb20..c9b39bd3d9 100644 --- a/helm/hugegraph/values-cluster.yaml +++ b/helm/hugegraph/values-cluster.yaml @@ -15,9 +15,11 @@ # limitations under the License. # -# Production starting point: 3 Server + 3 PD + 3 Store. +# Multi-node starting point: 3 Server + 3 PD + 3 Store. # Size this profile again for the real graph, traffic, and failure budget. # Usage: helm install hg ./helm/hugegraph -f values-cluster.yaml +# Entry and network policy inherit mode (easy by default). +# Add --set mode=strict for ClusterIP and NetworkPolicy, or override either explicitly. pd: replicas: 3 @@ -33,7 +35,7 @@ pd: limits: cpu: "2" memory: 2Gi - # Pinned to "required": production must not co-locate PD quorum members. + # Keep PD quorum members on separate nodes. antiAffinity: required pdb: enabled: true @@ -55,20 +57,9 @@ store: -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=512m -XX:+UseContainerSupport - # The Store commits far more than its heap. conf/application-pd.yml, which - # the shipped conf/application.yml includes as a Spring profile, sets - # rocksdb.total_memory_size to 32000000000, and RaftRocksdbOptions splits - # that value into a RocksDB write cache and block cache, so those native - # caches are bounded by 32 GB rather than by the container. On top of that - # the jraft log storage registers its own 1 GiB LRU block cache once per - # process, plus heap, direct memory, metaspace and about a thousand threads. - # A 4Gi limit was below the steady state and OOM-killed all three Stores - # after roughly 1 GB of data; 8Gi holds, with 4.42 GiB anonymous RSS - # measured on k3s (2026-09-19). The limit bounds the damage, it does not - # bound RocksDB: the chart cannot set rocksdb.total_memory_size today, - # because the Store entrypoint rebuilds SPRING_APPLICATION_JSON from its - # own variables and no conf file is mounted. Raise both numbers together - # with the data size, or lower the RocksDB budget in a custom image. + # Container limits do not configure JVM heap or RocksDB native cache budgets. + # Size this preset for the workload and configure those application budgets + # separately; javaOpts accepts explicit JVM/system-property overrides. resources: requests: cpu: "1" @@ -121,10 +112,8 @@ server: token: autoGenerate: true -# The optional Hubble UI is not enabled here: authentication is already on -# by default, so turn Hubble on with --set hubble.enabled=true (or the -# snippet below) when the browser UI is wanted. Current Hubble images still -# refuse to render if server.auth is explicitly disabled. +# Hubble is enabled by default. Disable with --set hubble.enabled=false. +# The optional resource override below remains available when needed. # # hubble: # enabled: true @@ -139,7 +128,4 @@ server: # cpu: "1" # memory: 1536Mi -# Production-shaped installs isolate the release; list outside clients in -# networkPolicy..extraIngress (see README, NetworkPolicy). -networkPolicy: - enabled: true +# Network isolation is selected by mode or an explicit enabled override. diff --git a/helm/hugegraph/values-strict.yaml b/helm/hugegraph/values-strict.yaml new file mode 100644 index 0000000000..576f79c3bd --- /dev/null +++ b/helm/hugegraph/values-strict.yaml @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Optional security defaults. Component values and --set override this mode. +mode: strict diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index afce158aa8..409a8bdda0 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -30,31 +30,47 @@ "$ref": "#/definitions/server" }, "global": { - "type": "object" + "type": "object", + "properties": { + "imageRegistry": { + "type": "string" + } + } }, "hubble": { "$ref": "#/definitions/hubble" }, "networkPolicy": { "$ref": "#/definitions/networkPolicy" + }, + "mode": { + "type": "string", + "enum": [ + "easy", + "strict" + ] + }, + "persistence": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + } } }, "definitions": { "networkPolicyPeers": { "type": "array", - "minItems": 1, "items": { - "type": "object", - "minProperties": 1 + "type": "object" } }, "networkPolicyIngressRules": { "type": "array", "items": { "type": "object", - "required": [ - "from" - ], "properties": { "from": { "$ref": "#/definitions/networkPolicyPeers" @@ -66,9 +82,6 @@ "type": "array", "items": { "type": "object", - "required": [ - "to" - ], "properties": { "to": { "$ref": "#/definitions/networkPolicyPeers" @@ -90,7 +103,10 @@ "additionalProperties": false, "properties": { "enabled": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] }, "pd": { "$ref": "#/definitions/networkPolicyComponent" @@ -280,6 +296,12 @@ }, "storageClassName": { "type": "string" + }, + "enabled": { + "type": [ + "boolean", + "null" + ] } } }, @@ -341,8 +363,16 @@ "properties": { "startup": { "allOf": [ - { "$ref": "#/definitions/probe" }, - { "properties": { "successThreshold": { "maximum": 1 } } } + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } ] }, "readiness": { @@ -350,8 +380,16 @@ }, "liveness": { "allOf": [ - { "$ref": "#/definitions/probe" }, - { "properties": { "successThreshold": { "maximum": 1 } } } + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } ] } } @@ -584,23 +622,51 @@ }, "allowInsecureExposure": { "type": "boolean", - "description": "Required acknowledgement for a non-ClusterIP PD Service: the exposed gRPC port has no authentication." + "description": "Legacy compatibility field; does not gate Service exposure." }, "restNodePort": { "type": [ "integer", "null" ], - "minimum": 30000, - "maximum": 32767 + "minimum": 0, + "maximum": 65535 }, "grpcNodePort": { "type": [ "integer", "null" ], - "minimum": 30000, - "maximum": 32767 + "minimum": 0, + "maximum": 65535 + }, + "externalTrafficPolicy": { + "type": "string", + "enum": [ + "Cluster", + "Local" + ] + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerIP": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array", + "items": { + "type": "string" + } + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "allocateLoadBalancerNodePorts": { + "type": "boolean" } } }, @@ -668,8 +734,7 @@ "$ref": "#/definitions/pdb" }, "waitImage": { - "type": "string", - "minLength": 1 + "type": "string" }, "waitResources": { "$ref": "#/definitions/resources" @@ -786,7 +851,6 @@ }, "waitImage": { "type": "string", - "minLength": 1, "description": "Image used by the Helm test hook" }, "waitResources": { @@ -820,7 +884,6 @@ "properties": { "password": { "type": "string", - "pattern": "^[\\x21-\\x39\\x3b-\\x5b\\x5d-\\x7e]*$", "description": "Inline admin password. Empty defers to existingSecret or autoGenerate. Printable ASCII with no space, no backslash and no colon, the same contract the Server wrapper enforces on an existingSecret: the Server decodes Basic-auth credentials as ASCII and splits them on every colon, so a non-ASCII password would be applied to the admin account and then answer 401 on every request, and a password with a colon would answer 400; the image entrypoint writes each space into rest-server.properties as a backslash-escaped space, which the Server keeps, and Commons Configuration trims a properties value and unescapes backslashes, so a space or a backslash would make the stored password and the effective one differ." }, "existingSecret": { @@ -847,15 +910,7 @@ "properties": { "value": { "type": "string", - "description": "JWT signing key. Either empty, which defers to existingSecret or autoGenerate, or at least 32 characters: the Server entrypoint rejects a shorter key and the pods CrashLoop.", - "anyOf": [ - { - "maxLength": 0 - }, - { - "minLength": 32 - } - ] + "description": "JWT signing key. Either empty, which defers to existingSecret or autoGenerate, or at least 32 characters: the Server entrypoint rejects a shorter key and the pods CrashLoop." }, "existingSecret": { "type": "string" @@ -910,6 +965,27 @@ } } } + ], + "allOf": [ + { + "if": { + "properties": { + "existingSecret": { + "const": "" + } + }, + "required": [ + "existingSecret" + ] + }, + "then": { + "properties": { + "password": { + "pattern": "^[\\x21-\\x39\\x3b-\\x5b\\x5d-\\x7e]*$" + } + } + } + } ] }, "token": { @@ -941,40 +1017,35 @@ } } } - ] - } - } - } - }, - { - "if": { - "properties": { - "enabled": { - "const": false - } - } - }, - "then": { - "properties": { - "admin": { - "properties": { - "existingSecret": { - "const": "" - }, - "password": { - "const": "" - } - } - }, - "token": { - "properties": { - "existingSecret": { - "const": "" - }, - "value": { - "const": "" + ], + "allOf": [ + { + "if": { + "properties": { + "existingSecret": { + "const": "" + } + }, + "required": [ + "existingSecret" + ] + }, + "then": { + "properties": { + "value": { + "anyOf": [ + { + "maxLength": 0 + }, + { + "minLength": 32 + } + ] + } + } + } } - } + ] } } } @@ -1018,7 +1089,7 @@ "description": "HTTP path for the Server readinessProbe; /readiness (apache/hugegraph#3212) on Server images that serve it, /versions (default) on older ones" }, "probes": { - "$ref": "#/definitions/probes" + "$ref": "#/definitions/serverProbes" }, "antiAffinity": { "type": "string", @@ -1091,6 +1162,11 @@ "advertiseUrl": { "type": "string", "description": "URL registered with PD via server.urls_to_pd. Empty uses the in-cluster Server Service URL. Set to an externally reachable URL for outside Hubble PD discovery." + }, + "storageWaitTimeoutSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400 } } }, @@ -1099,11 +1175,15 @@ "additionalProperties": false, "properties": { "type": { - "type": "string", + "type": [ + "string", + "null" + ], "enum": [ "ClusterIP", "NodePort", - "LoadBalancer" + "LoadBalancer", + null ] }, "annotations": { @@ -1111,15 +1191,43 @@ }, "allowInsecureExposure": { "type": "boolean", - "description": "Required acknowledgement for a non-ClusterIP Service: the exposed Server API or Hubble UI is plain HTTP, so credentials and tokens cross the network in cleartext, and Hubble has no login of its own." + "description": "Legacy compatibility field; does not gate Service exposure." }, "nodePort": { "type": [ "integer", "null" ], - "minimum": 30000, - "maximum": 32767 + "minimum": 0, + "maximum": 65535 + }, + "externalTrafficPolicy": { + "type": "string", + "enum": [ + "Cluster", + "Local" + ] + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerIP": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array", + "items": { + "type": "string" + } + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "allocateLoadBalancerNodePorts": { + "type": "boolean" } } }, @@ -1145,13 +1253,11 @@ "type": "object", "additionalProperties": false, "required": [ - "host", "paths" ], "properties": { "host": { - "type": "string", - "minLength": 1 + "type": "string" }, "paths": { "type": "array", @@ -1190,7 +1296,8 @@ "type": "object" }, "allowPlainHttp": { - "type": "boolean" + "type": "boolean", + "description": "Legacy compatibility field; HTTP Ingress needs no acknowledgement." } } }, @@ -1216,13 +1323,11 @@ "type": "object", "additionalProperties": false, "required": [ - "host", "paths" ], "properties": { "host": { - "type": "string", - "minLength": 1 + "type": "string" }, "paths": { "type": "array", @@ -1262,7 +1367,7 @@ }, "allowPlainHttp": { "type": "boolean", - "description": "Explicit opt-in to a TLS-less Server Ingress; without it the render fails when tls is empty." + "description": "Legacy compatibility field; HTTP Ingress needs no acknowledgement." } } }, @@ -1306,13 +1411,15 @@ "type": "object", "additionalProperties": false, "required": [ - "enabled", "size", "storageClassName" ], "properties": { "enabled": { - "type": "boolean" + "type": [ + "boolean", + "null" + ] }, "size": { "type": "string", @@ -1387,6 +1494,80 @@ "$ref": "#/definitions/probes" } } + }, + "serverStartupProbe": { + "type": "object", + "additionalProperties": false, + "required": [ + "periodSeconds" + ], + "properties": { + "periodSeconds": { + "type": "integer", + "minimum": 1 + }, + "failureThreshold": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 0 + }, + "successThreshold": { + "type": "integer", + "minimum": 1 + } + } + }, + "serverProbes": { + "type": "object", + "additionalProperties": false, + "required": [ + "startup", + "readiness", + "liveness" + ], + "properties": { + "startup": { + "allOf": [ + { + "$ref": "#/definitions/serverStartupProbe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + }, + "readiness": { + "$ref": "#/definitions/probe" + }, + "liveness": { + "allOf": [ + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + } + } } } } diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 6205e17396..daa4df2f3c 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -18,6 +18,14 @@ # Default values for HugeGraph HStore Helm chart. # See values-single.yaml and values-cluster.yaml for presets. +# easy: browser/API NodePorts; strict: internal Services and NetworkPolicy. +# Explicit component parameters always override these mode defaults. +mode: easy +global: + imageRegistry: "" +persistence: + enabled: true + fullnameOverride: "" nameOverride: "" @@ -85,6 +93,8 @@ pd: # When existingSecret and value are empty, create a kept release-pd-auth Secret. autoGenerate: true storage: + # Unset inherits persistence.enabled; false uses temporary emptyDir. + enabled: size: 10Gi storageClassName: "" resources: {} @@ -125,10 +135,7 @@ pd: service: type: ClusterIP annotations: {} - # A non-ClusterIP type exposes PD's gRPC port, which has no - # authentication (raft membership RPCs included). The render refuses it - # unless this acknowledgement is set and reachability is restricted by - # other means (NetworkPolicy, load balancer allowlist, firewall). + # Legacy acknowledgement, accepted for compatibility; no render gate. allowInsecureExposure: false # Optional fixed NodePorts; require service.type NodePort or LoadBalancer. restNodePort: @@ -205,6 +212,8 @@ store: rest: 8520 dataPath: /hugegraph-store/storage storage: + # Unset inherits persistence.enabled; false uses temporary emptyDir. + enabled: size: 50Gi storageClassName: "" resources: {} @@ -240,7 +249,8 @@ store: pdb: enabled: true minAvailable: 2 - waitImage: curlimages/curl:8.5.0 + # Optional tool image override; empty reuses the Store image. + waitImage: "" # HTTP path the init container polls on every PD peer; a majority must # answer 2xx before the Store starts. /v1/ready makes that majority a # raft quorum instead of a set of live listeners. @@ -329,8 +339,8 @@ server: pdb: enabled: false minAvailable: 2 - # Image used by the Helm test hook. - waitImage: curlimages/curl:8.5.0 + # Optional Helm test image override; empty reuses the Server image. + waitImage: "" # Resources for the Helm test hook container. Bounded by default so the hook # cannot run unlimited on a restricted or quota-managed namespace. testResources: @@ -344,6 +354,8 @@ server: # concurrent Server replicas never initialize the same backend. No # HG_SERVER_SKIP_INIT and no init Job. initStoreEnabled: false + # Maximum storage wait; startup probe defaults adapt to this value. + storageWaitTimeoutSeconds: 300 auth: # On by default: a chart-managed release-admin Secret supplies the password # unless admin.existingSecret or admin.password is set. Set enabled=false @@ -375,13 +387,10 @@ server: # Set this to a URL reachable from outside the cluster (NodePort, LoadBalancer, or Ingress) when an external Hubble uses PD mode; all replicas then announce that shared logical endpoint. advertiseUrl: "" service: - type: ClusterIP + # Unset inherits mode (easy: NodePort, strict: ClusterIP). + type: annotations: {} - # A non-ClusterIP type publishes the plain-HTTP Server API, and with it - # Basic-auth credentials and JWTs in cleartext, outside the cluster. The - # render refuses it unless this acknowledgement is set and reachability - # is restricted by other means (NetworkPolicy, load balancer allowlist, - # firewall). Prefer an HTTPS-terminating Ingress. + # Legacy acknowledgement, accepted for compatibility; no render gate. allowInsecureExposure: false ingress: enabled: false @@ -400,14 +409,10 @@ server: targetCPUUtilizationPercentage: 70 probes: startup: - # The guaranteed probe budget is (failureThreshold - 1) * periodSeconds, - # because kubelet may run the first probe immediately; 91 x 5 guarantees - # 450s, covering the 300s storage wait and Server process startup. The - # chart passes the guaranteed budget minus the storage wait to the image - # as HG_SERVER_STARTUP_TIMEOUT_S, so the start command and the probe - # give up together instead of the command self-killing at the image - # default of 120s. - failureThreshold: 91 + # A first probe can run immediately; (failureThreshold - 1) * periodSeconds + # must cover the storage wait plus at least 120 seconds for Server startup. + # Unset derives a budget from storageWaitTimeoutSeconds. + failureThreshold: periodSeconds: 5 timeoutSeconds: 5 readiness: @@ -419,10 +424,9 @@ server: failureThreshold: 3 timeoutSeconds: 5 -# Optional Hubble UI. Serves plain HTTP; keep it on a ClusterIP Service or -# behind an HTTPS-terminating Ingress, never directly on an untrusted network. +# Hubble UI is enabled by default; disable with hubble.enabled=false. hubble: - enabled: false + enabled: true # pd: discover the cluster through PD (enables the cluster operations view). # direct: talk to the Server client Service only, without PD discovery. # Current Hubble images require server.auth to be enabled: the UI login @@ -446,7 +450,8 @@ hubble: # size and storageClassName apply at install time only; the PVC is kept # on helm uninstall. persistence: - enabled: false + # Unset inherits persistence.enabled. + enabled: size: 1Gi storageClassName: "" resources: {} @@ -476,14 +481,10 @@ hubble: # This chart makes no Kubernetes API calls, so no token is mounted. automountServiceAccountToken: false service: - type: ClusterIP + # Unset inherits mode (easy: NodePort, strict: ClusterIP). + type: annotations: {} - # A non-ClusterIP type publishes Hubble's plain-HTTP UI, which has no - # login of its own and carries the graph credentials typed into it, - # outside the cluster. The render refuses it unless this - # acknowledgement is set and reachability is restricted by other means - # (NetworkPolicy, load balancer allowlist, firewall). Prefer an - # HTTPS-terminating Ingress. + # Legacy acknowledgement, accepted for compatibility; no render gate. allowInsecureExposure: false ingress: enabled: false @@ -495,9 +496,7 @@ hubble: - path: / pathType: Prefix tls: [] - # Hubble serves plain HTTP, so an Ingress without tls is rejected at - # render time unless this is explicitly set to true for a trusted - # network. + # Legacy acknowledgement, accepted for compatibility. allowPlainHttp: false probes: startup: @@ -513,20 +512,18 @@ hubble: failureThreshold: 3 timeoutSeconds: 5 -# Kubernetes NetworkPolicy: one policy per component that admits only the -# traffic the components exchange, plus DNS. Off here because these values -# cannot know this release's clients; values-cluster.yaml turns it on. Only +# Kubernetes NetworkPolicy: mode default plus explicit per-component rules. +# easy does not isolate clients; strict isolates release traffic and DNS. Only # enforced when the network plugin implements NetworkPolicy (kind v0.25+, # k3s, Calico, Cilium); elsewhere the objects are accepted and do nothing. # Nothing outside the release is admitted unless it is listed in # .extraIngress as standard NetworkPolicy ingress rules: apps in # other namespaces, Prometheus, Vermeer, and the Ingress controller. Exposing -# a component (NodePort/LoadBalancer pd, server or hubble Service, a Server or -# Hubble Ingress, server.advertiseUrl) with an empty extraIngress fails the -# render. Every extra rule must name its peers ("from", or "to" for egress); -# to admit any address, say so with an ipBlock such as 0.0.0.0/0. +# a component does not automatically add callers: configure extraIngress +# and extraEgress using native Kubernetes NetworkPolicy rules. networkPolicy: - enabled: false + # Unset inherits mode (easy: false, strict: true). + enabled: pd: extraIngress: [] store: diff --git a/helm/test_helm_package.py b/helm/test_helm_package.py index ad43d5d399..84843d1106 100644 --- a/helm/test_helm_package.py +++ b/helm/test_helm_package.py @@ -17,6 +17,7 @@ """Validate the artifact shipped to Helm, beyond source template tests.""" from pathlib import Path +import re import shutil import subprocess import tarfile @@ -63,7 +64,8 @@ def test_no_contributor_or_binary_documentation_files(self): def test_required_configuration_is_preserved(self): required = ("Chart.yaml", "README.md", "values.yaml", "values.schema.json", - "values-single.yaml", "values-cluster.yaml") + "values-single.yaml", "values-cluster.yaml", "values-strict.yaml", + "docs/operations.md", "examples/README.md") for name in required: with self.subTest(file=name): self.assertIn(name, self.files) @@ -79,8 +81,27 @@ def test_every_runtime_template_is_preserved(self): self.assertIn(name, self.files) self.assertEqual(source.read_bytes(), self.files[name]) + def test_internal_inventory_covers_workloads_and_helpers(self): + example = (CHART / "examples" / "README.md").read_text() + inventory = example.split("## Internal images", 1)[1].split("`global.imageRegistry`", 1)[0] + images = set(re.findall(r"`(hugegraph/[^`]+:[^`]+)`", inventory)) + self.assertEqual(4, len(images), "Document all four default component images") + registry = "registry.internal.example.com" + rendered = subprocess.run( + ["helm", "template", "inventory-check", str(self.package), + "--values", str(CHART / "examples" / "values-internal-registry.yaml")], + check=True, capture_output=True, text=True).stdout + selected = set(re.findall(r'^\s+image:\s*["\']?([^"\'\s]+)', rendered, re.MULTILINE)) + self.assertEqual({f"{registry}/{image}" for image in images}, selected, + "Mirror inventory must cover workloads, init containers and test hooks") + for name in ("README.md", "examples/values-internal-registry.yaml"): + text = (CHART / name).read_text() + for image in images: + self.assertIn(image.split(":", 1)[0], text, + f"{name} omits a required image repository") + def test_packaged_chart_renders_each_preset(self): - for preset in (None, "values-single.yaml", "values-cluster.yaml"): + for preset in (None, "values-single.yaml", "values-cluster.yaml", "values-strict.yaml"): with self.subTest(preset=preset or "default"): command = ["helm", "template", "package-check", str(self.package)] if preset: diff --git a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh index 77af2af89d..f3bf1437e1 100755 --- a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh +++ b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh @@ -22,7 +22,17 @@ log() { echo "[hugegraph-pd-entrypoint] $*"; } require_env() { local name="$1" if [[ -z "${!name:-}" ]]; then - echo "ERROR: missing required env '${name}'" >&2; exit 2 + echo "ERROR: missing required env '${name}'" >&2 + if [[ "${name}" == "HG_PD_AUTH_SECRET_KEY" ]]; then + echo "This shared credential configures PD REST Basic authentication (auth.secret-key)." >&2 + echo "Helm normally creates a shared Secret through pd.auth.autoGenerate=true and injects it" >&2 + echo "into HG_PD_AUTH_SECRET_KEY via the PD StatefulSet env.valueFrom.secretKeyRef." >&2 + echo "Check pd.auth.value or pd.auth.existingSecret and pd.auth.key, then verify the" >&2 + echo "rendered StatefulSet env and the referenced Secret name/key in the release namespace." >&2 + echo "For standalone containers, set HG_PD_AUTH_SECRET_KEY to the same value on every PD" >&2 + echo "and use that value for Server PD_AUTH_PASSWORD. Do not generate a separate value per node." >&2 + fi + exit 2 fi } diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java index 08c35287d8..0a0ad7ff07 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java @@ -20,7 +20,6 @@ import java.util.Arrays; import org.apache.hugegraph.testutil.Whitebox; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -50,32 +49,18 @@ public class RaftEngineReadinessTest { private static final PeerId LEADER = new PeerId("10.0.0.1", 8610); - private Node originalRaftNode; - private RaftStateMachine originalStateMachine; - + private RaftEngine engine; private Node mockNode; private RaftStateMachine stateMachine; @Before public void setUp() { - RaftEngine engine = RaftEngine.getInstance(); - originalRaftNode = engine.getRaftNode(); - originalStateMachine = Whitebox.getInternalState(engine, "stateMachine"); - - // A fresh machine so callbacks fired here reach no listeners other tests registered + // Other suites leave scheduled tasks reading the shared engine. Keep this mock + // private to the test so their isLeader() calls cannot affect interaction checks. + engine = new RaftEngine(); mockNode = mock(Node.class); - stateMachine = new RaftStateMachine(); + stateMachine = Whitebox.getInternalState(engine, "stateMachine"); Whitebox.setInternalState(engine, "raftNode", mockNode); - Whitebox.setInternalState(engine, "stateMachine", stateMachine); - // The count is cached on the singleton, so clear what an earlier test published - engine.refreshAlivePeerCount(); - } - - @After - public void tearDown() { - RaftEngine engine = RaftEngine.getInstance(); - Whitebox.setInternalState(engine, "raftNode", originalRaftNode); - Whitebox.setInternalState(engine, "stateMachine", originalStateMachine); } private static LeaderChangeContext ctx() { @@ -84,8 +69,8 @@ private static LeaderChangeContext ctx() { @Test public void testNotReadyBeforeRaftNodeStarts() { - Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", null); - RaftEngine engine = RaftEngine.getInstance(); + Whitebox.setInternalState(this.engine, "raftNode", null); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -98,7 +83,7 @@ public void testNotReadyBeforeRaftNodeStarts() { @Test public void testStartedNodeWithoutAnyCallbackIsNotReady() { - RaftEngine.RaftStatus status = RaftEngine.getInstance().getRaftStatus(); + RaftEngine.RaftStatus status = this.engine.getRaftStatus(); Assert.assertFalse(status.isReady()); Assert.assertEquals(State.STATE_UNINITIALIZED.name(), status.getState()); } @@ -108,7 +93,7 @@ public void testLeaderIsReady() { stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenReturn(Arrays.asList(LEADER, new PeerId("b", 1), new PeerId("c", 1))); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -122,7 +107,7 @@ public void testLeaderIsReady() { @Test public void testFollowerWithLeaderIsReady() { stateMachine.onStartFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -139,7 +124,7 @@ public void testFollowerLosingItsLeaderTurnsNotReady() { // The quorum-loss shape from the issue: heartbeats stop, jraft announces the loss stateMachine.onStartFollowing(ctx()); stateMachine.onStopFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; RaftEngine.RaftStatus status = engine.getRaftStatus(); Assert.assertFalse(status.isReady()); @@ -151,7 +136,7 @@ public void testFollowerLosingItsLeaderTurnsNotReady() { public void testLeaderSteppingDownTurnsNotReady() { stateMachine.onLeaderStart(5); stateMachine.onLeaderStop(Status.OK()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -165,14 +150,14 @@ public void testLeaderSteppingDownTurnsNotReady() { public void testErrorAndShutdownAreNotReadyEvenAfterLeadership() { stateMachine.onLeaderStart(5); stateMachine.onError(mock(RaftException.class)); - Assert.assertFalse(RaftEngine.getInstance().getRaftStatus().isReady()); + Assert.assertFalse(this.engine.getRaftStatus().isReady()); Assert.assertEquals(State.STATE_ERROR.name(), - RaftEngine.getInstance().getRaftStatus().getState()); + this.engine.getRaftStatus().getState()); stateMachine.onShutdown(); - Assert.assertFalse(RaftEngine.getInstance().getRaftStatus().isReady()); + Assert.assertFalse(this.engine.getRaftStatus().isReady()); Assert.assertEquals(State.STATE_SHUTDOWN.name(), - RaftEngine.getInstance().getRaftStatus().getState()); + this.engine.getRaftStatus().getState()); } @Test @@ -182,7 +167,7 @@ public void testProbeNeverTouchesTheRaftNode() { // getAlivePeerCount() included: it reads what the refresher thread published stateMachine.onStartFollowing(ctx()); stateMachine.onStopFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.getRaftStatus(); engine.hasLeader(); @@ -195,9 +180,9 @@ public void testProbeNeverTouchesTheRaftNode() { public void testAlivePeerCountSurvivesLeadershipLossRace() { stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenThrow(new IllegalStateException("Not leader")); - RaftEngine.getInstance().refreshAlivePeerCount(); + this.engine.refreshAlivePeerCount(); - Assert.assertEquals(-1, RaftEngine.getInstance().getAlivePeerCount()); + Assert.assertEquals(-1, this.engine.getAlivePeerCount()); } @Test @@ -207,8 +192,8 @@ public void testShutdownStateSurvivesTheNodeBeingDropped() { // uninitialized, which would read as a PD that has not started yet stateMachine.onLeaderStart(5); stateMachine.onShutdown(); - Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", null); - RaftEngine engine = RaftEngine.getInstance(); + Whitebox.setInternalState(this.engine, "raftNode", null); + RaftEngine engine = this.engine; RaftEngine.RaftStatus status = engine.getRaftStatus(); Assert.assertFalse(status.isReady()); @@ -223,7 +208,7 @@ public void testAlivePeerCountStopsAfterALeaderErrorsOrShutsDown() { // count off a node that errored or shut down and the gauge falls back to NaN stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenReturn(Arrays.asList(LEADER, new PeerId("b", 1))); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); Assert.assertEquals(2, engine.getAlivePeerCount()); @@ -248,7 +233,7 @@ public void testAlivePeerCountOnALeaderAlsoSkipsTheNode() { // value: listAlivePeers takes the node read lock before it checks for leadership, so // a scrape that called it would wait out an election that holds the write lock stateMachine.onLeaderStart(5); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.getAlivePeerCount(); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java index f38ffd78f3..1cede41e07 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java @@ -22,15 +22,21 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.text.ParseException; +import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; import org.apache.hugegraph.util.Log; +import org.glassfish.jersey.message.internal.AcceptableToken; +import org.glassfish.jersey.message.internal.HttpHeaderReader; import org.slf4j.Logger; import jakarta.inject.Singleton; import jakarta.ws.rs.NameBinding; import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.WriterInterceptor; @@ -45,16 +51,28 @@ public class CompressInterceptor implements WriterInterceptor { private static final Logger LOG = Log.logger(CompressInterceptor.class); + private static final Pattern TRAILING_DOT_QUALITY = + Pattern.compile("(;[ \t]*[qQ]=[01])\\.(?=[ \t]*(?:[,;]|\\z))"); + // Set compress output buffer size to 4KB (about 40~600 vertices) public static final int BUFFER_SIZE = 1024 * 4; + @Context + private jakarta.inject.Provider requestHeaders; + @Override public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - // If there is no annotation(like exception), we don't compress it - if (context.getAnnotations().length > 0) { + // Exceptions and other responses without @Compress remain unchanged. + Compress compression = getCompressAnnotation(context); + if (compression != null && !context.getHeaders().containsKey(HttpHeaders.CONTENT_ENCODING)) { + addVaryHeader(context.getHeaders()); + } + if (compression != null && + !context.getHeaders().containsKey(HttpHeaders.CONTENT_ENCODING) && + this.acceptsGzip()) { try { - this.compress(context); + this.compress(context, compression); } catch (Throwable e) { LOG.warn("Failed to compress response", e); /* @@ -68,10 +86,49 @@ public void aroundWriteTo(WriterInterceptorContext context) context.proceed(); } - private void compress(WriterInterceptorContext context) + private boolean acceptsGzip() { + String accept = this.requestHeaders.get().getHeaderString(HttpHeaders.ACCEPT_ENCODING); + if (accept == null || accept.isEmpty()) { + return false; + } + // Jersey 3.0.3 misreads legal q=0. and q=1.; retain its token/quality validation. + accept = TRAILING_DOT_QUALITY.matcher(accept).replaceAll("$1"); + int gzip = -1; + int wildcard = 0; + int identity = 0; + try { + // Jersey parses HTTP tokens and quality factors; explicit encodings override '*'. + for (AcceptableToken token : HttpHeaderReader.readAcceptToken(accept)) { + if (GZIP.equalsIgnoreCase(token.getToken())) { + gzip = token.getQuality(); + } else if ("*".equals(token.getToken())) { + wildcard = token.getQuality(); + } else if ("identity".equalsIgnoreCase(token.getToken())) { + identity = token.getQuality(); + } + } + } catch (ParseException e) { + return false; + } + int quality = gzip >= 0 ? gzip : wildcard; + // If no encoding is acceptable, retain the plain response (RFC 9110, 12.4.1). + // Rejecting it here could hide a POST that has already changed graph data. + return quality > 0 && quality >= identity; + } + + private static void addVaryHeader(MultivaluedMap headers) { + for (Object value : headers.getOrDefault(HttpHeaders.VARY, java.util.Collections.emptyList())) { + for (String field : value.toString().split(",")) { + if ("*".equals(field.trim()) || HttpHeaders.ACCEPT_ENCODING.equalsIgnoreCase(field.trim())) { + return; + } + } + } + headers.add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING); + } + + private void compress(WriterInterceptorContext context, Compress compression) throws IOException { - // Get compress info from the @Compress annotation - final Compress compression = getCompressAnnotation(context); final String encoding = compression.value(); final int buffer = compression.buffer(); @@ -97,7 +154,7 @@ private static Compress getCompressAnnotation(WriterInterceptorContext c) { return (Compress) annotation; } } - throw new AssertionError("Unable find @Compress annotation"); + return null; } @NameBinding diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh index 168efe8979..c5bc9c83de 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh @@ -29,7 +29,12 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" GRAPH_CONF="$TOP/conf/graphs/hugegraph.properties" -WAIT_STORAGE_TIMEOUT_S=300 +WAIT_STORAGE_TIMEOUT_S=${WAIT_STORAGE_TIMEOUT_S-300} +if [[ ! "${WAIT_STORAGE_TIMEOUT_S}" =~ ^[1-9][0-9]{0,4}$ ]] || \ + (( WAIT_STORAGE_TIMEOUT_S > 86400 )); then + echo "[wait-storage] ERROR: WAIT_STORAGE_TIMEOUT_S must be an integer from 1 to 86400 seconds" >&2 + exit 2 +fi WAIT_STORAGE_PD_CONNECT_TIMEOUT_S=2 WAIT_STORAGE_PD_MAX_TIMEOUT_S=3 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh index ed39b67492..2d17fccf8b 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh @@ -147,18 +147,57 @@ for wildcard in '*' 'health,*'; do fi done -# The secret is required, and must never be echoed to the log -if (cd "${TMP_DIR}" && env \ +# The secret is required, and failure must explain its purpose and injection +# path before the launcher is called. Remove the earlier stub output so that +# a silent startup cannot pass this check. +rm -f "${TMP_DIR}/spring.json" +if missing_out=$(cd "${TMP_DIR}" && env -u HG_PD_AUTH_SECRET_KEY \ HG_PD_GRPC_HOST=pd0 HG_PD_RAFT_ADDRESS=pd0:8610 \ HG_PD_RAFT_PEERS_LIST=pd0:8610 HG_PD_INITIAL_STORE_LIST=store0:8500 \ - ./docker-entrypoint.sh >/dev/null 2>&1); then + ./docker-entrypoint.sh 2>&1); then echo " FAIL missing secret: entrypoint started without HG_PD_AUTH_SECRET_KEY" FAIL=$((FAIL + 1)) else - echo " PASS missing secret is refused" - PASS=$((PASS + 1)) + missing_rc=$? + missing_valid=true + [[ "${missing_rc}" -eq 2 && ! -f "${TMP_DIR}/spring.json" ]] || missing_valid=false + for hint in "ERROR: missing required env 'HG_PD_AUTH_SECRET_KEY'" \ + 'PD REST Basic authentication (auth.secret-key)' \ + 'pd.auth.autoGenerate=true' 'env.valueFrom.secretKeyRef' \ + 'pd.auth.existingSecret and pd.auth.key' \ + 'release namespace' 'same value on every PD' 'PD_AUTH_PASSWORD'; do + [[ "${missing_out}" == *"${hint}"* ]] || missing_valid=false + done + if [[ "${missing_valid}" == true ]]; then + echo " PASS missing secret is refused with actionable guidance" + PASS=$((PASS + 1)) + else + echo " FAIL missing secret guidance or exit status" + printf '%s\n' "${missing_out}" + FAIL=$((FAIL + 1)) + fi +fi + +# Missing unrelated required variables retain the concise existing error. +if missing_out=$(cd "${TMP_DIR}" && env -u HG_PD_GRPC_HOST -u GRPC_HOST \ + HG_PD_RAFT_ADDRESS=pd0:8610 HG_PD_RAFT_PEERS_LIST=pd0:8610 \ + HG_PD_INITIAL_STORE_LIST=store0:8500 HG_PD_AUTH_SECRET_KEY=not-logged \ + ./docker-entrypoint.sh 2>&1); then + echo " FAIL missing gRPC host was accepted" + FAIL=$((FAIL + 1)) +else + missing_rc=$? + if [[ "${missing_rc}" -eq 2 && \ + "${missing_out}" == "ERROR: missing required env 'HG_PD_GRPC_HOST'" ]]; then + echo " PASS unrelated missing env retains its existing error" + PASS=$((PASS + 1)) + else + echo " FAIL unrelated missing env error changed" + FAIL=$((FAIL + 1)) + fi fi +# The configured secret must never be echoed to the log. log_out=$(cd "${TMP_DIR}" && env \ HG_PD_GRPC_HOST=pd0 HG_PD_RAFT_ADDRESS=pd0:8610 \ HG_PD_RAFT_PEERS_LIST=pd0:8610 HG_PD_INITIAL_STORE_LIST=store0:8500 \ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh index 6d7c4ce8d0..046f255d50 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh @@ -52,6 +52,7 @@ assert_output() { } assert_contract() { + local expected_timeout="${1:-300}" ! grep -q '/v1/health' "${CALL_LOG}" || \ fail "/v1/health must not gate readiness" [[ -s "${ARGS_LOG}" ]] || fail "curl was not called" @@ -68,11 +69,15 @@ assert_contract() { if grep -Fv -- '--max-time 3' "${ARGS_LOG}" | grep -q .; then fail "per-peer maximum timeout was not preserved" fi - assert_equal "outer timeout" "300s" "$(cat "${TIMEOUT_LOG}")" + assert_equal "outer timeout" "${expected_timeout}s" "$(cat "${TIMEOUT_LOG}")" } run_case() { local scenario="$1" peers="$2" abort_after="$3" password="${4:-test-password}" + local timeout_args=() + if [[ "$#" -ge 5 ]]; then + timeout_args=("WAIT_STORAGE_TIMEOUT_S=$5") + fi : > "${CALL_LOG}" : > "${ARGS_LOG}" : > "${CONFIG_LOG}" @@ -80,7 +85,8 @@ run_case() { : > "${TIMEOUT_LOG}" : > "${DIST_ROOT}/conf/graphs/hugegraph.properties" - CASE_OUTPUT=$(env \ + CASE_OUTPUT=$(env -u WAIT_STORAGE_TIMEOUT_S \ + ${timeout_args[@]+"${timeout_args[@]}"} \ PATH="${MOCK_BIN}:${PATH}" \ MOCK_SCENARIO="${scenario}" \ MOCK_ABORT_AFTER="${abort_after}" \ @@ -309,4 +315,23 @@ assert_output "No pd.peers configured, skipping storage wait" fail "warned about an unused PD credential with no pd.peers configured" echo " PASS no credential warning without pd.peers" -echo "10 passed, 0 failed" +for storage_timeout in 1 600 86400; do + run_case "pd1-up" "pd0:8620,pd1:8620" 6 "test-password" "${storage_timeout}" + assert_equal "custom timeout ${storage_timeout} rc" "0" "${CASE_RC}" + assert_output "Timeout = ${storage_timeout}s" + assert_contract "${storage_timeout}" + echo " PASS custom timeout ${storage_timeout}" +done + +for storage_timeout in '' 0 -1 1.5 abc 86401 999999999999999999999; do + run_case "pd1-up" "pd0:8620,pd1:8620" 6 "test-password" "${storage_timeout}" + assert_equal "invalid timeout '${storage_timeout}' rc" "2" "${CASE_RC}" + assert_output "WAIT_STORAGE_TIMEOUT_S must be an integer from 1 to 86400 seconds" + [[ ! -s "${TIMEOUT_LOG}" && ! -s "${CALL_LOG}" ]] || \ + fail "invalid timeout started polling PD" + [[ ! -s "${DIST_ROOT}/conf/graphs/hugegraph.properties" ]] || \ + fail "invalid timeout modified the graph config" + echo " PASS invalid timeout '${storage_timeout}'" +done + +echo "20 passed, 0 failed" diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 3cd65b0d8f..2776171d40 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -44,6 +44,7 @@ import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; import org.apache.hugegraph.unit.api.auth.LoginAPITest; import org.apache.hugegraph.unit.api.filter.AccessLogFilterTest; +import org.apache.hugegraph.unit.api.filter.CompressInterceptorTest; import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest; import org.apache.hugegraph.unit.api.filter.PathFilterTest; import org.apache.hugegraph.unit.api.filter.VertexAuthorizationTest; @@ -139,6 +140,7 @@ @Suite.SuiteClasses({ /* api filter */ AccessLogFilterTest.class, + CompressInterceptorTest.class, LoadDetectFilterTest.class, LoginAPITest.class, PathFilterTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.java new file mode 100644 index 0000000000..6fee1fc581 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.api.filter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; + +import org.apache.hugegraph.api.filter.CompressInterceptor; +import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.junit.Test; +import org.mockito.Mockito; + +import jakarta.inject.Provider; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +public class CompressInterceptorTest { + + private static final byte[] ENTITY = "{\"result\":\"hello HugeGraph\"}".getBytes(StandardCharsets.UTF_8); + + @Test + public void testPlainResponseWhenGzipNotAccepted() throws Exception { + for (String accept : new String[]{null, "", "identity", "br", "gzip;q=0", "*;q=0", + "identity;q=0", "gzip;q=0, identity;q=0", + "*;q=1, gzip;q=0", "gzip;q=0.5, identity;q=1", + "gzip;q=invalid"}) { + this.assertResponse(accept, false); + } + } + + @Test + public void testGzipResponseWhenAccepted() throws Exception { + for (String accept : new String[]{"gzip", "GZIP", "br, gzip", "gzip;q=0.5", "*", + "gzip;q=1, identity;q=0.5", "*;q=0, gzip;q=1"}) { + this.assertResponse(accept, true); + } + } + + @Test + public void testTrailingDotQualityFactors() throws Exception { + for (String accept : new String[]{"gzip;q=0.", "*;q=1, gzip;q=0.", + "gzip;q=0.5, identity;q=1.", "*;q=0.", + "*;q=0.5, identity;q=1.", + "*;q=1., GZIP; Q=0. ", + "gzip;q=0.0", "gzip;q=1.001", "gzip;q=1..", + "gzip;q=0.foo", "gzip;q=1. 0", "gzip;q=1.0000", + "gzip;Q =1.", "gzip;Q= 1."}) { + this.assertResponse(accept, false); + } + for (String accept : new String[]{"gzip;q=1.", "*;q=1.", + "gzip;q=1., identity;q=0.5", "*;q=0., gzip;q=1.", + "gzip;q=0.5, identity;q=0.", + "GZIP; Q=1. ", "GZIP;\tQ=1.\t", + "gzip;q=1.000", "gzip;q=0.001"}) { + this.assertResponse(accept, true); + } + } + + @Test + public void testPreserveVaryFields() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.VARY, "Origin"); + this.writeResponse("gzip", headers, true); + Assert.assertEquals(2, headers.get(HttpHeaders.VARY).size()); + Assert.assertEquals("Origin", headers.get(HttpHeaders.VARY).get(0)); + Assert.assertEquals(HttpHeaders.ACCEPT_ENCODING, headers.get(HttpHeaders.VARY).get(1)); + } + + @Test + public void testDoNotDuplicateVary() throws Exception { + for (String vary : new String[]{"Origin, accept-encoding", "*"}) { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.VARY, vary); + this.writeResponse("identity", headers, true); + Assert.assertEquals(1, headers.get(HttpHeaders.VARY).size()); + Assert.assertEquals(vary, headers.getFirst(HttpHeaders.VARY)); + } + } + + @Test + public void testPreserveExistingEncoding() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "br"); + headers.add(HttpHeaders.CONTENT_LENGTH, ENTITY.length); + Assert.assertArrayEquals(ENTITY, this.writeResponse("gzip", headers, true)); + Assert.assertEquals("br", headers.getFirst(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals(ENTITY.length, headers.getFirst(HttpHeaders.CONTENT_LENGTH)); + } + + @Test + public void testUnannotatedResponseRemainsPlain() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + Assert.assertArrayEquals(ENTITY, this.writeResponse("gzip", headers, false)); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_ENCODING)); + Assert.assertFalse(headers.containsKey(HttpHeaders.VARY)); + } + + @Compress + private static void compressedResource() { + } + + private void assertResponse(String accept, boolean compressed) throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_LENGTH, ENTITY.length); + byte[] response = this.writeResponse(accept, headers, true); + Assert.assertEquals(HttpHeaders.ACCEPT_ENCODING, headers.getFirst(HttpHeaders.VARY)); + if (compressed) { + Assert.assertEquals("gzip", headers.getFirst(HttpHeaders.CONTENT_ENCODING)); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_LENGTH)); + try (GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(response))) { + Assert.assertArrayEquals(ENTITY, input.readAllBytes()); + } + } else { + Assert.assertArrayEquals(ENTITY, response); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals(ENTITY.length, headers.getFirst(HttpHeaders.CONTENT_LENGTH)); + } + } + + private byte[] writeResponse(String accept, MultivaluedMap headers, + boolean annotated) throws Exception { + // Real Jersey headers and parser, without creating an HTTP server or socket. + URI uri = URI.create("http://localhost/gremlin"); + ContainerRequest request = new ContainerRequest(uri, uri, "POST", null, + new MapPropertiesDelegate()); + if (accept != null) { + request.getHeaders().add(HttpHeaders.ACCEPT_ENCODING, accept); + } + CompressInterceptor interceptor = new CompressInterceptor(); + Whitebox.setInternalState(interceptor, "requestHeaders", (Provider) () -> request); + WriterInterceptorContext context = Mockito.mock(WriterInterceptorContext.class); + Annotation[] annotations = annotated ? + CompressInterceptorTest.class.getDeclaredMethod("compressedResource") + .getAnnotations() : new Annotation[0]; + Mockito.when(context.getAnnotations()).thenReturn(annotations); + Mockito.when(context.getHeaders()).thenReturn(headers); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + OutputStream[] output = new OutputStream[]{bytes}; + Mockito.when(context.getOutputStream()).thenAnswer(invocation -> output[0]); + Mockito.doAnswer(invocation -> { + output[0] = invocation.getArgument(0); + return null; + }).when(context).setOutputStream(Mockito.any(OutputStream.class)); + Mockito.doAnswer(invocation -> { + output[0].write(ENTITY); + output[0].close(); + return null; + }).when(context).proceed(); + interceptor.aroundWriteTo(context); + Mockito.verify(context, Mockito.times(1)).proceed(); + return bytes.toByteArray(); + } +}