From 93147aeab4fbf3c160823f40557b550814458b05 Mon Sep 17 00:00:00 2001 From: imbajin Date: Mon, 12 Oct 2026 20:41:27 +0800 Subject: [PATCH 1/5] fix(docker): expose Server discovery identity - map service identity and graphspace environment inputs - expose Helm settings with existing default behavior - validate effective identity inputs before writing config - document naming constraints and paired website guidance --- .github/workflows/helm-chart-ci.yml | 11 ++++ docker/README.md | 29 ++++++++++ helm/hugegraph/README.md | 29 ++++++++++ helm/hugegraph/templates/_helpers.tpl | 2 +- .../templates/server-deployment.yaml | 8 +++ .../tests/server_discovery_test.yaml | 46 +++++++++++++++ helm/hugegraph/values.schema.json | 12 ++++ helm/hugegraph/values.yaml | 5 ++ .../docker/docker-entrypoint-test.sh | 57 +++++++++++++++++++ .../docker/docker-entrypoint.sh | 20 +++++++ 10 files changed, 218 insertions(+), 1 deletion(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 5ecfaa2074..1ef94bb614 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -152,6 +152,17 @@ jobs: helm template ci helm/hugegraph \ --set "${component}.probes.readiness.successThreshold=2" > /dev/null done + # These deployment inputs must serialize unchanged through the properties reader. + must_fail --set-string server.serviceId=café + must_fail --set-string server.serviceId=😀 + must_fail --set-string "$(printf 'server.serviceId=ops\tteam')" + # Match the Server API graph-space names, including its 48-character limit. + must_fail --set-string server.graphSpace=TeamA + must_fail --set-string server.graphSpace=team-a + must_fail --set-string server.graphSpace=1team + graph_space_max=$(printf 'a%.0s' {1..48}) + helm template ci helm/hugegraph --set-string "server.graphSpace=${graph_space_max}" > /dev/null + must_fail --set-string "server.graphSpace=${graph_space_max}a" must_fail --set pd.replicas=100 must_fail --set pd.pdb.minAvailable=3 must_fail --set server.hpa.enabled=true diff --git a/docker/README.md b/docker/README.md index 36473ff8c3..f4e310e5de 100644 --- a/docker/README.md +++ b/docker/README.md @@ -232,6 +232,35 @@ Standalone stores RocksDB data at `/hugegraph-server/rocksdb-data`. The HStore t `docker compose down` keeps named-volume data. `docker compose down -v` intentionally deletes it. +### Server service identity + +Images built from current master accept `HG_SERVER_SERVICE_ID` and +`HG_SERVER_GRAPHSPACE`, which write `server.service_id` and `server.graphspace` +in `conf/rest-server.properties`. A property keeps its existing value when both +its `HG_SERVER_*` variable and its older name are unset or empty; the shipped +defaults are both `DEFAULT`. +The older `SERVICE_ID` and `GRAPH_SPACE` variables emitted by the Kubernetes driver +are also accepted. A non-empty `HG_SERVER_*` value takes precedence over its older name. +These container identity inputs accept printable ASCII (bytes 0x20–0x7E); +spaces and backslashes in service IDs round-trip unchanged. Non-ASCII and control +characters are rejected before the entrypoint writes configuration, because the +Server reads properties using ISO-8859-1. This is the container input's +serialization limit; it does not change the core service-name contract. + +For a PD-backed Server, add the variables to the Server container's environment, +for example `HG_SERVER_SERVICE_ID=analytics` and `HG_SERVER_GRAPHSPACE=team_a`. +Create `team_a` in the existing cluster before starting the assigned Server; +these settings do not create graph spaces. The Compose files do not pass these +variables through from the host, so add explicit `environment:` entries to a +local Compose override. The Helm chart exposes the same settings as +[`server.serviceId` and `server.graphSpace`](../helm/hugegraph/README.md#server-service-identity). + +This selects the Server's PD service metadata. It does not change +`server.path_graphspace` for REST paths that omit `/graphspaces/`, or +configure Hubble's chosen service. Actual non-default service discovery, +authentication and graph-space isolation still require verification with a +running Linux cluster. + ### Server logs and crash files The Server container owns its foreground launcher and returns the JVM's exit status after startup, diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index ad7331f494..c8a94b40f6 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -567,6 +567,8 @@ default values. | `server.port` | Server REST port, container port, and Service port | `8080` | | `server.readinessPath` | Path the Server readinessProbe hits. Set `/readiness` once the Server image serves it (apache/hugegraph#3212); it answers 503 while the Server cannot serve graph traffic. Startup and liveness stay on `/versions` | `/versions` | | `server.backend` | Storage backend | `hstore` | +| `server.serviceId` | Non-default PD service name; `DEFAULT` preserves the image config | `DEFAULT` | +| `server.graphSpace` | Existing non-default graph space for the PD service; `DEFAULT` preserves the image config | `DEFAULT` | | `server.resources` | Server resources. `requests.cpu` is required when HPA is enabled | `{}` | | `server.podSecurityContext` | Pod-level securityContext, rendered only when set | `{}` | | `server.securityContext` | Container-level securityContext. Hardened by default; `runAsNonRoot` is not set because the published images run as root | `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` | @@ -622,6 +624,33 @@ utilization-based HPA requires a strictly positive `server.resources.requests.cpu`. +### Server service identity + +`server.serviceId` and `server.graphSpace` select the service metadata that +Servers use with PD. Both default to `DEFAULT`, which leaves the image's +`server.service_id` and `server.graphspace` untouched. A non-default value +injects `HG_SERVER_SERVICE_ID` or `HG_SERVER_GRAPHSPACE` for the image entrypoint. +`server.serviceId` accepts printable ASCII (bytes 0x20–0x7E), including spaces +and backslashes. The chart and entrypoint reject non-ASCII and control characters +to prevent the Server's ISO-8859-1 properties reader from changing the value; +this is a deployment input serialization limit, not a change to core service names: + +```bash +helm upgrade --install hg ./helm/hugegraph \ + --set-string server.serviceId=analytics \ + --set-string server.graphSpace=team_a +``` + +Use images built with these entrypoint mappings. Create the non-default graph +space in the existing cluster before deploying Servers assigned to it; the chart +does not create it. A graph-space name is `DEFAULT` or up to 48 lowercase letters, +digits and underscores, starting with a letter, as required by the Server API. All replicas in this Server Deployment use the same identity. +Changing the graph space changes the Server's assigned space; it does not move +existing graphs or change `server.path_graphspace`. Use explicit +`/graphspaces/team_a/...` REST paths, and select the matching space and service in +Hubble. Rendering and entrypoint checks verify configuration only; non-default +PD discovery, authentication and isolation require a running Linux cluster. + ### Reaching Hubble (pick one path) Most people should stop at **1**. Use **2** only if Hubble must run outside diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index edf747252d..d9dbcedd25 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -851,7 +851,7 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- $reservedEnv := dict "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "HG_PD_AUTH_SECRET_KEY" "JAVA_OPTS" "JAVA_OPTIONS") "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") - "server" (list "POD_IP" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_STARTUP_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") + "server" (list "POD_IP" "HG_SERVER_SERVICE_ID" "HG_SERVER_GRAPHSPACE" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_STARTUP_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_PD_PASSWORD" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} {{- range $component, $reserved := $reservedEnv -}} {{- $componentValues := get $.Values $component | default dict -}} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml index bfbdf16e6a..46961a1a19 100644 --- a/helm/hugegraph/templates/server-deployment.yaml +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -258,6 +258,14 @@ spec: fieldPath: status.podIP - name: HG_SERVER_BACKEND value: {{ .Values.server.backend | quote }} + {{- if ne (.Values.server.serviceId | default "DEFAULT") "DEFAULT" }} + - name: HG_SERVER_SERVICE_ID + value: {{ .Values.server.serviceId | quote }} + {{- end }} + {{- if ne (.Values.server.graphSpace | default "DEFAULT") "DEFAULT" }} + - name: HG_SERVER_GRAPHSPACE + value: {{ .Values.server.graphSpace | quote }} + {{- end }} - name: HG_SERVER_PD_PEERS value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} - name: HG_SERVER_PD_REST_ENDPOINT diff --git a/helm/hugegraph/tests/server_discovery_test.yaml b/helm/hugegraph/tests/server_discovery_test.yaml index 63aca4db3d..e06bc416b4 100644 --- a/helm/hugegraph/tests/server_discovery_test.yaml +++ b/helm/hugegraph/tests/server_discovery_test.yaml @@ -47,3 +47,49 @@ tests: asserts: - failedTemplate: errorPattern: "POD_IP" + + - it: preserves the image service identity by default + asserts: + - notExists: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")] + - notExists: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_GRAPHSPACE")] + + - it: selects a non-default service identity + set: + server.serviceId: analytics + server.graphSpace: team_a + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: analytics + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_GRAPHSPACE")].value + value: team_a + + - it: refuses an extraEnv that shadows the service ID + set: + server.extraEnv: + - name: HG_SERVER_SERVICE_ID + value: shadow + asserts: + - failedTemplate: + errorPattern: "HG_SERVER_SERVICE_ID" + + - it: refuses an extraEnv that shadows the graph space + set: + server.extraEnv: + - name: HG_SERVER_GRAPHSPACE + value: shadow + asserts: + - failedTemplate: + errorPattern: "HG_SERVER_GRAPHSPACE" + + - it: retains a service name containing spaces + set: + server.serviceId: ops team + server.graphSpace: team_a + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: ops team diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index afce158aa8..6770e10db2 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -762,6 +762,18 @@ "probes" ], "properties": { + "serviceId": { + "type": "string", + "minLength": 1, + "pattern": "^[ -~]+$", + "description": "Printable ASCII PD service name (bytes 0x20-0x7E), including spaces and backslashes. DEFAULT preserves the image config." + }, + "graphSpace": { + "type": "string", + "minLength": 1, + "pattern": "^(DEFAULT|[a-z][a-z0-9_]{0,47})$", + "description": "Existing PD graph space: DEFAULT or up to 48 lowercase letters, digits and underscores, starting with a letter. DEFAULT preserves the image config." + }, "replicas": { "type": "integer", "minimum": 1 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index 6205e17396..b99ef12d8d 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -274,6 +274,11 @@ store: server: replicas: 3 + # DEFAULT preserves the image config; non-default values select PD service metadata. + # serviceId accepts printable ASCII, including spaces and backslashes. + # Create a non-default graph space before starting a Server assigned to it. + serviceId: DEFAULT + graphSpace: DEFAULT image: repository: hugegraph/server # TODO: leave tag empty to inherit Chart.appVersion after the 1.8.0 diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh index e6823e897e..c69386bf73 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh @@ -329,5 +329,62 @@ done ) assert_start_timeout 120 +# Identity mappings apply on every start and leave mounted config alone when unset. +identity_conf="${TEST_HOME}/conf/rest-server.properties" +printf '\nserver.service_id=mounted-service\nserver.graphspace=mounted_space\n' >> "${identity_conf}" +(cd "${TEST_HOME}"; bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=mounted-service' "${identity_conf}" +grep -qx 'server.graphspace=mounted_space' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=DEFAULT HG_SERVER_GRAPHSPACE=DEFAULT \ + bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=DEFAULT' "${identity_conf}" +grep -qx 'server.graphspace=DEFAULT' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=service-a HG_SERVER_GRAPHSPACE=space_a bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-a' "${identity_conf}" +grep -qx 'server.graphspace=space_a' "${identity_conf}" +(cd "${TEST_HOME}"; SERVICE_ID=legacy-service GRAPH_SPACE=legacy_space bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=legacy-service' "${identity_conf}" +grep -qx 'server.graphspace=legacy_space' "${identity_conf}" +(cd "${TEST_HOME}"; SERVICE_ID=legacy-service GRAPH_SPACE=legacy_space \ + HG_SERVER_SERVICE_ID=service-b HG_SERVER_GRAPHSPACE=space_b bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-b' "${identity_conf}" +grep -qx 'server.graphspace=space_b' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID= HG_SERVER_GRAPHSPACE= bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-b' "${identity_conf}" +grep -qx 'server.graphspace=space_b' "${identity_conf}" + +# Empty new variables still allow the legacy driver values to apply. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID= HG_SERVER_GRAPHSPACE= \ + SERVICE_ID=legacy-empty GRAPH_SPACE=legacy_empty bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=legacy-empty' "${identity_conf}" +grep -qx 'server.graphspace=legacy_empty' "${identity_conf}" + +# Service permits spaces; Unicode escapes round-trip through the Server parser. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops team' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=ops\u0020team' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=' ops team ' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=\u0020ops\u0020team\u0020' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops\ team' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=ops\\\u0020team' "${identity_conf}" + +# Unsupported effective identity input must fail before changing mounted config. +cp "${identity_conf}" "${identity_conf}.before-invalid-identity" +for identity_name in HG_SERVER_SERVICE_ID HG_SERVER_GRAPHSPACE SERVICE_ID GRAPH_SPACE; do + for identity_value in 'café' '😀' $'ops\tteam' $'ops\nteam'; do + if (cd "${TEST_HOME}"; env "${identity_name}=${identity_value}" \ + bash ./docker-entrypoint.sh > ./docker/invalid-identity.log 2>&1); then + echo "unsupported ${identity_name} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'must be printable ASCII' "${TEST_HOME}/docker/invalid-identity.log" + cmp "${identity_conf}.before-invalid-identity" "${identity_conf}" + done +done +# Ignored legacy input does not override a supported non-empty new value. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=analytics SERVICE_ID='café' \ + HG_SERVER_GRAPHSPACE=team_a GRAPH_SPACE='😀' bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=analytics' "${identity_conf}" +grep -qx 'server.graphspace=team_a' "${identity_conf}" + echo "PASS: Docker entrypoint configures HStore discovery and authentication" bash "${SCRIPT_DIR}/test/test-docker-entrypoint-process.sh" diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh index 0e3cc4eab2..5ea626fa4d 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh @@ -96,6 +96,18 @@ migrate_env() { migrate_env "BACKEND" "HG_SERVER_BACKEND" migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS" +migrate_env "SERVICE_ID" "HG_SERVER_SERVICE_ID" +migrate_env "GRAPH_SPACE" "HG_SERVER_GRAPHSPACE" + +# The Server reads these properties as ISO-8859-1. Reject unsupported input +# before writing UTF-8 bytes that would select a different service identity. +for identity_name in HG_SERVER_SERVICE_ID HG_SERVER_GRAPHSPACE; do + if [[ -n "${!identity_name:-}" ]] && + (( $(printf '%s' "${!identity_name}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then + log "ERROR: ${identity_name} must be printable ASCII (bytes 0x20-0x7E)" + exit 1 + fi +done if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then LC_ALL=C @@ -166,6 +178,14 @@ fi set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_CLUSTER:-}" ]] && \ set_prop "cluster" "${HG_SERVER_CLUSTER}" "${REST_SERVER_CONF}" +if [[ -n "${HG_SERVER_SERVICE_ID:-}" ]]; then + # Service names can contain spaces. Commons Configuration keeps a backslash + # before a literal space, but decodes Unicode escapes, including at the edges. + service_id_encoded=$(encode_prop_value "${HG_SERVER_SERVICE_ID}" | sed 's/\\ /\\u0020/g') + set_prop_encoded "server.service_id" "${service_id_encoded}" "${REST_SERVER_CONF}" +fi +[[ -n "${HG_SERVER_GRAPHSPACE:-}" ]] && \ + set_prop "server.graphspace" "${HG_SERVER_GRAPHSPACE}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_REST_URL:-}" ]] && set_prop "restserver.url" \ "${HG_SERVER_REST_URL}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_MIN_FREE_MEMORY:-}" ]] && set_prop "restserver.min_free_memory" \ From 83bb23daec32784f5eea87b9bb87689bb45ee239 Mon Sep 17 00:00:00 2001 From: imbajin Date: Mon, 12 Oct 2026 21:12:48 +0800 Subject: [PATCH 2/5] fix(docker): validate graphspace inputs - check effective graphspace names before writing config - stabilize name validation with a scoped C locale - cover new and legacy names plus locale boundaries --- docker/README.md | 4 ++ .../docker/docker-entrypoint-test.sh | 40 +++++++++++++++++++ .../docker/docker-entrypoint.sh | 10 +++++ 3 files changed, 54 insertions(+) diff --git a/docker/README.md b/docker/README.md index f4e310e5de..84c328703f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -246,6 +246,10 @@ spaces and backslashes in service IDs round-trip unchanged. Non-ASCII and contro characters are rejected before the entrypoint writes configuration, because the Server reads properties using ISO-8859-1. This is the container input's serialization limit; it does not change the core service-name contract. +`HG_SERVER_GRAPHSPACE` and a migrated `GRAPH_SPACE` must also match the Server +API graph-space rule: `DEFAULT`, or up to 48 lowercase letters, digits and +underscores, starting with a letter. Invalid environment values are rejected +before writing configuration; existing mounted properties are preserved. For a PD-backed Server, add the variables to the Server container's environment, for example `HG_SERVER_SERVICE_ID=analytics` and `HG_SERVER_GRAPHSPACE=team_a`. diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh index c69386bf73..3561f017a3 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh @@ -386,5 +386,45 @@ done grep -qx 'server.service_id=analytics' "${identity_conf}" grep -qx 'server.graphspace=team_a' "${identity_conf}" +# Apply the existing graph-space naming rule to effective new and legacy input. +graph_space_max=$(printf 'a%.0s' {1..48}) +cp "${identity_conf}" "${identity_conf}.before-invalid-graphspace" +for graphspace_env in HG_SERVER_GRAPHSPACE GRAPH_SPACE; do + for graphspace in TeamA 1team team-a "${graph_space_max}a"; do + if (cd "${TEST_HOME}"; env "${graphspace_env}=${graphspace}" \ + bash ./docker-entrypoint.sh > ./docker/invalid-graphspace.log 2>&1); then + echo "invalid ${graphspace_env} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'HG_SERVER_GRAPHSPACE must be DEFAULT' "${TEST_HOME}/docker/invalid-graphspace.log" + cmp "${identity_conf}.before-invalid-graphspace" "${identity_conf}" + done + for graphspace in DEFAULT team_a "${graph_space_max}"; do + (cd "${TEST_HOME}"; env "${graphspace_env}=${graphspace}" bash ./docker-entrypoint.sh) + grep -Fxq "server.graphspace=${graphspace}" "${identity_conf}" + done + cp "${identity_conf}" "${identity_conf}.before-invalid-graphspace" +done +(cd "${TEST_HOME}"; HG_SERVER_GRAPHSPACE=team_a GRAPH_SPACE=TeamA bash ./docker-entrypoint.sh) +grep -qx 'server.graphspace=team_a' "${identity_conf}" + +# Some installed locales make [a-z] include uppercase letters; validation owns its locale. +for identity_locale in sk_SK.ISO8859-2 cs_CZ.ISO8859-2; do + case "$(LC_ALL="${identity_locale}" locale charmap 2>/dev/null)" in + ISO8859-2 | ISO-8859-2) ;; + *) continue ;; + esac + cp "${identity_conf}" "${identity_conf}.before-invalid-locale" + for graphspace_env in HG_SERVER_GRAPHSPACE GRAPH_SPACE; do + if (cd "${TEST_HOME}"; env LC_ALL="${identity_locale}" "${graphspace_env}=TeamA" \ + bash ./docker-entrypoint.sh > ./docker/invalid-graphspace-locale.log 2>&1); then + echo "invalid ${graphspace_env} under ${identity_locale} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'HG_SERVER_GRAPHSPACE must be DEFAULT' "${TEST_HOME}/docker/invalid-graphspace-locale.log" + cmp "${identity_conf}.before-invalid-locale" "${identity_conf}" + done +done + echo "PASS: Docker entrypoint configures HStore discovery and authentication" bash "${SCRIPT_DIR}/test/test-docker-entrypoint-process.sh" diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh index 5ea626fa4d..1922e823a6 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh @@ -109,6 +109,16 @@ for identity_name in HG_SERVER_SERVICE_ID HG_SERVER_GRAPHSPACE; do fi done +# Match the Server API graph-space naming contract after legacy migration. +if [[ -n "${HG_SERVER_GRAPHSPACE:-}" ]] && ! ( + export LC_ALL=C + [[ "${HG_SERVER_GRAPHSPACE}" =~ ^(DEFAULT|[a-z][a-z0-9_]{0,47})$ ]] +); then + log "ERROR: HG_SERVER_GRAPHSPACE must be DEFAULT or a name starting with" \ + "a lowercase letter, followed by lowercase letters, digits or underscores (1-48 characters)" + exit 1 +fi + if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then LC_ALL=C if (( ${#HG_SERVER_AUTH_TOKEN_SECRET} < 32 )); then From 91b00d294a2dd232166a00b0d7ccfd820f91ec73 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 13 Oct 2026 02:20:42 +0800 Subject: [PATCH 3/5] fix(server): preserve literal Helm service IDs - reserve legacy identity environment variables - escape dollars before Kubernetes environment expansion - verify current HugeConfig readback in Docker CI --- .github/workflows/docker-build-ci.yml | 13 ++++++++ helm/hugegraph/README.md | 7 +++-- helm/hugegraph/templates/_helpers.tpl | 2 +- .../templates/server-deployment.yaml | 2 +- .../tests/server_discovery_test.yaml | 26 ++++++++++++++++ .../docker/docker-entrypoint-test.sh | 27 +++++++++++++++- .../docker/test/ConfigReadback.java | 31 +++++++++++++++++++ 7 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java diff --git a/.github/workflows/docker-build-ci.yml b/.github/workflows/docker-build-ci.yml index 915557415b..55a9045141 100644 --- a/.github/workflows/docker-build-ci.yml +++ b/.github/workflows/docker-build-ci.yml @@ -93,9 +93,22 @@ jobs: timeout-minutes: 25 steps: - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'zulu' + cache: maven + cache-jdk: false + - name: Build the pinned configuration reader + run: | + mvn -B -ntp -pl hugegraph-commons/hugegraph-common -am clean compile dependency:build-classpath \ + -Dmdep.outputFile=target/config-readback-classpath.txt -Dmaven.javadoc.skip=true -DskipTests - name: Verify deployment shell contracts run: | bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh + HG_CONFIG_TEST_CLASSPATH="$PWD/hugegraph-commons/hugegraph-common/target/classes" + HG_CONFIG_TEST_CLASSPATH+=":$(cat hugegraph-commons/hugegraph-common/target/config-readback-classpath.txt)" + export HG_CONFIG_TEST_CLASSPATH bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh bash hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh \ hugegraph-server/hugegraph-dist/src/assembly/static \ diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index c8a94b40f6..79bb8ff9a4 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -631,7 +631,9 @@ Servers use with PD. Both default to `DEFAULT`, which leaves the image's `server.service_id` and `server.graphspace` untouched. A non-default value injects `HG_SERVER_SERVICE_ID` or `HG_SERVER_GRAPHSPACE` for the image entrypoint. `server.serviceId` accepts printable ASCII (bytes 0x20–0x7E), including spaces -and backslashes. The chart and entrypoint reject non-ASCII and control characters +and backslashes. Dollar signs are escaped in the manifest so Kubernetes passes +the service ID literally, including `$(POD_IP)` and `$$`. The chart and entrypoint +reject non-ASCII and control characters to prevent the Server's ISO-8859-1 properties reader from changing the value; this is a deployment input serialization limit, not a change to core service names: @@ -794,7 +796,8 @@ before anything reaches the cluster: the whole membership); disable the PD PDB or use an odd replica count. - `extraEnv` must not set chart-managed variable names (for example `HG_SERVER_INIT_STORE_ENABLED` or the PD/Store identity and topology - variables): entries render after the chart-owned variables and the last + variables, including the legacy Server names `SERVICE_ID` and `GRAPH_SPACE`): + entries render after the chart-owned variables and the last duplicate wins, so an override would silently bypass a validated contract. `JAVA_OPTS` and `JAVA_OPTIONS` are reserved for the same reason: the component start scripts skip automatic heap sizing and drop the chart's diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl index d9dbcedd25..e21b85aec0 100644 --- a/helm/hugegraph/templates/_helpers.tpl +++ b/helm/hugegraph/templates/_helpers.tpl @@ -851,7 +851,7 @@ start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). {{- $reservedEnv := dict "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "HG_PD_AUTH_SECRET_KEY" "JAVA_OPTS" "JAVA_OPTIONS") "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") - "server" (list "POD_IP" "HG_SERVER_SERVICE_ID" "HG_SERVER_GRAPHSPACE" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_STARTUP_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") + "server" (list "POD_IP" "HG_SERVER_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_STARTUP_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_PD_PASSWORD" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} {{- range $component, $reserved := $reservedEnv -}} {{- $componentValues := get $.Values $component | default dict -}} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml index 46961a1a19..268ffe6270 100644 --- a/helm/hugegraph/templates/server-deployment.yaml +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -260,7 +260,7 @@ spec: value: {{ .Values.server.backend | quote }} {{- if ne (.Values.server.serviceId | default "DEFAULT") "DEFAULT" }} - name: HG_SERVER_SERVICE_ID - value: {{ .Values.server.serviceId | quote }} + value: {{ .Values.server.serviceId | replace "$" "$$" | quote }} {{- end }} {{- if ne (.Values.server.graphSpace | default "DEFAULT") "DEFAULT" }} - name: HG_SERVER_GRAPHSPACE diff --git a/helm/hugegraph/tests/server_discovery_test.yaml b/helm/hugegraph/tests/server_discovery_test.yaml index e06bc416b4..92d5adfaaa 100644 --- a/helm/hugegraph/tests/server_discovery_test.yaml +++ b/helm/hugegraph/tests/server_discovery_test.yaml @@ -93,3 +93,29 @@ tests: - equal: path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value value: ops team + + - it: refuses an extraEnv that shadows the legacy service ID + set: + server.extraEnv: + - name: SERVICE_ID + value: shadow + asserts: + - failedTemplate: + errorPattern: "SERVICE_ID" + + - it: refuses an extraEnv that shadows the legacy graph space + set: + server.extraEnv: + - name: GRAPH_SPACE + value: shadow + asserts: + - failedTemplate: + errorPattern: "GRAPH_SPACE" + + - it: escapes all dollars so Kubernetes retains a literal service ID + set: + server.serviceId: 'x-$(POD_IP)-$$(POD_IP)-$(UNKNOWN)-$name-$' + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: 'x-$$(POD_IP)-$$$$(POD_IP)-$$(UNKNOWN)-$$name-$$' diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh index 3561f017a3..d87a09a8df 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh @@ -67,6 +67,18 @@ exit 0 EOF chmod +x "${TEST_HOME}/bin/"*.sh +# CI supplies the classpath built from this checkout and its pinned Commons dependencies. +if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then + javac -cp "${HG_CONFIG_TEST_CLASSPATH}" -d "${TEST_HOME}/docker" \ + "${SCRIPT_DIR}/test/ConfigReadback.java" +fi +assert_service_readback() { + if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then + java -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" ConfigReadback \ + "${TEST_HOME}/conf/rest-server.properties" "$1" + fi +} + ( cd "${TEST_HOME}" HG_SERVER_BACKEND=hstore \ @@ -331,7 +343,10 @@ assert_start_timeout 120 # Identity mappings apply on every start and leave mounted config alone when unset. identity_conf="${TEST_HOME}/conf/rest-server.properties" -printf '\nserver.service_id=mounted-service\nserver.graphspace=mounted_space\n' >> "${identity_conf}" +# Earlier secret-escaping fixtures leave escaped trailing whitespace in this file. +# Start identity cases with their own parseable configuration. +printf 'restserver.url=http://127.0.0.1:8080\nserver.service_id=mounted-service\nserver.graphspace=mounted_space\n' \ + > "${identity_conf}" (cd "${TEST_HOME}"; bash ./docker-entrypoint.sh) grep -qx 'server.service_id=mounted-service' "${identity_conf}" grep -qx 'server.graphspace=mounted_space' "${identity_conf}" @@ -362,10 +377,20 @@ grep -qx 'server.graphspace=legacy_empty' "${identity_conf}" # Service permits spaces; Unicode escapes round-trip through the Server parser. (cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops team' bash ./docker-entrypoint.sh) grep -Fxq 'server.service_id=ops\u0020team' "${identity_conf}" +assert_service_readback 'ops team' (cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=' ops team ' bash ./docker-entrypoint.sh) grep -Fxq 'server.service_id=\u0020ops\u0020team\u0020' "${identity_conf}" +assert_service_readback ' ops team ' (cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops\ team' bash ./docker-entrypoint.sh) grep -Fxq 'server.service_id=ops\\\u0020team' "${identity_conf}" +assert_service_readback 'ops\ team' + +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=' ops\ team ' bash ./docker-entrypoint.sh) +assert_service_readback ' ops\ team ' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops\u0020team' bash ./docker-entrypoint.sh) +assert_service_readback 'ops\u0020team' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='x-$(POD_IP)-$$(POD_IP)-$name-$' bash ./docker-entrypoint.sh) +assert_service_readback 'x-$(POD_IP)-$$(POD_IP)-$name-$' # Unsupported effective identity input must fail before changing mounted config. cp "${identity_conf}" "${identity_conf}.before-invalid-identity" diff --git a/hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java b/hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java new file mode 100644 index 0000000000..6c6b016bfc --- /dev/null +++ b/hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import org.apache.hugegraph.config.HugeConfig; + +public final class ConfigReadback { + + public static void main(String[] args) { + HugeConfig config = new HugeConfig(args[0]); + Object actual = config.getProperty("server.service_id"); + if (!args[1].equals(actual)) { + throw new AssertionError("Expected service ID [" + args[1] + "] but loaded [" + actual + "]"); + } + System.out.println("PASS: HugeConfig service ID readback [" + actual + "]"); + } +} From dd10fda3c801b94fc1a0380d509e4898bc31c374 Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 13 Oct 2026 15:23:56 +0800 Subject: [PATCH 4/5] fix(helm): select the graph checked by test - expose an explicit graph alias with existing defaults - retain authenticated per-Pod checks and retry limits - validate alias input and cover missing graph failures - explain target graph creation and binding requirements --- .github/workflows/helm-chart-ci.yml | 39 +++++++++++++++++-- helm/hugegraph/README.md | 22 +++++++++-- .../templates/tests/test-connection.yaml | 2 +- .../tests/test_hook_gremlin_test.yaml | 22 +++++++++++ helm/hugegraph/values.schema.json | 5 +++ helm/hugegraph/values.yaml | 3 ++ 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml index 1ef94bb614..ac66c5d19c 100644 --- a/.github/workflows/helm-chart-ci.yml +++ b/.github/workflows/helm-chart-ci.yml @@ -163,6 +163,16 @@ jobs: graph_space_max=$(printf 'a%.0s' {1..48}) helm template ci helm/hugegraph --set-string "server.graphSpace=${graph_space_max}" > /dev/null must_fail --set-string "server.graphSpace=${graph_space_max}a" + # The hook queries an explicit, fully qualified existing graph binding. + must_fail --set-string server.testGraphAlias= + must_fail --set-string server.testGraphAlias=team_a + must_fail --set-string server.testGraphAlias=team_a-demo-name + must_fail --set-string "server.testGraphAlias=team_a-demo'" + graph_name_max=$(printf 'g%.0s' {1..48}) + helm template ci helm/hugegraph \ + --set-string "server.testGraphAlias=${graph_space_max}-${graph_name_max}" > /dev/null + must_fail --set-string "server.testGraphAlias=${graph_space_max}a-demo" + must_fail --set-string "server.testGraphAlias=team_a-${graph_name_max}g" must_fail --set pd.replicas=100 must_fail --set pd.pdb.minAvailable=3 must_fail --set server.hpa.enabled=true @@ -345,7 +355,7 @@ jobs: printf '#!/bin/sh\nfor ip in ${STUB_IPS}; do printf "%%s STREAM host\\n" "$ip"; done\n' > /tmp/stub/getent chmod +x /tmp/stub/getent cat > /tmp/fixture.py <<'PY' - import http.server, os, sys, threading, time + import http.server, json, os, sys, threading, time calls = {} class H(http.server.BaseHTTPRequestHandler): def _send(self, code, body): @@ -357,7 +367,9 @@ jobs: def do_GET(self): self._send(200, b'{"versions":{}}') def do_POST(self): - self.rfile.read(int(self.headers.get('Content-Length', 0))) + body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0)))) + expected_alias = ('team_a-demo' if os.path.exists('/tmp/selected-graph') + else 'DEFAULT-hugegraph') ip = self.server.server_address[0] # broken-: every Gremlin call fails. flip-odd- / # flip-even-: the odd or even calls fail, counted from @@ -367,6 +379,7 @@ jobs: even = os.path.exists('/tmp/flip-even-' + ip) n = calls[ip] = (calls.get(ip, 0) + 1) if (odd or even) else 0 broken = (os.path.exists('/tmp/broken-' + ip) + or body.get('aliases', {}).get('graph') != expected_alias or (odd and n % 2 == 1) or (even and n % 2 == 0)) if broken: @@ -384,7 +397,7 @@ jobs: sleep 1 run_hook() { PATH="/tmp/stub:${PATH}" PASSWORD=ci-password HG_TEST_DEADLINE_S=8 \ - STUB_IPS="$1" sh /tmp/hook.sh + STUB_IPS="$1" sh "${2:-/tmp/hook.sh}" } # Healthy: three Pods, all Gremlin-bound. run_hook "127.0.0.1 127.0.0.2 127.0.0.3" @@ -419,6 +432,26 @@ jobs: exit 1 fi run_hook "127.0.0.1 127.0.0.2 127.0.0.3 ::1 ::2 ::3" + # A non-default graph space must query the explicitly selected graph. + # A missing alias still fails rather than weakening the per-Pod sweep. + touch /tmp/selected-graph + for alias in team_a-demo team_a-missing; do + helm template ci helm/hugegraph -s templates/tests/test-connection.yaml \ + --set-string server.graphSpace=team_a \ + --set-string "server.testGraphAlias=${alias}" > "/tmp/hook-${alias}.yaml" + done + python3 - <<'PY' + import yaml + for alias in ('team_a-demo', 'team_a-missing'): + d = yaml.safe_load(open('/tmp/hook-' + alias + '.yaml')) + open('/tmp/hook-' + alias + '.sh', 'w').write(d['spec']['containers'][0]['command'][2]) + PY + run_hook "127.0.0.1 127.0.0.2 127.0.0.3" /tmp/hook-team_a-demo.sh + if run_hook "127.0.0.1 127.0.0.2 127.0.0.3" /tmp/hook-team_a-missing.sh; then + echo "expected the hook to fail on a missing selected graph" >&2 + exit 1 + fi + rm /tmp/selected-graph # The auth-off render must carry no credentials. if helm template ci helm/hugegraph -s templates/tests/test-connection.yaml \ --set server.auth.enabled=false | grep -q 'Authorization'; then diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 79bb8ff9a4..43c03b9c0a 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -202,7 +202,10 @@ or recovery. Hubble health probes and the Server hook do not verify UI access. Besides the Service checks, the test resolves a headless Server Service and requires an authenticated, graph-bound Gremlin answer from every Server Pod -(at least the replica floor), because a Server that started during a PD roll +(at least the replica floor). `server.testGraphAlias` selects that binding and +defaults to `DEFAULT-hugegraph`. Set it to an existing target graph's full alias, +such as `team_a-demo`, to check that graph instead. The hook does not create a +graph or infer an alias from `server.graphSpace`. A Server that started during a PD roll can pass readiness and serve REST while every Gremlin call on it fails (see Troubleshooting, "Could not rebind"). Transient failures are retried for 150 seconds before the test fails and names the failing Pod IPs. Large HPA @@ -617,6 +620,7 @@ default values. | `server.probes.startup.failureThreshold` | Raised automatically so the guaranteed budget, `(failureThreshold - 1) * periodSeconds` (the first probe can fail immediately), is at least 450s | `91` | | `server.probes.startup.periodSeconds` | Startup probe interval | `5` | | `server.probes.*` | Same optional probe keys as PD | see `values.yaml` | +| `server.testGraphAlias` | Existing Gremlin graph binding checked on every Server Pod by `helm test` | `DEFAULT-hugegraph` | When `server.hpa.enabled` is `true` the Deployment omits `spec.replicas`, so a Helm upgrade does not overwrite the autoscaler's live replica count. Enabling @@ -653,6 +657,13 @@ existing graphs or change `server.path_graphspace`. Use explicit Hubble. Rendering and entrypoint checks verify configuration only; non-default PD discovery, authentication and isolation require a running Linux cluster. +To validate a graph in the selected space, create the graph there and set +`server.testGraphAlias` to its actual Gremlin binding, for example +`--set-string server.testGraphAlias=team_a-demo`. Creating `team_a` does not +create a `hugegraph` graph. The default hook checks the local `DEFAULT-hugegraph` +binding, which may exist independently of the Server's assigned PD graph space; +its success alone does not validate graphs in `team_a`. + ### Reaching Hubble (pick one path) Most people should stop at **1**. Use **2** only if Hubble must run outside @@ -1092,8 +1103,13 @@ curl -s --compressed -u "admin:${PASSWORD}" -H 'Content-Type: application/json' -d '{"gremlin":"graph.traversal().V().limit(1).count()","aliases":{"graph":"DEFAULT-hugegraph"}}' ``` -A healthy Pod answers with `result.data`; delete a Pod that answers -`Could not rebind`, and its replacement binds normally once PD is stable. +A healthy Pod answers with `result.data`. This example uses the default +`server.testGraphAlias`; replace `DEFAULT-hugegraph` with your configured alias. +If a Pod answers `Could not rebind`, first confirm that the requested graph +exists and the alias matches its binding. For a missing binding caused by the +PD startup failure above, delete the affected Pod once PD is stable so its +replacement can bind the graph. Restarting a Pod cannot create a missing graph +or correct the requested alias. `helm test` runs this same graph-bound query against every Server Pod through the headless Service, so a stuck replica now fails the release check instead of hiding behind the load balancer. The measurements behind diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml index 7d71d323ba..ea17f709e7 100644 --- a/helm/hugegraph/templates/tests/test-connection.yaml +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -74,7 +74,7 @@ spec: PORT="{{ .Values.server.port }}" FLOOR="{{ include "hugegraph.server.replicaFloor" . }}" DEADLINE_S="${HG_TEST_DEADLINE_S:-150}" - BODY='{"gremlin":"graph.traversal().V().limit(1).count()","aliases":{"graph":"DEFAULT-hugegraph"}}' + BODY={{ dict "gremlin" "graph.traversal().V().limit(1).count()" "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | squote }} {{- if .Values.server.auth.enabled }} AUTH_HEADER=$(printf 'admin:%s' "${PASSWORD}" | base64 | tr -d '\r\n') request() { diff --git a/helm/hugegraph/tests/test_hook_gremlin_test.yaml b/helm/hugegraph/tests/test_hook_gremlin_test.yaml index acd41bf25b..09c1df9141 100644 --- a/helm/hugegraph/tests/test_hook_gremlin_test.yaml +++ b/helm/hugegraph/tests/test_hook_gremlin_test.yaml @@ -60,6 +60,28 @@ tests: path: spec.containers[0].command[2] pattern: "FLOOR=\"3\"" + - it: checks the explicitly selected graph in a non-default graph space + template: tests/test-connection.yaml + set: + server.graphSpace: team_a + server.testGraphAlias: team_a-demo + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"team_a-demo"' + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "DEFAULT-hugegraph" + + - it: preserves the default binding when graphSpace changes without a selected graph + template: tests/test-connection.yaml + set: + server.graphSpace: team_a + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"DEFAULT-hugegraph"' + - it: floors the per-Pod count at the HPA minimum when HPA is enabled template: tests/test-connection.yaml set: diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json index 6770e10db2..6eef295e2c 100644 --- a/helm/hugegraph/values.schema.json +++ b/helm/hugegraph/values.schema.json @@ -774,6 +774,11 @@ "pattern": "^(DEFAULT|[a-z][a-z0-9_]{0,47})$", "description": "Existing PD graph space: DEFAULT or up to 48 lowercase letters, digits and underscores, starting with a letter. DEFAULT preserves the image config." }, + "testGraphAlias": { + "type": "string", + "pattern": "^(DEFAULT|[a-z][a-z0-9_]{0,47})-[A-Za-z][A-Za-z0-9_]{0,47}$", + "description": "Existing Gremlin graph binding checked by helm test, as -. Choose the target graph explicitly; server.graphSpace does not create or infer this binding." + }, "replicas": { "type": "integer", "minimum": 1 diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml index b99ef12d8d..a1eb760019 100644 --- a/helm/hugegraph/values.yaml +++ b/helm/hugegraph/values.yaml @@ -336,6 +336,9 @@ server: minAvailable: 2 # Image used by the Helm test hook. waitImage: curlimages/curl:8.5.0 + # Existing Gremlin graph binding checked on every Server Pod. Select the + # target graph explicitly; graphSpace does not create or infer this binding. + testGraphAlias: DEFAULT-hugegraph # Resources for the Helm test hook container. Bounded by default so the hook # cannot run unlimited on a restricted or quota-managed namespace. testResources: From a2906f9bc5bfa4ce6df873994ec6550ae153af2a Mon Sep 17 00:00:00 2001 From: imbajin Date: Tue, 13 Oct 2026 23:36:20 +0800 Subject: [PATCH 5/5] fix(server): retain full service names - preserve complete names when reloading service metadata - keep the service suffix when clearing a graph space - cover metadata write, reload, removal and space clearing - document the case of existing Gremlin graph bindings --- docker/README.md | 2 + helm/hugegraph/README.md | 9 +- .../tests/test_hook_gremlin_test.yaml | 9 + .../apache/hugegraph/core/GraphManager.java | 2 +- .../meta/managers/ServiceMetaManager.java | 7 +- .../apache/hugegraph/unit/UnitTestSuite.java | 2 + .../core/GraphManagerServiceNamesTest.java | 155 ++++++++++++++++++ 7 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java diff --git a/docker/README.md b/docker/README.md index 13ae45ec7b..7d800fedd4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -283,6 +283,8 @@ spaces and backslashes in service IDs round-trip unchanged. Non-ASCII and contro characters are rejected before the entrypoint writes configuration, because the Server reads properties using ISO-8859-1. This is the container input's serialization limit; it does not change the core service-name contract. +The full service ID, including `/` and `-`, is retained when service metadata is +reloaded or its graph space is cleared. `HG_SERVER_GRAPHSPACE` and a migrated `GRAPH_SPACE` must also match the Server API graph-space rule: `DEFAULT`, or up to 48 lowercase letters, digits and underscores, starting with a letter. Invalid environment values are rejected diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index 43c03b9c0a..5179ee84c1 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -205,7 +205,9 @@ requires an authenticated, graph-bound Gremlin answer from every Server Pod (at least the replica floor). `server.testGraphAlias` selects that binding and defaults to `DEFAULT-hugegraph`. Set it to an existing target graph's full alias, such as `team_a-demo`, to check that graph instead. The hook does not create a -graph or infer an alias from `server.graphSpace`. A Server that started during a PD roll +graph or infer an alias from `server.graphSpace`. Match the binding's case: a +local `Demo.properties` can bind `DEFAULT-Demo`, while REST-created graph names +are lowercase. A Server that started during a PD roll can pass readiness and serve REST while every Gremlin call on it fails (see Troubleshooting, "Could not rebind"). Transient failures are retried for 150 seconds before the test fails and names the failing Pod IPs. Large HPA @@ -639,7 +641,10 @@ and backslashes. Dollar signs are escaped in the manifest so Kubernetes passes the service ID literally, including `$(POD_IP)` and `$$`. The chart and entrypoint reject non-ASCII and control characters to prevent the Server's ISO-8859-1 properties reader from changing the value; -this is a deployment input serialization limit, not a change to core service names: +this is a deployment input serialization limit, not a change to core service names. + +The full service ID, including `/` and `-`, is retained when service metadata is +reloaded or its graph space is cleared. ```bash helm upgrade --install hg ./helm/hugegraph \ diff --git a/helm/hugegraph/tests/test_hook_gremlin_test.yaml b/helm/hugegraph/tests/test_hook_gremlin_test.yaml index 09c1df9141..41deb39cc2 100644 --- a/helm/hugegraph/tests/test_hook_gremlin_test.yaml +++ b/helm/hugegraph/tests/test_hook_gremlin_test.yaml @@ -82,6 +82,15 @@ tests: path: spec.containers[0].command[2] pattern: '"graph":"DEFAULT-hugegraph"' + - it: preserves a static graph binding with an uppercase local file name + template: tests/test-connection.yaml + set: + server.testGraphAlias: DEFAULT-Demo + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"DEFAULT-Demo"' + - it: floors the per-Pod count at the HPA minimum when HPA is enabled template: tests/test-connection.yaml set: diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java index c7d821bf38..bd66a0343f 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java @@ -1124,7 +1124,7 @@ public void clearGraphSpace(String name) { // Clear all services for (String key : this.services.keySet()) { if (key.startsWith(name + DELIMITER)) { - String[] parts = key.split(DELIMITER); + String[] parts = key.split(DELIMITER, 2); this.dropService(parts[0], parts[1]); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java index 3c03be3433..14a453aef8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java @@ -50,12 +50,11 @@ private static String serviceName(String graphSpace, String name) { public Map serviceConfigs(String graphSpace) { Map serviceMap = new HashMap<>(); - Map keyValues = this.metaDriver.scanWithPrefix( - this.serviceConfPrefix(graphSpace)); + String prefix = this.serviceConfPrefix(graphSpace); + Map keyValues = this.metaDriver.scanWithPrefix(prefix); for (Map.Entry entry : keyValues.entrySet()) { String key = entry.getKey(); - String[] parts = key.split(META_PATH_DELIMITER); - serviceMap.put(parts[parts.length - 1], + serviceMap.put(key.substring(prefix.length()), JsonUtil.fromJson(entry.getValue(), Service.class)); } return serviceMap; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index c36bed15f0..421fea5711 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -74,6 +74,7 @@ import org.apache.hugegraph.unit.core.ExceptionTest; import org.apache.hugegraph.unit.core.GraphManagerAdminInitTest; import org.apache.hugegraph.unit.core.GraphManagerConfigTest; +import org.apache.hugegraph.unit.core.GraphManagerServiceNamesTest; import org.apache.hugegraph.unit.core.GroovyScriptEngineCompatibilityTest; import org.apache.hugegraph.unit.core.HugeFeaturesTest; import org.apache.hugegraph.unit.core.HstoreSessionsTest; @@ -212,6 +213,7 @@ ExceptionTest.class, GraphManagerAdminInitTest.class, GraphManagerConfigTest.class, + GraphManagerServiceNamesTest.class, HstoreSessionsTest.class, BackendStoreInfoTest.class, TraversalUtilTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java new file mode 100644 index 0000000000..fa02979daa --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hugegraph.unit.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.meta.MetaDriver; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.meta.lock.LockResult; +import org.apache.hugegraph.meta.managers.ServiceMetaManager; +import org.apache.hugegraph.space.GraphSpace; +import org.apache.hugegraph.space.Service; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.Test; +import org.mockito.Mockito; + +public class GraphManagerServiceNamesTest { + + @Test + public void testReloadPreservesCompleteServiceNames() { + MetaDriver driver = storageDriver(); + ServiceMetaManager first = new ServiceMetaManager(driver, "cluster"); + for (String name : new String[]{"analytics", "team/analytics", + "analytics-prod", "team/analytics-prod"}) { + first.addServiceConfig("team_a", service(name)); + } + first.addServiceConfig("other", service("analytics")); + + ServiceMetaManager reloaded = new ServiceMetaManager(driver, "cluster"); + Map services = reloaded.serviceConfigs("team_a"); + Assert.assertEquals(4, services.size()); + for (String name : new String[]{"analytics", "team/analytics", + "analytics-prod", "team/analytics-prod"}) { + Assert.assertEquals(name, services.get(name).name()); + Assert.assertEquals(name, reloaded.service("team_a", name).name()); + } + Assert.assertEquals(1, reloaded.serviceConfigs("other").size()); + + reloaded.removeServiceConfig("team_a", "team/analytics"); + Assert.assertNull(reloaded.service("team_a", "team/analytics")); + Assert.assertNotNull(reloaded.service("team_a", "analytics")); + Assert.assertEquals(3, reloaded.serviceConfigs("team_a").size()); + } + + @Test + public void testClearSpaceDropsFullServiceNames() throws Exception { + MetaDriver driver = storageDriver(); + Constructor constructor = MetaManager.class.getDeclaredConstructor(); + constructor.setAccessible(true); + MetaManager metadata = constructor.newInstance(); + setField(metadata, "metaDriver", driver); + setField(metadata, "cluster", "cluster"); + Whitebox.invoke(MetaManager.class, new Class[]{String.class}, + "initManagers", metadata, "cluster"); + GraphManager manager = new GraphManager( + new HugeConfig(new PropertiesConfiguration()), + new EventHub("service-name-test")); + try { + setField(manager, "metaManager", metadata); + // Exercise the metadata-backed clear path without bootstrapping + // a PD/HStore cluster; the real managers use a storage-only seam. + setField(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("team_a", new GraphSpace("team_a")); + spaces.put("other", new GraphSpace("other")); + Map cache = Whitebox.getInternalState(manager, "services"); + for (String name : new String[]{"analytics", "analytics-prod", + "team/analytics-prod"}) { + Service service = service(name); + metadata.addServiceConfig("team_a", service); + String key = Whitebox.invoke(GraphManager.class, + new Class[]{String.class, String.class}, "serviceName", + null, "team_a", name); + cache.put(key, service); + } + metadata.addServiceConfig("other", service("analytics")); + cache.put("other-analytics", service("analytics")); + + manager.clearGraphSpace("team_a"); + + Assert.assertTrue(metadata.serviceConfigs("team_a").isEmpty()); + Assert.assertEquals(1, metadata.serviceConfigs("other").size()); + Assert.assertEquals(1, cache.size()); + Assert.assertTrue(cache.containsKey("other-analytics")); + } finally { + manager.close(); + } + } + + private static Service service(String name) { + return new Service(name, "admin", Service.ServiceType.OLTP, + Service.DeploymentType.MANUAL); + } + + private static void setField(Object target, String name, Object value) + throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static MetaDriver storageDriver() { + Map storage = new LinkedHashMap<>(); + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.doAnswer(call -> { + storage.put(call.getArgument(0), call.getArgument(1)); + return null; + }).when(driver).put(Mockito.anyString(), Mockito.anyString()); + Mockito.when(driver.get(Mockito.anyString())) + .thenAnswer(call -> storage.get(call.getArgument(0))); + Mockito.doAnswer(call -> { + storage.remove(call.getArgument(0)); + return null; + }).when(driver).delete(Mockito.anyString()); + Mockito.doAnswer(call -> { + storage.keySet().removeIf(key -> key.startsWith(call.getArgument(0))); + return null; + }).when(driver).deleteWithPrefix(Mockito.anyString()); + Mockito.when(driver.scanWithPrefix(Mockito.anyString())).thenAnswer(call -> + storage.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(call.getArgument(0))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + Mockito.when(driver.tryLock(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) + .thenAnswer(call -> { + LockResult lock = new LockResult(); + lock.lockSuccess(true); + return lock; + }); + return driver; + } +}