From 8c3e25f8743fea04916dc9c11bfa91aebe1cbb3c Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:32:03 +0000 Subject: [PATCH 01/39] feat(helm): add optional native Vermeer standalone and cluster workloads --- helm/hugegraph/Chart.yaml | 2 +- helm/hugegraph/templates/_helpers.tpl | 146 ++++++++++++++- helm/hugegraph/templates/_vermeer.tpl | 147 +++++++++++++++ .../templates/vermeer-auth-secret.yaml | 33 ++++ .../templates/vermeer-configmap.yaml | 51 ++++++ .../templates/vermeer-networkpolicy.yaml | 94 ++++++++++ helm/hugegraph/templates/vermeer-service.yaml | 92 ++++++++++ .../templates/vermeer-statefulset.yaml | 153 ++++++++++++++++ helm/hugegraph/values-cluster.yaml | 5 + helm/hugegraph/values-single.yaml | 5 + helm/hugegraph/values.schema.json | 173 +++++++++++++++++- helm/hugegraph/values.yaml | 79 ++++++++ 12 files changed, 970 insertions(+), 10 deletions(-) create mode 100644 helm/hugegraph/templates/_vermeer.tpl create mode 100644 helm/hugegraph/templates/vermeer-auth-secret.yaml create mode 100644 helm/hugegraph/templates/vermeer-configmap.yaml create mode 100644 helm/hugegraph/templates/vermeer-networkpolicy.yaml create mode 100644 helm/hugegraph/templates/vermeer-service.yaml create mode 100644 helm/hugegraph/templates/vermeer-statefulset.yaml diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml index e38988e2a7..71df068e16 100644 --- a/helm/hugegraph/Chart.yaml +++ b/helm/hugegraph/Chart.yaml @@ -19,7 +19,7 @@ apiVersion: v2 name: hugegraph-helm description: Helm chart for Apache HugeGraph HStore cluster with optional SeaTunnel 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" diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 5094d48670..c9ca366149 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -26,9 +26,12 @@ {{- 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 -}} {{- $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" "seatunnel" "vermeer" -}} {{- $component := get $.Values $comp -}} {{- $svc := get $component "service" | default dict -}} {{- $_ := set $component "service" $svc -}} @@ -48,10 +51,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)) -}} @@ -752,6 +763,8 @@ 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.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" -}} @@ -994,7 +1007,7 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). "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") -}} + "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 +1146,130 @@ 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 -}} +{{- 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 (get $v.service "nodePort") (not (has $v.service.type (list "NodePort" "LoadBalancer"))) -}} +{{- fail "vermeer.service.nodePort requires vermeer.service.type to be NodePort or LoadBalancer" -}} +{{- 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 $v.master.httpPort $v.master.grpcPort $v.worker.httpPort $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_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 }} diff --git a/helm/hugegraph/templates/_vermeer.tpl b/helm/hugegraph/templates/_vermeer.tpl new file mode 100644 index 0000000000..ce55a0cad5 --- /dev/null +++ b/helm/hugegraph/templates/_vermeer.tpl @@ -0,0 +1,147 @@ +{{/* +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. +You may obtain a copy at https://www.apache.org/licenses/LICENSE-2.0 . +Unless required by applicable law or agreed to in writing, software +is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS. +*/}} + +{{- 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 + 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 + {{- 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 + {{- 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-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/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..f71332a18d --- /dev/null +++ b/helm/hugegraph/templates/vermeer-networkpolicy.yaml @@ -0,0 +1,94 @@ +# +# 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.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }} + {{- if eq $role "master" }} + {{- if $.Values.networkPolicy.enabled }} + - 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 }} + {{- else }} + - ports: + {{- include "hugegraph.netpol.ports" (list $v.master.httpPort) | nindent 8 }} + {{- end }} + {{- 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.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..47eb9f4920 --- /dev/null +++ b/helm/hugegraph/templates/vermeer-service.yaml @@ -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. +# + +{{ 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: {{ $v.service.type }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $v.service $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $v.service "allocateLoadBalancerNodePorts" }} + {{- if ne $v.service.allocateLoadBalancerNodePorts nil }} + allocateLoadBalancerNodePorts: {{ $v.service.allocateLoadBalancerNodePorts }} + {{- end }} + {{- end }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: vermeer-master + ports: + - name: http + port: {{ $v.master.httpPort }} + targetPort: master-http + {{- with $v.service.nodePort }} + nodePort: {{ . }} + {{- end }} +--- +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..d051e0d060 --- /dev/null +++ b/helm/hugegraph/templates/vermeer-statefulset.yaml @@ -0,0 +1,153 @@ +# +# 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 }} +--- +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: {{ omit $v "auth" "hugegraphCredentials" | toJson | sha256sum | quote }} + checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" $ | quote }} + {{- if $master }} + checksum/vermeer-auth: {{ include "hugegraph.vermeer.authChecksum" $ | 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 }} 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..1f2b984eb3 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -113,6 +113,9 @@ "type": "string" } } + }, + "vermeer": { + "$ref": "#/definitions/vermeer" } }, "definitions": { @@ -136,12 +139,7 @@ "networkPolicyEgressRules": { "type": "array", "items": { - "type": "object", - "properties": { - "to": { - "$ref": "#/definitions/networkPolicyPeers" - } - } + "type": "object" } }, "networkPolicyComponent": { @@ -195,6 +193,17 @@ "$ref": "#/definitions/networkPolicyEgressRules" } } + }, + "vermeer": { + "type": "object", + "properties": { + "extraIngress": { + "$ref": "#/definitions/networkPolicyIngressRules" + }, + "extraEgress": { + "$ref": "#/definitions/networkPolicyEgressRules" + } + } } } }, @@ -1656,6 +1665,158 @@ ] } } + }, + "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/service" + }, + "auth": { + "type": "object", + "properties": { + "value": { + "type": "string", + "pattern": "^$|^[!-~]{4,}$" + }, + "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" + } + } + } } } } diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 0d49297668..1d2eec9cf8 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -596,6 +596,82 @@ 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 + # Select a build containing file credentials and /go/bin/vermeer-token. + tag: latest + digest: "" + pullPolicy: Always + auth: + # Token signing factor. existingSecret > value > a stable generated Secret. + value: "" + existingSecret: "" + key: token-factor + 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: + # Only the authenticated Master HTTP/UI entry is exposed. + type: + 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. @@ -620,6 +696,9 @@ 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: + extraIngress: [] + extraEgress: [] seatunnel: # Member port 5801 is restricted even when networkPolicy.enabled is false. # With full isolation, peers, DNS and the Server API are allowed by default. From c1680cc7cb40a29c51decf56ca29b59a6043a7a3 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:32:03 +0000 Subject: [PATCH 02/39] feat(helm): wire Vermeer into Hubble and deployment checks --- helm/hugegraph/templates/NOTES.txt | 66 ++++++++++++- helm/hugegraph/templates/_hubble-vermeer.tpl | 92 +++++++++++++++++++ .../templates/hubble-deployment.yaml | 35 ++++++- helm/hugegraph/templates/networkpolicy.yaml | 19 ++++ .../templates/tests/test-connection.yaml | 54 +++++++++++ 5 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 helm/hugegraph/templates/_hubble-vermeer.tpl diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 3ec7e7bc8d..ba91a63960 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,6 +67,59 @@ 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" . }} +{{- $vService := .Values.vermeer.service | default dict }} + + Vermeer: + In-cluster Master HTTP: http://{{ $vName }}.{{ .Release.Namespace }}.svc:{{ .Values.vermeer.master.httpPort }} +{{- if eq $vService.type "NodePort" }} + Native UI/API NodePort: + kubectl get svc -n {{ .Release.Namespace }} {{ $vName }} -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}{"\n"}' + kubectl get nodes -o wide + Open http://:{{ $vService.nodePort | default "" }}/ +{{- else if eq $vService.type "LoadBalancer" }} + Native UI/API LoadBalancer address: + kubectl get svc -n {{ .Release.Namespace }} {{ $vName }} -o wide + Once assigned, open http://:{{ .Values.vermeer.master.httpPort }}/ +{{- end }} + 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 }}/ + Generate a separate native UI token on demand (the command prints a credential; keep it private): + kubectl exec -n {{ .Release.Namespace }} {{ $vName }}-0 -c master -- /go/bin/vermeer-token -factor-file /var/run/vermeer/auth/token-factor -user admin -space '$DEFAULT' -client browser -i -quiet + Use this token in Vermeer's token login. It is separate from the HugeGraph admin password. +{{- 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 and .Values.networkPolicy.enabled (has $vService.type (list "NodePort" "LoadBalancer")) }} +{{- $vRules := get .Values.networkPolicy "vermeer" | default dict }} +{{- if empty (get $vRules "extraIngress") }} + Full NetworkPolicy isolation is enabled; allow native HTTP callers through networkPolicy.vermeer.extraIngress. +{{- end }} +{{- end }} +{{- 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 }} +{{- if eq .Values.vermeer.mode "cluster" }} + kubectl rollout restart -n {{ .Release.Namespace }} statefulset/{{ include "hugegraph.vermeer.workerName" . }} +{{- end }} + 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 }} + Generate a new native UI token after rotation; previously issued native tokens no longer work. +{{- end }} +{{- end }} {{- if (.Values.seatunnel | default dict).enabled }} SeaTunnel: @@ -177,6 +238,9 @@ 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 }} +{{- end }} {{- if $generated }} Chart-generated credentials are in use: {{ join ", " $generated }}. @@ -211,7 +275,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/_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/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/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 020c484f6a..2607fa1ebf 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -16,6 +16,7 @@ # {{ include "hugegraph.resolveValues" . }} +{{- $vermeerEnabled := (.Values.vermeer | default dict).enabled | default false }} {{- $smoke := `def ids = [] as Set; (graph.schema().getVertexLabels() + graph.schema().getEdgeLabels()).each { ids.addAll(it.indexLabels()) @@ -54,6 +55,10 @@ spec: securityContext: seccompProfile: type: RuntimeDefault + {{- if $vermeerEnabled }} + initContainers: + {{- include "hugegraph.vermeer.tokenInit" (dict "root" . "uid" "100" "gid" "101" "resources" .Values.server.testResources) | nindent 4 }} + {{- end }} containers: - name: curl image: {{ .Values.server.waitImage | quote }} @@ -184,3 +189,52 @@ spec: fi sleep 5 done + {{- if $vermeerEnabled }} + - 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 }} From 1d00720172ef5143b91c4fe959da4382bda0eb1d Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:32:40 +0000 Subject: [PATCH 03/39] fix(helm): normalize Vermeer ports before collision validation --- helm/hugegraph/templates/_helpers.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index c9ca366149..c540eed67c 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1228,7 +1228,7 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 $v.master.httpPort $v.master.grpcPort $v.worker.httpPort $v.worker.grpcPort -}} +{{- $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 -}} From da8217d2a8ca6a15b9eff1e03441535e652a1d23 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:33:43 +0000 Subject: [PATCH 04/39] fix(helm): require PD discovery for Hubble Vermeer integration --- helm/hugegraph/templates/_helpers.tpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index c540eed67c..ec7df4ae36 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1213,6 +1213,9 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- define "hugegraph.vermeer.validate" -}} {{- if (.Values.vermeer | default dict).enabled -}} {{- $v := .Values.vermeer -}} +{{- 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 not (has $v.mode (list "standalone" "cluster")) -}} {{- fail "vermeer.mode must be standalone or cluster" -}} {{- end -}} From 0cd17e2ee7b374bdb2d8de88cf9573ff64f162e6 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:32:48 +0000 Subject: [PATCH 05/39] test(helm): cover optional Vermeer lifecycle and authenticated integration --- .github/actions/helm-checks/action.yml | 12 +- .../tests/vermeer_auth_lookup_test.yaml | 125 +++++ helm/test_helm_package.py | 17 +- helm/test_helm_vermeer.py | 490 ++++++++++++++++++ 4 files changed, 641 insertions(+), 3 deletions(-) create mode 100644 helm/hugegraph/tests/vermeer_auth_lookup_test.yaml create mode 100644 helm/test_helm_vermeer.py diff --git a/.github/actions/helm-checks/action.yml b/.github/actions/helm-checks/action.yml index 344e4655dd..36c1ee1eaf 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,10 @@ 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: helm unittest shell: 'bash -e {0}' run: | @@ -63,7 +69,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 +508,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/tests/vermeer_auth_lookup_test.yaml b/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml new file mode 100644 index 0000000000..0187e986f1 --- /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/test_helm_package.py b/helm/test_helm_package.py index 0fea178a78..ae6ad1737b 100644 --- a/helm/test_helm_package.py +++ b/helm/test_helm_package.py @@ -78,7 +78,7 @@ 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) @@ -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_vermeer.py b/helm/test_helm_vermeer.py new file mode 100644 index 0000000000..46c28530ac --- /dev/null +++ b/helm/test_helm_vermeer.py @@ -0,0 +1,490 @@ +#!/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" + + +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}}, 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_only_master_http_inherits_public_entry_defaults(self): + for mode, service_type in (("easy", "NodePort"), ("strict", "ClusterIP")): + with self.subTest(mode=mode): + documents = self.documents({}, mode=mode) + services = vermeer_resources(documents, "Service") + public = [service for service in services if service["spec"].get("type") == "NodePort"] + self.assertEqual(1 if mode == "easy" else 0, len(public)) + for service in public: + self.assertEqual([6688], [port["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)) + self.assertEqual(service_type, http[0]["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": "NodePort", "nodePort": 31688}} + documents = self.documents(settings, mode="strict", fullnameOverride="analytics") + client, = [service for service in vermeer_resources(documents, "Service") + if service["spec"].get("type") == "NodePort"] + self.assertEqual([7688], [port["port"] for port in client["spec"]["ports"]]) + self.assertEqual(31688, client["spec"]["ports"][0]["nodePort"]) + 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_credentials_are_secret_references_and_not_configmap_or_process_arguments(self): + documents = self.documents({}) + secret, = vermeer_resources(documents, "Secret") + 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, "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"}}, + 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", "metadata-auth", "graph-admin"}, set(secrets)) + for name, key in (("native-auth", "signing-key"), ("metadata-auth", "service-key"), + ("graph-admin", "admin-key")): + 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, = vermeer_resources(documents, "Secret") + 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_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")) + 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_internal_rpc_remains_release_scoped_without_strict_egress(self): + for mode in ("standalone", "cluster"): + with self.subTest(mode=mode): + documents = self.documents({"mode": mode}, networkPolicy={"enabled": False}) + policies = resources(documents, "NetworkPolicy") + self.assertTrue(vermeer_resources(documents, "NetworkPolicy")) + master = next(workload for workload in self.workloads(documents) + if workload["metadata"]["labels"][COMPONENT] == "vermeer-master") + labels = pod(master)["metadata"]["labels"] + other_release = {**labels, INSTANCE: "other-release"} + other_component = {**labels, COMPONENT: "server"} + self.assertFalse(policy_allows(policies, labels, other_release, 6689, "ingress")) + self.assertFalse(policy_allows(policies, labels, other_component, 6689, "ingress")) + self.assertTrue(policy_allows(policies, labels, other_release, 6688, "ingress")) + + def test_native_network_extensions_are_preserved(self): + ingress = {"from": [{"ipBlock": {"cidr": "10.15.0.0/16"}}], "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_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_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_invalid_enabled_values_fail_before_install(self): + cases = ({"mode": "unknown"}, {"mode": "cluster", "worker": {"replicas": 0}}, + {"master": {"httpPort": 0}}, {"worker": {"grpcPort": 65536}}, + {"service": {"type": "ClusterIP", "nodePort": 31688}}, + {"auth": {"value": "", "autoGenerate": False}}, + {"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()) + + +if __name__ == "__main__": + unittest.main() From a771fa9640d2df4b7d81d22914f87fe975012b1d Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:34:30 +0000 Subject: [PATCH 06/39] test(helm): verify live Vermeer credentials and compatibility guards --- helm/hugegraph/tests/networkpolicy_test.yaml | 8 ++++---- helm/hugegraph/tests/vermeer_auth_lookup_test.yaml | 2 +- helm/test_helm_package.py | 2 +- helm/test_helm_vermeer.py | 11 +++++++++++ 4 files changed, 17 insertions(+), 6 deletions(-) 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/vermeer_auth_lookup_test.yaml b/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml index 0187e986f1..610811773e 100644 --- a/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml +++ b/helm/hugegraph/tests/vermeer_auth_lookup_test.yaml @@ -47,7 +47,7 @@ kubernetesProvider: app.kubernetes.io/instance: other app.kubernetes.io/component: pd spec: - replicas: 1 + replicas: "1" - apiVersion: v1 kind: Secret metadata: diff --git a/helm/test_helm_package.py b/helm/test_helm_package.py index ae6ad1737b..a9c480a947 100644 --- a/helm/test_helm_package.py +++ b/helm/test_helm_package.py @@ -81,7 +81,7 @@ def test_required_configuration_is_preserved(self): "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]) diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 46c28530ac..745e082d13 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -351,6 +351,9 @@ def test_hubble_wrapper_replaces_stale_and_duplicate_vermeer_properties(self): 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() @@ -484,6 +487,14 @@ def test_invalid_enabled_values_fail_before_install(self): result = self.render({}, hubble={"mode": "direct"}) self.assertNotEqual(0, result.returncode) self.assertIn("pd", result.stderr.lower()) + # --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__": From 6295386ac88125cc1cbbcba18c09e98f10fa6915 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:34:29 +0000 Subject: [PATCH 07/39] docs(helm): guide optional Vermeer deployment Document standalone and cluster compute topologies and automatic Hubble access. Fold optional engines in the README and refresh credentials, storage and image requirements. --- helm/hugegraph/README.md | 56 ++++++- helm/hugegraph/docs/distribution.md | 13 +- helm/hugegraph/docs/operations.md | 19 ++- helm/hugegraph/docs/seatunnel.md | 6 +- helm/hugegraph/docs/vermeer.md | 226 ++++++++++++++++++++++++++++ helm/hugegraph/examples/README.md | 20 +++ 6 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 helm/hugegraph/docs/vermeer.md diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 597beebd77..88574af239 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,9 +83,18 @@ 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. @@ -103,6 +114,30 @@ whose generated Secret is retained and reused during online upgrades. 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. + +The native Vermeer UI/API is also available: easy mode uses NodePort, strict +mode uses ClusterIP. Its token login is separate from Hubble login. Internal +gRPC remains private. See the [Vermeer deployment guide](docs/vermeer.md) for +matching-image requirements, installation, access, credentials and recovery. + +
+ ### Internal image registry Mirror the four component images (`hugegraph/pd`, `hugegraph/store`, @@ -118,6 +153,10 @@ 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. +Hubble and Vermeer images must include the corresponding integration features; +a moving `latest` tag alone does not establish compatibility. ### Storage @@ -139,6 +178,9 @@ 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). ### Server service identity @@ -182,7 +224,12 @@ still need verification with a running Linux cluster. See the ## Modes and entry points -`mode: easy` is the default. `mode: strict` changes Server/Hubble to ClusterIP +`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 and enabled +optional HTTP entries to ClusterIP and enables NetworkPolicy; authentication, Hubble and persistence stay enabled. Use `--set mode=strict` or `-f helm/hugegraph/values-strict.yaml`. @@ -221,6 +268,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` | diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md index 8728df0b28..fe1c8aa381 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.3.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.3.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,12 @@ 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. Hubble needs a matching build with native Vermeer support; +the Vermeer build must include the chart's file-based credentials and token helper. +Use the image compatibility requirements in the [Vermeer guide](vermeer.md#image-compatibility) +and pin the verified references. Do not assume a published Chart or a mutable +`latest` image already contains a source-only feature. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index b12fc95873..f7e67d69b2 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,14 @@ 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 token-signing factor. These 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 +148,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. diff --git a/helm/hugegraph/docs/seatunnel.md b/helm/hugegraph/docs/seatunnel.md index 14a209a2c2..30e38867d5 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` @@ -261,7 +261,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..ecf8b8fa2c --- /dev/null +++ b/helm/hugegraph/docs/vermeer.md @@ -0,0 +1,226 @@ +# 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 exposes the authenticated native UI/API. +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 and network-isolation 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. + +## Image compatibility + +Use a Vermeer build that contains `/go/bin/vermeer-token`, file-based token-factor +configuration, deployment PD/Server credential files, +and space-aware Worker availability. Hubble needs a build containing its native +Vermeer adapter and nine algorithm forms. Source support does not establish that +an existing `latest` image contains these changes. + +Select a matching published build, or build the corresponding source revisions +with their repository Dockerfiles before installation. Record the tested image +digests. Configure `vermeer.image.repository/tag/digest` and `hubble.image` to use +those builds; an explicit digest wins over the tag. Images inherit +`global.imageRegistry` and `imagePullSecrets`. Master, Worker and token +initialization reuse the same Vermeer image; no runtime package download is needed. + +## Install and enable + +Commands run from the HugeGraph repository root. Choose one installation after +selecting compatible images in `my-images.yaml`: + +```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 -f my-images.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 -f my-images.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. + +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. Unload the Vermeer copy when it is no longer needed. + +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. + +## Native UI and API access + +The Master HTTP listener defaults to 6688. Easy mode assigns a NodePort; strict +mode uses ClusterIP. Override `vermeer.service.type/nodePort` when needed. Find +the assigned entry with: + +```bash +kubectl -n hugegraph get svc hugegraph-vermeer-master +kubectl get nodes -o wide +``` + +Use the NodePort on a reachable node, or forward the Service locally: + +```bash +kubectl -n hugegraph port-forward svc/hugegraph-vermeer-master 6688:6688 +``` + +Open . Native Vermeer login uses its own administrator +token, not the HugeGraph password. Generate the token inside the Master container +without putting the signing factor in shell arguments: + +```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 +``` + +Paste that token into the native login form. The same native token can authorize +API requests using `Authorization: Bearer `. Keep it private; it carries +Vermeer administrator access. Hubble creates its service token automatically and +does not require this manual step or a separate native browser login. + +Only Master HTTP is exposed. Master gRPC 6689 and Worker HTTP/gRPC 6788/6789 stay +inside the release. The chart restricts internal gRPC callers even in easy mode. +Strict mode also isolates egress and admits the required Hubble, PD, Store, +Server and Worker traffic automatically. Add native +`networkPolicy.vermeer.extraIngress/extraEgress` rules for actual external +clients or data sources. NetworkPolicy enforcement requires a supporting CNI. + +## Deployment credentials and GitOps + +Vermeer 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. +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 native CLI uses the same token format. 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 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 roll the dependent workloads and Hubble as directed by +Helm NOTES. 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). + +## 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. Hubble's local output files under `./data/hubble` remain +in the container filesystem and are not covered by these claims. + +| 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. +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. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index 562eaad3d5..bc4004b525 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,6 +79,11 @@ 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. @@ -112,6 +125,13 @@ 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). +Enabled Vermeer also needs a stable `vermeer.auth.value` or +`vermeer.auth.existingSecret/key` holding its signing factor. 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` From fa5a3e43de34823d57b57de42d6dee3a28a9ef47 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:36:18 +0000 Subject: [PATCH 08/39] fix(helm): validate private token ownership and license --- helm/hugegraph/docs/vermeer.md | 5 +++++ helm/hugegraph/templates/_helpers.tpl | 9 +++++++++ helm/hugegraph/templates/_vermeer.tpl | 13 ++++++++++--- helm/test_helm_vermeer.py | 6 ++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index ecf8b8fa2c..84692d1e1f 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -164,6 +164,11 @@ 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 diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index ec7df4ae36..414e20220f 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1216,6 +1216,15 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 -}} diff --git a/helm/hugegraph/templates/_vermeer.tpl b/helm/hugegraph/templates/_vermeer.tpl index ce55a0cad5..58937f8753 100644 --- a/helm/hugegraph/templates/_vermeer.tpl +++ b/helm/hugegraph/templates/_vermeer.tpl @@ -2,10 +2,17 @@ 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. -You may obtain a copy at https://www.apache.org/licenses/LICENSE-2.0 . +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 -is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS. +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" -}} diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 745e082d13..1764c1043e 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -487,6 +487,12 @@ def test_invalid_enabled_values_fail_before_install(self): 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), From a44075689ee96cf35618c6b1974591bc00420757 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:36:08 +0000 Subject: [PATCH 09/39] fix(helm): keep Vermeer relative output inside its data volume --- helm/hugegraph/templates/_vermeer.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/hugegraph/templates/_vermeer.tpl b/helm/hugegraph/templates/_vermeer.tpl index 58937f8753..a9824f0f15 100644 --- a/helm/hugegraph/templates/_vermeer.tpl +++ b/helm/hugegraph/templates/_vermeer.tpl @@ -23,7 +23,7 @@ limitations under the License. - name: {{ $role }} image: {{ include "hugegraph.image" (dict "image" $v.image "appVersion" $root.Chart.AppVersion) | quote }} imagePullPolicy: {{ $v.image.pullPolicy }} - workingDir: /go/bin + workingDir: /go/bin/vermeer_data command: ["/go/bin/app"] args: - -env=/var/run/vermeer/config/{{ $role }}.ini From 33c286fcd7d02856cf9dce14c946a904d7f5b5de Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:36:46 +0000 Subject: [PATCH 10/39] docs(helm): clarify persisted Vermeer outputs --- helm/hugegraph/docs/vermeer.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 84692d1e1f..321eeeea30 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -182,8 +182,9 @@ synchronization procedure in [operations](operations.md#credentials). 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. Hubble's local output files under `./data/hubble` remain -in the container filesystem and are not covered by these claims. +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 | |---|---| From d8959c4148da568b168d2bfe11323caf5993c86d Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:36:53 +0000 Subject: [PATCH 11/39] test(helm): keep native algorithm outputs on role data volumes --- helm/test_helm_vermeer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 1764c1043e..137b8c6b1d 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -467,6 +467,20 @@ def test_process_probes_do_not_depend_on_space_worker_availability(self): 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"))) From eda0a9fe7216de118976e0929751aa98a8777728 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:38:19 +0000 Subject: [PATCH 12/39] docs(helm): refresh chart version examples --- helm/hugegraph/docs/distribution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md index fe1c8aa381..a784581d56 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.3.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.3.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 From 7f1b21956615f6a93ea118255ef960e60f4b5243 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:42:12 +0000 Subject: [PATCH 13/39] fix(helm): explain first Vermeer enablement on legacy releases --- helm/hugegraph/templates/_helpers.tpl | 9 ++++ helm/test_helm_vermeer_legacy.py | 65 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 helm/test_helm_vermeer_legacy.py diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 414e20220f..469943c728 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -29,6 +29,15 @@ {{- 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 -}} {{- $strict := eq (.Values.mode | default "easy") "strict" -}} {{- $serviceType := ternary "ClusterIP" "NodePort" (or $strict $legacy) -}} {{- range $comp := list "server" "hubble" "seatunnel" "vermeer" -}} diff --git a/helm/test_helm_vermeer_legacy.py b/helm/test_helm_vermeer_legacy.py new file mode 100644 index 0000000000..e6a08f78e0 --- /dev/null +++ b/helm/test_helm_vermeer_legacy.py @@ -0,0 +1,65 @@ +#!/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_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() From b47c77ba9f09dc8e2e1ca0d3c57957eca36f291d Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:43:32 +0000 Subject: [PATCH 14/39] fix(helm): keep test hook logs readable with Vermeer enabled --- .../templates/tests/test-connection.yaml | 54 ----------- .../templates/tests/vermeer-availability.yaml | 91 +++++++++++++++++++ 2 files changed, 91 insertions(+), 54 deletions(-) create mode 100644 helm/hugegraph/templates/tests/vermeer-availability.yaml diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 2607fa1ebf..020c484f6a 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -16,7 +16,6 @@ # {{ include "hugegraph.resolveValues" . }} -{{- $vermeerEnabled := (.Values.vermeer | default dict).enabled | default false }} {{- $smoke := `def ids = [] as Set; (graph.schema().getVertexLabels() + graph.schema().getEdgeLabels()).each { ids.addAll(it.indexLabels()) @@ -55,10 +54,6 @@ spec: securityContext: seccompProfile: type: RuntimeDefault - {{- if $vermeerEnabled }} - initContainers: - {{- include "hugegraph.vermeer.tokenInit" (dict "root" . "uid" "100" "gid" "101" "resources" .Values.server.testResources) | nindent 4 }} - {{- end }} containers: - name: curl image: {{ .Values.server.waitImage | quote }} @@ -189,52 +184,3 @@ spec: fi sleep 5 done - {{- if $vermeerEnabled }} - - 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/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 }} From 10f61ad9a1a3d1c2bd54f493bf65a5e051f986b7 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:44:44 +0000 Subject: [PATCH 15/39] docs(helm): explain legacy enablement and test discovery --- .github/actions/helm-checks/action.yml | 2 +- helm/hugegraph/Chart.yaml | 2 +- helm/hugegraph/docs/vermeer.md | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/actions/helm-checks/action.yml b/.github/actions/helm-checks/action.yml index 36c1ee1eaf..ffed6876d2 100644 --- a/.github/actions/helm-checks/action.yml +++ b/.github/actions/helm-checks/action.yml @@ -53,7 +53,7 @@ runs: - name: Vermeer integration regression shell: 'bash -e {0}' - run: python3 -m unittest discover -s helm -p 'test_helm_vermeer.py' -v + run: python3 -m unittest discover -s helm -p 'test_helm_vermeer*.py' -v - name: helm unittest shell: 'bash -e {0}' diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml index 71df068e16..6331c0afe9 100644 --- a/helm/hugegraph/Chart.yaml +++ b/helm/hugegraph/Chart.yaml @@ -17,7 +17,7 @@ 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.4.0 # TODO: set appVersion to "1.8.0" when the release images are published. diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 321eeeea30..e05b7438d8 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -63,6 +63,18 @@ 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 -f my-images.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 From f7ddc0d54f5901d1d737a333f1e10b4f8d360846 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:44:30 +0000 Subject: [PATCH 16/39] fix(helm): share the initialized PD metadata namespace between Server and Store --- helm/hugegraph/templates/_helpers.tpl | 44 ++++++++++++++++++- .../templates/server-deployment.yaml | 4 ++ .../templates/store-statefulset.yaml | 4 ++ helm/hugegraph/values.schema.json | 6 +++ helm/hugegraph/values.yaml | 3 ++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 469943c728..12de0fb570 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) -}} @@ -773,6 +777,7 @@ Cross-field validation that JSON Schema draft-07 cannot express. */}} {{- define "hugegraph.validateValues" -}} {{- include "hugegraph.resolveValues" . -}} +{{- include "hugegraph.metadataCluster.validate" . -}} {{- include "hugegraph.vermeer.validate" . -}} {{- if (.Values.seatunnel | default dict).enabled -}} {{- if not (has .Values.seatunnel.mode (list "standalone" "cluster")) -}} @@ -1014,8 +1019,8 @@ 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") + "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 -}} @@ -1294,3 +1299,38 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 }} 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/values.schema.json b/helm/hugegraph/values.schema.json index 1f2b984eb3..680e1f93e0 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -116,6 +116,12 @@ }, "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": { diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 1d2eec9cf8..da098a51fe 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: From 5c304b131c0f907259652907815b27e1e3a0aafe Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:44:31 +0000 Subject: [PATCH 17/39] test(helm): protect shared metadata namespace and legacy upgrades --- .../tests/metadata_cluster_upgrade_test.yaml | 244 ++++++++++++++++++ helm/test_helm_metadata_cluster.py | 146 +++++++++++ 2 files changed, 390 insertions(+) create mode 100644 helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml create mode 100644 helm/test_helm_metadata_cluster.py 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..ca1a8def53 --- /dev/null +++ b/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml @@ -0,0 +1,244 @@ +# +# 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 +release: + upgrade: true +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 + asserts: + - equal: + path: metadata.annotations["hugegraph.apache.org/metadata-cluster"] + value: hg-test + + - it: refuses changing an explicitly configured namespace + 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/test_helm_metadata_cluster.py b/helm/test_helm_metadata_cluster.py new file mode 100644 index 0000000000..dc148956cd --- /dev/null +++ b/helm/test_helm_metadata_cluster.py @@ -0,0 +1,146 @@ +#!/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_server_wrapper_overwrites_image_cluster_before_starting(self): + selected = "production-graph_42" + documents = self.documents({"metadataCluster": selected}) + container = self.container(self.workload(documents, "server"), "server") + environment = dict(os.environ) + for entry in container["env"]: + environment[entry["name"]] = str(entry.get("value", "fixture-secret")) + with tempfile.TemporaryDirectory(prefix="metadata-cluster-wrapper-") as directory: + root = Path(directory) + (root / "conf").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_text("#!/bin/sh\nexit 0\n") + entrypoint.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() From 07f95132c035843fd6c4a38a0bc41b29a5a9d107 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:46:24 +0000 Subject: [PATCH 18/39] docs(helm): preserve the initialized metadata namespace --- .github/actions/helm-checks/action.yml | 4 +++ helm/hugegraph/README.md | 9 +++++++ helm/hugegraph/docs/operations.md | 34 ++++++++++++++++++++++++++ helm/hugegraph/docs/vermeer.md | 7 ++++++ 4 files changed, 54 insertions(+) diff --git a/.github/actions/helm-checks/action.yml b/.github/actions/helm-checks/action.yml index ffed6876d2..f669ec060b 100644 --- a/.github/actions/helm-checks/action.yml +++ b/.github/actions/helm-checks/action.yml @@ -55,6 +55,10 @@ runs: 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: | diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 88574af239..1a135b0dc3 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -182,6 +182,15 @@ 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 `server.serviceId` and `server.graphSpace` select the Server's PD service metadata. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index f7e67d69b2..ca6ded2e64 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -287,6 +287,40 @@ 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. diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index e05b7438d8..5ddda143f6 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -22,6 +22,11 @@ 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. + ## Image compatibility Use a Vermeer build that contains `/go/bin/vermeer-token`, file-based token-factor @@ -236,6 +241,8 @@ 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 From 7b11859e8f7e7b5d269114df185afcddf071ceeb Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:45:55 +0000 Subject: [PATCH 19/39] test(helm): execute native metadata mapping and isolate upgrade fixtures --- .../tests/metadata_cluster_upgrade_test.yaml | 6 +++-- helm/test_helm_metadata_cluster.py | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml b/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml index ca1a8def53..03d644fb9a 100644 --- a/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml +++ b/helm/hugegraph/tests/metadata_cluster_upgrade_test.yaml @@ -18,8 +18,6 @@ suite: Metadata namespace changes protect existing Server metadata templates: - server-deployment.yaml -release: - upgrade: true kubernetesProvider: scheme: "apps/v1/Deployment": @@ -155,12 +153,16 @@ kubernetesProvider: 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: diff --git a/helm/test_helm_metadata_cluster.py b/helm/test_helm_metadata_cluster.py index dc148956cd..9a56c578b3 100644 --- a/helm/test_helm_metadata_cluster.py +++ b/helm/test_helm_metadata_cluster.py @@ -96,21 +96,37 @@ def test_legacy_values_missing_the_shared_field_keep_hg_test(self): self.assertFalse(any(document["metadata"].get("labels", {}).get(COMPONENT, "") .startswith("vermeer") for document in documents)) - def test_server_wrapper_overwrites_image_cluster_before_starting(self): + 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 = dict(os.environ) for entry in container["env"]: - environment[entry["name"]] = str(entry.get("value", "fixture-secret")) + 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_text("#!/bin/sh\nexit 0\n") + 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 + executable.write_text("#!/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) From 5b1f375a6bf7c1e749ead0561d93705a21fe50a8 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:49:58 +0000 Subject: [PATCH 20/39] test(helm): consume fixture init-store credentials before closing its pipe --- helm/test_helm_metadata_cluster.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/helm/test_helm_metadata_cluster.py b/helm/test_helm_metadata_cluster.py index 9a56c578b3..befbbfc52e 100644 --- a/helm/test_helm_metadata_cluster.py +++ b/helm/test_helm_metadata_cluster.py @@ -100,7 +100,7 @@ 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 = dict(os.environ) + 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: @@ -125,7 +125,11 @@ def test_rendered_server_environment_reaches_native_property_mapping(self): for name in ("enable-auth.sh", "wait-storage.sh", "init-store.sh", "start-hugegraph.sh", "wait-partition.sh"): executable = root / "bin" / name - executable.write_text("#!/bin/sh\nexit 0\n") + # 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) From e995d7d88bde7cd98ce1bbac005e2eefed8ebb39 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:53:07 +0000 Subject: [PATCH 21/39] docs(helm): require versioned file-backed service tokens --- helm/hugegraph/docs/vermeer.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 5ddda143f6..22c753a669 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -29,8 +29,8 @@ both components consistently. Retain the initialized value on upgrade; see ## Image compatibility -Use a Vermeer build that contains `/go/bin/vermeer-token`, file-based token-factor -configuration, deployment PD/Server credential files, +Use a Vermeer build that contains `/go/bin/vermeer-token`, versioned HMAC-SHA256 +file-based token-factor configuration, deployment PD/Server credential files, and space-aware Worker availability. Hubble needs a build containing its native Vermeer adapter and nine algorithm forms. Source support does not establish that an existing `latest` image contains these changes. @@ -159,7 +159,10 @@ 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 native CLI uses the same token format. Worker group `$` provides the common +The native CLI's `-factor-file` mode creates the matching versioned token. Do not +replace it with the legacy `-factor` option: inline configuration retains the +older signing protocol and its 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 From 8ad217fdbda41ed6e400c110fb7f23489da5fd17 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 04:55:05 +0000 Subject: [PATCH 22/39] docs(helm): clarify native token upgrades and compute cleanup --- helm/hugegraph/Chart.yaml | 1 + helm/hugegraph/docs/vermeer.md | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml index 6331c0afe9..1a835b045b 100644 --- a/helm/hugegraph/Chart.yaml +++ b/helm/hugegraph/Chart.yaml @@ -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/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 22c753a669..6754405e66 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -96,7 +96,18 @@ required in Hubble. Select a graph and open batch analysis: 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. Unload the Vermeer copy when it is no longer needed. +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 @@ -162,8 +173,8 @@ private temporary volume; the Hubble application mounts the token, not the facto The native CLI's `-factor-file` mode creates the matching versioned token. Do not replace it with the legacy `-factor` option: inline configuration retains the older signing protocol and its tokens are rejected by file-backed deployments. -Worker group `$` provides the common -pool, so newly created graph spaces do not require manual Worker allocation. +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 @@ -197,6 +208,10 @@ Helm NOTES. 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 obtain +a fresh native UI token; legacy SHA1 tokens and cookies are not accepted by file mode. + ## Storage and recovery Master and Worker use separate writable data directories, including when they From b23e7e7a912c2b84b1b206c7900579701dd6c21f Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:22:18 +0000 Subject: [PATCH 23/39] fix(helm): keep optional native engines internal and simplify generated SeaTunnel login --- helm/hugegraph/templates/_helpers.tpl | 40 +++++-- .../templates/seatunnel-auth-secret.yaml | 2 +- .../templates/seatunnel-networkpolicy.yaml | 10 +- helm/hugegraph/templates/seatunnel.yaml | 15 +-- .../templates/vermeer-networkpolicy.yaml | 9 +- helm/hugegraph/templates/vermeer-service.yaml | 15 +-- helm/hugegraph/values.schema.json | 106 ++++++++++++++---- helm/hugegraph/values.yaml | 16 +-- 8 files changed, 137 insertions(+), 76 deletions(-) diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 12de0fb570..1073ee22a6 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -44,7 +44,7 @@ Fail before dereferencing missing blocks; keep values.yaml the sole defaults sou {{- end -}} {{- $strict := eq (.Values.mode | default "easy") "strict" -}} {{- $serviceType := ternary "ClusterIP" "NodePort" (or $strict $legacy) -}} -{{- range $comp := list "server" "hubble" "seatunnel" "vermeer" -}} +{{- range $comp := list "server" "hubble" -}} {{- $component := get $.Values $comp -}} {{- $svc := get $component "service" | default dict -}} {{- $_ := set $component "service" $svc -}} @@ -52,6 +52,14 @@ Fail before dereferencing missing blocks; keep values.yaml the sole defaults sou {{- $_ := 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 -}} @@ -260,7 +268,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 }} @@ -777,6 +785,7 @@ 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 -}} @@ -791,9 +800,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) -}} @@ -1245,9 +1251,6 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- if ne .Values.server.backend "hstore" -}} {{- fail "vermeer.enabled requires server.backend=hstore" -}} {{- end -}} -{{- if and (get $v.service "nodePort") (not (has $v.service.type (list "NodePort" "LoadBalancer"))) -}} -{{- fail "vermeer.service.nodePort requires vermeer.service.type to be NodePort or LoadBalancer" -}} -{{- 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 -}} @@ -1334,3 +1337,24 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 }} diff --git a/helm/hugegraph/templates/seatunnel-auth-secret.yaml b/helm/hugegraph/templates/seatunnel-auth-secret.yaml index 918f1a5ac9..06ff3a45b9 100644 --- a/helm/hugegraph/templates/seatunnel-auth-secret.yaml +++ b/helm/hugegraph/templates/seatunnel-auth-secret.yaml @@ -31,7 +31,7 @@ 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 -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "seatunnel-admin" "length" 16) | 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/vermeer-networkpolicy.yaml b/helm/hugegraph/templates/vermeer-networkpolicy.yaml index f71332a18d..a9d45f6344 100644 --- a/helm/hugegraph/templates/vermeer-networkpolicy.yaml +++ b/helm/hugegraph/templates/vermeer-networkpolicy.yaml @@ -47,9 +47,8 @@ spec: {{- 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.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }} + {{- include "hugegraph.netpol.ports" (list $v.master.httpPort $v.master.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }} {{- if eq $role "master" }} - {{- if $.Values.networkPolicy.enabled }} - from: {{- if $.Values.hubble.enabled }} {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "hubble") | nindent 8 }} @@ -57,10 +56,6 @@ spec: {{- include "hugegraph.netpol.peer" (dict "root" $ "component" "test") | nindent 8 }} ports: {{- include "hugegraph.netpol.ports" (list $v.master.httpPort) | nindent 8 }} - {{- else }} - - ports: - {{- include "hugegraph.netpol.ports" (list $v.master.httpPort) | nindent 8 }} - {{- end }} {{- end }} {{- with get $rules "extraIngress" }} {{- toYaml . | nindent 4 }} @@ -71,7 +66,7 @@ spec: {{- 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.grpcPort $v.worker.grpcPort $v.worker.httpPort) | nindent 8 }} + {{- 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: diff --git a/helm/hugegraph/templates/vermeer-service.yaml b/helm/hugegraph/templates/vermeer-service.yaml index 47eb9f4920..baf6012d9b 100644 --- a/helm/hugegraph/templates/vermeer-service.yaml +++ b/helm/hugegraph/templates/vermeer-service.yaml @@ -30,17 +30,7 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: - type: {{ $v.service.type }} - {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} - {{- with get $v.service $field }} - {{ $field }}: {{ toYaml . | nindent 4 }} - {{- end }} - {{- end }} - {{- if hasKey $v.service "allocateLoadBalancerNodePorts" }} - {{- if ne $v.service.allocateLoadBalancerNodePorts nil }} - allocateLoadBalancerNodePorts: {{ $v.service.allocateLoadBalancerNodePorts }} - {{- end }} - {{- end }} + type: ClusterIP selector: {{- include "hugegraph.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: vermeer-master @@ -48,9 +38,6 @@ spec: - name: http port: {{ $v.master.httpPort }} targetPort: master-http - {{- with $v.service.nodePort }} - nodePort: {{ . }} - {{- end }} --- apiVersion: v1 kind: Service diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 680e1f93e0..2b1389cedc 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", @@ -189,27 +189,10 @@ } }, "seatunnel": { - "type": "object", - "additionalProperties": false, - "properties": { - "extraIngress": { - "$ref": "#/definitions/networkPolicyIngressRules" - }, - "extraEgress": { - "$ref": "#/definitions/networkPolicyEgressRules" - } - } + "$ref": "#/definitions/nativeEngineNetworkPolicy" }, "vermeer": { - "type": "object", - "properties": { - "extraIngress": { - "$ref": "#/definitions/networkPolicyIngressRules" - }, - "extraEgress": { - "$ref": "#/definitions/networkPolicyEgressRules" - } - } + "$ref": "#/definitions/nativeEngineNetworkPolicy" } } }, @@ -1729,7 +1712,7 @@ "$ref": "#/definitions/image" }, "service": { - "$ref": "#/definitions/service" + "$ref": "#/definitions/nativeEngineService" }, "auth": { "type": "object", @@ -1823,6 +1806,87 @@ } } } + }, + "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 da098a51fe..b3b9220f70 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -557,9 +557,9 @@ 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: @@ -622,8 +622,8 @@ vermeer: existingSecret: "" key: credentials.json service: - # Only the authenticated Master HTTP/UI entry is exposed. - type: + # Internal in both easy and strict modes; use Hubble or port-forward. + type: ClusterIP nodePort: annotations: {} master: @@ -676,8 +676,8 @@ vermeer: 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 @@ -700,9 +700,11 @@ networkPolicy: # 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. From f519cd0dacb4feb8af973401df44ba2b8d68c551 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:24:01 +0000 Subject: [PATCH 24/39] feat(helm): add a separate short native Vermeer admin login --- helm/hugegraph/templates/_helpers.tpl | 53 ++++++++++++++++++- helm/hugegraph/templates/_vermeer.tpl | 11 ++++ .../templates/vermeer-admin-secret.yaml | 33 ++++++++++++ .../templates/vermeer-statefulset.yaml | 1 + helm/hugegraph/values.schema.json | 20 +++++++ helm/hugegraph/values.yaml | 6 +++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 helm/hugegraph/templates/vermeer-admin-secret.yaml diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index 1073ee22a6..d93adb3b54 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -42,6 +42,9 @@ Fail before dereferencing missing blocks; keep values.yaml the sole defaults sou {{- 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" -}} @@ -1233,6 +1236,13 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 -}} @@ -1271,7 +1281,7 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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_PD_PASSWORD_FILE" "VERMEER_PD_PEERS" "VERMEER_HUGEGRAPH_USERNAME" "VERMEER_HUGEGRAPH_PASSWORD_FILE" "VERMEER_HUGEGRAPH_CREDENTIALS_FILE") -}} +{{- 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 -}} @@ -1358,3 +1368,44 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" 16) | 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/_vermeer.tpl b/helm/hugegraph/templates/_vermeer.tpl index a9824f0f15..84a0c58403 100644 --- a/helm/hugegraph/templates/_vermeer.tpl +++ b/helm/hugegraph/templates/_vermeer.tpl @@ -47,6 +47,8 @@ limitations under the License. {{- 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 @@ -77,6 +79,9 @@ limitations under the License. - 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 @@ -114,6 +119,12 @@ limitations under the License. - 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 }} 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-statefulset.yaml b/helm/hugegraph/templates/vermeer-statefulset.yaml index d051e0d060..84899f3cd4 100644 --- a/helm/hugegraph/templates/vermeer-statefulset.yaml +++ b/helm/hugegraph/templates/vermeer-statefulset.yaml @@ -61,6 +61,7 @@ spec: 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 }} diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 2b1389cedc..b74f24d1c8 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -1730,6 +1730,26 @@ }, "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" + } + } } } }, diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index b3b9220f70..80027cdc80 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -616,6 +616,12 @@ vermeer: existingSecret: "" key: token-factor autoGenerate: true + # Separate native UI login; Hubble continues to use the internal service token. + 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: From 1f713948fc5593579ba3221c627470332464f462 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:22:45 +0000 Subject: [PATCH 25/39] docs(helm): show internal native access and management passwords --- helm/hugegraph/templates/NOTES.txt | 65 +++++++++--------------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index ba91a63960..14c5e784e1 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -69,25 +69,18 @@ its init-container logs and PD readiness; see helm/hugegraph/docs/operations.md# Access: {{- if $vermeerEnabled }} {{- $vName := include "hugegraph.vermeer.masterName" . }} -{{- $vService := .Values.vermeer.service | default dict }} Vermeer: In-cluster Master HTTP: http://{{ $vName }}.{{ .Release.Namespace }}.svc:{{ .Values.vermeer.master.httpPort }} -{{- if eq $vService.type "NodePort" }} - Native UI/API NodePort: - kubectl get svc -n {{ .Release.Namespace }} {{ $vName }} -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}{"\n"}' - kubectl get nodes -o wide - Open http://:{{ $vService.nodePort | default "" }}/ -{{- else if eq $vService.type "LoadBalancer" }} - Native UI/API LoadBalancer address: - kubectl get svc -n {{ .Release.Namespace }} {{ $vName }} -o wide - Once assigned, open http://:{{ .Values.vermeer.master.httpPort }}/ -{{- end }} + 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 }}/ - Generate a separate native UI token on demand (the command prints a credential; keep it private): - kubectl exec -n {{ .Release.Namespace }} {{ $vName }}-0 -c master -- /go/bin/vermeer-token -factor-file /var/run/vermeer/auth/token-factor -user admin -space '$DEFAULT' -client browser -i -quiet - Use this token in Vermeer's token login. It is separate from the HugeGraph admin password. + 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 16 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. @@ -99,48 +92,31 @@ Access: 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 and .Values.networkPolicy.enabled (has $vService.type (list "NodePort" "LoadBalancer")) }} -{{- $vRules := get .Values.networkPolicy "vermeer" | default dict }} -{{- if empty (get $vRules "extraIngress") }} - Full NetworkPolicy isolation is enabled; allow native HTTP callers through networkPolicy.vermeer.extraIngress. -{{- end }} -{{- end }} {{- 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 }} -{{- if eq .Values.vermeer.mode "cluster" }} - kubectl rollout restart -n {{ .Release.Namespace }} statefulset/{{ include "hugegraph.vermeer.workerName" . }} -{{- end }} 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 }} - Generate a new native UI token after rotation; previously issued native tokens no longer work. + 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 @@ -153,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 16 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. @@ -240,6 +211,8 @@ existing account; see helm/hugegraph/docs/operations.md#credentials for account {{- 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 }} From 41416fac4441e79ad472201cfaa3c786d2cffb30 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:23:16 +0000 Subject: [PATCH 26/39] test(helm): enforce internal optional engines and shorter SeaTunnel credentials --- .../tests/seatunnel_auth_lookup_test.yaml | 77 ++++++++++++++++ helm/test_helm_seatunnel.py | 86 ++++++++++++++---- helm/test_helm_vermeer.py | 91 ++++++++++++++----- 3 files changed, 211 insertions(+), 43 deletions(-) create mode 100644 helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml 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..a2bba4292b --- /dev/null +++ b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml @@ -0,0 +1,77 @@ +# +# 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= +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 16 character alphanumeric password for a new release + template: seatunnel-auth-secret.yaml + release: + name: fresh + asserts: + - matchRegex: + path: data.password + decodeBase64: true + pattern: '^[A-Za-z0-9]{16}$' diff --git a/helm/test_helm_seatunnel.py b/helm/test_helm_seatunnel.py index 5f5e62e139..4ea7c2bfc1 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, @@ -309,8 +339,9 @@ def test_seatunnel_admin_is_generated_separately_and_kept_out_of_public_configur username = base64.b64decode(secret["data"]["username"]).decode() password = base64.b64decode(secret["data"]["password"]).decode() self.assertEqual("admin", username) - self.assertGreaterEqual(len(password), 32) + self.assertRegex(password, r"^[A-Za-z0-9]{16}$") 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"]) @@ -378,7 +409,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 +437,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 +457,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 index 137b8c6b1d..a017d59372 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -210,35 +210,47 @@ def test_native_config_uses_common_pool_and_internal_master(self): launch = " ".join(container.get("command", []) + container.get("args", [])) self.assertIn("-grpc_peer", launch) - def test_only_master_http_inherits_public_entry_defaults(self): - for mode, service_type in (("easy", "NodePort"), ("strict", "ClusterIP")): + 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") - public = [service for service in services if service["spec"].get("type") == "NodePort"] - self.assertEqual(1 if mode == "easy" else 0, len(public)) - for service in public: - self.assertEqual([6688], [port["port"] for port in service["spec"]["ports"]]) + 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)) - self.assertEqual(service_type, http[0]["spec"]["type"]) + 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": "NodePort", "nodePort": 31688}} + "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("type") == "NodePort"] + 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.assertEqual(31688, client["spec"]["ports"][0]["nodePort"]) + 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, = vermeer_resources(documents, "Secret") @@ -436,23 +448,41 @@ def test_strict_network_covers_reverse_rpc_peer_rpc_and_graph_loading(self): self.assertTrue(policy_allows(policies, labels[destination], labels[source], port, "ingress")) self.assertTrue(policy_allows(policies, labels[source], labels[destination], port, "egress")) - def test_internal_rpc_remains_release_scoped_without_strict_egress(self): - for mode in ("standalone", "cluster"): - with self.subTest(mode=mode): - documents = self.documents({"mode": mode}, networkPolicy={"enabled": False}) - policies = resources(documents, "NetworkPolicy") - self.assertTrue(vermeer_resources(documents, "NetworkPolicy")) - master = next(workload for workload in self.workloads(documents) - if workload["metadata"]["labels"][COMPONENT] == "vermeer-master") - labels = pod(master)["metadata"]["labels"] - other_release = {**labels, INSTANCE: "other-release"} - other_component = {**labels, COMPONENT: "server"} - self.assertFalse(policy_allows(policies, labels, other_release, 6689, "ingress")) - self.assertFalse(policy_allows(policies, labels, other_component, 6689, "ingress")) - self.assertTrue(policy_allows(policies, labels, other_release, 6688, "ingress")) + 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": [{"ipBlock": {"cidr": "10.15.0.0/16"}}], "ports": [{"port": 6688}]} + 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]}}) @@ -460,6 +490,14 @@ def test_native_network_extensions_are_preserved(self): 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")): @@ -491,7 +529,10 @@ def test_seatunnel_can_be_enabled_independently_without_changing_engine_identity 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}}, {"master": {"extraEnv": [{"name": "VERMEER_AUTH_TOKEN_FACTOR_FILE", "value": "/wrong"}]}}, {"worker": {"extraEnv": [{"name": "POD_IP", "value": "0.0.0.0"}]}}) From 558faa3924b47e8b0ee67dd41809fdb40504a8d4 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:25:38 +0000 Subject: [PATCH 27/39] test(helm): cover separate Vermeer native admin credentials --- .../tests/vermeer_admin_lookup_test.yaml | 112 ++++++++++++++++++ helm/test_helm_vermeer.py | 98 +++++++++++++-- helm/test_helm_vermeer_legacy.py | 32 +++++ 3 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 helm/hugegraph/tests/vermeer_admin_lookup_test.yaml 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..c1b8a05c18 --- /dev/null +++ b/helm/hugegraph/tests/vermeer_admin_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: 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= +tests: + - 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 short independent login for a fresh release + template: vermeer-admin-secret.yaml + release: + name: fresh + asserts: + - matchRegex: + path: data.password + decodeBase64: true + pattern: '^[A-Za-z0-9]{16}$' diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index a017d59372..5cb44329e7 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -34,6 +34,7 @@ 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): @@ -118,7 +119,8 @@ def render(self, vermeer=None, chart=CHART, preset=None, release="fixture", name if not deterministic_auth: values = {} if vermeer is not None: - values["vermeer"] = merged({"enabled": True, "auth": {"value": FACTOR}}, vermeer) + 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" @@ -253,20 +255,22 @@ def test_legacy_null_service_fields_keep_the_native_entry_internal(self): def test_credentials_are_secret_references_and_not_configmap_or_process_arguments(self): documents = self.documents({}) - secret, = vermeer_resources(documents, "Secret") + 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, "graph-test-admin-password", "pd-test-service-password"): + 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"}}, + 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")) @@ -274,14 +278,15 @@ def test_external_credential_secret_names_and_keys_are_preserved(self): 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", "metadata-auth", "graph-admin"}, set(secrets)) + 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")): + ("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, = vermeer_resources(documents, "Secret") + 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")) @@ -290,6 +295,81 @@ def test_generated_factor_and_pod_checksum_use_the_same_random_value(self): 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): + for mode in ("standalone", "cluster"): + with self.subTest(mode=mode): + documents = self.documents({"mode": mode, "auth": {"admin": {"password": ""}}}) + 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, r"^[A-Za-z0-9]{16}$") + 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"}}) @@ -534,6 +614,10 @@ def test_invalid_enabled_values_fail_before_install(self): {"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: diff --git a/helm/test_helm_vermeer_legacy.py b/helm/test_helm_vermeer_legacy.py index e6a08f78e0..e4e2335c0f 100644 --- a/helm/test_helm_vermeer_legacy.py +++ b/helm/test_helm_vermeer_legacy.py @@ -29,6 +29,38 @@ 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" From dd3f77f17689ca9c9e84d866d3c70d8db7ce4096 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:28:12 +0000 Subject: [PATCH 28/39] docs(helm): keep native engines private Keep Hubble as the default compute entry and document optional port-forwarded administration. Use generated short human passwords and move raw service tokens and image publishing into advanced guidance. Preserve metadata adoption, native token migration and compute cleanup instructions. --- helm/hugegraph/README.md | 41 +++++---- helm/hugegraph/docs/operations.md | 37 +++++--- helm/hugegraph/docs/seatunnel.md | 67 +++++++------- helm/hugegraph/docs/vermeer.md | 141 ++++++++++++++++++------------ helm/hugegraph/examples/README.md | 13 ++- 5 files changed, 177 insertions(+), 122 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 1a135b0dc3..edcfeac325 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -98,19 +98,20 @@ Set `seatunnel.enabled=true` to start one long-running hybrid member alongside H 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 16-character password; 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. @@ -131,10 +132,11 @@ 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. -The native Vermeer UI/API is also available: easy mode uses NodePort, strict -mode uses ClusterIP. Its token login is separate from Hubble login. Internal -gRPC remains private. See the [Vermeer deployment guide](docs/vermeer.md) for -matching-image requirements, installation, access, credentials and recovery. +Native Vermeer UI/API stays private behind ClusterIP in both modes. Optional +administration uses port-forward and a separate generated 16-character password; +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. @@ -237,14 +239,17 @@ still need verification with a running Linux cluster. See the 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 and enabled -optional HTTP entries to ClusterIP -and enables NetworkPolicy; authentication, Hubble and persistence stay enabled. +`mode: easy` is the default. `mode: strict` changes Server/Hubble to ClusterIP +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 | |---|---|---| @@ -262,7 +267,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. @@ -305,7 +310,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/operations.md b/helm/hugegraph/docs/operations.md index ca6ded2e64..f2fc7d6d77 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -31,8 +31,10 @@ 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 token-signing factor. These do not replace Server or PD -credentials. Their Secret reuse, external-source and rotation procedures are in +account and Vermeer's separate native administrator password. New human +passwords are 16 characters; upgrades 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. @@ -323,18 +325,25 @@ 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 +16-character administrator password; 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 30e38867d5..dc8c5b0dd2 100644 --- a/helm/hugegraph/docs/seatunnel.md +++ b/helm/hugegraph/docs/seatunnel.md @@ -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 16-character password in +`-auth` for a new installation. Online upgrades reuse +the existing password, including 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 diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 6754405e66..7f13097988 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -2,7 +2,9 @@ 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 exposes the authenticated native UI/API. +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 @@ -27,30 +29,14 @@ Server and Store also need the same PD metadata prefix. The shared both components consistently. Retain the initialized value on upgrade; see [namespace adoption](operations.md#pd-metadata-namespace) for older custom images. -## Image compatibility - -Use a Vermeer build that contains `/go/bin/vermeer-token`, versioned HMAC-SHA256 -file-based token-factor configuration, deployment PD/Server credential files, -and space-aware Worker availability. Hubble needs a build containing its native -Vermeer adapter and nine algorithm forms. Source support does not establish that -an existing `latest` image contains these changes. - -Select a matching published build, or build the corresponding source revisions -with their repository Dockerfiles before installation. Record the tested image -digests. Configure `vermeer.image.repository/tag/digest` and `hubble.image` to use -those builds; an explicit digest wins over the tag. Images inherit -`global.imageRegistry` and `imagePullSecrets`. Master, Worker and token -initialization reuse the same Vermeer image; no runtime package download is needed. - ## Install and enable -Commands run from the HugeGraph repository root. Choose one installation after -selecting compatible images in `my-images.yaml`: +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 -f my-images.yaml \ + -f helm/hugegraph/values-single.yaml \ --set vermeer.enabled=true --set vermeer.mode=standalone \ --wait --timeout 20m ``` @@ -58,7 +44,7 @@ helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ ```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 -f my-images.yaml \ + -f helm/hugegraph/values-cluster.yaml \ --set vermeer.enabled=true --set vermeer.mode=cluster \ --set vermeer.worker.replicas=3 --wait --timeout 20m ``` @@ -76,7 +62,7 @@ them, then pass the file without `--reuse-values`: 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 -f my-images.yaml --set vermeer.enabled=true \ + -f existing-values.yaml --set vermeer.enabled=true \ --wait --timeout 20m ``` @@ -119,48 +105,57 @@ 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. -## Native UI and API access - -The Master HTTP listener defaults to 6688. Easy mode assigns a NodePort; strict -mode uses ClusterIP. Override `vermeer.service.type/nodePort` when needed. Find -the assigned entry with: - -```bash -kubectl -n hugegraph get svc hugegraph-vermeer-master -kubectl get nodes -o wide -``` +## Optional native administration -Use the NodePort on a reachable node, or forward the Service locally: +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 . Native Vermeer login uses its own administrator -token, not the HugeGraph password. Generate the token inside the Master container -without putting the signing factor in shell arguments: +Open . Read the separate Vermeer administrator password: ```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 +kubectl -n hugegraph get secret hugegraph-vermeer-admin \ + -o jsonpath='{.data.password}' | base64 --decode ``` -Paste that token into the native login form. The same native token can authorize -API requests using `Authorization: Bearer `. Keep it private; it carries -Vermeer administrator access. Hubble creates its service token automatically and -does not require this manual step or a separate native browser login. - -Only Master HTTP is exposed. Master gRPC 6689 and Worker HTTP/gRPC 6788/6789 stay -inside the release. The chart restricts internal gRPC callers even in easy mode. -Strict mode also isolates egress and admits the required Hubble, PD, Store, -Server and Worker traffic automatically. Add native -`networkPolicy.vermeer.extraIngress/extraEgress` rules for actual external -clients or data sources. NetworkPolicy enforcement requires a supporting CNI. +Paste it into the **Admin credential** input; no username is required. New installations generate a +16-character password and keep it across online upgrades; existing credentials +are reused. 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 -Vermeer authentication uses a separate signing factor in the kept +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; @@ -203,14 +198,50 @@ uses that same UID so Hubble can read its private `0600` file. Setting 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 roll the dependent workloads and Hubble as directed by -Helm NOTES. An external Secret change alone does not roll Pods or regenerate -Hubble's token. Server password rotation also needs the existing account/Secret +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 obtain -a fresh native UI token; legacy SHA1 tokens and cookies are not accepted by 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 + +Vermeer images are published through the +[publish latest Vermeer workflow](https://github.com/hugegraph/actions/blob/master/.github/workflows/publish_latest_vermeer_image.yml). +Use a workflow-produced matching build when pinning an image; record the tested +tag/digest. Custom builds must include `/go/bin/vermeer-token`, versioned +HMAC-SHA256 file credentials, the native administrator password login and +space-aware Worker availability. Hubble needs its native Vermeer adapter and +nine algorithm forms. A mutable tag alone does not establish these capabilities. + +Configure `vermeer.image.repository/tag/digest` and `hubble.image` when pinning +builds; digest takes priority. Images inherit `global.imageRegistry` and +`imagePullSecrets`. Master, Worker and token initialization reuse the Vermeer +image, with no runtime package downloads. + +
## Storage and recovery diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index bc4004b525..8c00ec547a 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -125,8 +125,11 @@ 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). -Enabled Vermeer also needs a stable `vermeer.auth.value` or -`vermeer.auth.existingSecret/key` holding its signing factor. Optional external +For native administrator credentials, enabled Vermeer accepts +`vermeer.auth.admin.password` or `vermeer.auth.admin.existingSecret/key`; +new installations otherwise generate a kept 16-character password. 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) @@ -158,5 +161,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. From 3def8f2f9da31d4ea0eedaf9c102cef7368fa7a6 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:39:23 +0000 Subject: [PATCH 29/39] docs(helm): clarify native deployment helpers --- helm/hugegraph/docs/vermeer.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 7f13097988..5cc6287c3c 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -165,9 +165,10 @@ 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 native CLI's `-factor-file` mode creates the matching versioned token. Do not -replace it with the legacy `-factor` option: inline configuration retains the -older signing protocol and its tokens are rejected by file-backed deployments. +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. @@ -298,3 +299,16 @@ after restart, rerun the graph load/computation; a retained successful task reco 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`. + +
From 6eef5a372c022df456382869a79cd699cd467df5 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:43:58 +0000 Subject: [PATCH 30/39] fix(helm): pin Vermeer to the verified official multi-platform image --- helm/hugegraph/values.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 80027cdc80..33ac131cea 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -606,10 +606,11 @@ vermeer: mode: standalone image: repository: hugegraph/vermeer - # Select a build containing file credentials and /go/bin/vermeer-token. - tag: latest - digest: "" - pullPolicy: Always + # 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-2678ff91b56a + digest: sha256:9ef623d93e4feddc345c6ce445109c4039b029be83090d0eca97aedeba981a9e + pullPolicy: IfNotPresent auth: # Token signing factor. existingSecret > value > a stable generated Secret. value: "" From edfc67bca76d48890f12da4d9acf88b59466afea Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 05:45:50 +0000 Subject: [PATCH 31/39] docs(helm): record published Vermeer image Record the verified official multi-platform image, immutable index digest, source commit and publishing run. Keep ordinary enablement automatic and document digest precedence for custom tags. Preserve the four-image default mirror inventory and add optional-image guidance separately. --- helm/hugegraph/README.md | 5 ++-- helm/hugegraph/docs/distribution.md | 13 ++++++---- helm/hugegraph/docs/vermeer.md | 40 +++++++++++++++++++++-------- helm/hugegraph/examples/README.md | 8 ++++++ 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index edcfeac325..6b19e7e682 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -157,8 +157,9 @@ 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. -Hubble and Vermeer images must include the corresponding integration features; -a moving `latest` tag alone does not establish compatibility. +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 diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md index a784581d56..76966b0231 100644 --- a/helm/hugegraph/docs/distribution.md +++ b/helm/hugegraph/docs/distribution.md @@ -142,8 +142,11 @@ 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. Hubble needs a matching build with native Vermeer support; -the Vermeer build must include the chart's file-based credentials and token helper. -Use the image compatibility requirements in the [Vermeer guide](vermeer.md#image-compatibility) -and pin the verified references. Do not assume a published Chart or a mutable -`latest` image already contains a source-only feature. +the same Vermeer image. Its default is the officially published +`hugegraph/vermeer:helm-2678ff91b56a`, 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/vermeer.md b/helm/hugegraph/docs/vermeer.md index 5cc6287c3c..858cb96d23 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -229,18 +229,36 @@ its intended internal API integration. ### Image compatibility -Vermeer images are published through the +The Chart pins the officially published `hugegraph/vermeer:helm-2678ff91b56a` +image to OCI index digest +`sha256:9ef623d93e4feddc345c6ce445109c4039b029be83090d0eca97aedeba981a9e`. +The index provides Linux `amd64` and `arm64` images built from +[source commit 2678ff91b56a](https://github.com/hugegraph/hugegraph-computer/commit/2678ff91b56ae9501fbcdb0e1cf937144e508241) +by the successful [official publishing run](https://github.com/hugegraph/actions/actions/runs/40639306743). +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). -Use a workflow-produced matching build when pinning an image; record the tested -tag/digest. Custom builds must include `/go/bin/vermeer-token`, versioned -HMAC-SHA256 file credentials, the native administrator password login and -space-aware Worker availability. Hubble needs its native Vermeer adapter and -nine algorithm forms. A mutable tag alone does not establish these capabilities. - -Configure `vermeer.image.repository/tag/digest` and `hubble.image` when pinning -builds; digest takes priority. Images inherit `global.imageRegistry` and -`imagePullSecrets`. Master, Worker and token initialization reuse the Vermeer -image, with no runtime package downloads. +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. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index 8c00ec547a..e29228f316 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -89,6 +89,14 @@ before selecting Hubble/Vermeer builds. 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-2678ff91b56a` 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 From 00b06b326210cb6b0996940476a6b5af97db1cca Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 28 Oct 2026 14:25:58 +0800 Subject: [PATCH 32/39] fix(helm): scope Vermeer rollout checksums by role --- helm/hugegraph/templates/vermeer-statefulset.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/helm/hugegraph/templates/vermeer-statefulset.yaml b/helm/hugegraph/templates/vermeer-statefulset.yaml index 84899f3cd4..8838a1d081 100644 --- a/helm/hugegraph/templates/vermeer-statefulset.yaml +++ b/helm/hugegraph/templates/vermeer-statefulset.yaml @@ -27,6 +27,13 @@ {{- $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 @@ -57,7 +64,7 @@ spec: app.kubernetes.io/component: vermeer-{{ $role }} {{- with $v.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} annotations: - checksum/config: {{ omit $v "auth" "hugegraphCredentials" | toJson | sha256sum | quote }} + checksum/config: {{ $configChecksum | toJson | sha256sum | quote }} checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" $ | quote }} {{- if $master }} checksum/vermeer-auth: {{ include "hugegraph.vermeer.authChecksum" $ | quote }} @@ -151,4 +158,4 @@ spec: requests: storage: {{ $v.worker.storage.size }} {{- end }} -{{- end }} +{{- end }} \ No newline at end of file From e48b0ef3089fd44f1e3c86dab3c4f48bbb5db7fa Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 28 Oct 2026 14:26:39 +0800 Subject: [PATCH 33/39] test(helm): pin Vermeer role rollout boundaries --- helm/test_helm_vermeer_rollout.py | 126 ++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 helm/test_helm_vermeer_rollout.py 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() From 1e646d82d5f89f920c27eccb47dc045b6d3549d1 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 06:40:48 +0000 Subject: [PATCH 34/39] fix(helm): shorten easy mode admin passwords --- helm/hugegraph/README.md | 4 +- helm/hugegraph/docs/operations.md | 4 +- helm/hugegraph/docs/seatunnel.md | 4 +- helm/hugegraph/docs/vermeer.md | 6 +-- helm/hugegraph/examples/README.md | 2 +- helm/hugegraph/templates/NOTES.txt | 4 +- helm/hugegraph/templates/_helpers.tpl | 3 +- .../templates/seatunnel-auth-secret.yaml | 3 +- .../tests/seatunnel_auth_lookup_test.yaml | 37 +++++++++++++- .../tests/vermeer_admin_lookup_test.yaml | 33 +++++++++++- helm/hugegraph/values.yaml | 5 +- helm/test_helm_seatunnel.py | 50 ++++++++++--------- helm/test_helm_vermeer.py | 10 ++-- 13 files changed, 119 insertions(+), 46 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 6b19e7e682..37351b141d 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -110,7 +110,7 @@ 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 -with a newly generated 16-character password; online upgrades reuse the +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. @@ -133,7 +133,7 @@ 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 16-character password; +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. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index f2fc7d6d77..46c8c489bc 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -32,7 +32,7 @@ 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 16 characters; upgrades retain existing Secrets. Internal Vermeer +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 @@ -343,7 +343,7 @@ 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 -16-character administrator password; Hubble service tokens stay automatic. +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 dc8c5b0dd2..01becac622 100644 --- a/helm/hugegraph/docs/seatunnel.md +++ b/helm/hugegraph/docs/seatunnel.md @@ -90,9 +90,9 @@ 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 creates a 16-character password in +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 longer credentials from older releases. +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`. diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 858cb96d23..bbc615f212 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -15,7 +15,7 @@ It needs no Computer Operator, CRD, etcd or external coordination service. | `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 and network-isolation defaults; independent of both topologies | +| `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 @@ -123,8 +123,8 @@ kubectl -n hugegraph get secret hugegraph-vermeer-admin \ ``` Paste it into the **Admin credential** input; no username is required. New installations generate a -16-character password and keep it across online upgrades; existing credentials -are reused. Successful login creates a versioned session cookie valid for +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. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index e29228f316..c6b8999e30 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -135,7 +135,7 @@ account; follow the [account password and Secret synchronization steps](../docs/ For native administrator credentials, enabled Vermeer accepts `vermeer.auth.admin.password` or `vermeer.auth.admin.existingSecret/key`; -new installations otherwise generate a kept 16-character password. Advanced +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 diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 14c5e784e1..0a9f02a7f6 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -79,7 +79,7 @@ Access: 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 16 characters; upgrades keep existing credentials. + 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, @@ -129,7 +129,7 @@ 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 16 characters; upgrades keep existing credentials. + 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. diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index d93adb3b54..bd0c8770c8 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1392,7 +1392,8 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}} {{- get (get $secret "data") $key -}} {{- else -}} -{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" 16) | b64enc -}} +{{- $length := ternary 16 6 (eq (.Values.mode | default "easy") "strict") -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" $length) | b64enc -}} {{- end -}} {{- end -}} {{- end }} diff --git a/helm/hugegraph/templates/seatunnel-auth-secret.yaml b/helm/hugegraph/templates/seatunnel-auth-secret.yaml index 06ff3a45b9..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" "length" 16) | 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/tests/seatunnel_auth_lookup_test.yaml b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml index a2bba4292b..60f0606c3b 100644 --- a/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml +++ b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml @@ -36,6 +36,13 @@ kubernetesProvider: 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 @@ -66,12 +73,40 @@ tests: decodeBase64: true pattern: 'basic-auth-password: NextAdmin0123456' - - it: generates a 16 character alphanumeric password for a new release + - 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 index c1b8a05c18..f76cdaac20 100644 --- a/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml +++ b/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml @@ -44,7 +44,26 @@ kubernetesProvider: 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: @@ -101,10 +120,22 @@ tests: path: spec.template.metadata.annotations["checksum/vermeer-admin"] value: 384c79eb04541fb1216e49b3739cfa3ac87bf7a3ae3c84891eabda9421351b4b - - it: generates a short independent login for a fresh release + - 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 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 33ac131cea..10749d6c41 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -566,7 +566,7 @@ seatunnel: # 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: >- @@ -617,7 +617,8 @@ vermeer: existingSecret: "" key: token-factor autoGenerate: true - # Separate native UI login; Hubble continues to use the internal service token. + # 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: "" diff --git a/helm/test_helm_seatunnel.py b/helm/test_helm_seatunnel.py index 4ea7c2bfc1..e725392a3c 100644 --- a/helm/test_helm_seatunnel.py +++ b/helm/test_helm_seatunnel.py @@ -333,30 +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.assertRegex(password, r"^[A-Za-z0-9]{16}$") - 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"])) + 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'\"\\$" diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 5cb44329e7..9a36e7fa4c 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -296,13 +296,15 @@ def test_generated_factor_and_pod_checksum_use_the_same_random_value(self): self.assertEqual(checksum, pod(master)["metadata"]["annotations"]["checksum/vermeer-auth"]) def test_native_admin_password_is_short_separate_and_mounted_only_by_master(self): - for mode in ("standalone", "cluster"): - with self.subTest(mode=mode): - documents = self.documents({"mode": mode, "auth": {"admin": {"password": ""}}}) + 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, r"^[A-Za-z0-9]{16}$") + 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") From 346f564f823b641c162b4d832497cefd9db2dcc2 Mon Sep 17 00:00:00 2001 From: imbajin Date: Wed, 28 Oct 2026 14:47:30 +0800 Subject: [PATCH 35/39] revert(helm): keep native admin passwords strong - Restore 16-character generated Vermeer and SeaTunnel administrator credentials in every mode. - Preserve existing Secret reuse and the role-scoped Vermeer rollout fix. - Avoid weakening the default easy-mode authentication boundary without rate limiting. --- helm/hugegraph/README.md | 4 +- helm/hugegraph/docs/operations.md | 4 +- helm/hugegraph/docs/seatunnel.md | 4 +- helm/hugegraph/docs/vermeer.md | 6 +-- helm/hugegraph/examples/README.md | 2 +- helm/hugegraph/templates/NOTES.txt | 4 +- helm/hugegraph/templates/_helpers.tpl | 3 +- .../templates/seatunnel-auth-secret.yaml | 3 +- .../tests/seatunnel_auth_lookup_test.yaml | 37 +------------- .../tests/vermeer_admin_lookup_test.yaml | 33 +----------- helm/hugegraph/values.yaml | 5 +- helm/test_helm_seatunnel.py | 50 +++++++++---------- helm/test_helm_vermeer.py | 10 ++-- 13 files changed, 46 insertions(+), 119 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 37351b141d..6b19e7e682 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -110,7 +110,7 @@ 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 -with a newly generated 6-character password in `easy` or 16 characters in `strict`; online upgrades reuse the +with a newly generated 16-character password; 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. @@ -133,7 +133,7 @@ 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`; +administration uses port-forward and a separate generated 16-character password; 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. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index 46c8c489bc..f2fc7d6d77 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -32,7 +32,7 @@ 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 +passwords are 16 characters; upgrades 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 @@ -343,7 +343,7 @@ 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. +16-character administrator password; 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 01becac622..dc8c5b0dd2 100644 --- a/helm/hugegraph/docs/seatunnel.md +++ b/helm/hugegraph/docs/seatunnel.md @@ -90,9 +90,9 @@ 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 creates a random alphanumeric password: 6 characters in `easy`, 16 in `strict`, stored in +defaults to `admin`. An empty password creates a 16-character password in `-auth` for a new installation. Online upgrades reuse -the existing password, including on mode changes and for longer credentials from older releases. +the existing password, including 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`. diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index bbc615f212..858cb96d23 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -15,7 +15,7 @@ It needs no Computer Operator, CRD, etcd or external coordination service. | `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 | +| `mode: easy` / `strict` | HTTP entry and network-isolation 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 @@ -123,8 +123,8 @@ kubectl -n hugegraph get secret hugegraph-vermeer-admin \ ``` 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 +16-character password and keep it across online upgrades; existing credentials +are reused. 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. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index c6b8999e30..e29228f316 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -135,7 +135,7 @@ account; follow the [account password and Secret synchronization steps](../docs/ 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 +new installations otherwise generate a kept 16-character password. 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 diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 0a9f02a7f6..14c5e784e1 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -79,7 +79,7 @@ Access: 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. + New chart-generated management passwords have 16 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, @@ -129,7 +129,7 @@ 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. + New chart-generated entry passwords have 16 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. diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index bd0c8770c8..d93adb3b54 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1392,8 +1392,7 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- 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 -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" 16) | b64enc -}} {{- end -}} {{- end -}} {{- end }} diff --git a/helm/hugegraph/templates/seatunnel-auth-secret.yaml b/helm/hugegraph/templates/seatunnel-auth-secret.yaml index 9d20ce7fd3..06ff3a45b9 100644 --- a/helm/hugegraph/templates/seatunnel-auth-secret.yaml +++ b/helm/hugegraph/templates/seatunnel-auth-secret.yaml @@ -31,8 +31,7 @@ limitations under the License. {{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") "password") -}} {{- get (get $secret "data") "password" -}} {{- else -}} -{{- $length := ternary 16 6 (eq (.Values.mode | default "easy") "strict") -}} -{{- include "hugegraph.generatedCredential" (dict "root" . "name" "seatunnel-admin" "length" $length) | b64enc -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "seatunnel-admin" "length" 16) | b64enc -}} {{- end -}} {{- end -}} {{- end -}} diff --git a/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml index 60f0606c3b..a2bba4292b 100644 --- a/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml +++ b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml @@ -36,13 +36,6 @@ kubernetesProvider: 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 @@ -73,40 +66,12 @@ tests: decodeBase64: true pattern: 'basic-auth-password: NextAdmin0123456' - - it: generates a 6 character alphanumeric password for a new easy release + - it: generates a 16 character alphanumeric password for a new 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 index f76cdaac20..c1b8a05c18 100644 --- a/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml +++ b/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml @@ -44,26 +44,7 @@ kubernetesProvider: 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: @@ -120,22 +101,10 @@ tests: path: spec.template.metadata.annotations["checksum/vermeer-admin"] value: 384c79eb04541fb1216e49b3739cfa3ac87bf7a3ae3c84891eabda9421351b4b - - it: generates a 6 character independent login for a fresh easy release + - it: generates a short independent login for a fresh 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 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 10749d6c41..33ac131cea 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -566,7 +566,7 @@ seatunnel: # 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 6 characters in easy / 16 in strict; upgrades reuse the kept Secret. + # Empty generates a kept Secret; upgrades reuse the existing password. password: "" # Replaces the image's jvm_options; reserve memory outside the heap under 2Gi. javaOpts: >- @@ -617,8 +617,7 @@ vermeer: 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. + # Separate native UI login; Hubble continues to use the internal service token. admin: password: "" existingSecret: "" diff --git a/helm/test_helm_seatunnel.py b/helm/test_helm_seatunnel.py index e725392a3c..4ea7c2bfc1 100644 --- a/helm/test_helm_seatunnel.py +++ b/helm/test_helm_seatunnel.py @@ -333,32 +333,30 @@ 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" - 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"])) + 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.assertRegex(password, r"^[A-Za-z0-9]{16}$") + 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'\"\\$" diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 9a36e7fa4c..5cb44329e7 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -296,15 +296,13 @@ def test_generated_factor_and_pod_checksum_use_the_same_random_value(self): 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) + for mode in ("standalone", "cluster"): + with self.subTest(mode=mode): + documents = self.documents({"mode": mode, "auth": {"admin": {"password": ""}}}) 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.assertRegex(password, r"^[A-Za-z0-9]{16}$") 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") From 6da508674e707c498a1290f20c163f47e4517333 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 07:37:12 +0000 Subject: [PATCH 36/39] docs(helm): remove publishing run reference --- helm/hugegraph/docs/vermeer.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 858cb96d23..6c85289a50 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -233,8 +233,7 @@ The Chart pins the officially published `hugegraph/vermeer:helm-2678ff91b56a` image to OCI index digest `sha256:9ef623d93e4feddc345c6ce445109c4039b029be83090d0eca97aedeba981a9e`. The index provides Linux `amd64` and `arm64` images built from -[source commit 2678ff91b56a](https://github.com/hugegraph/hugegraph-computer/commit/2678ff91b56ae9501fbcdb0e1cf937144e508241) -by the successful [official publishing run](https://github.com/hugegraph/actions/actions/runs/40639306743). +[source commit 2678ff91b56a](https://github.com/hugegraph/hugegraph-computer/commit/2678ff91b56ae9501fbcdb0e1cf937144e508241). No manual image override is needed for ordinary enablement. Publishing uses the From 85da6cf5eb2b5c8b44deaa9a8d37ef9729e5b075 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 07:47:16 +0000 Subject: [PATCH 37/39] fix(helm): honor easy mode password defaults --- helm/hugegraph/README.md | 4 +- helm/hugegraph/docs/operations.md | 4 +- helm/hugegraph/docs/seatunnel.md | 4 +- helm/hugegraph/docs/vermeer.md | 6 +-- helm/hugegraph/examples/README.md | 2 +- helm/hugegraph/templates/NOTES.txt | 4 +- helm/hugegraph/templates/_helpers.tpl | 3 +- .../templates/seatunnel-auth-secret.yaml | 3 +- .../tests/seatunnel_auth_lookup_test.yaml | 37 +++++++++++++- .../tests/vermeer_admin_lookup_test.yaml | 33 +++++++++++- helm/hugegraph/values.yaml | 5 +- helm/test_helm_seatunnel.py | 50 ++++++++++--------- helm/test_helm_vermeer.py | 10 ++-- 13 files changed, 119 insertions(+), 46 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 6b19e7e682..37351b141d 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -110,7 +110,7 @@ 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 -with a newly generated 16-character password; online upgrades reuse the +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. @@ -133,7 +133,7 @@ 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 16-character password; +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. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md index f2fc7d6d77..46c8c489bc 100644 --- a/helm/hugegraph/docs/operations.md +++ b/helm/hugegraph/docs/operations.md @@ -32,7 +32,7 @@ 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 16 characters; upgrades retain existing Secrets. Internal Vermeer +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 @@ -343,7 +343,7 @@ 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 -16-character administrator password; Hubble service tokens stay automatic. +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 dc8c5b0dd2..01becac622 100644 --- a/helm/hugegraph/docs/seatunnel.md +++ b/helm/hugegraph/docs/seatunnel.md @@ -90,9 +90,9 @@ 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 creates a 16-character password in +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 longer credentials from older releases. +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`. diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 6c85289a50..4b39608bf6 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -15,7 +15,7 @@ It needs no Computer Operator, CRD, etcd or external coordination service. | `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 and network-isolation defaults; independent of both topologies | +| `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 @@ -123,8 +123,8 @@ kubectl -n hugegraph get secret hugegraph-vermeer-admin \ ``` Paste it into the **Admin credential** input; no username is required. New installations generate a -16-character password and keep it across online upgrades; existing credentials -are reused. Successful login creates a versioned session cookie valid for +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. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index e29228f316..c6b8999e30 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -135,7 +135,7 @@ account; follow the [account password and Secret synchronization steps](../docs/ For native administrator credentials, enabled Vermeer accepts `vermeer.auth.admin.password` or `vermeer.auth.admin.existingSecret/key`; -new installations otherwise generate a kept 16-character password. Advanced +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 diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt index 14c5e784e1..0a9f02a7f6 100644 --- a/helm/hugegraph/templates/NOTES.txt +++ b/helm/hugegraph/templates/NOTES.txt @@ -79,7 +79,7 @@ Access: 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 16 characters; upgrades keep existing credentials. + 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, @@ -129,7 +129,7 @@ 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 16 characters; upgrades keep existing credentials. + 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. diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index d93adb3b54..bd0c8770c8 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -1392,7 +1392,8 @@ over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict {{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}} {{- get (get $secret "data") $key -}} {{- else -}} -{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" 16) | b64enc -}} +{{- $length := ternary 16 6 (eq (.Values.mode | default "easy") "strict") -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "vermeer-admin" "length" $length) | b64enc -}} {{- end -}} {{- end -}} {{- end }} diff --git a/helm/hugegraph/templates/seatunnel-auth-secret.yaml b/helm/hugegraph/templates/seatunnel-auth-secret.yaml index 06ff3a45b9..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" "length" 16) | 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/tests/seatunnel_auth_lookup_test.yaml b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml index a2bba4292b..60f0606c3b 100644 --- a/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml +++ b/helm/hugegraph/tests/seatunnel_auth_lookup_test.yaml @@ -36,6 +36,13 @@ kubernetesProvider: 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 @@ -66,12 +73,40 @@ tests: decodeBase64: true pattern: 'basic-auth-password: NextAdmin0123456' - - it: generates a 16 character alphanumeric password for a new release + - 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 index c1b8a05c18..f76cdaac20 100644 --- a/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml +++ b/helm/hugegraph/tests/vermeer_admin_lookup_test.yaml @@ -44,7 +44,26 @@ kubernetesProvider: 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: @@ -101,10 +120,22 @@ tests: path: spec.template.metadata.annotations["checksum/vermeer-admin"] value: 384c79eb04541fb1216e49b3739cfa3ac87bf7a3ae3c84891eabda9421351b4b - - it: generates a short independent login for a fresh release + - 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 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 33ac131cea..10749d6c41 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -566,7 +566,7 @@ seatunnel: # 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: >- @@ -617,7 +617,8 @@ vermeer: existingSecret: "" key: token-factor autoGenerate: true - # Separate native UI login; Hubble continues to use the internal service token. + # 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: "" diff --git a/helm/test_helm_seatunnel.py b/helm/test_helm_seatunnel.py index 4ea7c2bfc1..e725392a3c 100644 --- a/helm/test_helm_seatunnel.py +++ b/helm/test_helm_seatunnel.py @@ -333,30 +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.assertRegex(password, r"^[A-Za-z0-9]{16}$") - 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"])) + 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'\"\\$" diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 5cb44329e7..9a36e7fa4c 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -296,13 +296,15 @@ def test_generated_factor_and_pod_checksum_use_the_same_random_value(self): self.assertEqual(checksum, pod(master)["metadata"]["annotations"]["checksum/vermeer-auth"]) def test_native_admin_password_is_short_separate_and_mounted_only_by_master(self): - for mode in ("standalone", "cluster"): - with self.subTest(mode=mode): - documents = self.documents({"mode": mode, "auth": {"admin": {"password": ""}}}) + 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, r"^[A-Za-z0-9]{16}$") + 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") From 17ff5c5b05093ba3ff7d7c0881e9c96a136ad42f Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 08:01:35 +0000 Subject: [PATCH 38/39] fix(helm): pin reviewed Vermeer runtime image --- helm/hugegraph/docs/distribution.md | 2 +- helm/hugegraph/docs/vermeer.md | 6 +++--- helm/hugegraph/examples/README.md | 2 +- helm/hugegraph/values.yaml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md index 76966b0231..7e1ebf5b24 100644 --- a/helm/hugegraph/docs/distribution.md +++ b/helm/hugegraph/docs/distribution.md @@ -143,7 +143,7 @@ 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-2678ff91b56a`, pinned by a Linux amd64/arm64 OCI index +`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 diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 4b39608bf6..0e53769ac7 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -229,11 +229,11 @@ its intended internal API integration. ### Image compatibility -The Chart pins the officially published `hugegraph/vermeer:helm-2678ff91b56a` +The Chart pins the officially published `hugegraph/vermeer:helm-30b67d93e7d0` image to OCI index digest -`sha256:9ef623d93e4feddc345c6ce445109c4039b029be83090d0eca97aedeba981a9e`. +`sha256:f9b69ef134069b432de1c9964374d7fdbfa53245d1fe7dcbce1e75b82b69dc9e`. The index provides Linux `amd64` and `arm64` images built from -[source commit 2678ff91b56a](https://github.com/hugegraph/hugegraph-computer/commit/2678ff91b56ae9501fbcdb0e1cf937144e508241). +[source commit 30b67d93e7d0](https://github.com/hugegraph/hugegraph-computer/commit/30b67d93e7d097374341cf46ee6a0703421512b1). No manual image override is needed for ordinary enablement. Publishing uses the diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md index c6b8999e30..85064c14ef 100644 --- a/helm/hugegraph/examples/README.md +++ b/helm/hugegraph/examples/README.md @@ -90,7 +90,7 @@ 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-2678ff91b56a` at the Chart's pinned Linux amd64/arm64 +`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 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 10749d6c41..5a27db8e5a 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -608,8 +608,8 @@ vermeer: 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-2678ff91b56a - digest: sha256:9ef623d93e4feddc345c6ce445109c4039b029be83090d0eca97aedeba981a9e + tag: helm-30b67d93e7d0 + digest: sha256:f9b69ef134069b432de1c9964374d7fdbfa53245d1fe7dcbce1e75b82b69dc9e pullPolicy: IfNotPresent auth: # Token signing factor. existingSecret > value > a stable generated Secret. From e0f6b071c31e645127226ee5c1217a4fb4196732 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 28 Oct 2026 08:04:58 +0000 Subject: [PATCH 39/39] fix(helm): validate native Vermeer signing key length --- helm/hugegraph/docs/vermeer.md | 2 ++ helm/hugegraph/values.schema.json | 2 +- helm/hugegraph/values.yaml | 1 + helm/test_helm_vermeer.py | 11 +++++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/helm/hugegraph/docs/vermeer.md b/helm/hugegraph/docs/vermeer.md index 0e53769ac7..541e55e790 100644 --- a/helm/hugegraph/docs/vermeer.md +++ b/helm/hugegraph/docs/vermeer.md @@ -160,6 +160,8 @@ Hubble service authentication uses a separate signing factor in the kept 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 diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index b74f24d1c8..d1a134cb05 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -1719,7 +1719,7 @@ "properties": { "value": { "type": "string", - "pattern": "^$|^[!-~]{4,}$" + "pattern": "^$|^[!-~]{16,}$" }, "existingSecret": { "type": "string" diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 5a27db8e5a..3e6070a5d4 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -613,6 +613,7 @@ vermeer: 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 diff --git a/helm/test_helm_vermeer.py b/helm/test_helm_vermeer.py index 9a36e7fa4c..7f350a0b01 100644 --- a/helm/test_helm_vermeer.py +++ b/helm/test_helm_vermeer.py @@ -608,6 +608,17 @@ def test_seatunnel_can_be_enabled_independently_without_changing_engine_identity 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}},