imbajin commented on code in PR #3149:
URL: https://github.com/apache/hugegraph/pull/3149#discussion_r3770576251


##########
docker/README.md:
##########
@@ -168,14 +173,109 @@ To validate local images without Compose replacing them 
with remote `latest`:
 
 ## 3-Node Cluster Quickstart
 
+The cluster and the Hubble add-on share one named Docker network so Hubble
+can attach to a running cluster without touching it. Treat that network as a
+trust boundary: only the Server layer performs real authentication. Store
+serves its control APIs with no credentials at all, and while PD's REST
+control APIs do require an `Authorization` header, PD only checks that the
+Basic-auth *user* is one of its internal service names and never validates
+the password — so any client on the network can read and drive them. Note
+that this stack depends on that behaviour: Hubble reaches PD as the service
+name `hubble` with an empty password, so tightening PD's credential check
+would also break Hubble's operations view. Any container on the host can
+also join the network by declaring the well-known name. One-time setup:
+write the required credentials to a mode-600 `docker/.env` and create the
+network.
+The cluster file requires both credentials — the admin password enables
+authentication, and every Server replica must share one token secret so a
+token issued by any server validates on all of them. The `:?` guards fire
+on every Compose subcommand, including `down`.
+
+```bash
+(
+  set -eu
+  cd docker
+  command -v openssl >/dev/null 2>&1 || { echo "openssl not found" >&2; exit 
1; }
+  [ -e .env ] || install -m 600 /dev/null .env
+  chmod 600 .env
+  # Keep appends on their own lines even if the file was hand-edited.
+  [ ! -s .env ] || [ -z "$(tail -c1 .env)" ] || printf '\n' >> .env
+  pat='^[[:space:]]*(export[[:space:]]+)?'
+  if ! grep -Eq "${pat}HUGEGRAPH_ADMIN_PASSWORD=" .env; then
+    admin_password="$(openssl rand -base64 12)"
+    printf "HUGEGRAPH_ADMIN_PASSWORD='%s'\n" "${admin_password}" >> .env
+    unset admin_password
+  fi
+  if ! grep -Eq "${pat}HUGEGRAPH_AUTH_TOKEN_SECRET=" .env; then
+    token_secret="$(openssl rand -hex 32)"
+    printf "HUGEGRAPH_AUTH_TOKEN_SECRET='%s'\n" "${token_secret}" >> .env
+    unset token_secret
+  fi
+  admin_value="$(sed -nE 
"s/${pat}HUGEGRAPH_ADMIN_PASSWORD='([^']*)'[[:space:]]*$/\\1/p" .env | tail 
-n1)"

Review Comment:
   ‼️ The cluster quickstart exits immediately after generating its own 
credentials. Evidence: the regex has an optional `export` capture before the 
value, but both sed commands substitute capture group 1, so 
`HUGEGRAPH_ADMIN_PASSWORD='...'` yields an empty value (and an `export` form 
yields the word `export`); lines 216/220 then fail. Use the value capture group 
and parse or explicitly reject all supported Compose dotenv formats 
consistently.



##########
docker/docker-compose-3pd-3store-3server.yml:
##########
@@ -187,16 +207,25 @@ services:
     <<: *server-common
     container_name: hg-server0
     hostname: server0
+    environment:
+      <<: *server-env
+      HG_SERVER_REST_URL: http://server0:8080

Review Comment:
   ⚠️ Each Server registers `http://serverN:8080` with PD while the host 
publishes ports 8080/8081/8082. Evidence: Server registration uses 
`restserver.url`, so a PD-aware client outside the Docker network receives 
`server0`/`server1`/`server2`, names that only resolve inside `hugegraph-net`. 
Provide a separate configurable advertised address or make the external client 
path use addresses it can resolve.



##########
docker/docker-compose-3pd-3store-3server.yml:
##########
@@ -31,6 +36,8 @@ volumes:
 
 # ── Shared service defaults ──────────────────────────────────────────
 x-pd-common: &pd-common
+  # Pin a release via HUGEGRAPH_VERSION in docker/.env; unset, the image

Review Comment:
   ‼️ This auth-required cluster can report healthy while remaining 
unauthenticated. Evidence: the documented compatible integration is newer than 
1.7.x, but the image defaults to floating `latest` with `pull_policy: missing`; 
an already-cached older image can ignore `PASSWORD` and the token secret, while 
the healthcheck probes public `/versions`. Pin or fail closed on incompatible 
images, or add an authenticated readiness smoke check that proves graph 
requests are rejected without credentials and succeed with them.



##########
docker/docker-compose-hubble.yml:
##########
@@ -0,0 +1,53 @@
+#
+# 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.
+#
+
+# Hubble add-on for the distributed cluster defined in
+# docker-compose-3pd-3store-3server.yml. Requires the pre-created cluster
+# network (docker network create hugegraph-net). See "Hubble for the
+# 3-Node Cluster" in docker/README.md for the attach and combined flows.
+
+networks:
+  hg-net:
+    external: true
+    name: ${HUGEGRAPH_NETWORK:-hugegraph-net}
+
+volumes:
+  hg-hubble-db:
+  hg-hubble-upload-files:
+
+services:
+  hubble:
+    # Pin a release via HUGEGRAPH_VERSION in docker/.env; unset, the image
+    # tag defaults to latest.
+    image: ${HUBBLE_IMAGE:-hugegraph/hubble:${HUGEGRAPH_VERSION:-latest}}
+    pull_policy: ${HUBBLE_PULL_POLICY:-missing}
+    container_name: hg-hubble
+    hostname: hubble
+    restart: unless-stopped
+    networks: [hg-net]
+    ports:
+      - "${HUBBLE_PUBLISH_HOST:-127.0.0.1}:8088:8088"
+    volumes:
+      - 
./hugegraph-hubble-3x3.properties:/hubble/conf/hugegraph-hubble.properties:ro
+      - hg-hubble-db:/hubble/db

Review Comment:
   ‼️ The H2 database is mounted at the wrong persistence boundary. Evidence: 
the Hubble image works from `/hubble` and its datasource is 
`jdbc:h2:file:./db`; mounting a volume at `/hubble/db` leaves the actual 
`/hubble/db.mv.db` outside that volume (or makes startup fail because 
`/hubble/db` is a directory). Point H2 at a file inside the mounted directory 
or mount the parent directory, then verify data survives container recreation.



##########
.github/workflows/server-ci.yml:
##########
@@ -138,6 +140,189 @@ jobs:
           check_compose docker/docker-compose.yml always always
           check_compose docker/docker-compose.dev.yml build missing
 
+          check_cluster_compose() {
+            local cluster="docker/docker-compose-3pd-3store-3server.yml"
+            local addon="docker/docker-compose-hubble.yml"
+            local rendered
+            local token_fixture=ci-test-token-secret-32-bytes-long
+            rendered="$(mktemp)"
+            if [ "${#token_fixture}" -lt 32 ]; then
+              echo "CI token fixture must be at least 32 bytes" >&2
+              return 1
+            fi
+
+            # RETURN only: an EXIT trap would fire after this function's
+            # `local rendered` has gone out of scope, which `set -u` turns
+            # into an "unbound variable" error. A hard errexit abort can
+            # therefore still leak one temp file, which is acceptable on an
+            # ephemeral runner.
+            trap 'rm -f "$rendered"' RETURN
+
+            # --env-file /dev/null on every invocation: Compose otherwise reads
+            # docker/.env automatically, and the quickstart tells operators to
+            # create one holding exactly the credentials these assertions
+            # control. Without it the checks pass in CI (which has no .env) but
+            # report false failures for anyone running them locally after
+            # following the quickstart, and the add-on render below — which
+            # deliberately unsets the variables to assert their defaults — 
would
+            # read whatever HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION that .env 
holds.
+            # Pinning an empty env file makes both renders depend only on what
+            # each invocation sets explicitly.
+
+            # Both cluster credentials are required and may not be empty.
+            # Each case asserts the guard fired for the *intended* variable:
+            # a bare non-zero exit would also be produced by a YAML error, a
+            # renamed file, or a missing docker binary.
+            assert_guard() { # assert_guard <blamed-var> <description>
+              local var="$1" desc="$2" err
+              if err="$(docker compose --env-file /dev/null -f "$cluster" \
+                          config -q 2>&1)"; then
+                echo "$cluster accepted $desc" >&2
+                return 1
+              fi
+              case "$err" in
+                *"$var"*) : ;;
+                *) echo "$cluster rejected $desc, but not because of $var: 
$err" >&2
+                   return 1 ;;
+              esac
+            }
+
+            ( unset HUGEGRAPH_ADMIN_PASSWORD
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an unset admin password" )
+            ( export HUGEGRAPH_ADMIN_PASSWORD=
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an empty admin password" )
+            ( unset HUGEGRAPH_AUTH_TOKEN_SECRET
+              export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an unset token secret" 
)
+            ( export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              export HUGEGRAPH_AUTH_TOKEN_SECRET=
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an empty token secret" 
)
+            # The add-on alone must define Hubble and nothing else, join the
+            # shared external network with its default name, and need no
+            # credentials or overrides.
+            env -u HUGEGRAPH_ADMIN_PASSWORD -u HUGEGRAPH_AUTH_TOKEN_SECRET \
+                -u HUGEGRAPH_NETWORK -u HUGEGRAPH_VERSION \
+                -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$addon" config --format 
json > "$rendered"
+            jq -e '
+                (.services | keys) == ["hubble"] and
+                .networks."hg-net".external == true and
+                .networks."hg-net".name == "hugegraph-net" and
+                (.services.hubble.networks | has("hg-net")) and
+                (.services.hubble | has("depends_on") | not) and
+                any(.services.hubble.volumes[];
+                    .target == "/hubble/conf/hugegraph-hubble.properties" and
+                    (.source | endswith("hugegraph-hubble-3x3.properties")))
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-db" and
+                    .target == "/hubble/db")
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-upload-files" and
+                    .target == "/hubble/upload-files")
+              ' "$rendered" >/dev/null
+
+            # The cluster's own default network name must match the add-on's,
+            # or the attach flow and the cluster land on different networks.
+            # The combined render below pins an override, so it cannot catch a
+            # drifting default.
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+              env -u HUGEGRAPH_NETWORK -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY \
+                  -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$cluster" \
+                config --format json > "$rendered"
+            jq -e '.networks."hg-net".name == "hugegraph-net"' \
+              "$rendered" >/dev/null
+
+            # The combined render carries the PD-registration and auth
+            # settings on every server replica and keeps Hubble on loopback.
+            # Rendered with non-default HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION so
+            # CI fails if any file stops honoring the overrides (the add-on
+            # render above covers the defaults).
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+            HUGEGRAPH_NETWORK=ci-test-net \
+            HUGEGRAPH_VERSION=ci-test-tag \
+              env -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST 
\
+              docker compose --env-file /dev/null -f "$cluster" -f "$addon" \

Review Comment:
   ⚠️ The new cluster contract remains render-only. Evidence: the combined 
check stops at `docker compose config` plus jq/grep and never creates the 
external network, starts the services, probes Hubble `/about`, verifies PD 
registration, or checks authenticated graph access. Add a Docker-capable smoke 
job for the combined and attach flows so entrypoint, DNS, registration, and 
auth failures cannot remain green.



##########
.github/workflows/server-ci.yml:
##########
@@ -138,6 +140,189 @@ jobs:
           check_compose docker/docker-compose.yml always always
           check_compose docker/docker-compose.dev.yml build missing
 
+          check_cluster_compose() {
+            local cluster="docker/docker-compose-3pd-3store-3server.yml"
+            local addon="docker/docker-compose-hubble.yml"
+            local rendered
+            local token_fixture=ci-test-token-secret-32-bytes-long
+            rendered="$(mktemp)"
+            if [ "${#token_fixture}" -lt 32 ]; then
+              echo "CI token fixture must be at least 32 bytes" >&2
+              return 1
+            fi
+
+            # RETURN only: an EXIT trap would fire after this function's
+            # `local rendered` has gone out of scope, which `set -u` turns
+            # into an "unbound variable" error. A hard errexit abort can
+            # therefore still leak one temp file, which is acceptable on an
+            # ephemeral runner.
+            trap 'rm -f "$rendered"' RETURN
+
+            # --env-file /dev/null on every invocation: Compose otherwise reads
+            # docker/.env automatically, and the quickstart tells operators to
+            # create one holding exactly the credentials these assertions
+            # control. Without it the checks pass in CI (which has no .env) but
+            # report false failures for anyone running them locally after
+            # following the quickstart, and the add-on render below — which
+            # deliberately unsets the variables to assert their defaults — 
would
+            # read whatever HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION that .env 
holds.
+            # Pinning an empty env file makes both renders depend only on what
+            # each invocation sets explicitly.
+
+            # Both cluster credentials are required and may not be empty.
+            # Each case asserts the guard fired for the *intended* variable:
+            # a bare non-zero exit would also be produced by a YAML error, a
+            # renamed file, or a missing docker binary.
+            assert_guard() { # assert_guard <blamed-var> <description>
+              local var="$1" desc="$2" err
+              if err="$(docker compose --env-file /dev/null -f "$cluster" \
+                          config -q 2>&1)"; then
+                echo "$cluster accepted $desc" >&2
+                return 1
+              fi
+              case "$err" in
+                *"$var"*) : ;;
+                *) echo "$cluster rejected $desc, but not because of $var: 
$err" >&2
+                   return 1 ;;
+              esac
+            }
+
+            ( unset HUGEGRAPH_ADMIN_PASSWORD
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an unset admin password" )
+            ( export HUGEGRAPH_ADMIN_PASSWORD=
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an empty admin password" )
+            ( unset HUGEGRAPH_AUTH_TOKEN_SECRET
+              export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an unset token secret" 
)
+            ( export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              export HUGEGRAPH_AUTH_TOKEN_SECRET=
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an empty token secret" 
)
+            # The add-on alone must define Hubble and nothing else, join the
+            # shared external network with its default name, and need no
+            # credentials or overrides.
+            env -u HUGEGRAPH_ADMIN_PASSWORD -u HUGEGRAPH_AUTH_TOKEN_SECRET \
+                -u HUGEGRAPH_NETWORK -u HUGEGRAPH_VERSION \
+                -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$addon" config --format 
json > "$rendered"
+            jq -e '
+                (.services | keys) == ["hubble"] and
+                .networks."hg-net".external == true and
+                .networks."hg-net".name == "hugegraph-net" and
+                (.services.hubble.networks | has("hg-net")) and
+                (.services.hubble | has("depends_on") | not) and
+                any(.services.hubble.volumes[];
+                    .target == "/hubble/conf/hugegraph-hubble.properties" and
+                    (.source | endswith("hugegraph-hubble-3x3.properties")))
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-db" and
+                    .target == "/hubble/db")
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-upload-files" and
+                    .target == "/hubble/upload-files")
+              ' "$rendered" >/dev/null
+
+            # The cluster's own default network name must match the add-on's,
+            # or the attach flow and the cluster land on different networks.
+            # The combined render below pins an override, so it cannot catch a
+            # drifting default.
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+              env -u HUGEGRAPH_NETWORK -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY \
+                  -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$cluster" \
+                config --format json > "$rendered"
+            jq -e '.networks."hg-net".name == "hugegraph-net"' \
+              "$rendered" >/dev/null
+
+            # The combined render carries the PD-registration and auth
+            # settings on every server replica and keeps Hubble on loopback.
+            # Rendered with non-default HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION so
+            # CI fails if any file stops honoring the overrides (the add-on
+            # render above covers the defaults).
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+            HUGEGRAPH_NETWORK=ci-test-net \
+            HUGEGRAPH_VERSION=ci-test-tag \
+              env -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST 
\
+              docker compose --env-file /dev/null -f "$cluster" -f "$addon" \
+                config --format json > "$rendered"
+            jq -e '
+                .name == "hugegraph-3x3" and
+                (.services | keys | length) == 10 and
+                .networks."hg-net".external == true and
+                .networks."hg-net".name == "ci-test-net" and
+                all(.services[]; .networks | has("hg-net")) and
+                .services.pd0.image == "hugegraph/pd:ci-test-tag" and
+                .services.store0.image == "hugegraph/store:ci-test-tag" and
+                .services.server0.image == "hugegraph/server:ci-test-tag" and
+                .services.hubble.image == "hugegraph/hubble:ci-test-tag" and
+                .services.server0.environment.HG_SERVER_USE_PD == "true" and
+                .services.server0.environment.HG_SERVER_CLUSTER == "hg" and
+                .services.server0.environment.HG_SERVER_REST_URL ==
+                  "http://server0:8080"; and
+                .services.server1.environment.HG_SERVER_REST_URL ==
+                  "http://server1:8080"; and
+                .services.server1.environment.HG_SERVER_AUTH_TOKEN_SECRET ==
+                  "ci-test-token-secret-32-bytes-long" and
+                .services.server2.environment.HG_SERVER_REST_URL ==
+                  "http://server2:8080"; and
+                .services.server2.environment.HG_SERVER_AUTH_TOKEN_SECRET ==
+                  "ci-test-token-secret-32-bytes-long" and
+                .services.server0.environment.HG_SERVER_INIT_STORE_ENABLED ==
+                  "false" and
+                .services.server0.environment.PASSWORD ==
+                  "ci-test-password" and
+                .services.server0.environment.HG_SERVER_AUTH_TOKEN_SECRET ==
+                  "ci-test-token-secret-32-bytes-long" and
+                (.services.hubble | has("depends_on") | not) and
+                .services.hubble.pull_policy == "missing" and
+                (.services.hubble.healthcheck.test[1] |
+                  contains("http://127.0.0.1:8088/about";) and
+                  contains("\"status\":200") and
+                  contains("\"name\":\"hugegraph-hubble\"")) and
+                any(.services.hubble.ports[];
+                    .target == 8088 and .published == "8088" and
+                    .host_ip == "127.0.0.1")
+              ' "$rendered" >/dev/null
+
+            # Hubble's properties file is mounted, not rendered, so Compose
+            # validation alone cannot catch it drifting from the services it
+            # describes. Tie the two together: renaming a service, changing a
+            # container hostname, or moving a REST port must be reflected in
+            # both places or CI fails here. Values are derived from the
+            # rendered model (hostnames and ports included) so the assertions
+            # cannot silently agree with a stale file.
+            local props="docker/hugegraph-hubble-3x3.properties"
+            local pd_peers store_targets cluster_name pd_rest
+            assert_props() { # assert_props <exact-line> <what-it-must-match>
+              grep -Fqx "$1" "$props" ||
+                { echo "$props: expected line '$1' ($2)" >&2; return 1; }
+            }
+            cluster_name="$(jq -r 
'.services.server0.environment.HG_SERVER_CLUSTER' \
+                              "$rendered")"
+            pd_peers="$(jq -r 
'.services.server0.environment.HG_SERVER_PD_PEERS' \
+                          "$rendered")"
+            store_targets="[$(jq -r '[.services | to_entries[]
+                                      | select(.key | startswith("store"))
+                                      | "http://"; + .value.hostname + ":"
+                                        + 
(.value.environment.HG_STORE_REST_PORT)]
+                                     | sort | join(",")' "$rendered")]"
+            pd_rest="$(jq -r --arg h "$(printf '%s' "${pd_peers}" | cut -d, 
-f1 | cut -d: -f1)" \
+                         '.services | to_entries[]
+                          | select(.value.hostname == $h)
+                          | .value.hostname + ":" + 
.value.environment.HG_PD_REST_PORT' \
+                         "$rendered")"
+            assert_props "cluster=${cluster_name}" "matches HG_SERVER_CLUSTER"

Review Comment:
   ⚠️ The Hubble properties consistency check can pass while PD mode is 
disabled. Evidence: `assert_props` validates `cluster`, `pd.peers`, 
`pd.server`, and Store targets, but never asserts `pd.enabled=true`; the 
current `server.direct_url` is only meaningful when PD mode is false. Add the 
explicit mode assertion and a runtime smoke check so Hubble cannot silently 
fall back to one direct Server.



##########
docker/docker-compose-hubble.yml:
##########
@@ -0,0 +1,53 @@
+#
+# 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.
+#
+
+# Hubble add-on for the distributed cluster defined in
+# docker-compose-3pd-3store-3server.yml. Requires the pre-created cluster
+# network (docker network create hugegraph-net). See "Hubble for the
+# 3-Node Cluster" in docker/README.md for the attach and combined flows.
+
+networks:
+  hg-net:
+    external: true
+    name: ${HUGEGRAPH_NETWORK:-hugegraph-net}
+
+volumes:
+  hg-hubble-db:

Review Comment:
   ⚠️ The two documented Hubble flows do not share these physical volumes. 
Evidence: attach runs under project `hugegraph-hubble`, while the combined flow 
runs under `hugegraph-3x3`; Compose therefore creates different 
project-prefixed H2 and upload volumes. Switching flows makes existing state 
and uploads appear to disappear. Give both volumes stable explicit names (with 
a multi-cluster override if needed) or document a supported volume migration.



##########
docker/README.md:
##########
@@ -196,11 +296,184 @@ curl http://localhost:8520/v1/health
 # Check Server (Graph API)
 curl http://localhost:8080/versions
 
-# List registered stores via PD
-curl http://localhost:8620/v1/stores
+# Every PD endpoint except /v1/health, /actuator/* and /v1/prom/targets/*
+# needs an Authorization header. PD only checks that the Basic-auth user is
+# one of its internal service names, so the empty password below is enough
+# — and it grants the full PD control plane, writes included, not just these
+# reads. That is exactly why the shared network must be treated as a trust
+# boundary. Without the header PD answers with an exception body, not data.
+pd_auth="Authorization: Basic $(printf 'hubble:' | base64)"
+
+# List registered stores via PD (expect three, each "state":"Up")
+curl -H "${pd_auth}" http://localhost:8620/v1/stores
 
 # List partitions
-curl http://localhost:8620/v1/partitions
+curl -H "${pd_auth}" http://localhost:8620/v1/partitions
+```
+
+Confirm authentication actually engaged — `/versions` stays open by design,
+so it cannot tell you whether auth is on. A graph read without credentials
+must be rejected:
+
+```bash
+cd docker
+# Expect 401 on all three replicas
+for port in 8080 8081 8082; do
+  curl -s -o /dev/null -w "${port}: %{http_code}\n" \
+    "http://localhost:${port}/graphs/hugegraph/schema/vertexlabels";
+done
+
+# And a signed-in read must succeed. Compose reads docker/.env by itself, but
+# your shell does not — load it first. Passing the credential through
+# --config keeps it out of argv, where `ps` would expose it to other users.
+set -a; . ./.env; set +a

Review Comment:
   ⚠️ This verification block executes the entire Compose dotenv file as shell 
code. Evidence: `set -a; . ./.env; set +a` evaluates command substitutions and 
other shell syntax before the curl; a value such as 
`HUGEGRAPH_ADMIN_PASSWORD=$(...)` runs on the operator host. Parse only the 
required key with a dotenv-aware parser or avoid sourcing `.env` directly.



##########
docker/docker-compose-3pd-3store-3server.yml:
##########
@@ -17,9 +17,14 @@
 
 name: hugegraph-3x3
 
+# The cluster network is shared with the Hubble add-on
+# (docker-compose-hubble.yml), so it is external and must exist first:
+#   docker network create hugegraph-net
+# Set HUGEGRAPH_NETWORK to use a differently named network.
 networks:
   hg-net:
-    driver: bridge
+    external: true

Review Comment:
   ‼️ The predictable external network and host-published control-plane ports 
expose unauthenticated PD/Store administration by default. Evidence: 
`hugegraph-net` is external and the file publishes PD/Store REST, gRPC, and 
Raft ports on all host interfaces; Store has no authentication and PD only 
checks the internal service username, not the password. Bind these ports to 
loopback/remove them by default, or require real authentication/TLS and 
explicit network ACLs before shipping this as the default quickstart.



##########
.github/workflows/server-ci.yml:
##########
@@ -138,6 +140,189 @@ jobs:
           check_compose docker/docker-compose.yml always always
           check_compose docker/docker-compose.dev.yml build missing
 
+          check_cluster_compose() {
+            local cluster="docker/docker-compose-3pd-3store-3server.yml"
+            local addon="docker/docker-compose-hubble.yml"
+            local rendered
+            local token_fixture=ci-test-token-secret-32-bytes-long
+            rendered="$(mktemp)"
+            if [ "${#token_fixture}" -lt 32 ]; then
+              echo "CI token fixture must be at least 32 bytes" >&2
+              return 1
+            fi
+
+            # RETURN only: an EXIT trap would fire after this function's
+            # `local rendered` has gone out of scope, which `set -u` turns
+            # into an "unbound variable" error. A hard errexit abort can
+            # therefore still leak one temp file, which is acceptable on an
+            # ephemeral runner.
+            trap 'rm -f "$rendered"' RETURN
+
+            # --env-file /dev/null on every invocation: Compose otherwise reads
+            # docker/.env automatically, and the quickstart tells operators to
+            # create one holding exactly the credentials these assertions
+            # control. Without it the checks pass in CI (which has no .env) but
+            # report false failures for anyone running them locally after
+            # following the quickstart, and the add-on render below — which
+            # deliberately unsets the variables to assert their defaults — 
would
+            # read whatever HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION that .env 
holds.
+            # Pinning an empty env file makes both renders depend only on what
+            # each invocation sets explicitly.
+
+            # Both cluster credentials are required and may not be empty.
+            # Each case asserts the guard fired for the *intended* variable:
+            # a bare non-zero exit would also be produced by a YAML error, a
+            # renamed file, or a missing docker binary.
+            assert_guard() { # assert_guard <blamed-var> <description>
+              local var="$1" desc="$2" err
+              if err="$(docker compose --env-file /dev/null -f "$cluster" \
+                          config -q 2>&1)"; then
+                echo "$cluster accepted $desc" >&2
+                return 1
+              fi
+              case "$err" in
+                *"$var"*) : ;;
+                *) echo "$cluster rejected $desc, but not because of $var: 
$err" >&2
+                   return 1 ;;
+              esac
+            }
+
+            ( unset HUGEGRAPH_ADMIN_PASSWORD
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an unset admin password" )
+            ( export HUGEGRAPH_ADMIN_PASSWORD=
+              export HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}"
+              assert_guard HUGEGRAPH_ADMIN_PASSWORD "an empty admin password" )
+            ( unset HUGEGRAPH_AUTH_TOKEN_SECRET
+              export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an unset token secret" 
)
+            ( export HUGEGRAPH_ADMIN_PASSWORD=ci-test-password
+              export HUGEGRAPH_AUTH_TOKEN_SECRET=
+              assert_guard HUGEGRAPH_AUTH_TOKEN_SECRET "an empty token secret" 
)
+            # The add-on alone must define Hubble and nothing else, join the
+            # shared external network with its default name, and need no
+            # credentials or overrides.
+            env -u HUGEGRAPH_ADMIN_PASSWORD -u HUGEGRAPH_AUTH_TOKEN_SECRET \
+                -u HUGEGRAPH_NETWORK -u HUGEGRAPH_VERSION \
+                -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$addon" config --format 
json > "$rendered"
+            jq -e '
+                (.services | keys) == ["hubble"] and
+                .networks."hg-net".external == true and
+                .networks."hg-net".name == "hugegraph-net" and
+                (.services.hubble.networks | has("hg-net")) and
+                (.services.hubble | has("depends_on") | not) and
+                any(.services.hubble.volumes[];
+                    .target == "/hubble/conf/hugegraph-hubble.properties" and
+                    (.source | endswith("hugegraph-hubble-3x3.properties")))
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-db" and
+                    .target == "/hubble/db")
+                and any(.services.hubble.volumes[];
+                    .source == "hg-hubble-upload-files" and
+                    .target == "/hubble/upload-files")
+              ' "$rendered" >/dev/null
+
+            # The cluster's own default network name must match the add-on's,
+            # or the attach flow and the cluster land on different networks.
+            # The combined render below pins an override, so it cannot catch a
+            # drifting default.
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+              env -u HUGEGRAPH_NETWORK -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY \
+                  -u HUBBLE_PUBLISH_HOST \
+              docker compose --env-file /dev/null -f "$cluster" \
+                config --format json > "$rendered"
+            jq -e '.networks."hg-net".name == "hugegraph-net"' \
+              "$rendered" >/dev/null
+
+            # The combined render carries the PD-registration and auth
+            # settings on every server replica and keeps Hubble on loopback.
+            # Rendered with non-default HUGEGRAPH_NETWORK/HUGEGRAPH_VERSION so
+            # CI fails if any file stops honoring the overrides (the add-on
+            # render above covers the defaults).
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+            HUGEGRAPH_AUTH_TOKEN_SECRET="${token_fixture}" \
+            HUGEGRAPH_NETWORK=ci-test-net \
+            HUGEGRAPH_VERSION=ci-test-tag \
+              env -u HUBBLE_IMAGE -u HUBBLE_PULL_POLICY -u HUBBLE_PUBLISH_HOST 
\
+              docker compose --env-file /dev/null -f "$cluster" -f "$addon" \
+                config --format json > "$rendered"
+            jq -e '
+                .name == "hugegraph-3x3" and
+                (.services | keys | length) == 10 and
+                .networks."hg-net".external == true and
+                .networks."hg-net".name == "ci-test-net" and
+                all(.services[]; .networks | has("hg-net")) and
+                .services.pd0.image == "hugegraph/pd:ci-test-tag" and
+                .services.store0.image == "hugegraph/store:ci-test-tag" and
+                .services.server0.image == "hugegraph/server:ci-test-tag" and
+                .services.hubble.image == "hugegraph/hubble:ci-test-tag" and
+                .services.server0.environment.HG_SERVER_USE_PD == "true" and

Review Comment:
   ⚠️ The contract comment says PD-registration and auth settings are carried 
by every Server replica, but the assertions only check `server0` for 
`HG_SERVER_USE_PD`, `HG_SERVER_CLUSTER`, `HG_SERVER_INIT_STORE_ENABLED`, and 
`PASSWORD`; `server1`/`server2` only check REST URL and token. A per-replica 
override can therefore pass CI while breaking discovery or initialization. 
Apply the required-value assertions to all three Server services.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to