diff --git a/.github/actions/helm-checks/action.yml b/.github/actions/helm-checks/action.yml
index 344e4655dd..f669ec060b 100644
--- a/.github/actions/helm-checks/action.yml
+++ b/.github/actions/helm-checks/action.yml
@@ -40,6 +40,8 @@ runs:
helm lint helm/hugegraph -f helm/hugegraph/values-strict.yaml
helm lint helm/hugegraph -f helm/hugegraph/examples/values-seatunnel-standalone.yaml
helm lint helm/hugegraph -f helm/hugegraph/examples/values-seatunnel-cluster.yaml
+ helm lint helm/hugegraph --set vermeer.enabled=true
+ helm lint helm/hugegraph --set vermeer.enabled=true --set vermeer.mode=cluster --set mode=strict
- name: packaged chart regression
shell: 'bash -e {0}'
@@ -49,6 +51,14 @@ runs:
shell: 'bash -e {0}'
run: python3 -m unittest discover -s helm -p 'test_helm_seatunnel.py' -v
+ - name: Vermeer integration regression
+ shell: 'bash -e {0}'
+ run: python3 -m unittest discover -s helm -p 'test_helm_vermeer*.py' -v
+
+ - name: Shared metadata namespace regression
+ shell: 'bash -e {0}'
+ run: python3 -m unittest discover -s helm -p 'test_helm_metadata_cluster.py' -v
+
- name: helm unittest
shell: 'bash -e {0}'
run: |
@@ -63,7 +73,8 @@ runs:
shell: 'bash -e {0}'
run: |
for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml" "-f helm/hugegraph/values-strict.yaml" \
- "-f helm/hugegraph/examples/values-seatunnel-standalone.yaml" "-f helm/hugegraph/examples/values-seatunnel-cluster.yaml"; do
+ "-f helm/hugegraph/examples/values-seatunnel-standalone.yaml" "-f helm/hugegraph/examples/values-seatunnel-cluster.yaml" \
+ "--set vermeer.enabled=true" "--set vermeer.enabled=true --set vermeer.mode=cluster --set mode=strict"; do
# shellcheck disable=SC2086 # $preset intentionally splits into flags
helm template ci helm/hugegraph $preset > /dev/null
done
@@ -501,7 +512,8 @@ runs:
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" "-f helm/hugegraph/values-strict.yaml" \
- "-f helm/hugegraph/examples/values-seatunnel-standalone.yaml" "-f helm/hugegraph/examples/values-seatunnel-cluster.yaml"; do
+ "-f helm/hugegraph/examples/values-seatunnel-standalone.yaml" "-f helm/hugegraph/examples/values-seatunnel-cluster.yaml" \
+ "--set vermeer.enabled=true" "--set vermeer.enabled=true --set vermeer.mode=cluster --set mode=strict"; 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
diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml
index e38988e2a7..1a835b045b 100644
--- a/helm/hugegraph/Chart.yaml
+++ b/helm/hugegraph/Chart.yaml
@@ -17,9 +17,9 @@
apiVersion: v2
name: hugegraph-helm
-description: Helm chart for Apache HugeGraph HStore cluster with optional SeaTunnel
+description: Helm chart for Apache HugeGraph HStore cluster with optional SeaTunnel and Vermeer
type: application
-version: 0.3.0
+version: 0.4.0
# TODO: set appVersion to "1.8.0" when the release images are published.
appVersion: "latest"
kubeVersion: ">=1.23.0-0"
@@ -33,6 +33,7 @@ icon: https://hugegraph.apache.org/img/logo.svg
sources:
- https://github.com/apache/hugegraph
- https://github.com/apache/seatunnel
+ - https://github.com/apache/hugegraph-computer/tree/master/vermeer
maintainers:
- name: HugeGraph Community
url: https://hugegraph.apache.org/
diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md
index 597beebd77..37351b141d 100644
--- a/helm/hugegraph/README.md
+++ b/helm/hugegraph/README.md
@@ -9,6 +9,8 @@ are not supported.
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.
+SeaTunnel and Vermeer are **disabled by default**, including in the single-node
+and cluster presets. Optional components add resources only when enabled.
## Install a published Chart
@@ -81,28 +83,63 @@ helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
Inactive credential values may remain in the values file when auth is disabled.
-### Optional SeaTunnel
+### Optional components
-Set `seatunnel.enabled=true` to start one persistent hybrid member alongside HugeGraph;
+| Component | Default | Enable when needed |
+|---|---|---|
+| Hubble | Enabled | `hubble.enabled=false` selects API-only deployment |
+| SeaTunnel | Disabled | `seatunnel.enabled=true` adds data import/export and its native UI |
+| Vermeer | Disabled | `vermeer.enabled=true` adds graph computation, Hubble integration and its native UI |
+
+
+SeaTunnel: optional import, export and migration
+
+Set `seatunnel.enabled=true` to start one long-running hybrid member alongside HugeGraph;
set `seatunnel.mode=cluster` to use multiple members of the same native StatefulSet
(two by default). SeaTunnel is disabled by default and its topology is independent
of the HugeGraph single-/multi-node presets. Both modes include the engine's task UI.
-The HTTP Service inherits NodePort in `easy` mode or ClusterIP in `strict` mode;
-`seatunnel.service` can explicitly override the type and NodePort.
+The native HTTP Service remains ClusterIP in both easy and strict mode.
+Use an authorized port-forward when native administration is needed.
-Member/CLI port 5801 is restricted to same-release peers by a SeaTunnel ingress
-NetworkPolicy in every mode, including when the chart-wide policy flag is disabled.
-Easy mode leaves the authenticated HTTP entry open. Actual isolation requires CNI
-support and trusted namespace RBAC; full isolation additionally limits egress.
+HTTP and member/CLI port 5801 are restricted to necessary same-release callers
+in both modes, including when the chart-wide policy flag is disabled. Strict
+additionally limits egress. Actual isolation needs a supporting CNI and trusted
+namespace RBAC.
The chart injects the same-release Server address, GraphSpace and administrator
Secret references into the mounted Source/Sink examples. Create the target graph
before submitting them manually; Helm does not create graphs or import data.
-UI and REST submission use a separate native HTTP Basic administrator account,
-whose generated Secret is retained and reused during online upgrades. Container
+UI and REST submission use a separate native HTTP Basic administrator account
+with a newly generated 6-character password in `easy` or 16 characters in `strict`; online upgrades reuse the
+existing Secret, including credentials created by older releases. Container
security and scheduling options follow the other components; see the
[SeaTunnel deployment guide](docs/seatunnel.md) for configuration and limits.
+
+
+
+Vermeer: optional graph computation with Hubble
+
+Enable Vermeer with `--set vermeer.enabled=true`. The default
+`vermeer.mode=standalone` runs one Master and one Worker as two containers in
+one Pod. Set `vermeer.mode=cluster` for one Master Pod and multiple Worker Pods
+(`vermeer.worker.replicas=3` by default). Both HugeGraph presets leave Vermeer
+disabled; choose the compute topology explicitly when enabling it.
+
+With Hubble enabled in its default PD mode, the chart configures the internal
+Master URL and service token automatically. Sign in to Hubble as the HugeGraph
+administrator, select an HStore graph, then load it and run an algorithm. The
+existing flow includes reload, task status/cancellation, paginated results and
+unload; it does not write results back into HugeGraph properties.
+
+Native Vermeer UI/API stays private behind ClusterIP in both modes. Optional
+administration uses port-forward and a separate generated password: 6 characters in `easy`, 16 in `strict`;
+Hubble needs no manual token configuration. Both engines always authenticate
+access, and their native HTTP/RPC policies allow only required internal callers.
+See the [Vermeer deployment guide](docs/vermeer.md) for administration and recovery.
+
+
+
### Internal image registry
Mirror the four component images (`hugegraph/pd`, `hugegraph/store`,
@@ -118,6 +155,11 @@ 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).
+When enabled, also mirror `apache/seatunnel` and/or `hugegraph/vermeer` at the
+configured versions. Vermeer token initialization reuses its own image.
+The optional Vermeer image is pinned to the officially published Linux
+amd64/arm64 index. See [verified image and custom overrides](docs/vermeer.md#image-compatibility)
+for its tag, digest, source and publishing run. Normal enablement needs no image override.
### Storage
@@ -139,6 +181,18 @@ 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.
+Vermeer adds separate Master and Worker data claims: two in standalone mode,
+or one Master claim plus one per Worker in cluster mode. Its storage flags
+inherit `persistence.enabled`; see [Vermeer storage](docs/vermeer.md#storage-and-recovery).
+
+### PD metadata namespace
+
+`metadataCluster` defaults to `hg-test`, preserving the namespace used by existing
+Server images. The chart gives Server and Store the same value so Store scans
+can find the Server's Schema. This is separate from a graph space or Raft group.
+Changing it does not migrate metadata; upgrades reject changes to a recorded
+namespace. See [metadata namespace adoption](docs/operations.md#pd-metadata-namespace)
+before upgrading a deployment that used custom metadata settings.
### Server service identity
@@ -182,13 +236,21 @@ still need verification with a running Linux cluster. See the
## Modes and entry points
+`values-single.yaml` and `values-cluster.yaml` select the HugeGraph topology.
+The independent `vermeer.mode` and `seatunnel.mode` select optional engine
+topologies; none of these presets enables an optional engine automatically.
+
`mode: easy` is the default. `mode: strict` changes Server/Hubble to ClusterIP
-and enables NetworkPolicy; authentication, Hubble and persistence stay enabled.
+and enables full NetworkPolicy isolation; authentication, Hubble and persistence stay enabled.
Use `--set mode=strict` or `-f helm/hugegraph/values-strict.yaml`.
**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.
+`persistence.enabled` and `global.imageRegistry`. SeaTunnel and Vermeer native
+Services are always ClusterIP and authenticated;
+their HTTP/RPC ingress stays restricted in both modes. No mode requires HTTPS.
+
+Server/Hubble entry choices:
| Entry | Access | When to use it |
|---|---|---|
@@ -206,7 +268,7 @@ Omission or `0` allocates automatically. Kubernetes validates the actual cluster
range; the chart does not force `30000–32767`.
A [hostless HTTP Ingress example](examples/values-http-ingress.yaml) is available.
-With NetworkPolicy enabled, add native rules for actual clients and controllers;
+With base-service NetworkPolicy enabled, add 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.
@@ -221,6 +283,9 @@ with `-f`; see [values.yaml](values.yaml) for all fields and their comments.
| `mode` | Entry and policy defaults | `easy` |
| `pd.replicas` / `store.replicas` / `server.replicas` | Component replicas | `3` / `3` / `3` |
| `hubble.enabled` | Install the UI | `true` |
+| `seatunnel.enabled` / `vermeer.enabled` | Install optional data/computation engines | `false` / `false` |
+| `vermeer.mode` | One Pod with two containers, or separate Master/Worker Pods | `standalone` |
+| `vermeer.worker.replicas` | Worker Pods in Vermeer cluster mode | `3` |
| `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` |
@@ -246,7 +311,7 @@ with `-f`; see [values.yaml](values.yaml) for all fields and their comments.
- `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`,
+- Base PD/Server/Hubble 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
diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md
index 8728df0b28..7e1ebf5b24 100644
--- a/helm/hugegraph/docs/distribution.md
+++ b/helm/hugegraph/docs/distribution.md
@@ -48,11 +48,11 @@ helm install hugegraph ./hugegraph-helm -n hugegraph \
```
Development packages append a workflow run number to the source chart version,
-for example `0.2.0-dev.123`. They may use `latest` service images. Helm OCI tags
+for example `0.4.0-dev.123`. They may use `latest` service images. Helm OCI tags
come from the chart's SemVer version; `helm-dev` and `latest` are not Chart versions.
Formal packages use the exact `Chart.version` maintained in source, for example
-`0.2.0`. They are published only after the corresponding HugeGraph service release
+`0.4.0`. They are published only after the corresponding HugeGraph service release
exists and the chart selects published fixed component images. A source tag
such as `helm-1.8.0` therefore denotes an already released service, not an
advance preview. Chart fixes may be released independently from a reviewed master
@@ -138,3 +138,15 @@ default Store wait container and Helm test hook reuse component images. Explicit
shown in the [private registry example](../examples/values-internal-registry.yaml).
The mirror must preserve the selected image references and support the cluster's
CPU architecture.
+
+SeaTunnel and Vermeer are disabled by default in every HugeGraph preset. When
+enabling either, also transfer its configured `apache/seatunnel` or
+`hugegraph/vermeer` image. Vermeer Master, Worker and token initialization use
+the same Vermeer image. Its default is the officially published
+`hugegraph/vermeer:helm-30b67d93e7d0`, pinned by a Linux amd64/arm64 OCI index
+digest. Preserve that index and the required platforms when mirroring it.
+The [Vermeer image reference](vermeer.md#image-compatibility) records the full
+digest, source commit, successful official publishing run and custom-tag override
+command. A tag override must also clear `vermeer.image.digest`, or set a different
+verified digest; the default digest otherwise continues to win. Hubble needs its
+native Vermeer adapter when selecting a custom build.
diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md
index b12fc95873..46c8c489bc 100644
--- a/helm/hugegraph/docs/operations.md
+++ b/helm/hugegraph/docs/operations.md
@@ -12,13 +12,13 @@ new version for upgrades. See [version selection](distribution.md#choose-a-versi
## Credentials
-Normal Helm installations generate/reuse three kept Secrets:
+The base installation generates/reuses three kept Secrets:
| Default Secret | Data key | Readers |
|---|---|---|
-| `-admin` | `password` | Server, Helm test |
+| `-admin` | `password` | Server, Helm test; enabled engines' deployment access |
| `-auth-token` | `token_secret` | All Server replicas |
-| `-pd-auth` | `secret-key` | PD, Server storage wait, Hubble |
+| `-pd-auth` | `secret-key` | PD, Server storage wait, Hubble; enabled Vermeer |
`existingSecret` takes priority over an inline value, which takes priority over
auto-generation. Existing Secrets are not managed by the chart. Override their
@@ -29,6 +29,16 @@ 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.
+Optional SeaTunnel and Vermeer are disabled in every HugeGraph preset. When
+enabled, they add independent entry credentials: SeaTunnel's native HTTP Basic
+account and Vermeer's separate native administrator password. New human
+passwords are 6 random alphanumeric characters in `easy`, 16 in `strict`; upgrades and mode changes retain existing Secrets. Internal Vermeer
+service authentication is automatic and separate from these passwords. These
+credentials do not replace Server or PD credentials. Their Secret reuse, external-source and rotation procedures are in
+the [SeaTunnel guide](seatunnel.md#seatunnel-entry-authentication) and
+[Vermeer guide](vermeer.md#deployment-credentials-and-gitops). Account password
+changes must also refresh dependent engines' deployment credentials.
+
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:
@@ -140,6 +150,11 @@ 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.
+The seven-volume base example does not allocate optional Vermeer data volumes.
+Vermeer uses separate Master/Worker claims even when both containers share one
+standalone Pod; see [storage and recovery](vermeer.md#storage-and-recovery).
+Its persisted metadata does not provide transparent task or in-memory result
+recovery. SeaTunnel's checkpoint directory is temporary and adds no PVCs.
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.
@@ -274,20 +289,61 @@ 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.
+## PD metadata namespace
+
+`metadataCluster` is the shared PD metadata prefix for Server and Store. It
+defaults to `hg-test`, matching the existing Server default. The chart sets
+`HG_SERVER_CLUSTER` and `PD_CLUSTER` together; use this shared field rather than
+overriding either environment variable or adding `-Dpd.cluster` to Store options.
+It is independent of graph spaces, partition identifiers and Raft membership.
+
+Keep the initialized value when upgrading. A different prefix does not migrate
+Schema or graph metadata, and the chart rejects a change from the namespace
+recorded on the existing Server Deployment. Standard older installations using
+`hg-test` need no adoption step.
+
+For an older custom image or a cluster value supplied through `valueFrom`, first
+inspect the running Server configuration and startup log (`Meta cluster bound
+to ...`) to establish its actual existing prefix. Only after verifying that
+value, record it on the Server Deployment and retain it in the Helm values:
+
+```bash
+# Replace current-prefix only with the verified existing Server metadata prefix.
+kubectl -n hugegraph annotate deployment hugegraph-server \
+ hugegraph.apache.org/metadata-cluster=current-prefix --overwrite
+# Set metadataCluster: current-prefix in the reviewed values used for upgrade.
+```
+
+This records an existing namespace; it is not a migration command. A conflicting
+literal `HG_SERVER_CLUSTER` is still rejected. With Store `updateStrategy=OnDelete`,
+apply the existing one-at-a-time Store update procedure before expecting a new
+`PD_CLUSTER` environment value to take effect. Retain data volumes throughout
+that procedure; restarting a temporary Store is not a data-preserving upgrade.
+
+If Vermeer loading reports a missing vertex/edge label from Store, compare this
+prefix on Server and Store before changing Schema or retrying data imports.
+
## 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.
+Easy mode leaves full base-service NetworkPolicy isolation off; strict enables
+it. Explicit base-service settings win. SeaTunnel and Vermeer always use private
+ClusterIP Services, authentication and native HTTP/RPC ingress restrictions,
+including with `networkPolicy.enabled=false`. Their extra ingress rules require
+explicit internal Pod/namespace selectors; IP blocks and allow-all forms are
+rejected. Strict additionally restricts engine egress. Actual isolation needs
+a NetworkPolicy-capable CNI.
+
+For base PD/Server/Hubble services, generated full-isolation rules allow component
+traffic and DNS. Admit actual outside clients/controllers with their
+`extraIngress`, and external destinations with `extraEgress`. Selecting strict
+mode and overriding a base Service to NodePort/LoadBalancer, setting `externalIPs`
+or enabling Ingress does not admit callers automatically. NOTES identifies
+exposed base components with no outside ingress rule. Empty outside rule lists
+can block clients; `{}` is an allow-all form for base-service rules only.
+Consider NodePort source NAT when selecting source ranges. For Hubble discovery
+via an external `server.advertiseUrl`, also configure Hubble's `extraEgress`.
+Native engine administration uses authorized port-forwarding and its separate
+administrator password (newly generated: 6 characters in `easy`, 16 in `strict`); Hubble service tokens stay automatic.
Client Services accept native Kubernetes traffic and load-balancer fields under
`pd.service`, `server.service` and `hubble.service`. LoadBalancer-specific fields
diff --git a/helm/hugegraph/docs/seatunnel.md b/helm/hugegraph/docs/seatunnel.md
index 14a209a2c2..01becac622 100644
--- a/helm/hugegraph/docs/seatunnel.md
+++ b/helm/hugegraph/docs/seatunnel.md
@@ -4,7 +4,7 @@ This chart adapts the [SeaTunnel 3.0.0 native hybrid Kubernetes deployment](http
for HugeGraph resource names, DNS, images, configuration and replica counts.
It does not introduce a subchart, operator, CRD or derived SeaTunnel image.
-SeaTunnel is disabled by default. When enabled, a persistent hybrid member both
+SeaTunnel is disabled by default. When enabled, a long-running hybrid member both
schedules and executes jobs. Cluster mode scales the same StatefulSet to multiple
members; it is not a `local-mode` Pod or Job that exits after one submission.
@@ -12,7 +12,7 @@ members; it is not a `local-mode` Pod or Job that exits after one submission.
|---|---|
| `values-single.yaml` | HugeGraph 1 PD + 1 Store + 1 Server using HStore |
| `values-cluster.yaml` | HugeGraph's own multi-replica topology |
-| `seatunnel.mode: standalone` | One persistent hybrid member |
+| `seatunnel.mode: standalone` | One long-running hybrid member |
| `seatunnel.mode: cluster`, `replicas: 2` | Two hybrid members; at least two are required |
Choose HugeGraph and SeaTunnel topologies independently. Top-level `mode: easy/strict`
@@ -24,7 +24,7 @@ the same built-in UI and API.
From the HugeGraph repository root, choose one installation:
```bash
-# HugeGraph 1 PD + 1 Store + 1 Server, plus one persistent SeaTunnel member
+# HugeGraph 1 PD + 1 Store + 1 Server, plus one long-running SeaTunnel member
helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
-f helm/hugegraph/values-single.yaml \
-f helm/hugegraph/examples/values-seatunnel-standalone.yaml \
@@ -37,18 +37,11 @@ helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
--wait --timeout 30m
```
-The default image is `apache/seatunnel:3.0.0`. Both modes expose the UI and REST
-API on HTTP 8080. Easy mode inherits a NodePort, while strict mode inherits
-ClusterIP. Override `seatunnel.service.type/nodePort` explicitly when needed.
-`helm install` prints access instructions; retrieve an automatically allocated
-HTTP NodePort with:
-
-```bash
-kubectl -n hugegraph get svc hugegraph-seatunnel \
- -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}{"\n"}'
-```
-
-For any Service type, port forwarding provides a local entry:
+The default image is `apache/seatunnel:3.0.0`. Both modes keep the native UI
+and REST API on a private ClusterIP Service, HTTP 8080. NodePort, LoadBalancer,
+fixed node ports and external IPs are not supported. `helm install` prints
+credential retrieval and optional administration commands. Forward the private Service
+in a separate terminal when native administration or a manual REST submission is needed:
```bash
kubectl -n hugegraph port-forward svc/hugegraph-seatunnel 8081:8080
@@ -59,14 +52,12 @@ described below. The UI shows cluster members, jobs, DAGs and logs. Submit jobs
through the CLI or authenticated REST API. Helm starts the engine and prepares
examples; it does not create graphs or run jobs.
-Only HTTP 8080 is exposed through NodePort or LoadBalancer. A separate ingress
-NetworkPolicy always restricts Hazelcast member/CLI port 5801 to same-release
-SeaTunnel peers, even in easy mode or with `networkPolicy.enabled=false`.
-Easy mode permits HTTP callers without adding an ingress rule and leaves egress
-unrestricted. When full NetworkPolicy isolation is enabled, explicitly allow
-HTTP callers and external source/sink destinations with
-`networkPolicy.seatunnel.extraIngress/extraEgress`. Explicit extra ingress rules
-can also broaden member access, so restrict them to trusted callers.
+Native HTTP 8080 and Hazelcast member/CLI port 5801 are restricted to their
+required release callers in both easy and strict mode, including when
+`networkPolicy.enabled=false`. Strict additionally restricts egress.
+Additional internal ingress uses explicit Pod/namespace selectors in
+`networkPolicy.seatunnel.extraIngress`; unrestricted and IP-based rules are
+rejected. External source/sink destinations can use `extraEgress` when needed.
Actual isolation requires a NetworkPolicy-capable CNI. Pod selectors are not
native Hazelcast authentication: use trusted namespace RBAC/admission controls
@@ -99,8 +90,10 @@ reused after re-enabling or reinstalling.
The UI, static resources and REST submission endpoints use native HTTP Basic
authentication. There is no added proxy or login service. `seatunnel.auth.username`
-defaults to `admin`. An empty password generates a strong password in
-`-auth`; online upgrades reuse the existing password.
+defaults to `admin`. An empty password creates a random alphanumeric password: 6 characters in `easy`, 16 in `strict`, stored in
+`-auth` for a new installation. Online upgrades reuse
+the existing password, including on mode changes and for longer credentials from older releases.
+Authentication stays enabled in both easy and strict mode.
Use a controlled values file to set or rotate `seatunnel.auth.username/password`.
Values cannot contain leading/trailing whitespace or `${...}` placeholders, and
@@ -108,8 +101,18 @@ usernames cannot contain a colon. Internal spaces and quotes are accepted.
Pin `seatunnel.auth.password` for offline templates or GitOps rendering; automatic
password reuse requires an online Helm lookup.
-The default release uses Secret `hugegraph-seatunnel-auth`. With port forwarding
-active, read its credentials into local variables and call REST:
+The default release uses Secret `hugegraph-seatunnel-auth`. Open the forwarded
+UI as `admin` with the password read by:
+
+```bash
+kubectl -n hugegraph get secret hugegraph-seatunnel-auth \
+ -o jsonpath='{.data.password}' | base64 --decode
+```
+
+
+Optional: call authenticated REST
+
+With port forwarding active, read the credentials into local variables and call REST:
```bash
ST_USERNAME="$(kubectl -n hugegraph get secret hugegraph-seatunnel-auth \
@@ -120,9 +123,11 @@ ST_PASSWORD="$(kubectl -n hugegraph get secret hugegraph-seatunnel-auth \
curl -fsS --user "$ST_USERNAME:$ST_PASSWORD" http://127.0.0.1:8081/overview
```
-Use the same credentials for the browser Basic login and any NodePort or
-LoadBalancer entry. External job submissions must authenticate to the protected
-HTTP entry using the [official REST API](https://github.com/apache/seatunnel/blob/v3.0.0/docs/en/engines/zeta/rest-api-and-web-ui.md).
+
+
+Use the same credentials for the native browser Basic login and REST requests
+through an authorized port-forward or trusted internal caller. REST submissions
+follow the [official REST API](https://github.com/apache/seatunnel/blob/v3.0.0/docs/en/engines/zeta/rest-api-and-web-ui.md).
The Secret contains `username`, `password` and the final `seatunnel.yaml`.
The engine configuration is mounted directly from the Secret, not embedded in
@@ -135,9 +140,9 @@ SeaTunnel authentication.
HTTP Basic does not authenticate the Hazelcast member/CLI protocol on 5801.
The examples below execute the CLI inside an engine Pod under Kubernetes RBAC;
-external clients should use REST. This is a single administrator entry, not
-per-user/per-job authorization or HugeGraph RBAC/SSO. Use an existing TLS entry
-when exposing the HTTP service outside a trusted network.
+manual clients use authenticated REST through an authorized port-forward.
+This is a single administrator entry, not per-user/per-job authorization or
+HugeGraph RBAC/SSO. Keep native services private.
## Images, credentials and native configuration
@@ -261,7 +266,7 @@ kubectl -n hugegraph exec sts/hugegraph-seatunnel -- \
--config /opt/seatunnel/config/hugegraph-source.conf
```
-`--master cluster` submits to the persistent engine, including standalone mode.
+`--master cluster` submits to the long-running engine, including standalone mode.
On an initially empty graph, expect a finished Sink with ten rows read/written,
and a Source with ten rows read into the executing member's log. Inspect UI
status/DAG, all member logs and the actual graph data:
diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md
new file mode 100644
index 0000000000..541e55e790
--- /dev/null
+++ b/helm/hugegraph/docs/vermeer.md
@@ -0,0 +1,333 @@
+# Deploy Vermeer with HugeGraph and Hubble
+
+Vermeer is optional and disabled by default, including with `values-single.yaml`
+and `values-cluster.yaml`. Enabling it deploys the native Master/Worker engine,
+configures Hubble automatically, and keeps its authenticated native UI/API private.
+For normal use, enable Vermeer and sign in to Hubble with the existing HugeGraph
+administrator account. Native administration is optional.
+It needs no Computer Operator, CRD, etcd or external coordination service.
+
+## Choose a topology
+
+| Setting | Result |
+|---|---|
+| `values-single.yaml` | HugeGraph: 1 PD + 1 Store + 1 Server, plus Hubble |
+| `values-cluster.yaml` | HugeGraph: 3 PD + 3 Store + 3 Server, plus Hubble |
+| `vermeer.mode: standalone` | One Pod containing separate Master and Worker containers |
+| `vermeer.mode: cluster` | One Master Pod plus `vermeer.worker.replicas` Worker Pods; default 3 |
+| `mode: easy` / `strict` | HTTP entry, network policies and generated-password defaults; independent of both topologies |
+
+Both Vermeer modes run the same two native process roles. Standalone co-locates
+them for a smaller deployment; cluster mode distributes the Workers. A cluster
+still has one Master: adding Workers does not provide Master high availability.
+Hubble's native loader requires HStore, PD discovery and `hubble.mode=pd`; the
+HugeGraph single-node preset meets these requirements. A RocksDB-only Server
+does not provide the same direct Store-loading path.
+
+Server and Store also need the same PD metadata prefix. The shared
+`metadataCluster` defaults to the existing Server namespace `hg-test` and wires
+both components consistently. Retain the initialized value on upgrade; see
+[namespace adoption](operations.md#pd-metadata-namespace) for older custom images.
+
+## Install and enable
+
+Commands run from the HugeGraph repository root. Choose one installation:
+
+```bash
+# Small HugeGraph topology, plus one Pod with Master and Worker containers
+helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
+ -f helm/hugegraph/values-single.yaml \
+ --set vermeer.enabled=true --set vermeer.mode=standalone \
+ --wait --timeout 20m
+```
+
+```bash
+# Multi-node HugeGraph topology, plus one Master and three Worker Pods
+helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
+ -f helm/hugegraph/values-cluster.yaml \
+ --set vermeer.enabled=true --set vermeer.mode=cluster \
+ --set vermeer.worker.replicas=3 --wait --timeout 20m
+```
+
+The two choices are independent: a small HugeGraph deployment can also use
+multiple Vermeer Workers. To enable Vermeer on an existing release, preserve its
+complete reviewed values and use `helm upgrade` with the same `vermeer` settings.
+Neither installation creates a sample graph, loads data or runs an algorithm.
+
+For a release created before Vermeer was added, `--reuse-values` cannot import
+the new component defaults. Export its effective values to a private file, review
+them, then pass the file without `--reuse-values`:
+
+```bash
+umask 077
+helm get values hugegraph -n hugegraph --all -o yaml > existing-values.yaml
+helm upgrade hugegraph ./helm/hugegraph -n hugegraph \
+ -f existing-values.yaml --set vermeer.enabled=true \
+ --wait --timeout 20m
+```
+
+Resource names honor `fullnameOverride`. With the default release name, the
+Master StatefulSet and HTTP Service are `hugegraph-vermeer-master`; its Pod is
+`hugegraph-vermeer-master-0`. Cluster Workers use StatefulSet
+`hugegraph-vermeer-worker`. Standalone has no separate Worker StatefulSet.
+
+## Use Hubble
+
+Keep Hubble enabled in its default PD mode and sign in using the existing
+HugeGraph administrator account. The chart sets `vermeer.url` to the internal
+Master Service and provides a service-token file. No address or token entry is
+required in Hubble. Select a graph and open batch analysis:
+
+1. Load or reload the selected graph into Vermeer.
+2. Choose one of Hubble's nine supported algorithms and submit it.
+3. Inspect asynchronous task status, cancel an unfinished task when needed,
+ and open its paginated vertex results.
+4. When needed, unload the compute copy through Hubble's existing administrator
+ API. The current page provides Load/Reload controls, not a separate Unload button.
+
+For example, an authenticated Hubble session can send
+`DELETE /api/v1.3/vermeer/DEFAULT-vermeer_demo` with this JSON body:
+
+```json
+{"graphspace":"DEFAULT","graph":"vermeer_demo"}
+```
+
+This removes the Vermeer copy and retains the HugeGraph source graph. Refresh the
+analysis page to see its unloaded state before loading it again.
+
+Results remain queryable through the native task API while their cache exists;
+they are not written into HugeGraph vertex properties. A successful historical
+task does not guarantee its in-memory results survive a restart. Direct Store
+scans bypass Server label permissions, so Hubble restricts this flow to HugeGraph
+super administrators. Ordinary graph readers retain their existing Server task
+view; the Vermeer service token does not grant them this administrator access.
+
+Setting `hubble.enabled=false` keeps Vermeer available through its native entry.
+`hubble.mode=direct` cannot be combined with an enabled native Vermeer integration.
+
+## Optional native administration
+
+Vermeer HTTP/UI is a private ClusterIP Service in both easy and strict mode.
+Use Hubble for graph computation. When native administration is needed, forward
+its Master HTTP listener to your local machine in a separate terminal:
+
+```bash
+kubectl -n hugegraph port-forward svc/hugegraph-vermeer-master 6688:6688
+```
+
+Open . Read the separate Vermeer administrator password:
+
+```bash
+kubectl -n hugegraph get secret hugegraph-vermeer-admin \
+ -o jsonpath='{.data.password}' | base64 --decode
+```
+
+Paste it into the **Admin credential** input; no username is required. New installations generate a
+6-character random alphanumeric password in `easy` or 16 characters in `strict`.
+Existing credentials are reused across online upgrades and mode changes. Successful login creates a versioned session cookie valid for
+24 hours. This password is separate from the HugeGraph/Hubble login. Hubble's
+service authentication is configured automatically and needs no manual token.
+
+Only Master reads the native administrator password. Set
+`vermeer.auth.admin.password` through controlled values, or reference an external
+Secret using `vermeer.auth.admin.existingSecret/key` (default key `password`).
+With neither source set, `vermeer.auth.admin.autoGenerate=true` creates the kept
+`-vermeer-admin` Secret. External Secret takes priority over the inline
+password and auto-generation. Read the actual Secret/key when overriding them.
+Changing an external administrator Secret requires restarting Master; this
+password rotation does not change Hubble's internal service token.
+
+Authentication stays enabled in both modes. Native Services do not support
+NodePort, LoadBalancer, fixed node ports or external IPs. NetworkPolicy limits
+Master HTTP and internal RPC to the required release callers even in easy mode;
+strict additionally controls egress. These policies need a supporting CNI.
+Additional native ingress uses explicit internal Pod/namespace selectors under
+`networkPolicy.vermeer.extraIngress`; unrestricted and IP-based ingress rules
+are rejected. External data destinations can use `extraEgress` when needed.
+
+## Deployment credentials and GitOps
+
+Both engines retain generated administrator credentials during online upgrades.
+For GitOps, provide stable fixed or external credentials through the matching
+component settings. Vermeer's human password uses `vermeer.auth.admin`; its
+internal service authentication is managed separately.
+
+
+Advanced: service identities, versioned tokens and GitOps
+
+Hubble service authentication uses a separate signing factor in the kept
+`-vermeer-auth` Secret, key `token-factor` by default. Online Helm
+upgrades reuse it. Configure `vermeer.auth.existingSecret/key` for an external
+Secret, or a controlled `vermeer.auth.value` for reproducible offline rendering;
+external Secret takes priority over the fixed value and auto-generation.
+Signing factors, including those in external Secrets, require at least 16
+characters; prefer a randomly generated value of 32 characters or more.
+Never place a real factor in a public values file.
+
+Master reads the factor from its mounted file. Hubble's init container reads the
+same factor and writes a long-lived native administrator service token to a
+private temporary volume; the Hubble application mounts the token, not the factor.
+The bundled `/go/bin/vermeer-token` accepts only `-factor-file` and rejects the
+inline `-factor` option. That legacy interface remains only in the source tool
+`tools/token/token_generator.go`; its older tokens are rejected by file-backed
+deployments.
+Worker group `$` provides the common pool, so newly created graph spaces do not
+require manual Worker allocation.
+
+The chart also wires both process roles to the release's PD peers and shared
+PD credential file. Worker Schema reads default to the existing deployment
+Server administrator Secret; no extra account is created. This deployment
+identity is separate from the user's Hubble session. To use an existing account
+with narrower Schema access, configure:
+
+```yaml
+vermeer:
+ hugegraphCredentials:
+ existingSecret: vermeer-schema-reader
+ key: credentials.json
+```
+
+That Secret key must contain JSON `{"username":"schema-reader","password":"..."}`.
+The account and required Schema-read permissions must already exist for every
+graph space and graph it will load. The chart does not reconcile future account
+memberships. A configured missing or invalid credential file fails the load.
+These credentials do not configure the separate native HugeGraph result writer.
+
+For a custom non-root Hubble image, set an explicit numeric `runAsUser` in
+`hubble.securityContext` or `hubble.podSecurityContext`. The token init container
+uses that same UID so Hubble can read its private `0600` file. Setting
+`runAsNonRoot=true` without a positive UID is rejected before installation.
+
+For GitOps, pin all credential sources before rendering; offline `helm template`
+cannot reuse live generated Secrets through `lookup`. Preserve the existing
+factor when adopting GitOps. Rotate it deliberately through its source values
+or secret manager, then restart Master and Hubble as directed by
+Helm NOTES. Workers do not mount this factor and need no factor-related restart.
+An external Secret change alone does not roll Pods or regenerate Hubble's token. Server password rotation also needs the existing account/Secret
+synchronization procedure in [operations](operations.md#credentials).
+
+Upgrade Master and the token helper together when adopting versioned file-mode
+tokens. Keep the factor Secret, recreate Hubble's token init container, and log
+in to the native UI again; legacy SHA1 tokens and cookies are not accepted by
+file mode.
+
+For trusted internal automation that specifically needs a raw service token,
+the native helper can generate one inside Master. This is not required for
+Hubble or the native UI password login:
+
+```bash
+kubectl -n hugegraph exec hugegraph-vermeer-master-0 -c master -- \
+ /go/bin/vermeer-token -factor-file /var/run/vermeer/auth/token-factor \
+ -user admin -space '$DEFAULT' -client helm -i -quiet
+```
+
+Keep this long-lived token private. Use `Authorization: Bearer ` only for
+its intended internal API integration.
+
+
+
+
+Advanced: image compatibility and publishing
+
+### Image compatibility
+
+The Chart pins the officially published `hugegraph/vermeer:helm-30b67d93e7d0`
+image to OCI index digest
+`sha256:f9b69ef134069b432de1c9964374d7fdbfa53245d1fe7dcbce1e75b82b69dc9e`.
+The index provides Linux `amd64` and `arm64` images built from
+[source commit 30b67d93e7d0](https://github.com/hugegraph/hugegraph-computer/commit/30b67d93e7d097374341cf46ee6a0703421512b1).
+No manual image override is needed for ordinary enablement.
+
+Publishing uses the
+[publish latest Vermeer workflow](https://github.com/hugegraph/actions/blob/master/.github/workflows/publish_latest_vermeer_image.yml).
+Custom builds must include `/go/bin/vermeer-token`, versioned HMAC-SHA256 file
+credentials, native administrator password login and space-aware Worker
+availability. Hubble needs its native Vermeer adapter and nine algorithm forms.
+A different tag alone does not establish these capabilities.
+
+`vermeer.image.digest` takes priority over the tag; a repository override also
+retains that digest. To select another reviewed tag, clear it explicitly:
+
+```bash
+helm upgrade hugegraph ./helm/hugegraph -n hugegraph -f my-values.yaml \
+ --set-string vermeer.image.tag='' \
+ --set-string vermeer.image.digest= --wait --timeout 20m
+```
+
+Retain the release's reviewed credentials, storage and metadata namespace in
+`my-values.yaml`. A custom immutable build can instead set its own verified
+`vermeer.image.digest`. Images inherit `global.imageRegistry` and
+`imagePullSecrets`; registry mirrors must preserve the selected index and
+required architectures. Master, Worker and token initialization reuse the
+Vermeer image, with no runtime package downloads.
+
+
+
+## Storage and recovery
+
+Master and Worker use separate writable data directories, including when they
+share one Pod. Each role mounts its own `/go/bin/vermeer_data` directory;
+logs use `/go/bin/vermeer_data/vermeer.log`. Never share a Pebble directory
+between process roles. Each process also works from that directory, so relative
+local output files under `./data/hubble` use its corresponding data volume.
+Keeping those files does not recreate the in-memory result-query cache.
+
+| Setting | Default |
+|---|---|
+| `vermeer.master.storage.enabled` / `worker.storage.enabled` | Inherit `persistence.enabled` |
+| `vermeer.master.storage.size` | `10Gi` |
+| `vermeer.worker.storage.size` | `20Gi` per Worker |
+| `vermeer.master.storage.storageClassName` / `worker.storage.storageClassName` | Cluster default when empty |
+
+Standalone adds two claims: the Master StatefulSet claim and the kept
+`-vermeer-standalone-worker-data` claim. Cluster adds one Master claim
+and one Worker claim per replica. Thus the default HugeGraph installation needs
+9 volumes with standalone Vermeer or 11 with its default three-Worker cluster;
+the small HugeGraph preset with standalone Vermeer needs 5. The seven-volume
+local-PV example covers only the base installation; prepare additional matching
+volumes before enabling Vermeer there.
+
+Claims are retained when disabling/uninstalling the engine or scaling Workers
+down. Explicitly disabled storage uses `emptyDir`; PVC failures never fall back
+to temporary data. Persistent Master metadata, Worker files and task history do
+not guarantee restoration of in-memory graphs, results or running tasks. After a
+restart, inspect state and reload graphs before resubmitting work as needed.
+
+Before changing topology or Worker count, stop submissions, finish/cancel tasks,
+and unload loaded compute graphs. Back up required metadata and retained files.
+Switching standalone/cluster does not migrate Worker-local data between the kept
+standalone claim and cluster Worker claims. Recheck Worker availability and reload
+graphs after the change. Do not promise uninterrupted mode switching, Master
+failover or automatic job recovery.
+
+## Verify and troubleshoot
+
+Run `helm test hugegraph -n hugegraph --logs` for the chart's enabled checks, then
+validate the actual Hubble flow with a dedicated graph and known expected
+algorithm results. Pod Ready and `/healthcheck` alone do not establish usable
+Worker allocation. Space-aware `/api/v1/workers/availability` checks whether the
+service token's space can obtain Workers.
+
+For queued loads, verify Worker registration, common-pool membership, internal
+gRPC reachability, PD discovery, Store endpoints and Server Schema credentials.
+If Store reports `no such vertex label` or `no such edge label`, verify that its
+`PD_CLUSTER` matches the Server's `HG_SERVER_CLUSTER` and recorded metadata prefix.
+For Hubble configuration errors, check matching image capabilities, PD mode,
+the native Master origin and the generated token file. For lost result pages
+after restart, rerun the graph load/computation; a retained successful task record
+does not recreate its result cache. Configure `vermeer.master.resources` and
+`worker.resources` for the real graph size and algorithm workspace; disk space
+does not replace sufficient Worker memory.
+
+
+Advanced: Worker peer connection timeout
+
+`grpc_dial_timeout` defaults to `5s`; `VERMEER_GRPC_DIAL_TIMEOUT` overrides it.
+The value must be a positive Go duration such as `500ms` or `10s`. It bounds
+blocking Worker peer dialing so an unreachable bootstrap peer fails within a
+limited time and Kubernetes can retry. It does not limit established streams
+or algorithm execution. If troubleshooting needs another budget, set the
+environment override on the affected role through `vermeer.master.extraEnv`
+or `vermeer.worker.extraEnv`.
+
+
diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md
index 562eaad3d5..85064c14ef 100644
--- a/helm/hugegraph/examples/README.md
+++ b/helm/hugegraph/examples/README.md
@@ -17,6 +17,12 @@ helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \
--set hubble.persistence.storageClassName=my-csi
```
+SeaTunnel and Vermeer are disabled in both HugeGraph presets. Enabled Vermeer
+adds two volumes in standalone mode, or one Master volume plus one per Worker
+in cluster mode. The default three-Worker cluster therefore adds four volumes.
+Prepare these separately when using the seven-volume local example below.
+See the [Vermeer storage guide](../docs/vermeer.md#storage-and-recovery).
+
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.
@@ -64,6 +70,8 @@ For a fresh temporary installation, `--set persistence.enabled=false` uses
`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.
+Vermeer Master/Worker storage flags also inherit that global choice; SeaTunnel
+checkpoints remain temporary local files rather than PVC-backed recovery storage.
## Internal images
@@ -71,11 +79,24 @@ requires a separate data migration/recovery procedure.
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.
+Enabled SeaTunnel additionally needs its configured `apache/seatunnel` image;
+enabled Vermeer needs a compatible `hugegraph/vermeer` build, reused for both
+process roles and token initialization. Mirror every enabled image and explicit
+helper override at the verified reference. See [image compatibility](../docs/vermeer.md#image-compatibility)
+before selecting Hubble/Vermeer builds.
`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`.
+When Vermeer is enabled, mirror its additional published image
+`hugegraph/vermeer:helm-30b67d93e7d0` at the Chart's pinned Linux amd64/arm64
+index digest. It is not part of the four-image default installation above.
+See the [verified image reference](../docs/vermeer.md#image-compatibility) for
+its source and publishing run. Changing only `vermeer.image.tag` leaves the
+pinned digest in effect; clear `vermeer.image.digest` explicitly for a custom
+tag, or provide that build's verified digest.
+
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
@@ -112,6 +133,16 @@ 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).
+For native administrator credentials, enabled Vermeer accepts
+`vermeer.auth.admin.password` or `vermeer.auth.admin.existingSecret/key`;
+new installations otherwise generate a kept random alphanumeric password: 6 characters in `easy`, 16 in `strict`. Advanced
+GitOps setups also preserve the automatically managed internal service identity
+through `vermeer.auth.value` or `vermeer.auth.existingSecret/key`. Optional external
+Schema credentials use `vermeer.hugegraphCredentials.existingSecret/key`; the
+referenced key contains a username/password JSON object. See
+[Vermeer deployment credentials](../docs/vermeer.md#deployment-credentials-and-gitops)
+for permissions, generated service tokens and rotation.
+
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`
@@ -138,5 +169,7 @@ access. LoadBalancer needs your infrastructure's controller.
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.
+NetworkPolicy enabled, add ingress rules for actual base-service clients.
+SeaTunnel/Vermeer native Services always remain ClusterIP and authenticated.
+Use authorized port-forwarding for native administration; their extra ingress
+rules require explicit internal Pod/namespace selectors, not allow-all rules.
diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt
index 3ec7e7bc8d..0a9f02a7f6 100644
--- a/helm/hugegraph/templates/NOTES.txt
+++ b/helm/hugegraph/templates/NOTES.txt
@@ -16,6 +16,7 @@ limitations under the License.
*/}}
{{ include "hugegraph.resolveValues" . }}
{{- $hubbleEnabled := .Values.hubble.enabled }}
+{{- $vermeerEnabled := (.Values.vermeer | default dict).enabled | default false }}
{{ .Release.Name }} installed in namespace {{ .Release.Namespace }} ({{ .Values.mode | default "legacy" }} mode).
PD {{ .Values.pd.replicas }} replica(s)
@@ -27,6 +28,9 @@ limitations under the License.
{{- if (.Values.seatunnel | default dict).enabled }}
SeaTunnel {{ .Values.seatunnel.mode }} mode
{{- end }}
+{{- if $vermeerEnabled }}
+ Vermeer {{ .Values.vermeer.mode }} mode (1 Master, {{ if eq .Values.vermeer.mode "standalone" }}1{{ else }}{{ .Values.vermeer.worker.replicas }}{{ end }} Worker{{ if eq .Values.vermeer.mode "cluster" }}(s){{ end }})
+{{- end }}
{{- $inheritedEntries := list }}
{{- range $component := list "server" "hubble" }}
{{- $cfg := get $.Values $component }}
@@ -49,6 +53,10 @@ Watch startup (run each command in a separate terminal):
{{- 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 $vermeerEnabled }}
+{{- if .Values.vermeer.master.storage.enabled }}{{ $claims = add $claims 1 }}{{ end }}
+{{- if .Values.vermeer.worker.storage.enabled }}{{ $claims = add $claims (ternary 1 (int .Values.vermeer.worker.replicas) (eq .Values.vermeer.mode "standalone")) }}{{ end }}
+{{- end }}
{{- if gt (int $claims) 0 }}
This configuration needs {{ $claims }} persistent volume claim(s). A dynamic
@@ -59,27 +67,56 @@ 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:
+{{- if $vermeerEnabled }}
+{{- $vName := include "hugegraph.vermeer.masterName" . }}
+
+ Vermeer:
+ In-cluster Master HTTP: http://{{ $vName }}.{{ .Release.Namespace }}.svc:{{ .Values.vermeer.master.httpPort }}
+ The authenticated UI/API stays on an internal ClusterIP Service in every mode.
+ Open a local connection only when needed:
+ Port-forward: kubectl port-forward -n {{ .Release.Namespace }} svc/{{ $vName }} {{ .Values.vermeer.master.httpPort }}:{{ .Values.vermeer.master.httpPort }}
+ Local native UI/API: http://127.0.0.1:{{ .Values.vermeer.master.httpPort }}/
+ Read the native management password into a local variable:
+ VERMEER_ADMIN_PASSWORD="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.vermeer.adminSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.vermeer.adminSecretKey" . | replace "." "\\." }}}' | base64 --decode)"
+ Use this password in the native UI's Admin credential field. Login creates a 24-hour session.
+ New chart-generated management passwords have {{ ternary 16 6 (eq .Values.mode "strict") }} characters; upgrades keep existing credentials.
+ This management password is separate from the HugeGraph administrator password and Hubble's service token.
+{{- if $hubbleEnabled }}
+ Hubble is connected automatically. Sign in as a HugeGraph super administrator, select a graph,
+ then open batch analysis, load it into Vermeer and run an algorithm. View results in async tasks.
+ Hubble keeps its service token in a private temporary volume; its running container never mounts the signing factor.
+{{- else }}
+ Hubble is disabled. Enable hubble.enabled=true with hubble.mode=pd for the integrated workflow.
+{{- end }}
+ Compute results are queried from Vermeer and are not written back to HugeGraph.
+ Master/Worker restarts can discard in-memory graph copies and result caches; reload and recompute when needed.
+ Internal gRPC ports admit only same-release Vermeer peers when the CNI enforces NetworkPolicy.
+ Deployment and credentials: helm/hugegraph/docs/vermeer.md
+{{- if .Values.vermeer.auth.existingSecret }}
+
+ After changing the existing signing-factor Secret, restart all consumers so tokens and verification agree:
+ kubectl rollout restart -n {{ .Release.Namespace }} statefulset/{{ $vName }}
+ kubectl rollout status -n {{ .Release.Namespace }} statefulset/{{ $vName }} --timeout=5m
+{{- if $hubbleEnabled }}
+ kubectl rollout restart -n {{ .Release.Namespace }} deployment/{{ include "hugegraph.hubble.name" . }}
+ kubectl rollout status -n {{ .Release.Namespace }} deployment/{{ include "hugegraph.hubble.name" . }} --timeout=5m
+{{- end }}
+ Sign in again with the management password after rotation; existing browser sessions no longer work.
+{{- end }}
+{{- if (.Values.vermeer.auth.admin | default dict).existingSecret }}
+
+ After changing the existing management-password Secret, restart the Master:
+ kubectl rollout restart -n {{ .Release.Namespace }} statefulset/{{ $vName }}
+ kubectl rollout status -n {{ .Release.Namespace }} statefulset/{{ $vName }} --timeout=5m
+{{- end }}
+{{- end }}
{{- if (.Values.seatunnel | default dict).enabled }}
SeaTunnel:
{{- $stName := include "hugegraph.seatunnel.name" . }}
-{{- $stService := .Values.seatunnel.service | default dict }}
In-cluster API: http://{{ $stName }}.{{ .Release.Namespace }}.svc:8080
-{{- if eq $stService.type "NodePort" }}
- HTTP NodePort:
- kubectl get svc -n {{ .Release.Namespace }} {{ $stName }} -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}{"\n"}'
- kubectl get nodes -o wide
- UI: http://:{{ $stService.nodePort | default "" }}/#/overview
- API: http://:{{ $stService.nodePort | default "" }}
-{{- else if eq $stService.type "LoadBalancer" }}
- LoadBalancer address:
- kubectl get svc -n {{ .Release.Namespace }} {{ $stName }} -o wide
- kubectl get svc -n {{ .Release.Namespace }} {{ $stName }} -o jsonpath='{.status.loadBalancer.ingress}{"\n"}'
- Once assigned, open http://:8080/#/overview; the API uses the same HTTP address.
-{{- else }}
- HTTP Service is ClusterIP; use port forwarding below for access outside the cluster.
-{{- end }}
- Port forwarding works with every Service type:
+ The authenticated UI/API stays on an internal ClusterIP Service in every mode.
+ Open a local connection only when needed:
kubectl port-forward -n {{ .Release.Namespace }} svc/{{ $stName }} 8081:8080
Local UI: http://127.0.0.1:8081/#/overview
Local API: http://127.0.0.1:8081
@@ -92,16 +129,11 @@ Access:
Use this account for the browser Basic login; with port forwarding active, check REST with:
curl --user "${ST_USERNAME}:${ST_PASSWORD}" http://127.0.0.1:8081/overview
These entry credentials are separate from HugeGraph connector credentials, even when HugeGraph auth is disabled.
+ New chart-generated entry passwords have {{ ternary 16 6 (eq .Values.mode "strict") }} characters; upgrades keep existing credentials.
Internal CLI/5801 has no HTTP Basic authentication. External submissions must use the protected REST API.
Online Helm upgrades retain the default credentials; rotate using seatunnel.auth.password in controlled values.
Pin a password for GitOps/template-only rendering. The auth Secret is retained after disabling or uninstalling.
Authentication, rotation and jobs: helm/hugegraph/docs/seatunnel.md
-{{- if and .Values.networkPolicy.enabled (has $stService.type (list "NodePort" "LoadBalancer")) }}
-{{- $stRules := get .Values.networkPolicy "seatunnel" | default dict }}
-{{- if empty (get $stRules "extraIngress") }}
- Full NetworkPolicy isolation is enabled; allow external HTTP callers through networkPolicy.seatunnel.extraIngress.
-{{- end }}
-{{- end }}
Same-release example jobs:
Server address, port, GraphSpace and credential references are injected; the job files are already mounted.
@@ -177,6 +209,11 @@ existing account; see helm/hugegraph/docs/operations.md#credentials for account
{{- 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 $vermeerEnabled }}
+{{- if and (not .Values.vermeer.auth.existingSecret) (not .Values.vermeer.auth.value) }}{{ $generated = append $generated "vermeer.auth" }}{{ end }}
+{{- $admin := .Values.vermeer.auth.admin | default dict }}
+{{- if and (not $admin.existingSecret) (not $admin.password) }}{{ $generated = append $generated "vermeer.auth.admin" }}{{ end }}
+{{- end }}
{{- if $generated }}
Chart-generated credentials are in use: {{ join ", " $generated }}.
@@ -211,7 +248,7 @@ Server registers {{ .Values.server.advertiseUrl }} with PD for external discover
A standalone Hubble also needs a reachable PD endpoint; see helm/hugegraph/docs/operations.md#networking-and-hubble.
{{- end }}
-Verify all Server Gremlin endpoints:
+Verify all Server Gremlin endpoints{{ if $vermeerEnabled }} and Vermeer service-space Worker availability{{ end }}:
helm test {{ .Release.Name }} --namespace {{ .Release.Namespace }} --logs --timeout 5m
diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl
index 5094d48670..bd0c8770c8 100644
--- a/helm/hugegraph/templates/_helpers.tpl
+++ b/helm/hugegraph/templates/_helpers.tpl
@@ -19,6 +19,10 @@
{{- define "hugegraph.resolveValues" -}}
{{- if not (get .Values "__hugegraphResolved") -}}
{{- $_ := set .Values "__hugegraphResolved" true -}}
+{{/* Old official Server images use hg-test when no cluster is configured. */}}
+{{- if not (hasKey .Values "metadataCluster") -}}
+{{- $_ := set .Values "metadataCluster" "hg-test" -}}
+{{- end -}}
{{- $legacy := not (hasKey .Values "mode") -}}
{{- if not (hasKey .Values "hubble") -}}
{{- $_ := set .Values "hubble" (dict "enabled" false "service" dict "persistence" dict) -}}
@@ -26,9 +30,24 @@
{{- if not (hasKey .Values "seatunnel") -}}
{{- $_ := set .Values "seatunnel" (dict "enabled" false "service" dict) -}}
{{- end -}}
+{{- if not (hasKey .Values "vermeer") -}}
+{{- $_ := set .Values "vermeer" (dict "enabled" false "service" dict) -}}
+{{- end -}}
+{{/* --reuse-values can combine an old release with only vermeer.enabled=true.
+Fail before dereferencing missing blocks; keep values.yaml the sole defaults source. */}}
+{{- if .Values.vermeer.enabled -}}
+{{- range $key := list "mode" "image" "auth" "hugegraphCredentials" "master" "worker" "probes" "terminationGracePeriodSeconds" "podLabels" "podAnnotations" -}}
+{{- if not (hasKey $.Values.vermeer $key) -}}
+{{- fail (printf "vermeer is missing new chart defaults (%s): legacy --reuse-values does not import newly added settings. Export and review existing effective values with helm get values --all -o yaml, then upgrade with -f existing-values.yaml --set vermeer.enabled=true without --reuse-values so Helm merges the new defaults" $key) -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- if and .Values.vermeer.enabled (not (hasKey .Values.vermeer.auth "admin")) -}}
+{{- fail "vermeer.auth.admin is missing new chart defaults: export and review existing effective values, then upgrade with -f existing-values.yaml without --reuse-values so Helm imports the admin login defaults" -}}
+{{- end -}}
{{- $strict := eq (.Values.mode | default "easy") "strict" -}}
{{- $serviceType := ternary "ClusterIP" "NodePort" (or $strict $legacy) -}}
-{{- range $comp := list "server" "hubble" "seatunnel" -}}
+{{- range $comp := list "server" "hubble" -}}
{{- $component := get $.Values $comp -}}
{{- $svc := get $component "service" | default dict -}}
{{- $_ := set $component "service" $svc -}}
@@ -36,6 +55,14 @@
{{- $_ := set $component "__serviceTypeInherited" $typeInherited -}}
{{- if $typeInherited -}}{{- $_ := set $svc "type" $serviceType -}}{{- end -}}
{{- end -}}
+{{- range $comp := list "seatunnel" "vermeer" -}}
+{{- $component := get $.Values $comp -}}
+{{- $svc := get $component "service" | default dict -}}
+{{- $_ := set $component "service" $svc -}}
+{{- if or (not (hasKey $svc "type")) (eq (get $svc "type" | toString) "") -}}
+{{- $_ := set $svc "type" "ClusterIP" -}}
+{{- 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 -}}
@@ -48,10 +75,18 @@
{{- $_ := set $component $key $storage -}}
{{- if or (not (hasKey $storage "enabled")) (eq (get $storage "enabled" | toString) "") -}}{{- $_ := set $storage "enabled" $persistent -}}{{- end -}}
{{- end -}}
+{{- if .Values.vermeer.enabled -}}
+{{- range $role := list "master" "worker" -}}
+{{- $storage := get (get $.Values.vermeer $role) "storage" -}}
+{{- if or (not (hasKey $storage "enabled")) (eq (get $storage "enabled" | toString) "") -}}
+{{- $_ := set $storage "enabled" $persistent -}}
+{{- end -}}
+{{- 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" "seatunnel" -}}
+{{- range $comp := list "pd" "store" "server" "hubble" "seatunnel" "vermeer" -}}
{{- $image := get (get $.Values $comp) "image" | default dict -}}
{{- if $image -}}
{{- $_ := set $image "repository" (include "hugegraph.registryImage" (dict "reference" $image.repository "registry" $registry)) -}}
@@ -236,7 +271,7 @@ next render finds the Secret through lookup instead).
{{- define "hugegraph.generatedCredential" -}}
{{- if not (hasKey .root.Values "__generatedCredentials") -}}{{- $_ := set .root.Values "__generatedCredentials" dict -}}{{- end -}}
{{- $memo := get .root.Values "__generatedCredentials" -}}
-{{- if not (hasKey $memo .name) -}}{{- $_ := set $memo .name (randAlphaNum 32) -}}{{- end -}}
+{{- if not (hasKey $memo .name) -}}{{- $_ := set $memo .name (randAlphaNum (int (get . "length" | default 32))) -}}{{- end -}}
{{- get $memo .name -}}
{{- end }}
@@ -752,6 +787,10 @@ 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" . -}}
+{{- include "hugegraph.nativeServices.validate" . -}}
+{{- include "hugegraph.metadataCluster.validate" . -}}
+{{- include "hugegraph.vermeer.validate" . -}}
{{- if (.Values.seatunnel | default dict).enabled -}}
{{- if not (has .Values.seatunnel.mode (list "standalone" "cluster")) -}}
{{- fail "seatunnel.mode must be standalone or cluster" -}}
@@ -764,9 +803,6 @@ Cross-field validation that JSON Schema draft-07 cannot express.
{{- include "hugegraph.resolveValues" . -}}
{{- if (.Values.seatunnel | default dict).enabled -}}
{{- $svc := .Values.seatunnel.service -}}
-{{- if and (get $svc "nodePort") (not (has $svc.type (list "NodePort" "LoadBalancer"))) -}}
-{{- fail "seatunnel.service.nodePort requires seatunnel.service.type to be NodePort or LoadBalancer" -}}
-{{- end -}}
{{- range $key := list "username" "password" -}}
{{- $value := get ($.Values.seatunnel.auth | default dict) $key | default "" -}}
{{- if or (ne $value (trim $value)) (contains "${" $value) -}}
@@ -992,9 +1028,9 @@ 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_SERVICE_ID" "HG_SERVER_GRAPHSPACE" "SERVICE_ID" "GRAPH_SPACE" "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_DEPLOY_IN_K8S" "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") -}}
+ "store" (list "PD_CLUSTER" "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 "HG_SERVER_CLUSTER" "POD_IP" "HG_SERVER_SERVICE_ID" "HG_SERVER_GRAPHSPACE" "SERVICE_ID" "GRAPH_SPACE" "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_DEPLOY_IN_K8S" "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" "HG_HUBBLE_VERMEER_URL" "HG_HUBBLE_VERMEER_TOKEN_FILE" "SPRING_DATASOURCE_URL") -}}
{{- range $component, $reserved := $reservedEnv -}}
{{- $componentValues := get $.Values $component | default dict -}}
{{- range $entry := get $componentValues "extraEnv" | default list -}}
@@ -1133,3 +1169,244 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict
{{- printf "%s:%s" $img.repository (default .appVersion $img.tag) -}}
{{- end -}}
{{- end }}
+
+{{/* Native Vermeer identities stay stable across standalone/cluster mode changes. */}}
+{{- define "hugegraph.vermeer.masterName" -}}
+{{- printf "%s-vermeer-master" (include "hugegraph.fullname" . | trunc 39 | trimSuffix "-") -}}
+{{- end }}
+{{- define "hugegraph.vermeer.workerName" -}}
+{{- printf "%s-vermeer-worker" (include "hugegraph.fullname" . | trunc 44 | trimSuffix "-") -}}
+{{- end }}
+{{- define "hugegraph.vermeer.configName" -}}
+{{- printf "%s-vermeer-config" (include "hugegraph.fullname" . | trunc 48 | trimSuffix "-") -}}
+{{- end }}
+{{- define "hugegraph.vermeer.standaloneWorkerDataName" -}}
+{{- printf "%s-vermeer-standalone-worker-data" (include "hugegraph.fullname" . | trunc 31 | trimSuffix "-") -}}
+{{- end }}
+{{/* Stable signing factor and rollout checksum; external Secrets contribute their revision. */}}
+{{- define "hugegraph.vermeer.authSecretName" -}}
+{{- $auth := get .Values.vermeer "auth" | default dict -}}
+{{- $existing := get $auth "existingSecret" | default "" -}}
+{{- if $existing -}}
+{{- $existing -}}
+{{- else -}}
+{{- printf "%s-vermeer-auth" (.Release.Name | trunc 50 | trimSuffix "-") -}}
+{{- end -}}
+{{- end }}
+
+{{- define "hugegraph.vermeer.authSecretKey" -}}
+{{- $auth := get .Values.vermeer "auth" | default dict -}}
+{{- get $auth "key" | default "token-factor" -}}
+{{- end }}
+
+{{- define "hugegraph.vermeer.authSecretValue" -}}
+{{- $auth := get .Values.vermeer "auth" | default dict -}}
+{{- $value := get $auth "value" | default "" -}}
+{{- if $value -}}
+{{- $value | b64enc -}}
+{{- else -}}
+{{- $name := include "hugegraph.vermeer.authSecretName" . -}}
+{{- $key := include "hugegraph.vermeer.authSecretKey" . -}}
+{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}}
+{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}}
+{{- get (get $secret "data") $key -}}
+{{- else -}}
+{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-auth") | b64enc -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
+
+{{- define "hugegraph.vermeer.authChecksum" -}}
+{{- $parts := list (include "hugegraph.vermeer.authSecretName" .) (include "hugegraph.vermeer.authSecretKey" .) -}}
+{{- $authCfg := get .Values.vermeer "auth" | default dict -}}
+{{- $inline := get $authCfg "value" | default "" -}}
+{{- $existing := get $authCfg "existingSecret" | default "" -}}
+{{- if $existing -}}
+{{- $secret := lookup "v1" "Secret" .Release.Namespace $existing -}}
+{{- if $secret -}}{{- $parts = append $parts (dig "metadata" "resourceVersion" "" $secret) -}}{{- end -}}
+{{- else if $inline -}}
+{{- $parts = append $parts (sha256sum $inline) -}}
+{{- else -}}
+{{- $parts = append $parts (include "hugegraph.vermeer.authSecretValue" . | b64dec | sha256sum) -}}
+{{- end -}}
+{{- join "|" $parts | sha256sum -}}
+{{- end }}
+
+
+{{- define "hugegraph.vermeer.validate" -}}
+{{- if (.Values.vermeer | default dict).enabled -}}
+{{- $v := .Values.vermeer -}}
+{{- $admin := $v.auth.admin -}}
+{{- if and (not $admin.existingSecret) (not $admin.password) (not $admin.autoGenerate) -}}
+{{- fail "vermeer.auth.admin requires existingSecret, password, or autoGenerate=true" -}}
+{{- end -}}
+{{- if and $admin.password (or (eq (trim $admin.password) "") (contains "\n" $admin.password) (contains "\r" $admin.password)) -}}
+{{- fail "vermeer.auth.admin.password must be one nonblank UTF-8 line; whitespace is preserved" -}}
+{{- end -}}
+{{- if and .Values.hubble.enabled (ne (.Values.hubble.mode | default "pd") "pd") -}}
+{{- fail "vermeer.enabled with Hubble requires hubble.mode=pd for graph-space discovery; disable Hubble to use Vermeer independently" -}}
+{{- end -}}
+{{- if .Values.hubble.enabled -}}
+{{- $hubbleContainer := .Values.hubble.securityContext | default dict -}}
+{{- $hubblePod := .Values.hubble.podSecurityContext | default dict -}}
+{{- $nonRoot := get $hubblePod "runAsNonRoot" | default false -}}
+{{- if hasKey $hubbleContainer "runAsNonRoot" -}}{{- $nonRoot = get $hubbleContainer "runAsNonRoot" -}}{{- end -}}
+{{- if and $nonRoot (le (int (include "hugegraph.hubble.vermeerUid" .)) 0) -}}
+{{- fail "Vermeer token files require an explicit positive hubble.securityContext.runAsUser or hubble.podSecurityContext.runAsUser when runAsNonRoot=true" -}}
+{{- end -}}
+{{- end -}}
+{{- if not (has $v.mode (list "standalone" "cluster")) -}}
+{{- fail "vermeer.mode must be standalone or cluster" -}}
+{{- end -}}
+{{- if ne .Values.server.backend "hstore" -}}
+{{- fail "vermeer.enabled requires server.backend=hstore" -}}
+{{- end -}}
+{{- if and (not $v.auth.existingSecret) (not $v.auth.value) (not $v.auth.autoGenerate) -}}
+{{- fail "vermeer.auth requires existingSecret, value, or autoGenerate=true" -}}
+{{- end -}}
+{{- if and (not $v.image.tag) (not $v.image.digest) -}}
+{{- fail "vermeer.image needs a tag or digest matching the file-credential and token-helper interface" -}}
+{{- end -}}
+{{- $ports := list (int $v.master.httpPort) (int $v.master.grpcPort) (int $v.worker.httpPort) (int $v.worker.grpcPort) -}}
+{{- if ne (len (uniq $ports)) 4 -}}
+{{- fail "vermeer master and worker HTTP/gRPC ports must be distinct" -}}
+{{- end -}}
+{{- range $reserved := list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component" -}}
+{{- if hasKey $v.podLabels $reserved -}}
+{{- fail (printf "vermeer.podLabels must not set %s" $reserved) -}}
+{{- end -}}
+{{- end -}}
+{{- range $key, $_ := $v.podAnnotations -}}
+{{- if hasPrefix "checksum/" $key -}}{{- fail "vermeer.podAnnotations must not set checksum/ annotations" -}}{{- end -}}
+{{- end -}}
+{{- range $role := list "master" "worker" -}}
+{{- range $entry := (get $v $role).extraEnv -}}
+{{- if has $entry.name (list "POD_IP" "VERMEER_AUTH_TOKEN_FACTOR_FILE" "VERMEER_ADMIN_PASSWORD_FILE" "VERMEER_PD_PASSWORD_FILE" "VERMEER_PD_PEERS" "VERMEER_HUGEGRAPH_USERNAME" "VERMEER_HUGEGRAPH_PASSWORD_FILE" "VERMEER_HUGEGRAPH_CREDENTIALS_FILE") -}}
+{{- fail (printf "vermeer.%s.extraEnv must not set chart-managed variable %s" $role $entry.name) -}}
+{{- end -}}
+{{- end -}}
+{{- if $.Release.IsUpgrade -}}
+{{- $name := include "hugegraph.vermeer.masterName" $ -}}
+{{- if eq $role "worker" -}}{{- $name = include "hugegraph.vermeer.workerName" $ -}}{{- end -}}
+{{- $live := lookup "apps/v1" "StatefulSet" $.Release.Namespace $name -}}
+{{- if $live -}}
+{{- $wasPersistent := false -}}
+{{- range $claim := dig "spec" "volumeClaimTemplates" list $live -}}
+{{- if eq (dig "metadata" "name" "" $claim) (printf "%s-data" $role) -}}{{- $wasPersistent = true -}}{{- end -}}
+{{- end -}}
+{{- if ne $wasPersistent (get (get $v $role) "storage").enabled -}}
+{{- fail (printf "vermeer.%s.storage.enabled cannot change on an initialized release; migrate retained data into a fresh workload" $role) -}}
+{{- end -}}
+{{- if eq $role "master" -}}
+{{- range $volume := dig "spec" "template" "spec" "volumes" list $live -}}
+{{- if and (eq $v.mode "standalone") (eq (get $volume "name") "worker-data") -}}
+{{- $workerPersistent := hasKey $volume "persistentVolumeClaim" -}}
+{{- if ne $workerPersistent $v.worker.storage.enabled -}}
+{{- fail "vermeer.worker.storage.enabled cannot change on an initialized standalone release; migrate retained data first" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
+
+{{/* Server owns the initialized namespace; Store must use that same PD key prefix. */}}
+{{- define "hugegraph.metadataCluster.validate" -}}
+{{- if regexMatch "(^|[[:space:]\"'])-Dpd[.]cluster(=|[[:space:]\"']|$)" (.Values.store.javaOpts | default "") -}}
+{{- fail "store.javaOpts must not set -Dpd.cluster; use shared metadataCluster so Server and Store read the same PD metadata namespace" -}}
+{{- end -}}
+{{- if .Release.IsUpgrade -}}
+{{- $live := lookup "apps/v1" "Deployment" .Release.Namespace (include "hugegraph.server.name" .) -}}
+{{- if $live -}}
+{{- $desired := .Values.metadataCluster -}}
+{{- $record := dig "metadata" "annotations" "hugegraph.apache.org/metadata-cluster" "" $live -}}
+{{- if and $record (ne $record $desired) -}}
+{{- fail "metadataCluster cannot change from the recorded Server metadata namespace: changing PD keys does not migrate existing metadata; retain the initialized value" -}}
+{{- end -}}
+{{- $observed := dict "explicit" false "unknown" false -}}
+{{- range $container := dig "spec" "template" "spec" "containers" list $live -}}
+{{- if eq (get $container "name") "server" -}}
+{{- range $entry := get $container "env" | default list -}}
+{{- if eq (get $entry "name") "HG_SERVER_CLUSTER" -}}
+{{- $_ := set $observed "explicit" true -}}
+{{- if hasKey $entry "valueFrom" -}}
+{{- $_ := set $observed "unknown" true -}}
+{{- else if ne (get $entry "value" | default "") $desired -}}
+{{- fail "metadataCluster cannot change from the existing Server HG_SERVER_CLUSTER value: changing PD keys does not migrate existing metadata; retain the initialized value" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- if and (not $record) (or (get $observed "unknown") (and (not (get $observed "explicit")) (ne $desired "hg-test"))) -}}
+{{- fail "metadataCluster adoption cannot verify the legacy Server namespace. First inspect the existing Server logs/configuration and record that same verified value in its hugegraph.apache.org/metadata-cluster Deployment annotation; then retain it as metadataCluster. This records existing metadata and does not migrate PD keys" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
+
+{{/* Native engines have no public Service entry, regardless of easy/strict mode. */}}
+{{- define "hugegraph.nativeServices.validate" -}}
+{{- range $component := list "seatunnel" "vermeer" -}}
+{{- $values := get $.Values $component | default dict -}}
+{{- if get $values "enabled" -}}
+{{- $svc := get $values "service" | default dict -}}
+{{- if ne (get $svc "type") "ClusterIP" -}}
+{{- fail (printf "%s.service must remain ClusterIP; native engines are accessed through authenticated internal callers or port-forward" $component) -}}
+{{- end -}}
+{{- if and (hasKey $svc "nodePort") (ne (get $svc "nodePort") nil) -}}
+{{- fail (printf "%s.service.nodePort is not supported for internal native engines" $component) -}}
+{{- end -}}
+{{- range $field := list "externalIPs" "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "allocateLoadBalancerNodePorts" -}}
+{{- if hasKey $svc $field -}}
+{{- fail (printf "%s.service.%s is not supported for internal native engines" $component $field) -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
+
+{{/* Native UI password is distinct from the internal HMAC signing factor. */}}
+{{- define "hugegraph.vermeer.adminSecretName" -}}
+{{- $admin := .Values.vermeer.auth.admin -}}
+{{- if $admin.existingSecret -}}{{- $admin.existingSecret -}}
+{{- else -}}{{- printf "%s-vermeer-admin" (.Release.Name | trunc 49 | trimSuffix "-") -}}{{- end -}}
+{{- end }}
+{{- define "hugegraph.vermeer.adminSecretKey" -}}
+{{- .Values.vermeer.auth.admin.key | default "password" -}}
+{{- end }}
+{{- define "hugegraph.vermeer.adminSecretValue" -}}
+{{- $admin := .Values.vermeer.auth.admin -}}
+{{- $name := include "hugegraph.vermeer.adminSecretName" . -}}
+{{- $key := include "hugegraph.vermeer.adminSecretKey" . -}}
+{{- if $admin.existingSecret -}}
+{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}}
+{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}}{{- get (get $secret "data") $key -}}{{- end -}}
+{{- else if $admin.password -}}
+{{- $admin.password | b64enc -}}
+{{- else -}}
+{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}}
+{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}}
+{{- get (get $secret "data") $key -}}
+{{- else -}}
+{{- $length := ternary 16 6 (eq (.Values.mode | default "easy") "strict") -}}
+{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" $length) | b64enc -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
+{{- define "hugegraph.vermeer.adminChecksum" -}}
+{{- $admin := .Values.vermeer.auth.admin -}}
+{{- $parts := list (include "hugegraph.vermeer.adminSecretName" .) (include "hugegraph.vermeer.adminSecretKey" .) -}}
+{{- if $admin.existingSecret -}}
+{{- $secret := lookup "v1" "Secret" .Release.Namespace $admin.existingSecret -}}
+{{- if $secret -}}{{- $parts = append $parts (dig "metadata" "resourceVersion" "" $secret) -}}{{- end -}}
+{{- else if $admin.password -}}
+{{- $parts = append $parts (sha256sum $admin.password) -}}
+{{- else -}}
+{{- $parts = append $parts (include "hugegraph.vermeer.adminSecretValue" . | b64dec | sha256sum) -}}
+{{- end -}}
+{{- join "|" $parts | sha256sum -}}
+{{- end }}
diff --git a/helm/hugegraph/templates/_hubble-vermeer.tpl b/helm/hugegraph/templates/_hubble-vermeer.tpl
new file mode 100644
index 0000000000..e1c87e1f01
--- /dev/null
+++ b/helm/hugegraph/templates/_hubble-vermeer.tpl
@@ -0,0 +1,92 @@
+{{/*
+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.
+*/}}
+
+{{/* Match the reader's UID: the token helper deliberately creates mode 0600. */}}
+{{- define "hugegraph.hubble.vermeerUid" -}}
+{{- $container := .Values.hubble.securityContext | default dict -}}
+{{- $pod := .Values.hubble.podSecurityContext | default dict -}}
+{{- if hasKey $container "runAsUser" -}}
+{{- $container.runAsUser -}}
+{{- else if hasKey $pod "runAsUser" -}}
+{{- $pod.runAsUser -}}
+{{- else -}}0{{- end -}}
+{{- end -}}
+
+{{- define "hugegraph.hubble.vermeerGid" -}}
+{{- $container := .Values.hubble.securityContext | default dict -}}
+{{- $pod := .Values.hubble.podSecurityContext | default dict -}}
+{{- if hasKey $container "runAsGroup" -}}
+{{- $container.runAsGroup -}}
+{{- else if hasKey $pod "runAsGroup" -}}
+{{- $pod.runAsGroup -}}
+{{- else -}}0{{- end -}}
+{{- end -}}
+
+{{/* Signing material exists only in this completed init container. */}}
+{{- define "hugegraph.vermeer.tokenInit" -}}
+{{- $root := .root -}}
+- name: vermeer-token
+ image: {{ include "hugegraph.image" (dict "image" $root.Values.vermeer.image "appVersion" $root.Chart.AppVersion) | quote }}
+ imagePullPolicy: {{ $root.Values.vermeer.image.pullPolicy }}
+ command: ["/go/bin/vermeer-token"]
+ args:
+ - -factor-file
+ - /var/run/hugegraph/vermeer-factor/factor
+ - -output-file
+ - /var/run/hugegraph/vermeer/token
+ - -quiet
+ - -user
+ - admin
+ - -space
+ - "$DEFAULT"
+ - -client
+ - helm
+ - -i
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
+ readOnlyRootFilesystem: true
+ runAsUser: {{ .uid }}
+ runAsGroup: {{ .gid }}
+ runAsNonRoot: {{ ne (int .uid) 0 }}
+ seccompProfile:
+ type: RuntimeDefault
+ {{- with .resources }}
+ resources:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ volumeMounts:
+ - name: vermeer-factor
+ mountPath: /var/run/hugegraph/vermeer-factor
+ readOnly: true
+ - name: vermeer-token
+ mountPath: /var/run/hugegraph/vermeer
+{{- end -}}
+
+{{- define "hugegraph.vermeer.tokenVolumes" -}}
+- name: vermeer-factor
+ secret:
+ secretName: {{ include "hugegraph.vermeer.authSecretName" . | quote }}
+ defaultMode: 0444
+ items:
+ - key: {{ include "hugegraph.vermeer.authSecretKey" . | quote }}
+ path: factor
+- name: vermeer-token
+ emptyDir:
+ medium: Memory
+{{- end -}}
diff --git a/helm/hugegraph/templates/_vermeer.tpl b/helm/hugegraph/templates/_vermeer.tpl
new file mode 100644
index 0000000000..84a0c58403
--- /dev/null
+++ b/helm/hugegraph/templates/_vermeer.tpl
@@ -0,0 +1,165 @@
+{{/*
+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.
+*/}}
+
+{{- define "hugegraph.vermeer.container" -}}
+{{- $root := .root -}}
+{{- $role := .role -}}
+{{- $v := $root.Values.vermeer -}}
+{{- $roleValues := get $v $role -}}
+- name: {{ $role }}
+ image: {{ include "hugegraph.image" (dict "image" $v.image "appVersion" $root.Chart.AppVersion) | quote }}
+ imagePullPolicy: {{ $v.image.pullPolicy }}
+ workingDir: /go/bin/vermeer_data
+ command: ["/go/bin/app"]
+ args:
+ - -env=/var/run/vermeer/config/{{ $role }}.ini
+ - -log_prefix=/go/bin/vermeer_data/vermeer.log
+ {{- if eq $role "worker" }}
+ - {{ printf "-grpc_peer=%s:%v" (ternary "127.0.0.1" "$(POD_IP)" (eq $v.mode "standalone")) $v.worker.grpcPort | quote }}
+ {{- end }}
+ {{- with $v.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ env:
+ - name: POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: VERMEER_PD_PEERS
+ value: {{ splitList "," (include "hugegraph.pd.grpcPeersList" $root) | toJson | quote }}
+ - name: VERMEER_PD_PASSWORD_FILE
+ value: /var/run/vermeer/pd/password
+ {{- if eq $role "master" }}
+ - name: VERMEER_AUTH_TOKEN_FACTOR_FILE
+ value: /var/run/vermeer/auth/token-factor
+ - name: VERMEER_ADMIN_PASSWORD_FILE
+ value: /var/run/vermeer/admin/password
+ {{- else if $v.hugegraphCredentials.existingSecret }}
+ - name: VERMEER_HUGEGRAPH_CREDENTIALS_FILE
+ value: /var/run/vermeer/hugegraph/credentials.json
+ {{- else if $root.Values.server.auth.enabled }}
+ - name: VERMEER_HUGEGRAPH_USERNAME
+ value: admin
+ - name: VERMEER_HUGEGRAPH_PASSWORD_FILE
+ value: /var/run/vermeer/hugegraph/password
+ {{- end }}
+ {{- with $roleValues.extraEnv }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ ports:
+ - name: {{ $role }}-http
+ containerPort: {{ $roleValues.httpPort }}
+ - name: {{ $role }}-grpc
+ containerPort: {{ $roleValues.grpcPort }}
+ volumeMounts:
+ - name: {{ $role }}-data
+ mountPath: /go/bin/vermeer_data
+ - name: vermeer-config
+ mountPath: /var/run/vermeer/config
+ readOnly: true
+ - name: pd-auth
+ mountPath: /var/run/vermeer/pd
+ readOnly: true
+ {{- if eq $role "master" }}
+ - name: vermeer-auth
+ mountPath: /var/run/vermeer/auth
+ readOnly: true
+ - name: vermeer-admin
+ mountPath: /var/run/vermeer/admin
+ readOnly: true
+ {{- else if or $v.hugegraphCredentials.existingSecret $root.Values.server.auth.enabled }}
+ - name: hugegraph-auth
+ mountPath: /var/run/vermeer/hugegraph
+ readOnly: true
+ {{- end }}
+ {{- range $probe := list "startup" "readiness" "liveness" }}
+ {{ $probe }}Probe:
+ {{- if eq $role "master" }}
+ httpGet:
+ path: /healthcheck
+ port: master-http
+ {{- else }}
+ tcpSocket:
+ port: worker-http
+ {{- end }}
+ {{- toYaml (get $v.probes $probe) | nindent 4 }}
+ {{- end }}
+ {{- with $roleValues.resources }}
+ resources:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- end }}
+
+{{- define "hugegraph.vermeer.volumes" -}}
+{{- $root := .root -}}
+{{- $worker := .worker -}}
+{{- $master := .master -}}
+- name: vermeer-config
+ configMap:
+ name: {{ include "hugegraph.vermeer.configName" $root }}
+- name: pd-auth
+ secret:
+ secretName: {{ include "hugegraph.pd.authSecretName" $root }}
+ items:
+ - key: {{ include "hugegraph.pd.authSecretKey" $root | quote }}
+ path: password
+{{- if $master }}
+- name: vermeer-admin
+ secret:
+ secretName: {{ include "hugegraph.vermeer.adminSecretName" $root }}
+ items:
+ - key: {{ include "hugegraph.vermeer.adminSecretKey" $root | quote }}
+ path: password
+- name: vermeer-auth
+ secret:
+ secretName: {{ include "hugegraph.vermeer.authSecretName" $root }}
+ items:
+ - key: {{ include "hugegraph.vermeer.authSecretKey" $root | quote }}
+ path: token-factor
+{{- end }}
+{{- if and $worker (or $root.Values.vermeer.hugegraphCredentials.existingSecret $root.Values.server.auth.enabled) }}
+- name: hugegraph-auth
+ secret:
+ {{- if $root.Values.vermeer.hugegraphCredentials.existingSecret }}
+ secretName: {{ $root.Values.vermeer.hugegraphCredentials.existingSecret | quote }}
+ items:
+ - key: {{ $root.Values.vermeer.hugegraphCredentials.key | quote }}
+ path: credentials.json
+ {{- else }}
+ secretName: {{ include "hugegraph.server.authSecretName" $root }}
+ items:
+ - key: {{ include "hugegraph.server.authSecretKey" $root | quote }}
+ path: password
+ {{- end }}
+{{- end }}
+{{- end }}
+
+{{- define "hugegraph.vermeer.claim" -}}
+- metadata:
+ name: {{ .role }}-data
+ spec:
+ accessModes: ["ReadWriteOnce"]
+ {{- if eq .storage.storageClassName "-" }}
+ storageClassName: ""
+ {{- else if .storage.storageClassName }}
+ storageClassName: {{ .storage.storageClassName | quote }}
+ {{- end }}
+ resources:
+ requests:
+ storage: {{ .storage.size }}
+{{- end }}
diff --git a/helm/hugegraph/templates/hubble-deployment.yaml b/helm/hugegraph/templates/hubble-deployment.yaml
index bd83736de2..bf977fff70 100644
--- a/helm/hugegraph/templates/hubble-deployment.yaml
+++ b/helm/hugegraph/templates/hubble-deployment.yaml
@@ -24,6 +24,7 @@
{{- fail "hubble.mode must be one of: pd, direct" }}
{{- end }}
{{- $pdMode := eq $mode "pd" }}
+{{- $vermeerEnabled := (.Values.vermeer | default dict).enabled | default false }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -53,6 +54,9 @@ spec:
# Rolls Hubble when the PD REST Secret written into its properties changes.
checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" . | quote }}
{{- end }}
+ {{- if $vermeerEnabled }}
+ checksum/vermeer-auth: {{ include "hugegraph.vermeer.authChecksum" . | quote }}
+ {{- end }}
{{- with .Values.hubble.podAnnotations }}{{ toYaml . | nindent 8 }}{{- end }}
spec:
automountServiceAccountToken: {{ get (get .Values.hubble "serviceAccount" | default dict) "automountServiceAccountToken" | default false }}
@@ -87,11 +91,19 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
+ {{- if $vermeerEnabled }}
+ initContainers:
+ {{- include "hugegraph.vermeer.tokenInit" (dict "root" . "uid" (include "hugegraph.hubble.vermeerUid" .) "gid" (include "hugegraph.hubble.vermeerGid" .) "resources" .Values.hubble.resources) | nindent 8 }}
+ {{- end }}
containers:
- name: hubble
image: {{ include "hugegraph.image" (dict "image" .Values.hubble.image "appVersion" $.Chart.AppVersion) | quote }}
imagePullPolicy: {{ .Values.hubble.image.pullPolicy }}
- {{- with .Values.hubble.securityContext }}
+ {{- $securityContext := deepCopy (.Values.hubble.securityContext | default dict) }}
+ {{- if $vermeerEnabled }}
+ {{- $_ := set $securityContext "runAsUser" (int (include "hugegraph.hubble.vermeerUid" .)) }}
+ {{- end }}
+ {{- with $securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -156,6 +168,9 @@ spec:
{{- end }}
while IFS= read -r LINE || [[ -n "${LINE}" ]]; do
case "${LINE}" in
+ vermeer.url=*|vermeer.token_file=*)
+ # Written once below, including explicit blanks when disabled.
+ ;;
pd.enabled=*)
printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}"
FOUND_PD_ENABLED=true
@@ -207,6 +222,12 @@ spec:
;;
esac
done <"${CONF}"
+ {{- if $vermeerEnabled }}
+ printf 'vermeer.url=%s\nvermeer.token_file=%s\n' \
+ "${HG_HUBBLE_VERMEER_URL}" "${HG_HUBBLE_VERMEER_TOKEN_FILE}" >>"${TMP}"
+ {{- else }}
+ printf 'vermeer.url=\nvermeer.token_file=\n' >>"${TMP}"
+ {{- end }}
if [[ "${FOUND_PD_ENABLED}" == false ]]; then
printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}"
fi
@@ -255,6 +276,10 @@ spec:
- name: http
containerPort: {{ .Values.hubble.port }}
env:
+ - name: HG_HUBBLE_VERMEER_URL
+ value: {{ if $vermeerEnabled }}{{ printf "http://%s:%v" (include "hugegraph.vermeer.masterName" .) .Values.vermeer.master.httpPort | quote }}{{ else }}""{{ end }}
+ - name: HG_HUBBLE_VERMEER_TOKEN_FILE
+ value: {{ if $vermeerEnabled }}"/var/run/hugegraph/vermeer/token"{{ else }}""{{ end }}
{{- if $pdMode }}
- name: HG_HUBBLE_PD_PEERS
value: {{ include "hugegraph.pd.grpcPeersList" . | quote }}
@@ -280,6 +305,11 @@ spec:
volumeMounts:
- name: data
mountPath: /hubble-data
+ {{- if $vermeerEnabled }}
+ - name: vermeer-token
+ mountPath: /var/run/hugegraph/vermeer
+ readOnly: true
+ {{- end }}
startupProbe:
httpGet:
path: /actuator/health
@@ -308,6 +338,9 @@ spec:
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
+ {{- if $vermeerEnabled }}
+ {{- include "hugegraph.vermeer.tokenVolumes" . | nindent 8 }}
+ {{- end }}
- name: data
{{- if .Values.hubble.persistence.enabled }}
persistentVolumeClaim:
diff --git a/helm/hugegraph/templates/networkpolicy.yaml b/helm/hugegraph/templates/networkpolicy.yaml
index cbe2f4ccac..08b52d271c 100644
--- a/helm/hugegraph/templates/networkpolicy.yaml
+++ b/helm/hugegraph/templates/networkpolicy.yaml
@@ -32,6 +32,7 @@ Ingress does not automatically change these rules.
{{- $serverPort := .Values.server.port }}
{{- $hubbleEnabled := .Values.hubble.enabled }}
{{- $hubblePd := and $hubbleEnabled (eq .Values.hubble.mode "pd") }}
+{{- $vermeerEnabled := (.Values.vermeer | default dict).enabled | default false }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
@@ -55,6 +56,10 @@ spec:
- from:
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }}
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }}
+ {{- if $vermeerEnabled }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-master") | nindent 8 }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-worker") | nindent 8 }}
+ {{- end }}
{{- if $hubblePd }}
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "hubble") | nindent 8 }}
{{- end }}
@@ -92,6 +97,10 @@ spec:
{{- include "hugegraph.netpol.ports" (list $store.raft) | nindent 8 }}
- from:
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }}
+ {{- if $vermeerEnabled }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-master") | nindent 8 }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-worker") | nindent 8 }}
+ {{- end }}
ports:
{{- include "hugegraph.netpol.ports" (list $store.grpc $store.rest) | nindent 8 }}
{{- if $hubblePd }}
@@ -138,6 +147,10 @@ spec:
{{- if (.Values.seatunnel | default dict).enabled }}
{{- include "hugegraph.netpol.seatunnelPeer" . | nindent 8 }}
{{- end }}
+ {{- if $vermeerEnabled }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-master") | nindent 8 }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-worker") | nindent 8 }}
+ {{- end }}
ports:
{{- include "hugegraph.netpol.ports" (list $serverPort) | nindent 8 }}
{{- with get (get $np "server" | default dict) "extraIngress" }}
@@ -184,6 +197,12 @@ spec:
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }}
ports:
{{- include "hugegraph.netpol.ports" (list $serverPort) | nindent 8 }}
+ {{- if $vermeerEnabled }}
+ - to:
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "vermeer-master") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list .Values.vermeer.master.httpPort) | nindent 8 }}
+ {{- end }}
{{- if $hubblePd }}
- to:
{{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }}
diff --git a/helm/hugegraph/templates/seatunnel-auth-secret.yaml b/helm/hugegraph/templates/seatunnel-auth-secret.yaml
index 918f1a5ac9..9d20ce7fd3 100644
--- a/helm/hugegraph/templates/seatunnel-auth-secret.yaml
+++ b/helm/hugegraph/templates/seatunnel-auth-secret.yaml
@@ -31,7 +31,8 @@ limitations under the License.
{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") "password") -}}
{{- get (get $secret "data") "password" -}}
{{- else -}}
-{{- include "hugegraph.generatedCredential" (dict "root" . "name" "seatunnel-admin") | b64enc -}}
+{{- $length := ternary 16 6 (eq (.Values.mode | default "easy") "strict") -}}
+{{- include "hugegraph.generatedCredential" (dict "root" . "name" "seatunnel-admin" "length" $length) | b64enc -}}
{{- end -}}
{{- end -}}
{{- end -}}
diff --git a/helm/hugegraph/templates/seatunnel-networkpolicy.yaml b/helm/hugegraph/templates/seatunnel-networkpolicy.yaml
index faddf24c86..4a919cc4a8 100644
--- a/helm/hugegraph/templates/seatunnel-networkpolicy.yaml
+++ b/helm/hugegraph/templates/seatunnel-networkpolicy.yaml
@@ -37,11 +37,13 @@ spec:
{{- include "hugegraph.netpol.seatunnelPeer" . | nindent 8 }}
ports:
{{- include "hugegraph.netpol.ports" (list 5801 8080) | nindent 8 }}
- {{- if not .Values.networkPolicy.enabled }}
- # Easy mode keeps the authenticated HTTP entry open, never the member port.
- - ports:
+ - from:
+ {{- if .Values.hubble.enabled }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "hubble") | nindent 8 }}
+ {{- end }}
+ {{- include "hugegraph.netpol.peer" (dict "root" . "component" "test") | nindent 8 }}
+ ports:
{{- include "hugegraph.netpol.ports" (list 8080) | nindent 8 }}
- {{- end }}
{{- with get $rules "extraIngress" }}
{{- toYaml . | nindent 4 }}
{{- end }}
diff --git a/helm/hugegraph/templates/seatunnel.yaml b/helm/hugegraph/templates/seatunnel.yaml
index fabe1f269e..72dec9a198 100644
--- a/helm/hugegraph/templates/seatunnel.yaml
+++ b/helm/hugegraph/templates/seatunnel.yaml
@@ -122,24 +122,11 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
- type: {{ $svc.type }}
- {{- 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: {{ $svc.allocateLoadBalancerNodePorts }}
- {{- end }}
- {{- end }}
+ type: ClusterIP
ports:
- name: http
port: 8080
targetPort: http
- {{- with $svc.nodePort }}
- nodePort: {{ . }}
- {{- end }}
selector:
{{- include "hugegraph.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: seatunnel
diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml
index 6c10a8d33a..190a67a942 100644
--- a/helm/hugegraph/templates/server-deployment.yaml
+++ b/helm/hugegraph/templates/server-deployment.yaml
@@ -31,6 +31,8 @@ metadata:
labels:
{{- include "hugegraph.labels" . | nindent 4 }}
app.kubernetes.io/component: server
+ annotations:
+ hugegraph.apache.org/metadata-cluster: {{ .Values.metadataCluster | quote }}
spec:
{{- if not .Values.server.hpa.enabled }}
replicas: {{ .Values.server.replicas }}
@@ -253,6 +255,8 @@ spec:
- name: http
containerPort: {{ .Values.server.port }}
env:
+ - name: HG_SERVER_CLUSTER
+ value: {{ .Values.metadataCluster | quote }}
- name: POD_IP
valueFrom:
fieldRef:
diff --git a/helm/hugegraph/templates/store-statefulset.yaml b/helm/hugegraph/templates/store-statefulset.yaml
index 75ea6f5ba3..e76832df75 100644
--- a/helm/hugegraph/templates/store-statefulset.yaml
+++ b/helm/hugegraph/templates/store-statefulset.yaml
@@ -23,6 +23,8 @@ metadata:
labels:
{{- include "hugegraph.labels" . | nindent 4 }}
app.kubernetes.io/component: store
+ annotations:
+ hugegraph.apache.org/metadata-cluster: {{ .Values.metadataCluster | quote }}
spec:
serviceName: {{ include "hugegraph.store.name" . }}
replicas: {{ .Values.store.replicas }}
@@ -143,6 +145,8 @@ spec:
- name: rest
containerPort: {{ .Values.store.ports.rest }}
env:
+ - name: PD_CLUSTER
+ value: {{ .Values.metadataCluster | quote }}
- name: POD_NAME
valueFrom:
fieldRef:
diff --git a/helm/hugegraph/templates/tests/vermeer-availability.yaml b/helm/hugegraph/templates/tests/vermeer-availability.yaml
new file mode 100644
index 0000000000..20b2fc543c
--- /dev/null
+++ b/helm/hugegraph/templates/tests/vermeer-availability.yaml
@@ -0,0 +1,91 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if (.Values.vermeer | default dict).enabled }}
+# Keep one main container per test Pod: Helm's --logs does not select a container.
+apiVersion: v1
+kind: Pod
+metadata:
+ name: "{{ include "hugegraph.server.name" . | trunc 50 | trimSuffix "-" }}-vermeer-test"
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: test
+ annotations:
+ "helm.sh/hook": test
+ "helm.sh/hook-delete-policy": before-hook-creation
+spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ automountServiceAccountToken: false
+ restartPolicy: Never
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+ initContainers:
+ {{- include "hugegraph.vermeer.tokenInit" (dict "root" . "uid" "100" "gid" "101" "resources" .Values.server.testResources) | nindent 4 }}
+ containers:
+ - name: vermeer
+ image: {{ .Values.server.waitImage | quote }}
+ {{- if .Values.server.__waitImageInherited }}
+ imagePullPolicy: {{ .Values.server.image.pullPolicy }}
+ {{- end }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
+ readOnlyRootFilesystem: true
+ runAsGroup: 101
+ runAsNonRoot: true
+ runAsUser: 100
+ {{- with .Values.server.testResources }}
+ resources:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ command:
+ - sh
+ - -c
+ - |
+ set -eu
+ URL="http://{{ include "hugegraph.vermeer.masterName" . }}:{{ .Values.vermeer.master.httpPort }}/api/v1/workers/availability"
+ TOKEN=$(cat /var/run/hugegraph/vermeer/token)
+ START=$(date +%s)
+ DEADLINE_S="${HG_TEST_DEADLINE_S:-150}"
+ while :; do
+ # Scope readiness to the same service credential Hubble uses.
+ # A globally registered Worker may belong to another space.
+ if OUT=$(printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" |
+ curl --config - -fsS --connect-timeout 3 --max-time 20 "${URL}") &&
+ printf '%s' "${OUT}" | grep -Eq '"available"[[:space:]]*:[[:space:]]*true'; then
+ echo 'helm test: Vermeer service token and allocated Worker availability OK'
+ exit 0
+ fi
+ if [ $(( $(date +%s) - START )) -ge "${DEADLINE_S}" ]; then
+ echo 'helm test: Vermeer unavailable for the Hubble service space; inspect Worker allocation and authentication' >&2
+ exit 1
+ fi
+ sleep 5
+ done
+ volumeMounts:
+ - name: vermeer-token
+ mountPath: /var/run/hugegraph/vermeer
+ readOnly: true
+ volumes:
+ {{- include "hugegraph.vermeer.tokenVolumes" . | nindent 4 }}
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-admin-secret.yaml b/helm/hugegraph/templates/vermeer-admin-secret.yaml
new file mode 100644
index 0000000000..5fbaf5bfb4
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-admin-secret.yaml
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if and .Values.vermeer.enabled (not .Values.vermeer.auth.admin.existingSecret) }}
+{{- include "hugegraph.validateValues" . }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "hugegraph.vermeer.adminSecretName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+ annotations:
+ helm.sh/resource-policy: keep
+type: Opaque
+data:
+ {{ include "hugegraph.vermeer.adminSecretKey" . | quote }}: {{ include "hugegraph.vermeer.adminSecretValue" . | quote }}
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-auth-secret.yaml b/helm/hugegraph/templates/vermeer-auth-secret.yaml
new file mode 100644
index 0000000000..7c8cd19a51
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-auth-secret.yaml
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if and .Values.vermeer.enabled (not .Values.vermeer.auth.existingSecret) }}
+{{- include "hugegraph.validateValues" . }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "hugegraph.vermeer.authSecretName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+ annotations:
+ helm.sh/resource-policy: keep
+type: Opaque
+data:
+ {{ include "hugegraph.vermeer.authSecretKey" . | quote }}: {{ include "hugegraph.vermeer.authSecretValue" . | quote }}
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-configmap.yaml b/helm/hugegraph/templates/vermeer-configmap.yaml
new file mode 100644
index 0000000000..16233c8c6b
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-configmap.yaml
@@ -0,0 +1,51 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if .Values.vermeer.enabled }}
+{{- include "hugegraph.validateValues" . }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "hugegraph.vermeer.configName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+data:
+ master.ini: |
+ [default]
+ run_mode=master
+ log_level=info
+ debug_mode=release
+ http_peer=0.0.0.0:{{ .Values.vermeer.master.httpPort }}
+ grpc_peer={{ ternary "127.0.0.1" "0.0.0.0" (eq .Values.vermeer.mode "standalone") }}:{{ .Values.vermeer.master.grpcPort }}
+ master_peer=127.0.0.1:{{ .Values.vermeer.master.grpcPort }}
+ auth=token
+ task_strategy=1
+ task_parallel_num=1
+ start_chan_size=10
+ ticker_interval=1
+ worker.ini: |
+ [default]
+ run_mode=worker
+ log_level=info
+ debug_mode=release
+ http_peer=0.0.0.0:{{ .Values.vermeer.worker.httpPort }}
+ master_peer={{ if eq .Values.vermeer.mode "standalone" }}127.0.0.1{{ else }}{{ include "hugegraph.vermeer.masterName" . }}-internal.{{ .Release.Namespace }}.svc{{ end }}:{{ .Values.vermeer.master.grpcPort }}
+ worker_group=$
+ auth=none
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-networkpolicy.yaml b/helm/hugegraph/templates/vermeer-networkpolicy.yaml
new file mode 100644
index 0000000000..a9d45f6344
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-networkpolicy.yaml
@@ -0,0 +1,89 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if .Values.vermeer.enabled }}
+{{- $v := .Values.vermeer }}
+{{- $rules := get .Values.networkPolicy "vermeer" | default dict }}
+{{- range $role := list "master" "worker" }}
+{{- if or (eq $role "master") (eq $v.mode "cluster") }}
+{{- $name := include "hugegraph.vermeer.masterName" $ }}
+{{- if eq $role "worker" }}{{- $name = include "hugegraph.vermeer.workerName" $ }}{{- end }}
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: {{ $name }}
+ labels:
+ {{- include "hugegraph.labels" $ | nindent 4 }}
+ app.kubernetes.io/component: vermeer-{{ $role }}
+spec:
+ podSelector:
+ matchLabels:
+ {{- include "hugegraph.selectorLabels" $ | nindent 6 }}
+ app.kubernetes.io/component: vermeer-{{ $role }}
+ policyTypes:
+ - Ingress
+ {{- if $.Values.networkPolicy.enabled }}
+ - Egress
+ {{- end }}
+ ingress:
+ # Internal gRPC and Worker HTTP are never admitted from arbitrary Pods.
+ - from:
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "vermeer-master") | nindent 8 }}
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "vermeer-worker") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $v.master.httpPort $v.master.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }}
+ {{- if eq $role "master" }}
+ - from:
+ {{- if $.Values.hubble.enabled }}
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "hubble") | nindent 8 }}
+ {{- end }}
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "test") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $v.master.httpPort) | nindent 8 }}
+ {{- end }}
+ {{- with get $rules "extraIngress" }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ {{- if $.Values.networkPolicy.enabled }}
+ egress:
+ - to:
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "vermeer-master") | nindent 8 }}
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "vermeer-worker") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $v.master.httpPort $v.master.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }}
+ - to:
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "pd") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $.Values.pd.ports.grpc $.Values.pd.ports.rest) | nindent 8 }}
+ - to:
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "store") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $.Values.store.ports.grpc $.Values.store.ports.rest) | nindent 8 }}
+ - to:
+ {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "server") | nindent 8 }}
+ ports:
+ {{- include "hugegraph.netpol.ports" (list $.Values.server.port) | nindent 8 }}
+ {{- include "hugegraph.netpol.dns" $ | nindent 4 }}
+ {{- with get $rules "extraEgress" }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-service.yaml b/helm/hugegraph/templates/vermeer-service.yaml
new file mode 100644
index 0000000000..baf6012d9b
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-service.yaml
@@ -0,0 +1,79 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if .Values.vermeer.enabled }}
+{{- $v := .Values.vermeer }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "hugegraph.vermeer.masterName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+ {{- with $v.service.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ type: ClusterIP
+ selector:
+ {{- include "hugegraph.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+ ports:
+ - name: http
+ port: {{ $v.master.httpPort }}
+ targetPort: master-http
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "hugegraph.vermeer.masterName" . }}-internal
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+spec:
+ clusterIP: None
+ publishNotReadyAddresses: true
+ selector:
+ {{- include "hugegraph.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-master
+ ports:
+ - name: grpc
+ port: {{ $v.master.grpcPort }}
+ targetPort: master-grpc
+{{- if eq $v.mode "cluster" }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "hugegraph.vermeer.workerName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-worker
+spec:
+ clusterIP: None
+ publishNotReadyAddresses: true
+ selector:
+ {{- include "hugegraph.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-worker
+ ports:
+ - name: grpc
+ port: {{ $v.worker.grpcPort }}
+ targetPort: worker-grpc
+{{- end }}
+{{- end }}
diff --git a/helm/hugegraph/templates/vermeer-statefulset.yaml b/helm/hugegraph/templates/vermeer-statefulset.yaml
new file mode 100644
index 0000000000..8838a1d081
--- /dev/null
+++ b/helm/hugegraph/templates/vermeer-statefulset.yaml
@@ -0,0 +1,161 @@
+#
+# 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.
+#
+
+{{ include "hugegraph.resolveValues" . }}
+{{- if .Values.vermeer.enabled }}
+{{- include "hugegraph.validateValues" . }}
+{{- $v := .Values.vermeer }}
+{{- $roles := list "master" }}
+{{- if eq $v.mode "cluster" }}{{- $roles = append $roles "worker" }}{{- end }}
+{{- range $role := $roles }}
+{{- $master := eq $role "master" }}
+{{- $worker := or (not $master) (eq $v.mode "standalone") }}
+{{- $roleValues := get $v $role }}
+{{- $name := include "hugegraph.vermeer.masterName" $ }}
+{{- if not $master }}{{- $name = include "hugegraph.vermeer.workerName" $ }}{{- end }}
+{{- $configChecksum := dict }}
+{{- if $master }}
+{{- $_ := set $configChecksum "master" (dict "mode" $v.mode "httpPort" $v.master.httpPort "grpcPort" $v.master.grpcPort) }}
+{{- end }}
+{{- if $worker }}
+{{- $_ := set $configChecksum "worker" (dict "mode" $v.mode "masterGrpcPort" $v.master.grpcPort "httpPort" $v.worker.httpPort "grpcPort" $v.worker.grpcPort) }}
+{{- end }}
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: {{ $name }}
+ labels:
+ {{- include "hugegraph.labels" $ | nindent 4 }}
+ app.kubernetes.io/component: vermeer-{{ $role }}
+spec:
+ serviceName: {{ $name }}{{ if $master }}-internal{{ end }}
+ replicas: {{ if $master }}1{{ else }}{{ $v.worker.replicas }}{{ end }}
+ podManagementPolicy: Parallel
+ updateStrategy:
+ type: RollingUpdate
+ {{- if $roleValues.storage.enabled }}
+ persistentVolumeClaimRetentionPolicy:
+ whenDeleted: Retain
+ whenScaled: Retain
+ {{- end }}
+ selector:
+ matchLabels:
+ {{- include "hugegraph.selectorLabels" $ | nindent 6 }}
+ app.kubernetes.io/component: vermeer-{{ $role }}
+ template:
+ metadata:
+ labels:
+ {{- include "hugegraph.selectorLabels" $ | nindent 8 }}
+ app.kubernetes.io/component: vermeer-{{ $role }}
+ {{- with $v.podLabels }}{{ toYaml . | nindent 8 }}{{- end }}
+ annotations:
+ checksum/config: {{ $configChecksum | toJson | sha256sum | quote }}
+ checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" $ | quote }}
+ {{- if $master }}
+ checksum/vermeer-auth: {{ include "hugegraph.vermeer.authChecksum" $ | quote }}
+ checksum/vermeer-admin: {{ include "hugegraph.vermeer.adminChecksum" $ | quote }}
+ {{- end }}
+ {{- if $worker }}
+ {{- if $v.hugegraphCredentials.existingSecret }}
+ {{- $live := lookup "v1" "Secret" $.Release.Namespace $v.hugegraphCredentials.existingSecret | default dict }}
+ checksum/hugegraph-auth: {{ printf "%s|%s|%s" $v.hugegraphCredentials.existingSecret $v.hugegraphCredentials.key (dig "metadata" "resourceVersion" "" $live) | sha256sum | quote }}
+ {{- else if $.Values.server.auth.enabled }}
+ checksum/hugegraph-auth: {{ include "hugegraph.server.authChecksum" $ | quote }}
+ {{- end }}
+ {{- end }}
+ {{- with $v.podAnnotations }}{{ toYaml . | nindent 8 }}{{- end }}
+ spec:
+ automountServiceAccountToken: false
+ terminationGracePeriodSeconds: {{ $v.terminationGracePeriodSeconds }}
+ {{- with $.Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.topologySpreadConstraints }}
+ topologySpreadConstraints:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $v.priorityClassName }}
+ priorityClassName: {{ . | quote }}
+ {{- end }}
+ containers:
+ {{- if $master }}
+ {{- include "hugegraph.vermeer.container" (dict "root" $ "role" "master") | nindent 8 }}
+ {{- end }}
+ {{- if $worker }}
+ {{- include "hugegraph.vermeer.container" (dict "root" $ "role" "worker") | nindent 8 }}
+ {{- end }}
+ volumes:
+ {{- include "hugegraph.vermeer.volumes" (dict "root" $ "master" $master "worker" $worker) | nindent 8 }}
+ {{- if not $roleValues.storage.enabled }}
+ - name: {{ $role }}-data
+ emptyDir: {}
+ {{- end }}
+ {{- if and $master $worker }}
+ - name: worker-data
+ {{- if $v.worker.storage.enabled }}
+ persistentVolumeClaim:
+ claimName: {{ include "hugegraph.vermeer.standaloneWorkerDataName" $ }}
+ {{- else }}
+ emptyDir: {}
+ {{- end }}
+ {{- end }}
+ {{- if $roleValues.storage.enabled }}
+ volumeClaimTemplates:
+ {{- include "hugegraph.vermeer.claim" (dict "role" $role "storage" $roleValues.storage) | nindent 4 }}
+ {{- end }}
+{{- end }}
+{{- if and (eq $v.mode "standalone") $v.worker.storage.enabled }}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: {{ include "hugegraph.vermeer.standaloneWorkerDataName" . }}
+ labels:
+ {{- include "hugegraph.labels" . | nindent 4 }}
+ app.kubernetes.io/component: vermeer-worker
+ annotations:
+ helm.sh/resource-policy: keep
+spec:
+ accessModes: ["ReadWriteOnce"]
+ {{- if eq $v.worker.storage.storageClassName "-" }}
+ storageClassName: ""
+ {{- else if $v.worker.storage.storageClassName }}
+ storageClassName: {{ $v.worker.storage.storageClassName | quote }}
+ {{- end }}
+ resources:
+ requests:
+ storage: {{ $v.worker.storage.size }}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml b/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml
new file mode 100644
index 0000000000..03d644fb9a
--- /dev/null
+++ b/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml
@@ -0,0 +1,246 @@
+#
+# 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: Metadata namespace changes protect existing Server metadata
+templates:
+ - server-deployment.yaml
+kubernetesProvider:
+ scheme:
+ "apps/v1/Deployment":
+ gvr:
+ group: apps
+ version: v1
+ resource: deployments
+ namespaced: true
+ "apps/v1/StatefulSet":
+ gvr:
+ group: apps
+ version: v1
+ resource: statefulsets
+ namespaced: true
+ objects:
+ - apiVersion: apps/v1
+ kind: StatefulSet
+ metadata:
+ name: other-hugegraph-pd
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/instance: other
+ app.kubernetes.io/component: pd
+ spec:
+ replicas: "1"
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: RELEASE-NAME-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: RELEASE-NAME
+ annotations:
+ hugegraph.apache.org/metadata-cluster: hg-test
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env:
+ - name: HG_SERVER_CLUSTER
+ value: hg-test
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: custom-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: custom
+ annotations:
+ hugegraph.apache.org/metadata-cluster: production
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env:
+ - name: HG_SERVER_CLUSTER
+ value: production
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: legacy-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: legacy
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env: []
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: mismatch-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: mismatch
+ annotations:
+ hugegraph.apache.org/metadata-cluster: hg-test
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env:
+ - name: HG_SERVER_CLUSTER
+ value: production
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: unknown-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: unknown
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env:
+ - name: HG_SERVER_CLUSTER
+ valueFrom:
+ secretKeyRef:
+ name: external-namespace
+ key: cluster
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: adopted-hugegraph-server
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/component: server
+ app.kubernetes.io/instance: adopted
+ annotations:
+ hugegraph.apache.org/metadata-cluster: production
+ spec:
+ template:
+ spec:
+ containers:
+ - name: server
+ env:
+ - name: HG_SERVER_CLUSTER
+ valueFrom:
+ secretKeyRef:
+ name: external-namespace
+ key: cluster
+tests:
+ - it: permits an unchanged explicitly recorded namespace
+ release:
+ upgrade: true
+ asserts:
+ - equal:
+ path: metadata.annotations["hugegraph.apache.org/metadata-cluster"]
+ value: hg-test
+
+ - it: refuses changing an explicitly configured namespace
+ release:
+ upgrade: true
+ set:
+ metadataCluster: production
+ asserts:
+ - failedTemplate:
+ errorPattern: metadataCluster
+
+ - it: preserves an existing custom namespace when explicitly selected
+ release:
+ name: custom
+ upgrade: true
+ set:
+ metadataCluster: production
+ asserts:
+ - equal:
+ path: metadata.annotations["hugegraph.apache.org/metadata-cluster"]
+ value: production
+
+ - it: refuses replacing a custom namespace with the default
+ release:
+ name: custom
+ upgrade: true
+ asserts:
+ - failedTemplate:
+ errorPattern: metadataCluster
+
+ - it: retains the official historical Server default when no record exists
+ release:
+ name: legacy
+ upgrade: true
+ asserts:
+ - equal:
+ path: metadata.annotations["hugegraph.apache.org/metadata-cluster"]
+ value: hg-test
+
+ - it: requires verification before adopting a legacy deployment into a custom namespace
+ release:
+ name: legacy
+ upgrade: true
+ set:
+ metadataCluster: production
+ asserts:
+ - failedTemplate:
+ errorPattern: metadataCluster
+
+ - it: refuses an environment value that conflicts with its recorded annotation
+ release:
+ name: mismatch
+ upgrade: true
+ asserts:
+ - failedTemplate:
+ errorPattern: metadataCluster
+
+ - it: cannot infer a valueFrom namespace without a verified record
+ release:
+ name: unknown
+ upgrade: true
+ asserts:
+ - failedTemplate:
+ errorPattern: metadataCluster
+
+ - it: permits explicit adoption of a verified valueFrom namespace
+ release:
+ name: adopted
+ upgrade: true
+ set:
+ metadataCluster: production
+ asserts:
+ - equal:
+ path: metadata.annotations["hugegraph.apache.org/metadata-cluster"]
+ value: production
+
+ - it: permits a fresh custom install independently of existing upgrade records
+ release:
+ name: legacy
+ upgrade: false
+ set:
+ metadataCluster: production
+ asserts:
+ - equal:
+ path: metadata.annotations["hugegraph.apache.org/metadata-cluster"]
+ value: production
diff --git a/helm/hugegraph/tests/networkpolicy_test.yaml b/helm/hugegraph/tests/networkpolicy_test.yaml
index 91124ceb86..f8505e3063 100644
--- a/helm/hugegraph/tests/networkpolicy_test.yaml
+++ b/helm/hugegraph/tests/networkpolicy_test.yaml
@@ -832,7 +832,7 @@ tests:
documentIndex: 0
- equal:
path: metadata.labels["helm.sh/chart"]
- value: hugegraph-helm-0.3.0
+ value: hugegraph-helm-0.4.0
documentIndex: 0
- equal:
path: metadata.labels["app.kubernetes.io/version"]
@@ -840,7 +840,7 @@ tests:
documentIndex: 1
- equal:
path: metadata.labels["helm.sh/chart"]
- value: hugegraph-helm-0.3.0
+ value: hugegraph-helm-0.4.0
documentIndex: 1
- equal:
path: metadata.labels["app.kubernetes.io/version"]
@@ -848,7 +848,7 @@ tests:
documentIndex: 2
- equal:
path: metadata.labels["helm.sh/chart"]
- value: hugegraph-helm-0.3.0
+ value: hugegraph-helm-0.4.0
documentIndex: 2
- equal:
path: metadata.labels["app.kubernetes.io/version"]
@@ -856,7 +856,7 @@ tests:
documentIndex: 3
- equal:
path: metadata.labels["helm.sh/chart"]
- value: hugegraph-helm-0.3.0
+ value: hugegraph-helm-0.4.0
documentIndex: 3
- it: appends extra rules verbatim
diff --git a/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml
new file mode 100644
index 0000000000..60f0606c3b
--- /dev/null
+++ b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml
@@ -0,0 +1,112 @@
+#
+# 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: SeaTunnel shorter generated passwords preserve installed credentials
+templates:
+ - seatunnel-auth-secret.yaml
+ - seatunnel.yaml
+set:
+ seatunnel.enabled: true
+kubernetesProvider:
+ scheme:
+ "v1/Secret":
+ gvr:
+ version: v1
+ resource: secrets
+ namespaced: true
+ objects:
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: RELEASE-NAME-hugegraph-seatunnel-auth
+ namespace: NAMESPACE
+ data:
+ password: bGVnYWN5LWFkbWluLTAxMjM0NTY3ODlhYmNkZWZnaGk=
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: six-hugegraph-seatunnel-auth
+ namespace: NAMESPACE
+ data:
+ password: QWIxMnhZ
+tests:
+ - it: retains a legacy 32 character password during a no-change upgrade
+ template: seatunnel-auth-secret.yaml
+ asserts:
+ - equal:
+ path: data.password
+ decodeBase64: true
+ value: legacy-admin-0123456789abcdefghi
+ - equal:
+ path: metadata.annotations["helm.sh/resource-policy"]
+ value: keep
+ - matchRegex:
+ path: data["seatunnel.yaml"]
+ decodeBase64: true
+ pattern: 'basic-auth-password: legacy-admin-0123456789abcdefghi'
+
+ - it: honors an explicit password rotation instead of restoring the old value
+ template: seatunnel-auth-secret.yaml
+ set:
+ seatunnel.auth.password: NextAdmin0123456
+ asserts:
+ - equal:
+ path: data.password
+ decodeBase64: true
+ value: NextAdmin0123456
+ - matchRegex:
+ path: data["seatunnel.yaml"]
+ decodeBase64: true
+ pattern: 'basic-auth-password: NextAdmin0123456'
+
+ - it: generates a 6 character alphanumeric password for a new easy release
+ template: seatunnel-auth-secret.yaml
+ release:
+ name: fresh
+ asserts:
+ - matchRegex:
+ path: data.password
+ decodeBase64: true
+ pattern: '^[A-Za-z0-9]{6}$'
+
+ - it: generates a 16 character alphanumeric password for a new strict release
+ template: seatunnel-auth-secret.yaml
+ release:
+ name: fresh
+ set:
+ mode: strict
+ asserts:
+ - matchRegex:
+ path: data.password
+ decodeBase64: true
+ pattern: '^[A-Za-z0-9]{16}$'
+
+ - it: preserves an existing easy password when switching to strict
+ template: seatunnel-auth-secret.yaml
+ release:
+ name: six
+ set:
+ mode: strict
+ asserts:
+ - equal:
+ path: data.password
+ decodeBase64: true
+ value: Ab12xY
+ - matchRegex:
+ path: data["seatunnel.yaml"]
+ decodeBase64: true
+ pattern: 'basic-auth-password: Ab12xY'
diff --git a/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml b/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml
new file mode 100644
index 0000000000..f76cdaac20
--- /dev/null
+++ b/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml
@@ -0,0 +1,143 @@
+#
+# 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: Vermeer native login preserves its independent password during upgrades
+templates:
+ - vermeer-admin-secret.yaml
+ - vermeer-statefulset.yaml
+set:
+ vermeer.enabled: true
+kubernetesProvider:
+ scheme:
+ "v1/Secret":
+ gvr:
+ version: v1
+ resource: secrets
+ namespaced: true
+ objects:
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: RELEASE-NAME-vermeer-admin
+ namespace: NAMESPACE
+ data:
+ password: UGVyc2lzdGVkTG9naW4xMjM=
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: external-ui
+ namespace: NAMESPACE
+ resourceVersion: "17"
+ data:
+ ui-password: b3BlcmF0b3ItbG9naW4=
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: six-vermeer-admin
+ namespace: NAMESPACE
+ data:
+ password: QWIxMnhZ
+tests:
+ - it: preserves an existing easy password when switching to strict
+ template: vermeer-admin-secret.yaml
+ release:
+ name: six
+ set:
+ mode: strict
+ asserts:
+ - equal:
+ path: data.password
+ decodeBase64: true
+ value: Ab12xY
+
+ - it: reuses the installed native login password
+ template: vermeer-admin-secret.yaml
+ asserts:
+ - equal:
+ path: data.password
+ decodeBase64: true
+ value: PersistedLogin123
+ - equal:
+ path: metadata.annotations["helm.sh/resource-policy"]
+ value: keep
+
+ - it: hashes the retained value so a no-change upgrade does not roll Master
+ template: vermeer-statefulset.yaml
+ documentSelector:
+ path: kind
+ value: StatefulSet
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-admin"]
+ value: 7dca73c0c56b428dd060708b58055b2cd1e9a38d31017cde4c7881c3d4ef8b40
+
+ - it: keeps that checksum when pinning the installed password explicitly
+ template: vermeer-statefulset.yaml
+ documentSelector:
+ path: kind
+ value: StatefulSet
+ set:
+ vermeer.auth.admin.password: PersistedLogin123
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-admin"]
+ value: 7dca73c0c56b428dd060708b58055b2cd1e9a38d31017cde4c7881c3d4ef8b40
+
+ - it: does not manage the external login Secret
+ template: vermeer-admin-secret.yaml
+ set:
+ vermeer.auth.admin.existingSecret: external-ui
+ vermeer.auth.admin.key: ui-password
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: tracks the external Secret revision and ignores an unused inline password
+ template: vermeer-statefulset.yaml
+ documentSelector:
+ path: kind
+ value: StatefulSet
+ set:
+ vermeer.auth.admin.existingSecret: external-ui
+ vermeer.auth.admin.key: ui-password
+ vermeer.auth.admin.password: unused-inline-password
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-admin"]
+ value: 384c79eb04541fb1216e49b3739cfa3ac87bf7a3ae3c84891eabda9421351b4b
+
+ - it: generates a 6 character independent login for a fresh easy release
+ template: vermeer-admin-secret.yaml
+ release:
+ name: fresh
+ asserts:
+ - matchRegex:
+ path: data.password
+ decodeBase64: true
+ pattern: '^[A-Za-z0-9]{6}$'
+
+ - it: generates a 16 character independent login for a fresh strict release
+ template: vermeer-admin-secret.yaml
+ release:
+ name: fresh
+ set:
+ mode: strict
+ asserts:
+ - matchRegex:
+ path: data.password
+ decodeBase64: true
+ pattern: '^[A-Za-z0-9]{16}$'
diff --git a/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml b/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml
new file mode 100644
index 0000000000..610811773e
--- /dev/null
+++ b/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml
@@ -0,0 +1,125 @@
+#
+# 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.
+#
+
+# Exercise lookup against existing Secrets: template-only renders cannot
+# establish that an online no-change upgrade retains its signing identity.
+suite: Vermeer authentication survives online upgrades
+templates:
+ - vermeer-auth-secret.yaml
+ - vermeer-statefulset.yaml
+ - hubble-deployment.yaml
+set:
+ vermeer.enabled: true
+kubernetesProvider:
+ scheme:
+ "v1/Secret":
+ gvr:
+ version: v1
+ resource: secrets
+ namespaced: true
+ "apps/v1/StatefulSet":
+ gvr:
+ group: apps
+ version: v1
+ resource: statefulsets
+ namespaced: true
+ objects:
+ - apiVersion: apps/v1
+ kind: StatefulSet
+ metadata:
+ name: other-hugegraph-pd
+ namespace: NAMESPACE
+ labels:
+ app.kubernetes.io/instance: other
+ app.kubernetes.io/component: pd
+ spec:
+ replicas: "1"
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: RELEASE-NAME-vermeer-auth
+ namespace: NAMESPACE
+ resourceVersion: "41"
+ data:
+ token-factor: c3RlYWR5LXZlcm1lZXItZmFjdG9y
+ - apiVersion: v1
+ kind: Secret
+ metadata:
+ name: external-vermeer
+ namespace: NAMESPACE
+ resourceVersion: "17"
+ data:
+ factor-key: b3BlcmF0b3ItbWFuYWdlZC1mYWN0b3I=
+tests:
+ - it: reuses a generated signing factor from the live Secret
+ template: vermeer-auth-secret.yaml
+ asserts:
+ - equal:
+ path: data["token-factor"]
+ value: c3RlYWR5LXZlcm1lZXItZmFjdG9y
+ - equal:
+ path: metadata.annotations["helm.sh/resource-policy"]
+ value: keep
+
+ - it: hashes the reused factor by value so a no-change upgrade does not roll Master
+ template: vermeer-statefulset.yaml
+ documentSelector:
+ path: kind
+ value: StatefulSet
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-auth"]
+ value: 5b204ecdd8e8b41e3e2a969363a783f90f93310fb3bef1852d4741a81a28cbf9
+
+ - it: shares the same signing identity checksum with Hubble
+ template: hubble-deployment.yaml
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-auth"]
+ value: 5b204ecdd8e8b41e3e2a969363a783f90f93310fb3bef1852d4741a81a28cbf9
+
+ - it: keeps the same checksum when explicitly pinning the reused factor
+ template: vermeer-statefulset.yaml
+ documentSelector:
+ path: kind
+ value: StatefulSet
+ set:
+ vermeer.auth.value: steady-vermeer-factor
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-auth"]
+ value: 5b204ecdd8e8b41e3e2a969363a783f90f93310fb3bef1852d4741a81a28cbf9
+
+ - it: does not manage an operator-supplied Secret
+ template: vermeer-auth-secret.yaml
+ set:
+ vermeer.auth.existingSecret: external-vermeer
+ vermeer.auth.key: factor-key
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: tracks the external Secret revision and ignores an unused inline factor
+ template: hubble-deployment.yaml
+ set:
+ vermeer.auth.existingSecret: external-vermeer
+ vermeer.auth.key: factor-key
+ vermeer.auth.value: unused-inline-factor
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["checksum/vermeer-auth"]
+ value: 363adfb0852476240f5701d50037622bfad246079a77e5e11886fa33fc045de0
diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml
index c9b39bd3d9..98e7d99058 100644
--- a/helm/hugegraph/values-cluster.yaml
+++ b/helm/hugegraph/values-cluster.yaml
@@ -129,3 +129,8 @@ server:
# memory: 1536Mi
# Network isolation is selected by mode or an explicit enabled override.
+
+# Optional computation follows this preset only when explicitly enabled.
+vermeer:
+ enabled: false
+ mode: cluster
diff --git a/helm/hugegraph/values-single.yaml b/helm/hugegraph/values-single.yaml
index 92f4acf4d5..0b8f78b81f 100644
--- a/helm/hugegraph/values-single.yaml
+++ b/helm/hugegraph/values-single.yaml
@@ -44,3 +44,8 @@ server:
autoGenerate: true
token:
autoGenerate: true
+
+# Optional computation follows this preset only when explicitly enabled.
+vermeer:
+ enabled: false
+ mode: standalone
diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json
index 4c0713c864..d1a134cb05 100644
--- a/helm/hugegraph/values.schema.json
+++ b/helm/hugegraph/values.schema.json
@@ -78,7 +78,7 @@
"minimum": 1
},
"service": {
- "$ref": "#/definitions/service"
+ "$ref": "#/definitions/nativeEngineService"
},
"auth": {
"type": "object",
@@ -113,6 +113,15 @@
"type": "string"
}
}
+ },
+ "vermeer": {
+ "$ref": "#/definitions/vermeer"
+ },
+ "metadataCluster": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_.-]+$",
+ "description": "Shared Server/Store PD metadata namespace. Preserve the initialized Server value; changing it does not migrate data."
}
},
"definitions": {
@@ -136,12 +145,7 @@
"networkPolicyEgressRules": {
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "to": {
- "$ref": "#/definitions/networkPolicyPeers"
- }
- }
+ "type": "object"
}
},
"networkPolicyComponent": {
@@ -185,16 +189,10 @@
}
},
"seatunnel": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "extraIngress": {
- "$ref": "#/definitions/networkPolicyIngressRules"
- },
- "extraEgress": {
- "$ref": "#/definitions/networkPolicyEgressRules"
- }
- }
+ "$ref": "#/definitions/nativeEngineNetworkPolicy"
+ },
+ "vermeer": {
+ "$ref": "#/definitions/nativeEngineNetworkPolicy"
}
}
},
@@ -1656,6 +1654,259 @@
]
}
}
+ },
+ "vermeerRole": {
+ "type": "object",
+ "properties": {
+ "httpPort": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "grpcPort": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "storage": {
+ "$ref": "#/definitions/storage"
+ },
+ "extraEnv": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "httpPort",
+ "grpcPort",
+ "storage"
+ ]
+ },
+ "vermeer": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "mode": {
+ "enum": [
+ "standalone",
+ "cluster"
+ ]
+ },
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "service": {
+ "$ref": "#/definitions/nativeEngineService"
+ },
+ "auth": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "pattern": "^$|^[!-~]{16,}$"
+ },
+ "existingSecret": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "minLength": 1
+ },
+ "autoGenerate": {
+ "type": "boolean"
+ },
+ "admin": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "password": {
+ "type": "string",
+ "pattern": "^[^\\r\\n]*$"
+ },
+ "existingSecret": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "minLength": 1
+ },
+ "autoGenerate": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ "hugegraphCredentials": {
+ "type": "object",
+ "properties": {
+ "existingSecret": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "master": {
+ "$ref": "#/definitions/vermeerRole"
+ },
+ "worker": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/vermeerRole"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "replicas": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 99
+ }
+ }
+ }
+ ]
+ },
+ "probes": {
+ "$ref": "#/definitions/probes"
+ },
+ "terminationGracePeriodSeconds": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "podSecurityContext": {
+ "type": "object"
+ },
+ "securityContext": {
+ "type": "object"
+ },
+ "nodeSelector": {
+ "type": "object"
+ },
+ "tolerations": {
+ "type": "array"
+ },
+ "affinity": {
+ "type": "object"
+ },
+ "topologySpreadConstraints": {
+ "type": "array"
+ },
+ "priorityClassName": {
+ "type": "string"
+ },
+ "podLabels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "podAnnotations": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "nativeEngineService": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "ClusterIP",
+ null
+ ]
+ },
+ "annotations": {
+ "type": "object"
+ },
+ "nodePort": {
+ "type": "null",
+ "description": "Compatibility placeholder only; native engines cannot expose NodePorts."
+ }
+ }
+ },
+ "nativeEngineIngressRules": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "from"
+ ],
+ "properties": {
+ "from": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "podSelector": {
+ "type": "object"
+ },
+ "namespaceSelector": {
+ "type": "object"
+ }
+ },
+ "anyOf": [
+ {
+ "required": [
+ "podSelector"
+ ]
+ },
+ {
+ "required": [
+ "namespaceSelector"
+ ]
+ }
+ ]
+ }
+ },
+ "ports": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "nativeEngineNetworkPolicy": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "extraIngress": {
+ "$ref": "#/definitions/nativeEngineIngressRules"
+ },
+ "extraEgress": {
+ "$ref": "#/definitions/networkPolicyEgressRules"
+ }
+ }
}
}
}
diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml
index 0d49297668..3e6070a5d4 100644
--- a/helm/hugegraph/values.yaml
+++ b/helm/hugegraph/values.yaml
@@ -21,6 +21,9 @@
# easy: browser/API NodePorts; strict: internal Services and NetworkPolicy.
# Explicit component parameters always override these mode defaults.
mode: easy
+# Shared PD metadata namespace used by Server and Store Schema reads.
+# Preserve the existing Server value on upgrades; this is not a migration switch.
+metadataCluster: hg-test
global:
imageRegistry: ""
persistence:
@@ -554,16 +557,16 @@ seatunnel:
tag: "3.0.0"
pullPolicy: IfNotPresent
service:
- # Unset inherits mode (easy: NodePort, strict: ClusterIP).
- type:
- # Omitted/0 allocates automatically; only the HTTP/UI port is exposed.
+ # Internal in both easy and strict modes; use Hubble or port-forward.
+ type: ClusterIP
+ # Reserved compatibility field; native engines cannot use a NodePort.
nodePort:
annotations: {}
auth:
# Native HTTP Basic authentication for the Web UI and all REST endpoints.
# Independent of the HugeGraph credential used by source/sink connectors.
username: admin
- # Empty generates a kept Secret; upgrades reuse the existing password.
+ # Empty generates 6 characters in easy / 16 in strict; upgrades reuse the kept Secret.
password: ""
# Replaces the image's jvm_options; reserve memory outside the heap under 2Gi.
javaOpts: >-
@@ -596,9 +599,94 @@ seatunnel:
# Keep member port 5801 and HTTP 8080 when overriding discovery/engine files.
config: {}
+# Optional native graph-computing engine. Both modes remain disabled until opted in.
+# standalone runs Master + Worker containers together; cluster uses separate Pods.
+vermeer:
+ enabled: false
+ mode: standalone
+ image:
+ repository: hugegraph/vermeer
+ # Official matching build with file credentials, native admin login and token helper.
+ # Pin the multi-platform manifest; clear digest when selecting a different tag.
+ tag: helm-30b67d93e7d0
+ digest: sha256:f9b69ef134069b432de1c9964374d7fdbfa53245d1fe7dcbce1e75b82b69dc9e
+ pullPolicy: IfNotPresent
+ auth:
+ # Token signing factor. existingSecret > value > a stable generated Secret.
+ # File-backed signing keys require at least 16 characters; prefer 32 random characters.
+ value: ""
+ existingSecret: ""
+ key: token-factor
+ autoGenerate: true
+ # Separate native UI login: 6 characters in easy / 16 in strict for new passwords.
+ # Hubble continues to use the internal service token; upgrades reuse the kept Secret.
+ admin:
+ password: ""
+ existingSecret: ""
+ key: password
+ autoGenerate: true
+ # Optional JSON {"username":"...","password":"..."} for Schema reads.
+ # Empty reuses the deployment's Server administrator Secret as a password file.
+ hugegraphCredentials:
+ existingSecret: ""
+ key: credentials.json
+ service:
+ # Internal in both easy and strict modes; use Hubble or port-forward.
+ type: ClusterIP
+ nodePort:
+ annotations: {}
+ master:
+ httpPort: 6688
+ grpcPort: 6689
+ resources: {}
+ storage:
+ enabled:
+ size: 10Gi
+ storageClassName: ""
+ extraEnv: []
+ worker:
+ # Ignored in standalone mode, which always has one colocated Worker.
+ replicas: 3
+ httpPort: 6788
+ grpcPort: 6789
+ resources: {}
+ storage:
+ enabled:
+ size: 20Gi
+ storageClassName: ""
+ extraEnv: []
+ podSecurityContext: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop: ["ALL"]
+ seccompProfile:
+ type: RuntimeDefault
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+ topologySpreadConstraints: []
+ priorityClassName: ""
+ podAnnotations: {}
+ podLabels: {}
+ terminationGracePeriodSeconds: 60
+ probes:
+ startup:
+ failureThreshold: 60
+ periodSeconds: 5
+ timeoutSeconds: 3
+ readiness:
+ failureThreshold: 3
+ periodSeconds: 10
+ timeoutSeconds: 3
+ liveness:
+ failureThreshold: 3
+ periodSeconds: 20
+ timeoutSeconds: 3
+
# Kubernetes NetworkPolicy: mode default plus explicit per-component rules.
-# easy keeps HTTP clients open; strict isolates release traffic and DNS.
-# Enabled SeaTunnel always restricts member port 5801 to its release peers.
+# easy exposes only Server/Hubble; strict additionally isolates release egress.
+# Native engine HTTP and RPC ports always require explicit internal callers.
# Enforced only 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
@@ -620,7 +708,12 @@ networkPolicy:
# For Hubble's optional outside endpoints (es.urls, prometheus.url set
# through extraEnv). PD, Store and Server reach only the release and DNS.
extraEgress: []
+ vermeer:
+ # Extra callers must use Pod/namespace selectors; no ipBlock or open rules.
+ extraIngress: []
+ extraEgress: []
seatunnel:
+ # Extra callers must use Pod/namespace selectors; no ipBlock or open rules.
# Member port 5801 is restricted even when networkPolicy.enabled is false.
# With full isolation, peers, DNS and the Server API are allowed by default.
# Add trusted submitters/monitoring using native NetworkPolicy rules.
diff --git a/helm/test_helm_metadata_cluster.py b/helm/test_helm_metadata_cluster.py
new file mode 100644
index 0000000000..befbbfc52e
--- /dev/null
+++ b/helm/test_helm_metadata_cluster.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Keep Server schema writes and Store schema reads in the same PD namespace."""
+
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+import yaml
+
+
+CHART = Path(__file__).resolve().parent / "hugegraph"
+COMPONENT = "app.kubernetes.io/component"
+CLUSTER_ANNOTATION = "hugegraph.apache.org/metadata-cluster"
+
+
+class MetadataClusterChartTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ if shutil.which("helm") is None:
+ raise RuntimeError("Helm is required for the metadata namespace regression")
+
+ def render(self, values=None, chart=CHART, upgrade=False):
+ with tempfile.TemporaryDirectory(prefix="metadata-cluster-values-") as directory:
+ configuration = Path(directory) / "values.yaml"
+ configuration.write_text(yaml.safe_dump(values or {}))
+ command = ["helm", "template", "metadata", str(chart), "--namespace", "custom-namespace",
+ "--values", str(configuration)]
+ if upgrade:
+ command.append("--is-upgrade")
+ return subprocess.run(command, capture_output=True, text=True, timeout=120)
+
+ def documents(self, values=None, **arguments):
+ result = self.render(values, **arguments)
+ self.assertEqual(0, result.returncode, result.stderr)
+ return [document for document in yaml.safe_load_all(result.stdout) if document]
+
+ def workload(self, documents, component):
+ workload, = [document for document in documents
+ if document["kind"] in ("StatefulSet", "Deployment") and
+ document["metadata"]["labels"].get(COMPONENT) == component]
+ return workload
+
+ def container(self, workload, name):
+ return next(container for container in workload["spec"]["template"]["spec"]["containers"]
+ if container["name"] == name)
+
+ def assert_namespace(self, documents, expected):
+ server = self.workload(documents, "server")
+ store = self.workload(documents, "store")
+ self.assertEqual(expected, server["metadata"]["annotations"][CLUSTER_ANNOTATION])
+ for workload, role, variable in ((server, "server", "HG_SERVER_CLUSTER"),
+ (store, "store", "PD_CLUSTER")):
+ entries = [entry for entry in self.container(workload, role)["env"]
+ if entry["name"] == variable]
+ self.assertEqual([{"name": variable, "value": expected}], entries)
+
+ def test_default_retains_the_existing_server_metadata_namespace(self):
+ defaults = yaml.safe_load((CHART / "values.yaml").read_text())
+ self.assertEqual("hg-test", defaults["metadataCluster"])
+ self.assert_namespace(self.documents(), "hg-test")
+
+ def test_custom_metadata_namespace_reaches_both_roles_with_or_without_vermeer(self):
+ for enabled in (False, True):
+ with self.subTest(vermeer=enabled):
+ documents = self.documents({"metadataCluster": "production-graph_42",
+ "vermeer": {"enabled": enabled}})
+ self.assert_namespace(documents, "production-graph_42")
+
+ def test_legacy_values_missing_the_shared_field_keep_hg_test(self):
+ with tempfile.TemporaryDirectory(prefix="metadata-cluster-legacy-") as directory:
+ chart = Path(directory) / "chart"
+ shutil.copytree(CHART, chart)
+ (chart / "values.yaml").write_bytes((CHART / "testdata/values-pre-hardening.yaml").read_bytes())
+ for upgrade in (False, True):
+ with self.subTest(upgrade=upgrade):
+ documents = self.documents(chart=chart, upgrade=upgrade)
+ self.assert_namespace(documents, "hg-test")
+ self.assertFalse(any(document["metadata"].get("labels", {}).get(COMPONENT, "")
+ .startswith("vermeer") for document in documents))
+
+ def test_rendered_server_environment_reaches_native_property_mapping(self):
+ selected = "production-graph_42"
+ documents = self.documents({"metadataCluster": selected})
+ container = self.container(self.workload(documents, "server"), "server")
+ environment = {"PATH": os.environ["PATH"], "LC_ALL": "C"}
+ for entry in container["env"]:
+ environment[entry["name"]] = str(entry.get("value", "fixture-secret-" + "x" * 32))
+ with tempfile.TemporaryDirectory(prefix="metadata-cluster-wrapper-") as directory:
+ root = Path(directory)
+ (root / "conf").mkdir()
+ (root / "conf/graphs").mkdir()
+ (root / "conf/graphs/hugegraph.properties").write_text("backend=hstore\n")
+ (root / "bin").mkdir()
+ properties = root / "conf/rest-server.properties"
+ properties.write_text("cluster=unrelated-image-default\nunrelated.setting=preserved\n")
+ entrypoint = root / "docker-entrypoint.sh"
+ entrypoint.write_bytes((CHART.parents[1] / "hugegraph-server/hugegraph-dist/docker/"
+ "docker-entrypoint.sh").read_bytes())
+ entrypoint.chmod(0o755)
+ # Execute the real property mapping with service startup and
+ # readiness isolated; this fixture starts no JVM or HTTP client.
+ (root / "bin/util.sh").write_text(
+ "record_jvm_startup_diagnostics() { :; }\n"
+ "diagnose_jvm_startup_failure() { :; }\n"
+ "read_property() { printf 'http://127.0.0.1:8080'; }\n"
+ "wait_for_startup() { :; }\n")
+ for name in ("enable-auth.sh", "wait-storage.sh", "init-store.sh",
+ "start-hugegraph.sh", "wait-partition.sh"):
+ executable = root / "bin" / name
+ # The entrypoint pipes PASSWORD into init-store. Consume the
+ # stream before returning so scheduler order cannot give its
+ # printf producer SIGPIPE under the real pipefail setting.
+ executable.write_text("#!/bin/sh\ncat >/dev/null\n" if name == "init-store.sh"
+ else "#!/bin/sh\nexit 0\n")
+ executable.chmod(0o755)
+ result = subprocess.run(["bash", "-c", container["args"][0]], cwd=root,
+ env=environment, capture_output=True, text=True, timeout=15)
+ self.assertEqual(0, result.returncode, result.stderr)
+ final = properties.read_text().splitlines()
+ self.assertIn("unrelated.setting=preserved", final)
+ self.assertEqual(["cluster=" + selected], [line for line in final if line.startswith("cluster=")])
+
+ def test_component_environment_cannot_bypass_the_shared_namespace(self):
+ for role, variable in (("server", "HG_SERVER_CLUSTER"), ("store", "PD_CLUSTER")):
+ with self.subTest(role=role):
+ result = self.render({role: {"extraEnv": [{"name": variable, "value": "other"}]}})
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn(variable, result.stderr)
+
+ def test_store_system_property_cannot_override_the_shared_namespace(self):
+ for namespace in ("other", "hg-test"):
+ with self.subTest(namespace=namespace):
+ result = self.render({"store": {"javaOpts": "-Xmx512m -Dpd.cluster=" + namespace}})
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("metadataCluster", result.stderr)
+ self.assert_namespace(self.documents({"store": {"javaOpts": "-Xmx512m -Dpd.cluster.metrics=true"}}),
+ "hg-test")
+
+ def test_empty_or_non_string_namespace_is_rejected(self):
+ for namespace in ("", 42):
+ with self.subTest(namespace=namespace):
+ result = self.render({"metadataCluster": namespace})
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("metadataCluster", result.stderr)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/helm/test_helm_package.py b/helm/test_helm_package.py
index 0fea178a78..a9c480a947 100644
--- a/helm/test_helm_package.py
+++ b/helm/test_helm_package.py
@@ -78,10 +78,10 @@ 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-strict.yaml",
- "docs/operations.md", "docs/distribution.md", "examples/README.md")
+ "docs/operations.md", "docs/distribution.md", "docs/vermeer.md", "examples/README.md")
for name in required:
with self.subTest(file=name):
- self.assertIn(name, self.files)
+ self.assertIn(name, self.files.keys())
# Helm normalizes Chart.yaml and values.yaml while packaging.
if name not in ("Chart.yaml", "values.yaml"):
self.assertEqual((CHART / name).read_bytes(), self.files[name])
@@ -128,6 +128,21 @@ def test_packaged_chart_renders_each_preset(self):
self.assertTrue(b"kind: Deployment" in rendered.stdout,
"Packaged chart did not render a Deployment")
+ def test_packaged_optional_vermeer_images_include_native_token_helper(self):
+ registry = "registry.internal.example.com"
+ for mode in ("standalone", "cluster"):
+ with self.subTest(mode=mode):
+ rendered = subprocess.run(
+ ["helm", "template", "package-check", str(self.package),
+ "--set", "vermeer.enabled=true", "--set", f"vermeer.mode={mode}",
+ "--set", f"global.imageRegistry={registry}"],
+ check=True, capture_output=True, text=True).stdout
+ selected = set(re.findall(r'^\s+image:\s*["\']?([^"\'\s]+)', rendered, re.MULTILINE))
+ self.assertEqual(5, len(selected),
+ "Optional Vermeer must reuse its native image for token helpers")
+ self.assertTrue(all(image.startswith(registry + "/") for image in selected))
+ self.assertIn("/go/bin/vermeer-token", rendered)
+
if __name__ == "__main__":
unittest.main()
diff --git a/helm/test_helm_seatunnel.py b/helm/test_helm_seatunnel.py
index 5f5e62e139..e725392a3c 100644
--- a/helm/test_helm_seatunnel.py
+++ b/helm/test_helm_seatunnel.py
@@ -38,6 +38,28 @@ def matches(selector, labels):
return all(labels.get(key) == value for key, value in selector.items())
+def ingress_allows(policy, peer, port):
+ for rule in policy["ingress"]:
+ if rule.get("ports") and not any(entry["port"] == port and
+ entry.get("protocol", "TCP") == "TCP"
+ for entry in rule["ports"]):
+ continue
+ if not rule.get("from"):
+ return True
+ for candidate in rule["from"]:
+ if "ipBlock" in candidate:
+ continue
+ selector = candidate.get("podSelector", {})
+ if not matches(selector.get("matchLabels", {}), peer):
+ continue
+ expressions_match = all(
+ expression["operator"] == "In" and peer.get(expression["key"]) in expression["values"]
+ for expression in selector.get("matchExpressions", []))
+ if expressions_match:
+ return True
+ return False
+
+
def engine_resources(documents, kind=None):
return [doc for doc in documents if (kind is None or doc["kind"] == kind)
and doc["metadata"].get("labels", {}).get("app.kubernetes.io/component") == "seatunnel"]
@@ -199,27 +221,35 @@ def test_dns_discovery_and_services_are_scoped_to_engine_and_release(self):
self.assertIn(discovery["service-dns"] + ":5801", client["network"]["cluster-members"])
self.assertFalse(any(doc["kind"] in ("Role", "RoleBinding") for doc in engine_resources(documents)))
- def test_http_entry_inherits_mode_and_preserves_explicit_service_settings(self):
- for mode, service_type in (("easy", "NodePort"), ("strict", "ClusterIP")):
+ def test_http_entry_remains_internal_and_preserves_annotations(self):
+ for mode in ("easy", "strict"):
with self.subTest(mode=mode):
documents = self.documents({}, mode=mode)
- api, = [doc for doc in engine_resources(documents, "Service")
- if doc["spec"].get("clusterIP") != "None"]
- self.assertEqual(service_type, api["spec"]["type"])
- self.assertNotIn("nodePort", api["spec"]["ports"][0])
- for mode, service in (("easy", {"type": "ClusterIP"}),
- ("strict", {"type": "NodePort", "nodePort": 31081})):
- with self.subTest(mode=mode, service=service):
+ for service in engine_resources(documents, "Service"):
+ self.assertEqual("ClusterIP", service["spec"].get("type", "ClusterIP"))
+ self.assertNotIn("externalIPs", service["spec"])
+ self.assertFalse(any("nodePort" in port for port in service["spec"]["ports"]))
+ for mode in ("easy", "strict"):
+ service = {"type": "ClusterIP", "annotations": {"internal.example.com/owner": "imports"}}
+ with self.subTest(mode=mode):
documents = self.documents({"service": service}, mode=mode)
headless, = [doc for doc in engine_resources(documents, "Service")
if doc["spec"].get("clusterIP") == "None"]
api, = [doc for doc in engine_resources(documents, "Service") if doc is not headless]
- self.assertEqual(service["type"], api["spec"]["type"])
- if "nodePort" in service:
- self.assertEqual(service["nodePort"], api["spec"]["ports"][0]["nodePort"])
+ self.assertEqual("ClusterIP", api["spec"]["type"])
+ self.assertEqual("imports", api["metadata"]["annotations"]["internal.example.com/owner"])
+ self.assertNotIn("nodePort", api["spec"]["ports"][0])
self.assertNotIn("nodePort", headless["spec"]["ports"][0])
- result = self.render({"service": {"type": "ClusterIP", "nodePort": 31081}})
- self.assertNotEqual(0, result.returncode)
+ for service in ({"type": "NodePort"}, {"type": "LoadBalancer"}, {"nodePort": 0},
+ {"type": "ClusterIP", "nodePort": 31081}, {"externalIPs": ["192.0.2.10"]}):
+ with self.subTest(service=service):
+ self.assertNotEqual(0, self.render({"service": service}).returncode)
+
+ def test_legacy_null_service_fields_keep_the_native_entry_internal(self):
+ documents = self.documents({"service": {"type": None, "nodePort": None}})
+ for service in engine_resources(documents, "Service"):
+ self.assertEqual("ClusterIP", service["spec"].get("type", "ClusterIP"))
+ self.assertFalse(any("nodePort" in port for port in service["spec"]["ports"]))
def test_local_graph_defaults_follow_rendered_server_and_admin_secret(self):
documents = self.documents({}, namespace="graphs", release="fixture-" + "r" * 40,
@@ -303,29 +333,32 @@ def test_demo_jobs_are_mounted_and_keep_credentials_as_placeholders(self):
def test_seatunnel_admin_is_generated_separately_and_kept_out_of_public_configuration(self):
graph_password = "graph-only-admin-password"
- documents = self.documents({"auth": {"password": ""}},
- server={"auth": {"admin": {"password": graph_password}}})
- secret = engine_auth_secret(documents)
- username = base64.b64decode(secret["data"]["username"]).decode()
- password = base64.b64decode(secret["data"]["password"]).decode()
- self.assertEqual("admin", username)
- self.assertGreaterEqual(len(password), 32)
- self.assertNotEqual(graph_password, password)
- http = yaml.safe_load(engine_config(documents)["seatunnel.yaml"])["seatunnel"]["engine"]["http"]
- self.assertTrue(http["enable-basic-auth"])
- self.assertEqual(username, http["basic-auth-username"])
- self.assertEqual(password, http["basic-auth-password"])
- for configmap in engine_resources(documents, "ConfigMap"):
- self.assertNotIn("seatunnel.yaml", configmap["data"])
- self.assertNotIn(password, yaml.safe_dump(configmap["data"]))
- workload, = engine_resources(documents, "StatefulSet")
- self.assertNotIn(password, yaml.safe_dump(pod(workload)))
- volumes = {volume["name"] for volume in pod(workload)["spec"]["volumes"]
- if volume.get("secret", {}).get("secretName") == secret["metadata"]["name"]}
- container, = pod(workload)["spec"]["containers"]
- self.assertTrue(any(mount["name"] in volumes
- and mount["mountPath"] == "/opt/seatunnel/config/seatunnel.yaml"
- for mount in container["volumeMounts"]))
+ for mode, length in (("easy", 6), ("strict", 16)):
+ with self.subTest(mode=mode):
+ documents = self.documents({"auth": {"password": ""}}, mode=mode,
+ server={"auth": {"admin": {"password": graph_password}}})
+ secret = engine_auth_secret(documents)
+ username = base64.b64decode(secret["data"]["username"]).decode()
+ password = base64.b64decode(secret["data"]["password"]).decode()
+ self.assertEqual("admin", username)
+ self.assertRegex(password, rf"^[A-Za-z0-9]{{{length}}}$")
+ self.assertNotEqual(graph_password, password)
+ self.assertEqual("keep", secret["metadata"]["annotations"]["helm.sh/resource-policy"])
+ http = yaml.safe_load(engine_config(documents)["seatunnel.yaml"])["seatunnel"]["engine"]["http"]
+ self.assertTrue(http["enable-basic-auth"])
+ self.assertEqual(username, http["basic-auth-username"])
+ self.assertEqual(password, http["basic-auth-password"])
+ for configmap in engine_resources(documents, "ConfigMap"):
+ self.assertNotIn("seatunnel.yaml", configmap["data"])
+ self.assertNotIn(password, yaml.safe_dump(configmap["data"]))
+ workload, = engine_resources(documents, "StatefulSet")
+ self.assertNotIn(password, yaml.safe_dump(pod(workload)))
+ volumes = {volume["name"] for volume in pod(workload)["spec"]["volumes"]
+ if volume.get("secret", {}).get("secretName") == secret["metadata"]["name"]}
+ container, = pod(workload)["spec"]["containers"]
+ self.assertTrue(any(mount["name"] in volumes
+ and mount["mountPath"] == "/opt/seatunnel/config/seatunnel.yaml"
+ for mount in container["volumeMounts"]))
def test_native_basic_auth_preserves_special_credentials_and_cannot_be_disabled_by_config(self):
username = "ops'\"\\$"
@@ -378,7 +411,9 @@ def test_invalid_enabled_mode_and_cluster_size_fail_before_install(self):
self.assertNotEqual(0, self.render(values).returncode)
def test_strict_policies_allow_engine_server_dns_and_native_extensions(self):
- extra_in = {"from": [{"ipBlock": {"cidr": "10.10.0.0/16"}}], "ports": [{"port": 8080}]}
+ extra_in = {"from": [{"namespaceSelector": {"matchLabels": {"team": "imports"}},
+ "podSelector": {"matchLabels": {"app": "import-client"}}}],
+ "ports": [{"port": 8080}]}
extra_out = {"to": [{"ipBlock": {"cidr": "10.20.0.0/16"}}], "ports": [{"port": 9099}]}
documents = self.documents({"mode": "cluster"}, mode="strict", server={"port": 8182},
networkPolicy={"seatunnel": {"extraIngress": [extra_in], "extraEgress": [extra_out]}})
@@ -404,7 +439,7 @@ def allows(rules, direction, selector, port):
dns = [port for rule in engine["egress"] if not rule.get("to") for port in rule["ports"]]
self.assertTrue({("UDP", 53), ("TCP", 53)} <= {(p["protocol"], p["port"]) for p in dns})
- def test_member_ingress_is_isolated_when_full_network_policy_is_disabled(self):
+ def test_http_and_member_ingress_remain_isolated_without_full_network_policy(self):
for mode in ("easy", "strict"):
with self.subTest(mode=mode):
documents = self.documents({}, mode=mode, networkPolicy={"enabled": False})
@@ -424,7 +459,24 @@ def test_member_ingress_is_isolated_when_full_network_policy_is_disabled(self):
self.assertFalse(matches(selector, {**labels, "app.kubernetes.io/component": "server"}))
self.assertFalse(matches(selector, {**labels, "app.kubernetes.io/instance": "other"}))
open_rules = [rule for rule in spec["ingress"] if not rule.get("from")]
- self.assertEqual([{"ports": [{"protocol": "TCP", "port": 8080}]}], open_rules)
+ self.assertEqual([], open_rules)
+ self.assertFalse(any("ipBlock" in peer for rule in spec["ingress"] for peer in rule["from"]))
+ for component in ("seatunnel", "hubble", "test"):
+ peer = {**labels, "app.kubernetes.io/component": component}
+ self.assertTrue(ingress_allows(spec, peer, 8080))
+ self.assertEqual(component == "seatunnel", ingress_allows(spec, peer, 5801))
+ for peer in ({**labels, "app.kubernetes.io/component": "server"},
+ {**labels, "app.kubernetes.io/instance": "other"}):
+ for port in (8080, 5801):
+ self.assertFalse(ingress_allows(spec, peer, port))
+
+ def test_native_ingress_extensions_require_explicit_internal_pod_selectors(self):
+ for rule in ({}, {"ports": [{"port": 8080}]}, {"from": []}, {"from": [{}]},
+ {"from": [{"ipBlock": {"cidr": "0.0.0.0/0"}}]},
+ {"from": [{"ipBlock": {"cidr": "10.0.0.0/8"}}]}):
+ with self.subTest(rule=rule):
+ result = self.render({}, networkPolicy={"seatunnel": {"extraIngress": [rule]}})
+ self.assertNotEqual(0, result.returncode)
def test_engine_security_defaults_and_native_scheduling_overrides(self):
workload, = engine_resources(self.documents({}), "StatefulSet")
diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py
new file mode 100644
index 0000000000..7f350a0b01
--- /dev/null
+++ b/helm/test_helm_vermeer.py
@@ -0,0 +1,659 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Validate optional native Vermeer deployment and its integration boundaries."""
+
+import base64
+import configparser
+import copy
+import hashlib
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+import yaml
+
+
+CHART = Path(__file__).resolve().parent / "hugegraph"
+COMPONENT = "app.kubernetes.io/component"
+INSTANCE = "app.kubernetes.io/instance"
+FACTOR = "vermeer-test-factor-0123456789abcdef0123456789abcdef"
+NATIVE_ADMIN = "VermeerAdmin0123"
+
+
+def resources(documents, kind=None, component=None):
+ return [doc for doc in documents if (kind is None or doc["kind"] == kind)
+ and (component is None or
+ doc["metadata"].get("labels", {}).get(COMPONENT) == component)]
+
+
+def vermeer_resources(documents, kind=None):
+ return [doc for doc in resources(documents, kind)
+ if doc["metadata"].get("labels", {}).get(COMPONENT, "").startswith("vermeer")]
+
+
+def pod(workload):
+ return workload["spec"]["template"]
+
+
+def env(container):
+ return {entry["name"]: entry for entry in container.get("env", [])}
+
+
+def merged(left, right):
+ result = copy.deepcopy(left)
+ for key, value in right.items():
+ if isinstance(value, dict) and isinstance(result.get(key), dict):
+ result[key] = merged(result[key], value)
+ else:
+ result[key] = copy.deepcopy(value)
+ return result
+
+
+def selector_matches(selector, labels):
+ if any(labels.get(key) != value for key, value in selector.get("matchLabels", {}).items()):
+ return False
+ for expression in selector.get("matchExpressions", []):
+ key, operator = expression["key"], expression["operator"]
+ values = expression.get("values", [])
+ if operator == "In" and labels.get(key) not in values:
+ return False
+ if operator == "NotIn" and labels.get(key) in values:
+ return False
+ if operator == "Exists" and key not in labels:
+ return False
+ if operator == "DoesNotExist" and key in labels:
+ return False
+ return True
+
+
+def policy_allows(policies, target, peer, port, direction):
+ policy_type, peer_key = ("Ingress", "from") if direction == "ingress" else ("Egress", "to")
+ selected = [policy["spec"] for policy in policies
+ if policy_type in policy["spec"].get("policyTypes", ["Ingress"])
+ and selector_matches(policy["spec"]["podSelector"], target)]
+ if not selected:
+ return True
+ for policy in selected:
+ for rule in policy.get(direction, []):
+ if rule.get("ports") and not any(entry["port"] == port and
+ entry.get("protocol", "TCP") == "TCP"
+ for entry in rule["ports"]):
+ continue
+ if not rule.get(peer_key):
+ return True
+ if any("ipBlock" not in candidate and
+ selector_matches(candidate.get("podSelector", {}), peer)
+ for candidate in rule[peer_key]):
+ return True
+ return False
+
+
+class VermeerChartTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ if shutil.which("helm") is None:
+ raise RuntimeError("Helm is required for the Vermeer deployment regression")
+
+ def render(self, vermeer=None, chart=CHART, preset=None, release="fixture", namespace="graphs",
+ upgrade=False, deterministic_auth=True, **overrides):
+ values = {"pd": {"auth": {"value": "pd-test-service-password"}},
+ "server": {"auth": {"admin": {"password": "graph-test-admin-password"},
+ "token": {"value": "0123456789abcdef0123456789abcdef"}}}}
+ if not deterministic_auth:
+ values = {}
+ if vermeer is not None:
+ values["vermeer"] = merged({"enabled": True, "auth": {
+ "value": FACTOR, "admin": {"password": NATIVE_ADMIN}}}, vermeer)
+ values = merged(values, overrides)
+ with tempfile.TemporaryDirectory(prefix="vermeer-values-") as directory:
+ configuration = Path(directory) / "values.yaml"
+ configuration.write_text(yaml.safe_dump(values))
+ command = ["helm", "template", release, str(chart), "--namespace", namespace]
+ if preset:
+ command.extend(["--values", str(CHART / preset)])
+ command.extend(["--values", str(configuration)])
+ if upgrade:
+ command.append("--is-upgrade")
+ return subprocess.run(command, capture_output=True, text=True, timeout=120)
+
+ def documents(self, vermeer=None, **values):
+ result = self.render(vermeer, **values)
+ self.assertEqual(0, result.returncode, result.stderr)
+ return [doc for doc in yaml.safe_load_all(result.stdout) if doc]
+
+ def workloads(self, documents):
+ return vermeer_resources(documents, "StatefulSet")
+
+ def role_container(self, documents, role):
+ return next(container for workload in self.workloads(documents)
+ for container in pod(workload)["spec"]["containers"]
+ if container["name"] == role)
+
+ def test_optional_components_stay_disabled_in_every_default_preset(self):
+ defaults = yaml.safe_load((CHART / "values.yaml").read_text())
+ self.assertFalse(defaults["vermeer"]["enabled"])
+ self.assertFalse(defaults["seatunnel"]["enabled"])
+ for preset in (None, "values-single.yaml", "values-cluster.yaml", "values-strict.yaml"):
+ with self.subTest(preset=preset):
+ documents = self.documents(preset=preset)
+ self.assertEqual([], vermeer_resources(documents))
+ self.assertEqual([], resources(documents, component="seatunnel"))
+
+ def test_legacy_values_without_vermeer_render_install_upgrade_and_registry(self):
+ with tempfile.TemporaryDirectory(prefix="vermeer-legacy-") as directory:
+ chart = Path(directory) / "chart"
+ shutil.copytree(CHART, chart)
+ (chart / "values.yaml").write_bytes((CHART / "testdata/values-pre-hardening.yaml").read_bytes())
+ for upgrade, registry in ((False, ""), (True, ""), (True, "registry.example.com")):
+ with self.subTest(upgrade=upgrade, registry=registry):
+ documents = self.documents(chart=chart, upgrade=upgrade, deterministic_auth=False,
+ **{"global": {"imageRegistry": registry}})
+ self.assertEqual([], vermeer_resources(documents))
+
+ def test_standalone_shares_one_pod_with_two_native_processes(self):
+ documents = self.documents({})
+ workload, = self.workloads(documents)
+ self.assertEqual(1, workload["spec"]["replicas"])
+ self.assertEqual("vermeer-master", workload["metadata"]["labels"][COMPONENT])
+ self.assertEqual({"master", "worker"},
+ {container["name"] for container in pod(workload)["spec"]["containers"]})
+ self.assertFalse(pod(workload)["spec"].get("automountServiceAccountToken", True))
+ self.assertEqual([], vermeer_resources(documents, "Deployment"))
+ self.assertEqual([], vermeer_resources(documents, "Job"))
+
+ def test_cluster_has_one_master_and_configurable_workers(self):
+ for replicas in (3, 5):
+ with self.subTest(replicas=replicas):
+ documents = self.documents({"mode": "cluster", "worker": {"replicas": replicas}})
+ workloads = {workload["metadata"]["labels"][COMPONENT]: workload
+ for workload in self.workloads(documents)}
+ self.assertEqual({"vermeer-master", "vermeer-worker"}, set(workloads))
+ self.assertEqual(1, workloads["vermeer-master"]["spec"]["replicas"])
+ self.assertEqual(replicas, workloads["vermeer-worker"]["spec"]["replicas"])
+ for component, role in (("vermeer-master", "master"), ("vermeer-worker", "worker")):
+ self.assertEqual([role], [container["name"] for container in
+ pod(workloads[component])["spec"]["containers"]])
+
+ def test_native_config_uses_common_pool_and_internal_master(self):
+ for mode in ("standalone", "cluster"):
+ with self.subTest(mode=mode):
+ documents = self.documents({"mode": mode}, fullnameOverride="analysis")
+ configmap, = vermeer_resources(documents, "ConfigMap")
+ data = configmap["data"]
+ master, worker = configparser.ConfigParser(), configparser.ConfigParser()
+ master.read_string(data["master.ini"])
+ worker.read_string(data["worker.ini"])
+ self.assertEqual("master", master["default"]["run_mode"])
+ self.assertEqual("worker", worker["default"]["run_mode"])
+ self.assertEqual("token", master["default"]["auth"])
+ self.assertEqual("$", worker["default"]["worker_group"])
+ self.assertIn("127.0.0.1" if mode == "standalone" else "analysis-vermeer-master",
+ worker["default"]["master_peer"])
+ container = self.role_container(documents, "worker")
+ self.assertTrue(any(entry.get("valueFrom", {}).get("fieldRef", {}).get("fieldPath") ==
+ "status.podIP" for entry in container.get("env", [])))
+ launch = " ".join(container.get("command", []) + container.get("args", []))
+ self.assertIn("-grpc_peer", launch)
+
+ def test_native_services_remain_internal_in_every_entry_mode(self):
+ for mode in ("easy", "strict"):
+ with self.subTest(mode=mode):
+ documents = self.documents({}, mode=mode)
+ services = vermeer_resources(documents, "Service")
+ self.assertTrue(services)
+ for service in services:
+ self.assertEqual("ClusterIP", service["spec"].get("type", "ClusterIP"))
+ self.assertNotIn("externalIPs", service["spec"])
+ self.assertFalse(any("nodePort" in port for port in service["spec"]["ports"]))
+ http = [service for service in services if service["spec"].get("clusterIP") != "None"
+ and [port["port"] for port in service["spec"]["ports"]] == [6688]]
+ self.assertEqual(1, len(http))
+ for component in ("server", "hubble"):
+ entry, = [service for service in resources(documents, "Service", component)
+ if service["spec"].get("clusterIP") != "None"]
+ self.assertEqual("NodePort" if mode == "easy" else "ClusterIP", entry["spec"]["type"])
+
+ def test_explicit_entry_and_custom_ports_follow_the_rendered_release(self):
+ settings = {"mode": "cluster", "master": {"httpPort": 7688, "grpcPort": 7689},
+ "worker": {"httpPort": 7788, "grpcPort": 7789},
+ "service": {"type": "ClusterIP", "annotations": {"internal.example.com/owner": "analytics"}}}
+ documents = self.documents(settings, mode="strict", fullnameOverride="analytics")
+ client, = [service for service in vermeer_resources(documents, "Service")
+ if service["spec"].get("clusterIP") != "None"
+ and [port["port"] for port in service["spec"]["ports"]] == [7688]]
+ self.assertEqual([7688], [port["port"] for port in client["spec"]["ports"]])
+ self.assertNotIn("nodePort", client["spec"]["ports"][0])
+ self.assertEqual("analytics", client["metadata"]["annotations"]["internal.example.com/owner"])
+ self.assertTrue(all(workload["metadata"]["name"].startswith("analytics-vermeer-")
+ for workload in self.workloads(documents)))
+ hubble, = resources(documents, "Deployment", "hubble")
+ self.assertEqual("http://analytics-vermeer-master:7688",
+ env(pod(hubble)["spec"]["containers"][0])["HG_HUBBLE_VERMEER_URL"]["value"])
+
+ def test_legacy_null_service_fields_keep_the_native_entry_internal(self):
+ documents = self.documents({"service": {"type": None, "nodePort": None}})
+ for service in vermeer_resources(documents, "Service"):
+ self.assertEqual("ClusterIP", service["spec"].get("type", "ClusterIP"))
+ self.assertFalse(any("nodePort" in port for port in service["spec"]["ports"]))
+
+ def test_credentials_are_secret_references_and_not_configmap_or_process_arguments(self):
+ documents = self.documents({})
+ secret, = [secret for secret in vermeer_resources(documents, "Secret")
+ if "token-factor" in secret["data"]]
+ self.assertEqual(FACTOR, base64.b64decode(secret["data"]["token-factor"]).decode())
+ self.assertEqual("keep", secret["metadata"]["annotations"]["helm.sh/resource-policy"])
+ for document in documents:
+ if document["kind"] != "Secret":
+ serialized = yaml.safe_dump(document)
+ for value in (FACTOR, NATIVE_ADMIN, "graph-test-admin-password", "pd-test-service-password"):
+ self.assertNotIn(value, serialized)
+ for container in (self.role_container(documents, "master"), self.role_container(documents, "worker")):
+ self.assertIn("VERMEER_PD_PASSWORD_FILE", env(container))
+ self.assertIn("VERMEER_PD_PEERS", env(container))
+
+ def test_external_credential_secret_names_and_keys_are_preserved(self):
+ documents = self.documents({"auth": {"existingSecret": "native-auth", "key": "signing-key",
+ "admin": {"existingSecret": "native-admin", "key": "login"}}},
+ pd={"auth": {"existingSecret": "metadata-auth", "key": "service-key"}},
+ server={"auth": {"admin": {"existingSecret": "graph-admin", "key": "admin-key"}}})
+ self.assertEqual([], vermeer_resources(documents, "Secret"))
+ master = next(workload for workload in self.workloads(documents)
+ if workload["metadata"]["labels"][COMPONENT] == "vermeer-master")
+ secrets = {volume["secret"]["secretName"]: volume["secret"]
+ for volume in pod(master)["spec"].get("volumes", []) if "secret" in volume}
+ self.assertEqual({"native-auth", "native-admin", "metadata-auth", "graph-admin"}, set(secrets))
+ for name, key in (("native-auth", "signing-key"), ("metadata-auth", "service-key"),
+ ("graph-admin", "admin-key"), ("native-admin", "login")):
+ self.assertIn(key, [entry["key"] for entry in secrets[name]["items"]])
+
+ def test_generated_factor_and_pod_checksum_use_the_same_random_value(self):
+ documents = self.documents({"auth": {"value": ""}})
+ secret, = [secret for secret in vermeer_resources(documents, "Secret")
+ if "token-factor" in secret["data"]]
+ factor = base64.b64decode(secret["data"]["token-factor"]).decode()
+ self.assertGreaterEqual(len(factor), 32)
+ self.assertNotIn(factor, ("pd-test-service-password", "graph-test-admin-password"))
+ digest = lambda value: hashlib.sha256(value.encode()).hexdigest()
+ checksum = digest(secret["metadata"]["name"] + "|token-factor|" + digest(factor))
+ master, = resources(documents, "StatefulSet", "vermeer-master")
+ self.assertEqual(checksum, pod(master)["metadata"]["annotations"]["checksum/vermeer-auth"])
+
+ def test_native_admin_password_is_short_separate_and_mounted_only_by_master(self):
+ cases = (("standalone", "easy", 6), ("cluster", "easy", 6),
+ ("standalone", "strict", 16), ("cluster", "strict", 16))
+ for mode, entry_mode, length in cases:
+ with self.subTest(mode=mode, entry_mode=entry_mode):
+ documents = self.documents({"mode": mode, "auth": {"admin": {"password": ""}}}, mode=entry_mode)
+ secret, = [secret for secret in vermeer_resources(documents, "Secret")
+ if secret["metadata"]["name"] == "fixture-vermeer-admin"]
+ password = base64.b64decode(secret["data"]["password"]).decode()
+ self.assertRegex(password, rf"^[A-Za-z0-9]{{{length}}}$")
+ self.assertNotIn(password, (FACTOR, "graph-test-admin-password", "pd-test-service-password"))
+ self.assertEqual("keep", secret["metadata"]["annotations"]["helm.sh/resource-policy"])
+ self.assert_master_only_admin_mount(documents, secret["metadata"]["name"], "password")
+ digest = lambda value: hashlib.sha256(value.encode()).hexdigest()
+ expected = digest(secret["metadata"]["name"] + "|password|" + digest(password))
+ master, = resources(documents, "StatefulSet", "vermeer-master")
+ self.assertEqual(expected, pod(master)["metadata"]["annotations"]["checksum/vermeer-admin"])
+ for document in documents:
+ if document["kind"] != "Secret":
+ self.assertNotIn(password, yaml.safe_dump(document))
+
+ def assert_master_only_admin_mount(self, documents, secret_name, key):
+ master_count = 0
+ for workload in resources(documents, "StatefulSet") + resources(documents, "Deployment"):
+ spec = pod(workload)["spec"]
+ admin_volumes = {volume["name"]: volume["secret"] for volume in spec.get("volumes", [])
+ if volume.get("secret", {}).get("secretName") == secret_name}
+ for container in spec.get("containers", []) + spec.get("initContainers", []):
+ if workload["metadata"]["labels"].get(COMPONENT) == "vermeer-master" and container["name"] == "master":
+ master_count += 1
+ path = env(container)["VERMEER_ADMIN_PASSWORD_FILE"]["value"]
+ mounts = [mount for mount in container["volumeMounts"] if mount["name"] in admin_volumes]
+ self.assertEqual(1, len(mounts))
+ self.assertTrue(mounts[0]["readOnly"])
+ self.assertEqual(mounts[0]["mountPath"] + "/password", path)
+ self.assertEqual([{"key": key, "path": "password"}],
+ admin_volumes[mounts[0]["name"]]["items"])
+ else:
+ self.assertNotIn("VERMEER_ADMIN_PASSWORD_FILE", env(container))
+ self.assertFalse(any(mount["name"] in admin_volumes
+ for mount in container.get("volumeMounts", [])))
+ if workload["metadata"]["labels"].get(COMPONENT) != "vermeer-master":
+ self.assertEqual({}, admin_volumes)
+ self.assertEqual(1, master_count)
+
+ def test_native_admin_external_secret_key_is_referenced_without_managing_or_leaking_it(self):
+ documents = self.documents({"mode": "cluster", "auth": {"admin": {
+ "existingSecret": "operator-native-admin", "key": "ui-password", "password": "unused-inline"}}})
+ self.assertFalse(any(secret["metadata"]["name"] == "operator-native-admin"
+ or secret["metadata"]["name"] == "fixture-vermeer-admin"
+ for secret in resources(documents, "Secret")))
+ self.assert_master_only_admin_mount(documents, "operator-native-admin", "ui-password")
+ for document in documents:
+ self.assertNotIn("unused-inline", yaml.safe_dump(document))
+
+ def test_native_admin_rotation_rolls_master_without_changing_worker_or_hubble(self):
+ first = self.documents({"mode": "cluster"})
+ second = self.documents({"mode": "cluster", "auth": {"admin": {"password": "RotatedAdmin0123"}}})
+ before, = resources(first, "StatefulSet", "vermeer-master")
+ after, = resources(second, "StatefulSet", "vermeer-master")
+ self.assertNotEqual(pod(before)["metadata"]["annotations"]["checksum/vermeer-admin"],
+ pod(after)["metadata"]["annotations"]["checksum/vermeer-admin"])
+ self.assertEqual(pod(before)["metadata"]["annotations"]["checksum/vermeer-auth"],
+ pod(after)["metadata"]["annotations"]["checksum/vermeer-auth"])
+ for component, kind in (("vermeer-worker", "StatefulSet"), ("hubble", "Deployment")):
+ before, = resources(first, kind, component)
+ after, = resources(second, kind, component)
+ self.assertEqual(pod(before), pod(after))
+
+ def test_native_admin_preserves_an_explicit_single_line_password(self):
+ password = " ops'\"\\$密码: "
+ documents = self.documents({"auth": {"admin": {"password": password, "key": "login-key"}}})
+ secret, = [secret for secret in vermeer_resources(documents, "Secret")
+ if secret["metadata"]["name"] == "fixture-vermeer-admin"]
+ self.assertEqual(password, base64.b64decode(secret["data"]["login-key"]).decode())
+ self.assert_master_only_admin_mount(documents, secret["metadata"]["name"], "login-key")
+
+ def test_schema_reader_override_replaces_the_server_admin_reference(self):
+ documents = self.documents({"hugegraphCredentials": {"existingSecret": "schema-reader",
+ "key": "graph-credentials"}})
+ worker = self.role_container(documents, "worker")
+ self.assertIn("VERMEER_HUGEGRAPH_CREDENTIALS_FILE", env(worker))
+ self.assertNotIn("VERMEER_HUGEGRAPH_PASSWORD_FILE", env(worker))
+ workload, = self.workloads(documents)
+ secret_volumes = [volume["secret"] for volume in pod(workload)["spec"]["volumes"]
+ if "secret" in volume]
+ self.assertFalse(any(volume["secretName"] == "fixture-admin" for volume in secret_volumes))
+ schema, = [volume for volume in secret_volumes if volume["secretName"] == "schema-reader"]
+ self.assertEqual([{"key": "graph-credentials", "path": "credentials.json"}], schema["items"])
+
+ def test_auth_rotation_changes_only_relevant_vermeer_and_hubble_checksums(self):
+ first, second = self.documents({}), self.documents({"auth": {"value": FACTOR + "rotated"}})
+ for component, kind in (("vermeer-master", "StatefulSet"), ("hubble", "Deployment")):
+ before, = resources(first, kind, component)
+ after, = resources(second, kind, component)
+ self.assertNotEqual(pod(before)["metadata"]["annotations"]["checksum/vermeer-auth"],
+ pod(after)["metadata"]["annotations"]["checksum/vermeer-auth"])
+ for component, kind in (("pd", "StatefulSet"), ("store", "StatefulSet"), ("server", "Deployment")):
+ before, = resources(first, kind, component)
+ after, = resources(second, kind, component)
+ self.assertEqual(pod(before), pod(after))
+
+ def test_hubble_uses_native_helper_and_keeps_signing_factor_out_of_app_container(self):
+ documents = self.documents({})
+ hubble, = resources(documents, "Deployment", "hubble")
+ spec = pod(hubble)["spec"]
+ app, = spec["containers"]
+ token, = [container for container in spec["initContainers"] if container["name"] == "vermeer-token"]
+ self.assertEqual(self.role_container(documents, "master")["image"], token["image"])
+ self.assertEqual(["/go/bin/vermeer-token"], token["command"])
+ self.assertIn("-quiet", token["args"])
+ self.assertIn("-i", token["args"])
+ self.assertEqual("$DEFAULT", token["args"][token["args"].index("-space") + 1])
+ factor_volumes = {volume["name"] for volume in spec["volumes"] if
+ volume.get("secret", {}).get("secretName") == "fixture-vermeer-auth"}
+ self.assertTrue(factor_volumes)
+ self.assertFalse(any(mount["name"] in factor_volumes for mount in app["volumeMounts"]))
+ token_path = env(app)["HG_HUBBLE_VERMEER_TOKEN_FILE"]["value"]
+ self.assertTrue(any(mount.get("readOnly") and token_path.startswith(mount["mountPath"])
+ for mount in app["volumeMounts"]))
+
+ def test_hubble_disabled_leaves_native_engine_operational(self):
+ documents = self.documents({}, hubble={"enabled": False})
+ self.assertEqual([], resources(documents, component="hubble"))
+ self.assertEqual(1, len(self.workloads(documents)))
+ self.assertEqual({"master", "worker"}, {container["name"] for workload in self.workloads(documents)
+ for container in pod(workload)["spec"]["containers"]})
+
+ def test_disabled_integration_clears_image_defaults_and_does_not_mount_token(self):
+ documents = self.documents()
+ hubble, = resources(documents, "Deployment", "hubble")
+ spec = pod(hubble)["spec"]
+ app, = spec["containers"]
+ self.assertEqual("", env(app)["HG_HUBBLE_VERMEER_URL"]["value"])
+ self.assertEqual("", env(app)["HG_HUBBLE_VERMEER_TOKEN_FILE"]["value"])
+ self.assertFalse(any(container["name"] == "vermeer-token" for container in spec.get("initContainers", [])))
+ script = "\n".join(app.get("args", []))
+ self.assertIn("vermeer.url", script)
+ self.assertIn("vermeer.token_file", script)
+ self.assertNotIn("fixture-vermeer-master", script)
+
+ def test_hubble_wrapper_replaces_stale_and_duplicate_vermeer_properties(self):
+ for enabled in (False, True):
+ with self.subTest(enabled=enabled):
+ documents = self.documents({} if enabled else None)
+ hubble, = resources(documents, "Deployment", "hubble")
+ container, = pod(hubble)["spec"]["containers"]
+ environment = dict(os.environ)
+ for entry in container.get("env", []):
+ environment[entry["name"]] = str(entry.get("value", "fixture-secret"))
+ if not enabled:
+ environment["HG_HUBBLE_VERMEER_URL"] = "http://stale-runtime-default"
+ environment["HG_HUBBLE_VERMEER_TOKEN_FILE"] = "/stale-runtime-token"
+ with tempfile.TemporaryDirectory(prefix="vermeer-hubble-wrapper-") as directory:
+ root = Path(directory)
+ (root / "conf").mkdir()
+ (root / "bin").mkdir()
+ properties = root / "conf/hugegraph-hubble.properties"
+ properties.write_text("unrelated.setting=preserved\n"
+ "vermeer.url=http://stale-image-default\n"
+ "vermeer.token_file=/stale-token\n"
+ "vermeer.url=http://duplicate-default\n")
+ start = root / "bin/start-hubble.sh"
+ start.write_text("#!/bin/sh\nexit 0\n")
+ start.chmod(0o755)
+ result = subprocess.run(container["command"] + container["args"],
+ cwd=root, env=environment, capture_output=True,
+ text=True, timeout=15)
+ self.assertEqual(0, result.returncode, result.stderr)
+ final = properties.read_text().splitlines()
+ self.assertIn("unrelated.setting=preserved", final)
+ expected_url = environment["HG_HUBBLE_VERMEER_URL"] if enabled else ""
+ expected_token = environment["HG_HUBBLE_VERMEER_TOKEN_FILE"] if enabled else ""
+ self.assertEqual(["vermeer.url=" + expected_url],
+ [line for line in final if line.startswith("vermeer.url=")])
+ self.assertEqual(["vermeer.token_file=" + expected_token],
+ [line for line in final if line.startswith("vermeer.token_file=")])
+
+ def test_persistence_inheritance_and_explicit_temporary_storage(self):
+ for mode in ("standalone", "cluster"):
+ for persistent in (False, True):
+ with self.subTest(mode=mode, persistent=persistent):
+ documents = self.documents({"mode": mode}, persistence={"enabled": persistent})
+ claims = [claim for workload in self.workloads(documents)
+ for claim in workload["spec"].get("volumeClaimTemplates", [])]
+ claims += vermeer_resources(documents, "PersistentVolumeClaim")
+ self.assertEqual(2 if persistent else 0, len(claims))
+ for workload in self.workloads(documents):
+ for container in pod(workload)["spec"]["containers"]:
+ self.assertTrue(any(mount["mountPath"] == "/go/bin/vermeer_data"
+ for mount in container["volumeMounts"]))
+ documents = self.documents({"master": {"storage": {"enabled": False}},
+ "worker": {"storage": {"enabled": True, "size": "7Gi"}}},
+ persistence={"enabled": False})
+ workload, = self.workloads(documents)
+ self.assertEqual([], workload["spec"].get("volumeClaimTemplates", []))
+ claim, = vermeer_resources(documents, "PersistentVolumeClaim")
+ self.assertEqual("7Gi", claim["spec"]["resources"]["requests"]["storage"])
+
+ def test_registry_digest_and_pull_secrets_cover_both_processes_and_token_helper(self):
+ digest = "sha256:" + "a" * 64
+ documents = self.documents({"image": {"repository": "team/vermeer", "tag": "ignored", "digest": digest,
+ "pullPolicy": "Never"}},
+ **{"global": {"imageRegistry": "registry.internal.example.com/project"},
+ "imagePullSecrets": [{"name": "private-registry"}]})
+ expected = "registry.internal.example.com/project/team/vermeer@" + digest
+ for workload in self.workloads(documents):
+ self.assertEqual([{"name": "private-registry"}], pod(workload)["spec"]["imagePullSecrets"])
+ for container in pod(workload)["spec"]["containers"]:
+ self.assertEqual(expected, container["image"])
+ self.assertEqual("Never", container["imagePullPolicy"])
+ hubble, = resources(documents, "Deployment", "hubble")
+ helper = next(container for container in pod(hubble)["spec"]["initContainers"]
+ if container["name"] == "vermeer-token")
+ self.assertEqual(expected, helper["image"])
+ self.assertEqual("Never", helper["imagePullPolicy"])
+
+ def test_strict_network_covers_reverse_rpc_peer_rpc_and_graph_loading(self):
+ documents = self.documents({"mode": "cluster"}, mode="strict", server={"port": 8184})
+ policies = resources(documents, "NetworkPolicy")
+ labels = {component: pod(workload)["metadata"]["labels"] for component in
+ ("pd", "store", "server", "hubble", "vermeer-master", "vermeer-worker")
+ for workload in resources(documents, "StatefulSet") + resources(documents, "Deployment")
+ if workload["metadata"]["labels"].get(COMPONENT) == component}
+ paths = [("hubble", "vermeer-master", 6688),
+ ("vermeer-worker", "vermeer-master", 6689),
+ ("vermeer-master", "vermeer-worker", 6789),
+ ("vermeer-worker", "vermeer-worker", 6789),
+ ("vermeer-master", "pd", 8686), ("vermeer-worker", "pd", 8686),
+ ("vermeer-worker", "store", 8500), ("vermeer-worker", "server", 8184)]
+ for source, destination, port in paths:
+ with self.subTest(source=source, destination=destination, port=port):
+ self.assertTrue(policy_allows(policies, labels[destination], labels[source], port, "ingress"))
+ self.assertTrue(policy_allows(policies, labels[source], labels[destination], port, "egress"))
+
+ def test_native_http_and_rpc_are_release_scoped_without_strict_egress(self):
+ for entry_mode in ("easy", "strict"):
+ for mode in ("standalone", "cluster"):
+ with self.subTest(entry_mode=entry_mode, mode=mode):
+ self.assert_native_ingress(mode, entry_mode)
+
+ def assert_native_ingress(self, mode, entry_mode):
+ documents = self.documents({"mode": mode}, mode=entry_mode,
+ networkPolicy={"enabled": False})
+ policies = resources(documents, "NetworkPolicy")
+ self.assertTrue(vermeer_resources(documents, "NetworkPolicy"))
+ for policy in vermeer_resources(documents, "NetworkPolicy"):
+ self.assertEqual(["Ingress"], policy["spec"]["policyTypes"])
+ self.assertFalse(any(not rule.get("from") for rule in policy["spec"]["ingress"]))
+ self.assertFalse(any("ipBlock" in peer for rule in policy["spec"]["ingress"]
+ for peer in rule["from"]))
+ for workload in self.workloads(documents):
+ labels = pod(workload)["metadata"]["labels"]
+ is_master = labels[COMPONENT] == "vermeer-master"
+ http_port, rpc_port = (6688, 6689) if is_master else (6788, 6789)
+ for component in ("vermeer-master", "vermeer-worker", "hubble", "test"):
+ peer = {**labels, COMPONENT: component}
+ self.assertEqual(is_master or component.startswith("vermeer-"),
+ policy_allows(policies, labels, peer, http_port, "ingress"))
+ self.assertEqual(component.startswith("vermeer-"),
+ policy_allows(policies, labels, peer, rpc_port, "ingress"))
+ for peer in ({**labels, INSTANCE: "other-release"},
+ {**labels, COMPONENT: "server"}, {**labels, COMPONENT: "unknown"}):
+ for port in (http_port, rpc_port):
+ self.assertFalse(policy_allows(policies, labels, peer, port, "ingress"))
+
+ def test_native_network_extensions_are_preserved(self):
+ ingress = {"from": [{"namespaceSelector": {"matchLabels": {"team": "analytics"}},
+ "podSelector": {"matchLabels": {"app": "analysis-client"}}}],
+ "ports": [{"port": 6688}]}
+ egress = {"to": [{"ipBlock": {"cidr": "10.16.0.0/16"}}], "ports": [{"port": 9443}]}
+ documents = self.documents({"mode": "cluster"}, mode="strict", networkPolicy={
+ "vermeer": {"extraIngress": [ingress], "extraEgress": [egress]}})
+ for policy in vermeer_resources(documents, "NetworkPolicy"):
+ self.assertIn(ingress, policy["spec"]["ingress"])
+ self.assertIn(egress, policy["spec"]["egress"])
+
+ def test_native_ingress_extensions_require_explicit_internal_pod_selectors(self):
+ for rule in ({}, {"ports": [{"port": 6688}]}, {"from": []}, {"from": [{}]},
+ {"from": [{"ipBlock": {"cidr": "0.0.0.0/0"}}]},
+ {"from": [{"ipBlock": {"cidr": "10.0.0.0/8"}}]}):
+ with self.subTest(rule=rule):
+ result = self.render({}, networkPolicy={"vermeer": {"extraIngress": [rule]}})
+ self.assertNotEqual(0, result.returncode)
+
+ def test_process_probes_do_not_depend_on_space_worker_availability(self):
+ for container in (self.role_container(self.documents({}), "master"),
+ self.role_container(self.documents({}), "worker")):
+ for probe in ("startupProbe", "livenessProbe", "readinessProbe"):
+ self.assertIn(probe, container)
+ self.assertNotIn("workers/availability", yaml.safe_dump(container[probe]))
+
+ def test_relative_algorithm_outputs_are_inside_each_roles_writable_data_volume(self):
+ for mode in ("standalone", "cluster"):
+ with self.subTest(mode=mode):
+ documents = self.documents({"mode": mode})
+ for workload in self.workloads(documents):
+ for container in pod(workload)["spec"]["containers"]:
+ mount, = [entry for entry in container["volumeMounts"]
+ if entry["mountPath"] == "/go/bin/vermeer_data"]
+ self.assertFalse(mount.get("readOnly", False))
+ directory = Path(container["workingDir"])
+ self.assertTrue(directory == Path(mount["mountPath"]) or
+ Path(mount["mountPath"]) in directory.parents,
+ "Relative Hubble algorithm output must use the role data volume")
+
+ def test_seatunnel_can_be_enabled_independently_without_changing_engine_identity(self):
+ documents = self.documents({}, seatunnel={"enabled": True, "auth": {"password": "seatunnel-test-password"}})
+ self.assertEqual(1, len(resources(documents, "StatefulSet", "seatunnel")))
+ self.assertEqual(1, len(self.workloads(documents)))
+ self.assertTrue(all(secret["metadata"]["name"] != "fixture-vermeer-auth"
+ for secret in resources(documents, "Secret", "seatunnel")))
+
+ def test_signing_factor_matches_the_native_file_length_boundary(self):
+ for length in (4, 15):
+ with self.subTest(length=length):
+ result = self.render({"auth": {"value": "x" * length}})
+ self.assertNotEqual(0, result.returncode)
+ self.assertRegex(result.stderr, r"vermeer[./]auth[./]value")
+ documents = self.documents({"auth": {"value": "x" * 16,
+ "admin": {"password": "Ab12xY"}}})
+ secret, = [item for item in resources(documents, "Secret") if "token-factor" in item["data"]]
+ self.assertEqual("x" * 16, base64.b64decode(secret["data"]["token-factor"]).decode())
+
+ def test_invalid_enabled_values_fail_before_install(self):
+ cases = ({"mode": "unknown"}, {"mode": "cluster", "worker": {"replicas": 0}},
+ {"master": {"httpPort": 0}}, {"worker": {"grpcPort": 65536}},
+ {"service": {"type": "NodePort"}}, {"service": {"type": "LoadBalancer"}},
+ {"service": {"nodePort": 0}},
+ {"service": {"type": "ClusterIP", "nodePort": 31688}},
+ {"service": {"externalIPs": ["192.0.2.10"]}},
+ {"auth": {"value": "", "autoGenerate": False}},
+ {"auth": {"admin": {"password": "", "autoGenerate": False}}},
+ {"auth": {"admin": {"password": " "}}},
+ {"auth": {"admin": {"password": "line1\nline2"}}},
+ {"master": {"extraEnv": [{"name": "VERMEER_ADMIN_PASSWORD_FILE", "value": "/wrong"}]}},
+ {"master": {"extraEnv": [{"name": "VERMEER_AUTH_TOKEN_FACTOR_FILE", "value": "/wrong"}]}},
+ {"worker": {"extraEnv": [{"name": "POD_IP", "value": "0.0.0.0"}]}})
+ for settings in cases:
+ with self.subTest(settings=settings):
+ self.assertNotEqual(0, self.render(settings).returncode)
+ result = self.render({}, hubble={"mode": "direct"})
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("pd", result.stderr.lower())
+ for location in ("securityContext", "podSecurityContext"):
+ with self.subTest(non_root_context=location):
+ result = self.render({}, hubble={location: {"runAsNonRoot": True}})
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("runAsUser", result.stderr)
+ self.documents({}, hubble={location: {"runAsNonRoot": True, "runAsUser": 10001}})
+ # --set yields integer types distinct from values.yaml's numbers;
+ # collision validation must compare their numeric values.
+ result = subprocess.run(["helm", "template", "fixture", str(CHART),
+ "--set", "vermeer.enabled=true", "--set",
+ "vermeer.master.httpPort=6788"],
+ capture_output=True, text=True, timeout=120)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("distinct", result.stderr)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/helm/test_helm_vermeer_legacy.py b/helm/test_helm_vermeer_legacy.py
new file mode 100644
index 0000000000..e4e2335c0f
--- /dev/null
+++ b/helm/test_helm_vermeer_legacy.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Cover the first Vermeer enablement on a release that predates its defaults."""
+
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+import yaml
+
+
+CHART = Path(__file__).resolve().parent / "hugegraph"
+
+
+class VermeerLegacyEnablementTest(unittest.TestCase):
+ def test_release_without_native_login_defaults_gets_an_actionable_upgrade_error(self):
+ with tempfile.TemporaryDirectory(prefix="vermeer-admin-upgrade-") as directory:
+ chart = Path(directory) / "chart"
+ shutil.copytree(CHART, chart)
+ old_values = yaml.safe_load((CHART / "values.yaml").read_text())
+ old_values["vermeer"]["auth"].pop("admin")
+ existing_values = Path(directory) / "existing-values.yaml"
+ existing_values.write_text(yaml.safe_dump(old_values))
+ (chart / "values.yaml").write_text(yaml.safe_dump(old_values))
+ for enabled in (False, True):
+ with self.subTest(enabled=enabled):
+ result = subprocess.run(
+ ["helm", "template", "legacy", str(chart), "--is-upgrade",
+ "--set", "vermeer.enabled=" + str(enabled).lower()],
+ text=True, capture_output=True, timeout=120)
+ if enabled:
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("vermeer.auth.admin is missing new chart defaults", result.stderr)
+ self.assertIn("-f existing-values.yaml", result.stderr)
+ self.assertNotIn("nil pointer", result.stderr)
+ else:
+ self.assertEqual(0, result.returncode, result.stderr)
+ result = subprocess.run(
+ ["helm", "template", "legacy", str(CHART), "--is-upgrade",
+ "-f", str(existing_values), "--set", "vermeer.enabled=true"],
+ text=True, capture_output=True, timeout=120)
+ self.assertEqual(0, result.returncode, result.stderr)
+ documents = [doc for doc in yaml.safe_load_all(result.stdout) if doc]
+ self.assertTrue(any(doc["kind"] == "Secret"
+ and doc["metadata"]["name"] == "legacy-vermeer-admin"
+ for doc in documents))
+
+ def test_first_enablement_explains_how_to_import_new_defaults(self):
+ with tempfile.TemporaryDirectory(prefix="vermeer-first-enable-") as directory:
+ chart = Path(directory) / "chart"
+ shutil.copytree(CHART, chart)
+ old_values = yaml.safe_load((CHART / "values.yaml").read_text())
+ old_values.pop("vermeer")
+ old_values["networkPolicy"].pop("vermeer", None)
+ existing_values = Path(directory) / "existing-values.yaml"
+ existing_values.write_text(yaml.safe_dump(old_values))
+ (chart / "values.yaml").write_text(yaml.safe_dump(old_values))
+ command = ["helm", "template", "legacy", str(chart), "--is-upgrade",
+ "--set", "vermeer.enabled=true"]
+ result = subprocess.run(command, text=True, capture_output=True)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("legacy --reuse-values does not import", result.stderr)
+ self.assertIn("-f existing-values.yaml", result.stderr)
+ self.assertNotIn("nil pointer", result.stderr)
+ self.assertNotIn("wrong type", result.stderr)
+
+ # The documented values-file workflow imports defaults from the new chart.
+ result = subprocess.run(
+ ["helm", "template", "legacy", str(CHART), "--is-upgrade",
+ "-f", str(existing_values), "--set", "vermeer.enabled=true"],
+ text=True, capture_output=True)
+ self.assertEqual(0, result.returncode, result.stderr)
+ documents = [doc for doc in yaml.safe_load_all(result.stdout) if doc]
+ workloads = [doc for doc in documents if doc["kind"] == "StatefulSet"
+ and doc["metadata"]["labels"].get("app.kubernetes.io/component")
+ == "vermeer-master"]
+ self.assertEqual(1, len(workloads))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/helm/test_helm_vermeer_rollout.py b/helm/test_helm_vermeer_rollout.py
new file mode 100644
index 0000000000..5a7c0bdcef
--- /dev/null
+++ b/helm/test_helm_vermeer_rollout.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Pin role-scoped Vermeer rollout boundaries."""
+
+import copy
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+import yaml
+
+
+CHART = Path(__file__).resolve().parent / "hugegraph"
+COMPONENT = "app.kubernetes.io/component"
+
+
+def merged(left, right):
+ result = copy.deepcopy(left)
+ for key, value in right.items():
+ if isinstance(value, dict) and isinstance(result.get(key), dict):
+ result[key] = merged(result[key], value)
+ else:
+ result[key] = copy.deepcopy(value)
+ return result
+
+
+class VermeerRolloutContractTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ if shutil.which("helm") is None:
+ raise RuntimeError("Helm is required for the Vermeer rollout regression")
+
+ def render(self, vermeer=None):
+ values = {
+ "pd": {"auth": {"value": "pd-test-service-password"}},
+ "server": {
+ "auth": {
+ "admin": {"password": "graph-test-admin-password"},
+ "token": {"value": "0123456789abcdef0123456789abcdef"},
+ }
+ },
+ "vermeer": {
+ "enabled": True,
+ "mode": "cluster",
+ "auth": {
+ "value": "vermeer-test-factor-0123456789abcdef",
+ "admin": {"password": "VermeerAdmin0123"},
+ },
+ },
+ }
+ if vermeer:
+ values["vermeer"] = merged(values["vermeer"], vermeer)
+ with tempfile.TemporaryDirectory(prefix="vermeer-rollout-") as directory:
+ configuration = Path(directory) / "values.yaml"
+ configuration.write_text(yaml.safe_dump(values))
+ result = subprocess.run(
+ ["helm", "template", "fixture", str(CHART),
+ "--namespace", "graphs", "--values", str(configuration)],
+ capture_output=True, text=True, timeout=120,
+ )
+ self.assertEqual(0, result.returncode, result.stderr)
+ return [doc for doc in yaml.safe_load_all(result.stdout) if doc]
+
+ @staticmethod
+ def workload(documents, component):
+ return next(
+ document for document in documents
+ if document.get("kind") == "StatefulSet"
+ and document.get("metadata", {}).get("labels", {}).get(COMPONENT) == component
+ )
+
+ def checksum(self, documents, component):
+ workload = self.workload(documents, component)
+ return workload["spec"]["template"]["metadata"]["annotations"]["checksum/config"]
+
+ def test_worker_only_values_do_not_roll_the_single_master(self):
+ baseline = self.render()
+ master = self.workload(baseline, "vermeer-master")["spec"]["template"]
+ cases = (
+ {"worker": {"replicas": 5}},
+ {"worker": {"resources": {"requests": {"cpu": "750m", "memory": "1Gi"}}}},
+ {"worker": {"storage": {"size": "7Gi"}}},
+ {"service": {"annotations": {"internal.example.com/owner": "analytics"}}},
+ )
+ for override in cases:
+ with self.subTest(override=override):
+ rendered = self.render(override)
+ current = self.workload(rendered, "vermeer-master")["spec"]["template"]
+ self.assertEqual(master, current)
+
+ def test_config_checksum_tracks_only_the_role_inputs_that_are_read(self):
+ baseline = self.render()
+ master = self.checksum(baseline, "vermeer-master")
+ worker = self.checksum(baseline, "vermeer-worker")
+
+ master_http = self.render({"master": {"httpPort": 7688}})
+ self.assertNotEqual(master, self.checksum(master_http, "vermeer-master"))
+ self.assertEqual(worker, self.checksum(master_http, "vermeer-worker"))
+
+ master_grpc = self.render({"master": {"grpcPort": 7689}})
+ self.assertNotEqual(master, self.checksum(master_grpc, "vermeer-master"))
+ self.assertNotEqual(worker, self.checksum(master_grpc, "vermeer-worker"))
+
+ worker_ports = self.render({"worker": {"httpPort": 7788, "grpcPort": 7789}})
+ self.assertEqual(master, self.checksum(worker_ports, "vermeer-master"))
+ self.assertNotEqual(worker, self.checksum(worker_ports, "vermeer-worker"))
+
+
+if __name__ == "__main__":
+ unittest.main()