Skip to content

feat(helm): add HStore deployment chart - #3218

Draft
bitflicker64 wants to merge 58 commits into
apache:masterfrom
hugegraph:feat/hstore-helm-chart-3132
Draft

bitflicker64 wants to merge 58 commits into
apache:masterfrom
hugegraph:feat/hstore-helm-chart-3132

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

Add an official Helm installation path for the distributed HugeGraph HStore topology on Kubernetes. The chart packages the PD, Store and Server startup contract that otherwise has to be reconstructed by each operator, with authentication on by default and an optional Hubble UI.

This PR succeeds #3132 (closed, history and earlier review threads preserved there). Its head is the org branch hugegraph:feat/hstore-helm-chart-3132, the same branch as hugegraph#221, so this PR and the org-side review stay on one branch and cannot drift. Review happens on hugegraph#221, where the GitHub review apps can run; ASF repository settings block their permissions here.

Main Changes

  • helm/hugegraph/: chart 0.1.0 with PD and Store StatefulSets, a Server Deployment, an optional Hubble Deployment, three values presets (values.yaml, values-single.yaml, values-cluster.yaml), a full values.schema.json, and 66 helm-unittest cases.
  • Encodes the distributed startup contract: Server skips init-store and registers per-Pod with PD, the Store waits for a PD quorum on /v1/ready, PD readiness is quorum-aware while startup and liveness stay on /v1/health, and HG_SERVER_STARTUP_TIMEOUT_S is derived from the startup probe budget.
  • One shared PD REST secret (pd.auth) consumed by PD, the Server storage wait and Hubble; chart-managed admin and JWT Secrets with bring-your-own existingSecret paths.
  • .github/workflows/helm-chart-ci.yml: lint, renders on all presets, fail-path guards, kubeconform, a legacy --reuse-values fixture render, and packaging.
  • README covering install, upgrade rolls, disaster recovery, troubleshooting and Limitations.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • Helm Chart CI: lint, preset renders, 15 fail-path guards, kubeconform (1.23.0 and current), legacy-values render, packaging.
    • Fault campaigns on Kind (2026-08-21 to 2026-09-10, summarized in the 2026-09-10 campaign comment): install, PD leader and follower loss, 2-of-3 PD loss, Store loss, PVC reattach, rolling restart under load, secret rotation, image upgrade.
    • Independent 8/8 fault battery on a three-node k3s by @SebastianGruza (report, scripts in SebastianGruza/hugegraph-validation).

Current work queue, from the review on #221 (2026-09-17): fix the inline findings, run the helm-unittest suites in CI, and one current-image lifecycle run (managed PD/JWT rotation, Store replacement with a persisted record, the documented recovery procedure). Image tags and appVersion stay latest until a 1.8.x release exists; the pin is a follow-up commit.

Does this PR potentially affect the following parts?

  • Dependencies
  • Modify configurations
  • The public API
  • Other affects (adds a Helm chart under helm/ and one CI workflow; no Java changes)
  • Nope

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

Documentation files in this PR or paired hugegraph-doc PR:
helm/hugegraph/README.md, plus the paired site page in apache/hugegraph-doc#494 (EN and CN, written against 05f3d9e0b).

…tore disabled

The Server wrapper now writes auth.admin_pa from the auth Secret alongside
usePD and pd.peers, so an auth-enabled release keeps its configured admin
password with init_store.enabled=false instead of silently falling back to
the public default. The Secret value is rejected when it contains
properties-parser metacharacters that would inject config lines or store a
different password than the Secret holds.

The new hubble component deploys the Hubble UI as a single-replica
Deployment with pd and direct wiring modes, optional Ingress and H2
persistence, schema validation, render-time guards, docs, and CI coverage.
PD-meta installs (auth enabled, or Hubble in pd mode) also announce the
Server client Service URL to PD via server.urls_to_pd and
server.deploy_in_k8s so discovery clients receive a resolvable address
instead of the 0.0.0.0 default, and the Hubble wrapper writes server.host
so current images bind all interfaces. Because current Hubble images
authenticate their login against the cluster, rendering Hubble without
server.auth fails unless explicitly overridden.

The CI invalid-value step now fails on every case rather than only its
last line, and positive renders cover both Hubble modes.

Validated against a composition of master 1716c77 plus the current heads
of apache#3119 (edf07d0), apache#3126 (b40c42f), and apache#3130 (198de19): fresh
auth-enabled installs reach Ready with zero restarts, the admin credential
comes from the Secret while unauthenticated and default-password requests
get 401, and Hubble logs in with the Secret credential and reads cluster
metadata through PD discovery, with its H2 metadata persisted on the PVC.
…xtraEnv overrides

A PD PodDisruptionBudget with minAvailable below floor(replicas/2)+1
permits voluntary evictions that leave PD without a Raft majority, so the
render now requires the quorum floor in addition to the existing
blocks-all-drains upper bound. With 2 replicas no valid budget exists;
the README documents the even/odd contract.

extraEnv entries render after the chart-owned variables and Kubernetes
lets the last duplicate win, so a duplicate name could silently override
a validated contract such as HG_SERVER_INIT_STORE_ENABLED=false. Each
component's extraEnv now rejects its chart-managed variable names.

Boundary and negative render cases for both rules are part of the CI
invalid-value step.
The run failed at startup because azure/setup-helm@v4 is not on the
ASF-approved actions allowlist; install the pinned helm release from
the official tarball in a plain run step instead.
…cheduling defaults

Fresh installs now seed PD with partition.default-shard-count derived from
the Store count (3 when store.replicas is at least 3, otherwise 1, matching
PD's odd-only constraint and its 2-to-1 clamp), so a default 3-Store
deployment gets store-level HA instead of the image default of one shard
replica per partition. The seed is delivered as -D system properties
prepended into the PD JAVA_OPTS ahead of pd.javaOpts, preserving the start
script's automatic heap sizing; it applies at first bootstrap only, after
which PD metadata is authoritative, all documented together with the
resulting initial partition count change.

pd.antiAffinity and store.antiAffinity default to preferred so the chart
schedules on clusters with fewer nodes than replicas; values-cluster.yaml
keeps required for both, NOTES warns when PD quorum members may co-locate,
and the README documents the upgrade implications.

The Disaster Recovery documentation describes what current PD builds
actually do: the scheduled patrol only marks silent stores Offline, shard
reconciliation and tombstone processing run only via the manual
/v1/task/patrolPartitions endpoint, and the pd.patrol-interval and
store.max-down-time properties are bound but never read, which is why the
chart does not expose them. Periodic leader balancing and recovery metrics
are referenced as upstream feature requests.

extraEnv now also rejects JAVA_OPTIONS for pd, store, and server, because
the start scripts drop the chart-managed JAVA_OPTS entirely when it is set.
Schema accepts numeric strings for the new keys, values-file integers at or
above one million no longer fail as scientific notation, and CI asserts the
rendered -D content, covers both shard-count validation messages, and
kubeconforms the sharded render.
…#3137)

Document that the creating Server is consistent at HTTP 200 after apache#3138,
while cross-replica convergence and PD-owned creation remain open upstream.
Multi-replica Server pods must use one JWT signing key or Hubble login
fails behind the Service; chart-manage or BYO via server.auth.tokenSecret.
When auth is enabled without existingSecret, create a stable release-admin
Secret so operators are not forced to pre-create credentials.
Default installs get a chart-managed admin Secret; leave Hubble off so
API-only clusters stay lean, and enable the UI with one flag when wanted.
Split server.auth into admin and token blocks so inline credentials
(password/value) are distinct from Kubernetes Secret refs
(existingSecret), matching the clearer values API for operators.
Let Server register a reachable URL with PD and optionally expose the PD
client Service, so standalone Hubble can discover the cluster without
in-cluster DNS. Fail closed when advertiseUrl is set without PD meta mode.
PD resolves its raft peer hostnames once at boot and freezes the result
as the RPC allowlist. With podManagementPolicy: Parallel a PD can start
before its peers A records are published, freeze a partial allowlist,
and permanently block the elected leader, wedging a fresh install. Add
a wait-for-pd-dns init container mirroring the store wait-for-pd gate:
hold the pd container until every peer hostname resolves, bounded by
pd.waitTimeoutSeconds (values: pd.waitEnabled escape hatch, waitImage,
waitTimeoutSeconds, waitResources, schema-enforced). busybox nslookup
ignores resolv.conf search domains, so the gate derives the cluster
domain and retries the FQDN. The PD headless Service publishes
not-ready addresses, so records appear at IP assignment and the gate
cannot deadlock. First-boot mitigation only: a PD rescheduled with a
new pod IP still requires the image-side allowlist refresh, noted
under Limitations.

Hardening and docs in the same pass:
- explicit updateStrategy and persistentVolumeClaimRetentionPolicy
  (Retain/Retain; honored on K8s >=1.27) on the PD and Store
  StatefulSets, value-driven and schema-typed
- checksum/auth pod annotation on the Server Deployment so rotating an
  existingSecret rolls Server pods on the next upgrade; hashes Secret
  names, keys, and resourceVersion, never Secret data. Best-effort:
  template-only pipelines render a constant, and the first upgrade
  after a fresh install rolls Server once
- NOTES.txt no longer echoes the admin password
- README: release-name assumption note, port-forward instead of exec
  curl in troubleshooting, Upgrading notes for the one-time PD and
  Server rollouts this version causes, and Limitations entries for the
  image-side PD allowlist and PD management REST issues (chart 0.1.1)
This PR introduces the chart, so there is no published 0.1.0 to bump from
and the version should stay at the initial one until a release happens.

Reword the Upgrading note for the same reason: it described rolls that
happen when upgrading "from an earlier revision", which does not exist
here. State the forward-looking behavior instead, that any Pod template
change rolls that workload, and keep the PD allowlist caveat and the
first-upgrade Server roll, both of which still apply.
The lint-and-render legacy guard caught a regression: pd.waitImage was
added to the pd required list, so a release created before the field
existed failed schema validation under --reuse-values. Make the field
optional and default the image in the template instead, matching how
waitTimeoutSeconds already degrades.

Also flip the gate condition to "not explicitly disabled" so those same
legacy releases pick the DNS gate up on upgrade rather than silently
losing the first-boot protection because waitEnabled is absent from
their values.
The guard rejected advertiseUrl unless auth was on or in-chart Hubble ran
in pd mode, on the premise that only then does the chart register
server.urls_to_pd. That premise stopped being true when the chart moved
every Server to PD meta mode unconditionally: server-deployment.yaml sets
$pdMeta := true, so urls_to_pd is always written. Rendering with
advertiseUrl set, auth off and Hubble off now emits
HG_SERVER_URLS_TO_PD with the configured URL, which the guard had been
refusing to produce.

The URL-format guard (absolute http:// or https:// only) is unchanged.
Correct the README passage that documented the removed requirement.
PD resolves its raft peer allowlist once at boot, so under Kubernetes a
peer whose Pod IP was unpublished at that moment, or changes later, is
rejected: a first-boot race that could wedge an install, and a permanent
rejection after any PD reschedule. The upstream raft.ip-whitelist.enabled
switch fixes this at the source, leaving peer authentication to
Kubernetes-level controls.

Render -Draft.ip-whitelist.enabled=false into PD's derived JAVA_OPTS via
the new pd.raftIpWhitelistEnabled value (default false, schema-typed;
images predating the switch ignore the property), and remove the interim
wait-for-pd-dns init container with its pd.wait* values, schema entries,
and peer-hosts helper. The gate only ever mitigated the first-boot half
of the problem.

Verified on Kubernetes with a PD image carrying the switch: two
deterministic installs, PD follower/leader crash, PD and Store majority
loss, leader network partition, PVC reattach, rolling restart under write
load, auth on every Server replica, a 0.1.2-to-this upgrade, Secret
rotation, and a zero-override install followed through the documented
first-run steps. Zero "Blocked connection" and zero "Could not resolve
allowlist entry" lines across the entire campaign; the pod-IP recycle
case that previously failed reproducibly now passes.

Also update the chart CI's JAVA_OPTS assertions, which matched a
quote-terminated shard-count prefix and therefore broke once the whitelist
flag was appended, and bring the README in line: the Limitations and
Upgrading sections described the removed init container, and the values
table had no row for the new setting.
…ning

Setting server.auth.token.value shorter than 32 bytes passed every chart
check and then failed at runtime: all Server pods crash looped on the
entrypoint assertion and helm install --wait timed out with no indication
of the cause. Add a minLength to the schema so the value is rejected at
render time with a precise path.

The default values set no container resources, so each JVM sizes its heap
against total node memory rather than a cgroup limit. On a multi-node
cluster where several pods share a node the heaps oversubscribe it. Note
the cluster preset in the README and warn from NOTES.txt.

Images could only be referenced as repository:tag, so a release could not
pin an immutable digest. Add an optional image.digest per component that
takes precedence over the tag.

Give the Helm test hook bounded default resources so it is admissible on
quota-managed namespaces.
The chart shipped no unit tests. Cover the behaviour that fails silently:
the PD raft whitelist flag and derived JAVA_OPTS, image digest pinning,
the Server auth checksum annotation and Secret wiring, update strategy and
PVC retention, quorum arithmetic at one three and five replicas, the test
hook resource bounds, and every validateValues guard asserted against its
real error message.
Record the values that had no documentation: the optional image.digest for
each component, and the 32 byte minimum on server.auth.token.value that the
schema now enforces.

Add the credential lifecycle that operators hit first. The chart-managed
auth Secret survives uninstall and is reused by a later install of the same
release name, and helm template cannot read an existing Secret so the
password it prints is only a render-time placeholder. Add a worked example
for supplying an admin Secret instead of letting the chart generate one.

Correct two stale statements: the Limitations note pointed to the Disaster
Recovery section as being below when it is above, and the pinning advice
said three component tags when there are four, since Hubble also tracks a
mutable tag.
Picks up apache#3159 (321ba4d), the REST API adaptation for Hubble and the
k8s-mode enhancements that the pending confirmation pass depends on,
along with apache#3140, apache#3153, apache#3173, apache#3171, apache#3177, apache#3176, apache#3178 and apache#3149.

No conflicts, and no change to helm/.
Every Server replica registered the shared client Service URL with PD, so
the discovery registry collapsed three replicas into one logical entry and
PD-discovered clients such as Hubble saw a single Server. The registration
is a lease renewed by a per-instance heartbeat, so a replaced Pod's entry
ages out on its own; announcing each Pod's own IP keeps the replica list
truthful without leaving permanent stale entries.

Empty server.advertiseUrl now announces the Pod IP through a POD_IP
downward-API variable. Setting advertiseUrl keeps its meaning as the shared
external endpoint for an outside Hubble. POD_IP joins the reserved
environment names so extraEnv cannot shadow it.

Ports c2d086d from helm-dev and adds a unit suite covering the fieldRef,
the default URL shape, the advertiseUrl override and the reserved name.
…commands

The Limitations entry said the PD management endpoints reject requests on
current images, so the Disaster Recovery flow might be unavailable. Measuring
it shows the opposite. PD compares the Basic-auth username against a fixed
internal set and never reads the password, so any password, including an
empty one, is accepted for those names. The endpoints are effectively
unauthenticated rather than unusable, which is a caveat about exposure, not
availability.

That also makes the documented recovery commands wrong. Written without a
credential they answer HTTP 200 with an Unauthorized body and the task never
runs, so an operator mid-incident sees success and gets nothing. Add the
credential the endpoints actually require and say why the password is empty.

Also record that success, refusal and a missing credential all return HTTP
200, so the status code carries no signal for health checks.
…ment health vs ready

PD's /v1/health answers 200 as soon as the REST listener is up and never
consults raft, so every PD and Store probe and the Store init container's
PD wait count listeners, not quorum members (apache#3183). The
fix, apache#3185, adds /v1/ready from 1.8.0.

- pd.readinessPath and store.waitPath, both defaulting to /v1/health, so the
  switch to /v1/ready is a values change made with the 1.8.0 pin; the
  schema rejects paths without a leading slash
- README: Limitations entries for the liveness-only health endpoint and for
  the 45 second discovery lease (measured 30 to 35 seconds); the Store wait
  is described as a PD wait rather than a quorum wait; the Server now
  registers its Pod IP, not the Service URL
- NOTES and the init container messages no longer claim a quorum
- tests: pd_readiness_path_test.yaml, five cases
Points at apache#3185 and says the defaults flip with the 1.8.0
image pin, so the change is not lost once that PR merges.
PD images from 1.8.0 (apache#3189) check the Basic-auth password
of every management call against auth.secret-key and refuse to start without
one. The chart now keeps that value in a kept release-pd-auth Secret, or in
pd.auth.existingSecret, and hands it to the three readers: PD as
HG_PD_AUTH_SECRET_KEY, the Server storage wait as PD_AUTH_PASSWORD, and
Hubble as operations.pd.password written into its properties file by the
existing wrapper. A checksum/pd-auth annotation on the three Pod templates
rolls them when the Secret changes; the Server annotations block is now
rendered unconditionally for it. Priority and lookup semantics mirror
server.auth.token. Older images ignore the password, so the wiring is
harmless on the images the draft currently tracks.

The values schema requires one of existingSecret, value or autoGenerate and
refuses newlines, carriage returns and backslashes in an inline value, since
it lands in a Java properties file; the template guard repeats the first
rule for values that bypass the schema. The three chart-managed variables
join the reserved extraEnv lists.

README: Chart Details bullet, four parameter rows, Disaster Recovery calls
carry the secret, and the Limitations bullet separates the 1.7.0 behaviour
from 1.8.0. NOTES prints how to read the secret. New suite
pd_auth_secret_test.yaml, 9 tests; 58 in total. Lint on three presets;
renders 16 objects by default and 19 with Hubble.

Measured on a kind cluster with images built from master plus apache#3185, apache#3187
and apache#3189: the Secret is created, PD starts with the variable, the Server
storage wait passes with the credential, and Hubble lists all nine nodes.
bitflicker64 and others added 5 commits September 17, 2026 03:03
Bring over the onboarding pieces the testing branch already had: a
pre-install cluster check, --wait --timeout on the primary install
command, the Kind/minikube local-build section (with the pullPolicy
override called out as required, since this chart defaults Always), a
Hubble row in the component table, and the resource naming pattern.

Close three notes from the 2026-09-16 independent test run on apache#3132:
document the liveness bound on a stalled component, document how to
stage a rollout given the schema's one-replica floor, and enable the
Server PodDisruptionBudget in values-cluster.yaml so a node drain
cannot evict every Server at once (the default stays off, as recorded).

Also move the server.ingress.allowPlainHttp rejection into the schema
(a server-specific ingress definition without the key), so the refusal
happens at validation with a clear message instead of mid-render; the
template guard stays as a backstop.
A PD leader that vanishes without closing its sockets (a freeze or a
network partition, unlike a crash) is waited on for the full
raft.rpc-timeout per attempt, so the image default of 10000 ms leaves
the cluster without a PD leader for about a minute. Measured twice on
the chart: 67 s with a blackholed leader (2026-09-10 campaign) and
56 s with a SIGSTOP-frozen leader, against 8.5 s with the timeout at
3000 ms (independent k3s run on apache#3132, 2026-09-16). Data-plane writes
were unaffected in both runs; everything that needs PD waited.

Render -Draft.rpc-timeout from a new pd.raftRpcTimeoutMs value,
defaulting to 3000, placed with the other derived -D properties ahead
of pd.javaOpts so an explicit flag there still wins. Empty preserves
the image default, and a values set stored before the key existed
renders without the flag. Two unit tests cover the override and the
opt-out; the CI verbatim JAVA_OPTS greps carry the new flag.
- render explicit zero grace periods for all workloads
- preserve omitted fields for legacy values
- cover zero overrides across four workload templates
- restrict startup and liveness success thresholds to one
- preserve readiness thresholds above one
- cover invalid probe overrides for all components
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 36.59%. Comparing base (fbbeaa9) to head (5a48969).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3218      +/-   ##
============================================
- Coverage     41.12%   36.59%   -4.54%     
+ Complexity     7209     4664    -2545     
============================================
  Files           802      545     -257     
  Lines         69393    51420   -17973     
  Branches       9237     7245    -1992     
============================================
- Hits          28541    18815    -9726     
+ Misses        37588    30288    -7300     
+ Partials       3264     2317     -947     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The workflow lints, renders and validates but never invokes the
helm-unittest suites under helm/hugegraph/tests/, so a change can break
every one of them while CI stays green. Install the plugin pinned at
v1.1.2 and run it after lint.

Also check the helm and kubeconform tarballs against their published
sha256 sums instead of piping an unverified download into the job.
Review fixes from #221:

- Require store.pdb.minAvailable >= store.replicas - 1. Shards keep
  their copies on a subset of the Stores, so a budget that permits two
  concurrent evictions (the old default with four or five Stores) can
  remove a shard majority even while the PDB is satisfied. The default
  3-Store topology is unchanged.
- Reject podLabels that set app.kubernetes.io/name, instance or
  component on any of the four workloads: they render after the
  chart-managed labels and would break the selectors, Services and PDBs
  that match on them.
- Accept an empty hubble.image.tag when a digest is set; the digest is
  a complete image identity.
Review fixes from #221:

- The checksum annotations were lookup-only, so an upgrade that changes
  an inline pd.auth.value, admin password or JWT value hashed the OLD
  Secret's resourceVersion and did not roll the reading pods in that
  same upgrade. Fold a hash of the desired inline value into
  hugegraph.pd.authChecksum and hugegraph.server.authChecksum, only
  when that inline value is live (no existingSecret overriding it); the
  autoGenerate and existingSecret paths are unchanged.
- Schema now rejects inline admin passwords the Server wrapper refuses
  at startup (leading whitespace, backslash, CR/LF), failing at install
  instead of CrashLooping after it.
- pd.auth.value is restricted to printable ASCII with no leading
  whitespace and no backslashes: Hubble reads its properties file as
  ISO-8859-1, strips leading whitespace, and its Java properties parser
  unescapes backslashes, so such values silently stop matching what PD
  holds.
Review fixes from #221:

Raft and shard membership are persisted, and deleting Pods does not
reconfigure them: a 3-to-1 PD shrink permanently loses quorum, and
removing a Store strands the shard copies it holds. validateValues now
reads the live StatefulSet through lookup and fails an upgrade whose
replica count is below it. Template-only renders have no live object
and skip the guard, and an operator who finished the documented manual
procedure has already scaled the live StatefulSet, so the upgrade
passes once live and desired match.

README: the manual scale-down procedure under Scaling, a note that
kubectl scale does not survive the next helm upgrade, and an Upgrading
bullet recommending store.updateStrategy=OnDelete for production image
rolls, because Store readiness reports the listener while shard
recovery is still catching up.
Review fixes from #221:

- Quote the configurable Secret data keys in the three Secret templates,
  so a key like "on" or "01" stays the literal string the workloads
  reference instead of being reinterpreted by YAML.
- A regression test renders a Secret with the key "on" and asserts the
  literal key survives; a CI render greps the quoted key verbatim, since
  the unit framework's YAML 1.2 parser cannot see the 1.1 coercion.
- Escape dots in the NOTES.txt jsonpath commands, so a dotted key such
  as admin.password reads the flat Secret entry instead of querying a
  nested field and printing an empty credential.
- Default the pd readiness path and the Store wait path to /v1/ready in
  the templates too, matching values.yaml, NOTES and the schema for a
  release whose stored values predate the keys.
- Scope the BestEffort NOTES warning to the case where no component sets
  resources; a milder note already covers the partial case.
- README: enable Hubble with --reuse-values so the upgrade keeps the
  release's existing overrides; add -n <namespace> to the troubleshooting
  commands; use four-space continuations in the new command blocks.
Review fixes from #221:

- A non-ClusterIP pd.service now requires
  pd.service.allowInsecureExposure=true: the exposed gRPC port carries
  no authentication, raft membership RPCs included, so exposure must be
  an explicit decision taken after restricting reachability elsewhere.
- A Server Ingress without tls is refused unless
  server.ingress.allowPlainHttp=true; auth is on by default, so a plain
  HTTP route carries Basic-auth credentials and JWTs. Replaces the old
  rule that rejected the key outright, and mirrors the Hubble opt-in.
- hubble.securityContext.readOnlyRootFilesystem=true is refused: the
  Hubble wrapper writes its properties file inside the image at startup
  and the chart mounts no writable volume there.
- HG_SERVER_STARTUP_TIMEOUT_S is now the startup probe budget minus the
  image's 300-second storage wait (150 s by default; the 450 s probe
  floor keeps the result at 150 or more, with the image's 120 s minimum
  kept as a guard), so the start command and kubelet give up together
  instead of the image outliving the probe.
- The default PD announcement brackets an IPv6 POD_IP, which a URL
  requires; an explicit server.advertiseUrl stays untouched.
- README documents the exposure acknowledgements, refreshes the
  validation-rules list for the new gates, adds an Upgrading note for
  releases that already expose PD or serve a TLS-less Ingress, and uses
  HTTPS in the outside-Hubble direct URL example, since login sends
  credentials over it.
Review fixes from #221:

The recovery task endpoints execute locally on the PD that receives
them and answer a follower with empty success, so the documented
procedure could report three successes while recovering nothing. The
runbook now identifies the leader through /v1/members and port-forwards
that Pod before triggering tasks.

A Store replaced with an empty PVC registers under a new Store ID while
the old one stays Offline with its shard memberships; the patrol
repairs only Tombstone members, so it never touches the Offline entry.
The runbook adds the retirement step: mark the old ID Tombstone with
POST /v1/store/<id> {"storeState":"Tombstone"}, which hands its shards
to the patrol, then patrol and verify the shard groups. DELETE only
erases the record and strands the memberships, so it is documented as
cleanup after the patrol, never the retirement itself.
The v1.1.x plugin manifests carry a platformHooks field that Helm
3.16.2 rejects at plugin load, so the new unittest step failed in
seconds on both PRs; the local gate missed it because this machine
runs Helm 4. Verified against the CI's exact 3.16.2 binary with an
isolated plugin home: v1.0.0 installs and runs the suites.

Under v1.0.0 a schema abort is a suite error rather than a matchable
failed render, which broke exactly one case, the pd.auth
no-secret-source rejection. That input moves to the workflow's
must-fail render guards, where the same class of schema rejection is
already asserted; nothing is asserted less than before. 71 cases pass
under 3.16.2 with v1.0.0 and under Helm 4 alike.
Follow-ups from CodeRabbit's 2026-09-18 pass on #221:

- Install helm-unittest by the v1.0.0 tag's commit SHA, so a moved tag
  cannot change what executes on the runner.
- The recovery runbook ran two port-forwards and three curls as one
  foreground sequence, which blocks at the first forward. Note the
  second terminal, stop the Service forward before the leader forward,
  and move the inline comments to their own lines within 100 columns.
- Exclude a leading form feed from inline admin passwords: the config
  reader trims it when the admin is first created, so the Secret and
  the effective password would silently differ.
Review fixes from #221:

The helm test Pod never received .Values.imagePullSecrets, so on an
authenticated private mirror the four workloads pull with credentials
while helm test enters ImagePullBackOff. The hook now propagates the
setting like the workloads do, with a unit case.

PD and Store storage sizes live in the StatefulSet volumeClaimTemplates,
which Kubernetes forbids changing, so an upgrade with a new size was
rejected in full with nothing telling the operator the supported path.
The parameter tables mark both sizes install-time and Upgrading gains
the resize procedure: patch each PVC, wait, recreate the StatefulSet
with --cascade=orphan, upgrade with the matching value.
…very

Two gaps a review of the replies surfaced before posting them:

The pd.auth.existingSecret row now states the same value constraint as
the inline path (printable ASCII, no leading whitespace, no
backslashes); an external Secret bypasses the schema, so the rule has
to live in the contract text.

The recovery runbook now re-reads /v1/members after the task sequence:
if leadership moved mid-sequence, the later tasks ran on a follower and
did nothing, so they are rerun on the new leader.

Also strips trailing whitespace the workflow picked up in e62cac3.
Add server.readinessPath (default /versions, so nothing changes on
current images), mirroring pd.readinessPath: values.yaml, the schema
(pattern ^/) and the Server readinessProbe in server-deployment.yaml,
plus a README parameter row and a two-case unit suite (74 total).

On a Server image that serves GET /readiness (apache#3212,
proposed in apache#3221), setting the value to /readiness
makes a Server that cannot serve graph traffic answer 503 and drop out
of the Service instead of returning 500 to every graph request.
Startup and liveness stay on /versions so a Server that merely lost
its storage is not restarted.

Implements the change proposed and measured by @SebastianGruza in
#229 (six fault scenarios at 1 Hz sampling, zero
readiness transitions across Store and PD rolling restarts).
The Store commits well beyond its heap. conf/application.yml includes the
pd Spring profile, and conf/application-pd.yml sets
rocksdb.total_memory_size to 32000000000; RaftRocksdbOptions splits that
into a RocksDB write cache and block cache, so both are bounded by 32 GB
rather than by the container. The jraft log storage registers a further
1 GiB LRU cache once per process.

Against -Xmx1024m -XX:MaxDirectMemorySize=512m the old 4Gi limit sat below
the steady state: a k3s run OOM-killed all three Stores after about 1 GB of
data and they stayed in CrashLoopBackOff, while 8Gi held at 4.42 GiB
anonymous RSS. Request 5Gi, limit 8Gi, with the accounting recorded in the
preset and in Limitations.

The limit bounds the damage, not RocksDB. The Store entrypoint rebuilds
SPRING_APPLICATION_JSON from its own variables and the chart mounts no
config file, so rocksdb.total_memory_size cannot be set from the chart
today; that is noted as an image-side gap.

Reported with measurements by @SebastianGruza on #221.
Both auth checksums appended the live Secret resourceVersion unconditionally,
including when the credential was an active inline value. That rolled the
Pods a second time on the no-change upgrade after a rotation: the rotation
render still saw the old resourceVersion, and the next render saw the new one
with the same desired value.

The revision input is now chosen per credential. An active inline value
contributes its digest alone, so a rotation rolls once. An external or
chart-generated Secret keeps the live resourceVersion, which is the only
signal it has. Server admin and token select independently.

Tests pin the exact annotation for four source combinations, so the parts
list cannot change unnoticed; the existingSecret cases set an inline value
too and prove it stays out. A render carries no live Secret, so the lookup
half stays for the lifecycle run.

Answers the open thread on #221.
The OnDelete procedure told the operator to wait for the replaced Store to
show Up in PD. That is not a recovery barrier. StoreNodeService.register()
persists StoreState.Up and only then notifies the Store, where
HgStoreEngine.stateChanged starts restoreLocalPartitionEngine(); a failure
there is logged and the state stays Up. The Store is Up before it has
restored anything, so an operator following the old text could delete the
next replica while the first was still rejoining.

Replace it with the strongest check these images support: per shard group,
the full shard count, exactly one leader, and the replaced Store back in the
groups it holds, read from the PD leader through /v1/shardGroups.

State the residue plainly. That is PD's membership record, not proof that
the Store finished loading its partitions and caught up, and no endpoint
reports restoration-complete, so the text asks for a margin, keeps the PDB
floor as the backstop, and names the missing image-side signal.

Answers the P1 recheck on #221.
HugeConfig reads the Server properties file through
Configurations.properties(), whose reader trims the value, so a password with
a trailing space or tab was stored padded in the Secret and applied to the
first-created admin account trimmed. Authenticating with the value the Secret
holds then fails, with nothing in the install to suggest why.

The admin-password pattern already refused leading whitespace but accepted a
trailing space, tab or form feed; pd.auth.value accepted a trailing space.
Both now require the last character to be non-whitespace, and a single
character value stays valid. An inner space is still allowed, because only
the ends are trimmed.

Schema aborts cannot be matched by the unit framework at v1.0.0, so the CI
job gains three must-fail cases and one positive control for the inner space.

Answers the P2 recheck on #221.
Two halves of the same mistake: the chart guarded PD and Store shrinks but
let a PD increase through, and the documented Store drain named two tasks
that cannot retire a healthy Store.

PD. The rendered peer list reaches raft only as NodeOptions.setInitialConf,
which jraft applies when a node bootstraps without its own configuration. On
an initialized group a 3-to-5 upgrade adds Pods and leaves the voting
configuration at three. Membership moves through RaftEngine.changePeerList,
which the PD client API reaches and no REST route exposes, so the chart
refuses the upgrade and says where the operation actually lives instead of
offering a sequence nobody has run here.

Store. patrolPartitions reallocates groups whose shard count is wrong and
hands off groups belonging to Stores already in Tombstone; balancePartitions
spreads shards over the active Stores, the leaving one included. Neither
retires it, so the old completion condition could never arrive. The
procedure now transitions the leaving Store to Tombstone, the same path the
recovery runbook uses, checks the remaining Stores still satisfy the
persisted replication factor, maps ordinals to Store ids, and waits on
/v1/shardGroups before the StatefulSet shrinks.

The guard needs a live StatefulSet, so a fresh install at five PD replicas
still renders; that case is pinned in the topology suite.

Answers two P2 rechecks on #221.
PD's /v1/health returns 200 as soon as Jetty is up and never consults raft.
With three PDs that is the right liveness signal, because restarting a
follower for a normal election would turn one election into a rolling
outage. With one PD there is no election to lose: a PD that steps down and
cannot recover, as after a failed raft snapshot on a full disk
(apache#3222), keeps answering /v1/health while serving no writes,
and liveness never restarts it.

Add pd.livenessPath, empty by default and derived: /v1/health above one
replica, /v1/ready at one. The startup probe follows the same path, because
Kubernetes suppresses liveness until startup succeeds, so leaving startup on
/v1/health would give a single PD only the 60 s liveness budget to reach raft
readiness after a restart and a slow log replay would crash-loop; following
liveness charges that window to the 300 s startup budget instead.

Multi-PD renders are unchanged. The value stops being needed if PD starts
answering 503 from /v1/health in this state.

Reported with measurements by @SebastianGruza on #221.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add an official Helm chart for distributed HStore deployment

2 participants