bitflicker64 commented on code in PR #3149:
URL: https://github.com/apache/hugegraph/pull/3149#discussion_r3888191227
##########
.github/workflows/server-ci.yml:
##########
@@ -138,6 +140,366 @@ 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 \
+ -u HUBBLE_DB_VOLUME -u HUBBLE_UPLOAD_VOLUME \
+ 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
+ .volumes."hg-hubble-db".name == "hugegraph-hubble-db" and
+ .volumes."hg-hubble-db".external == true and
+ .volumes."hg-hubble-upload-files".name ==
+ "hugegraph-hubble-upload-files" and
+ .volumes."hg-hubble-upload-files".external == true and
+ .services.hubble.environment.SPRING_DATASOURCE_URL ==
+ "jdbc:h2:file:/hubble/db/hubble;DB_CLOSE_ON_EXIT=FALSE" 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 HUGEGRAPH_PULL_POLICY -u HUGEGRAPH_CONTROL_PLANE_HOST \
+ -u HUGEGRAPH_SERVER_PUBLISH_HOST -u HUBBLE_PROPERTIES \
+ -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
\
+ -u HUBBLE_DB_VOLUME -u HUBBLE_UPLOAD_VOLUME \
+ -u HUGEGRAPH_PULL_POLICY -u HUGEGRAPH_CONTROL_PLANE_HOST \
+ -u HUGEGRAPH_SERVER_PUBLISH_HOST -u HUBBLE_PROPERTIES \
+ docker compose --env-file /dev/null -f "$cluster" -f "$addon" \
+ config --format json > "$rendered"
+ jq -e '
+ . as $root |
+ .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
+ all(["server0", "server1", "server2"][];
+ $root.services[.].environment.HG_SERVER_USE_PD == "true" and
+ $root.services[.].environment.HG_SERVER_CLUSTER == "hg" and
+ $root.services[.].environment.HG_SERVER_INIT_STORE_ENABLED ==
+ "false" and
+ $root.services[.].environment.PASSWORD ==
+ "ci-test-password" and
+ $root.services[.].environment.HG_SERVER_AUTH_TOKEN_SECRET ==
+ "ci-test-token-secret-32-bytes-long") and
+ (.services.server0.healthcheck.test[1] |
+ contains("= 401") and (contains("PASSWORD") | not)) and
+ .services.server0.environment.HG_SERVER_REST_URL ==
+ "http://server0:8080" and
+ .services.server1.environment.HG_SERVER_REST_URL ==
+ "http://server1:8080" and
+ .services.server2.environment.HG_SERVER_REST_URL ==
+ "http://server2:8080" and
+ (.services.hubble | has("depends_on") | not) and
+ .services.hubble.pull_policy == "always" and
+ .volumes."hg-hubble-db".name == "hugegraph-hubble-db" and
+ .volumes."hg-hubble-db".external == true and
+ .volumes."hg-hubble-upload-files".name ==
+ "hugegraph-hubble-upload-files" and
+ .volumes."hg-hubble-upload-files".external == true and
+ .services.hubble.environment.SPRING_DATASOURCE_URL ==
+ "jdbc:h2:file:/hubble/db/hubble;DB_CLOSE_ON_EXIT=FALSE" 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") and
+ all(["pd0", "pd1", "pd2", "store0", "store1", "store2",
+ "server0", "server1", "server2"][];
+ all($root.services[.].ports[]; .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"
+ assert_props "pd.enabled=true" "keeps Hubble in PD mode"
+ assert_props "auth.enabled=true" "keeps Hubble in authenticated
mode"
+ assert_props "pd.peers=${pd_peers}" "matches HG_SERVER_PD_PEERS"
+ assert_props "pd.server=${pd_rest}" "names a real PD REST endpoint"
+ assert_props "operations.store.allowed_targets=${store_targets}" \
+ "lists every Store REST endpoint"
+
+ # The documented opt-out must actually opt out: no admin password,
+ # no shared token secret, and a healthcheck that does not demand
+ # the 401/200 pair the authenticated stack requires. A silent
+ # failure here would leave users on an unauthenticated cluster
+ # that still advertises itself as authenticated.
+ # Use the exact placeholders the README documents, including the
+ # deliberately short token: it must never reach a container, and
+ # the assertions below are what prove the override drops it.
+ local non_auth="docker/docker-compose-3x3.non-auth.yml"
+ HUGEGRAPH_ADMIN_PASSWORD=non-auth-placeholder \
+ HUGEGRAPH_AUTH_TOKEN_SECRET=non-auth-placeholder \
+ docker compose --env-file /dev/null -f "$cluster" -f "$non_auth"
\
+ config --format json > "$rendered"
+ jq -e '
+ . as $root |
+ all(["server0", "server1", "server2"][];
+ ($root.services[.].environment | has("PASSWORD") | not) and
+ ($root.services[.].environment
+ | has("HG_SERVER_AUTH_TOKEN_SECRET") | not) and
+ ($root.services[.].healthcheck.test[1]
+ | contains("/versions") and (contains("401") | not)))
+ ' "$rendered" >/dev/null
+ # The non-auth properties file is never booted by CI, so without
+ # this its topology can drift from the compose files unnoticed and
+ # users become the detection mechanism. Same contract, same source
+ # of truth, only the auth mode differs.
+ props="docker/hugegraph-hubble-3x3.non-auth.properties"
+ assert_props "auth.enabled=false" "runs Hubble without a login"
+ assert_props "cluster=${cluster_name}" "matches HG_SERVER_CLUSTER"
+ assert_props "pd.enabled=true" "keeps Hubble in PD mode"
+ assert_props "pd.peers=${pd_peers}" "matches HG_SERVER_PD_PEERS"
+ assert_props "pd.server=${pd_rest}" "names a real PD REST endpoint"
+ assert_props "operations.store.allowed_targets=${store_targets}" \
+ "lists every Store REST endpoint"
+ }
+
+ check_cluster_compose
+
+ # Only runs when this contract changes. The step boots the published
+ # images to prove the Compose wiring end to end, so leaving it on every
+ # build would turn a slow or rate-limited registry into a red run for
+ # pull requests that touch none of these files.
+ - name: Check whether the Compose contract changed
+ if: ${{ env.BACKEND == 'rocksdb' }}
+ id: compose_changed
+ run: |
+ set -euo pipefail
+ base="${{ github.event.pull_request.base.sha }}"
Review Comment:
⚠️ This gate is inert on `push`, so the ten-container smoke test runs on
every master build.
`github.event.pull_request.base.sha` is only populated for `pull_request`
events, and this workflow also triggers on `push` to `master`, `release-*` and
`test-*` (`server-ci.yml:3-9`). On those runs `base` is the empty string, `[ -n
"$base" ]` is false, `changed` stays at its `true` initialiser, and the smoke
step's `if` is satisfied — so every merge to master boots the nine-container
cluster twice with `--wait-timeout 420` plus the attach flow at 180, and pulls
`hugegraph/{pd,store,server,hubble}` from Docker Hub because all four are
`pull_policy: always`.
That is the outcome the step's own comment says it exists to avoid ("leaving
it on every build would turn a slow or rate-limited registry into a red run"),
and on master a red run blocks everyone rather than one PR. The pull_request
half is fine: `actions/checkout` with `fetch-depth: 5` (`:50`) leaves the merge
commit's parents in the object store, so both `git cat-file -e` and the `git
diff` resolve.
Requested change: add `github.event_name == 'pull_request'` to the smoke
step's `if`, or give the push case something to diff against — `base="${{
github.event.before }}"` when the pull_request payload is absent — so the gate
means the same thing on both event types.
##########
.github/workflows/server-ci.yml:
##########
@@ -138,6 +140,366 @@ 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 \
+ -u HUBBLE_DB_VOLUME -u HUBBLE_UPLOAD_VOLUME \
+ 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
+ .volumes."hg-hubble-db".name == "hugegraph-hubble-db" and
+ .volumes."hg-hubble-db".external == true and
+ .volumes."hg-hubble-upload-files".name ==
+ "hugegraph-hubble-upload-files" and
+ .volumes."hg-hubble-upload-files".external == true and
+ .services.hubble.environment.SPRING_DATASOURCE_URL ==
+ "jdbc:h2:file:/hubble/db/hubble;DB_CLOSE_ON_EXIT=FALSE" 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 HUGEGRAPH_PULL_POLICY -u HUGEGRAPH_CONTROL_PLANE_HOST \
+ -u HUGEGRAPH_SERVER_PUBLISH_HOST -u HUBBLE_PROPERTIES \
+ -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
\
+ -u HUBBLE_DB_VOLUME -u HUBBLE_UPLOAD_VOLUME \
+ -u HUGEGRAPH_PULL_POLICY -u HUGEGRAPH_CONTROL_PLANE_HOST \
+ -u HUGEGRAPH_SERVER_PUBLISH_HOST -u HUBBLE_PROPERTIES \
+ docker compose --env-file /dev/null -f "$cluster" -f "$addon" \
+ config --format json > "$rendered"
+ jq -e '
+ . as $root |
+ .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
+ all(["server0", "server1", "server2"][];
+ $root.services[.].environment.HG_SERVER_USE_PD == "true" and
+ $root.services[.].environment.HG_SERVER_CLUSTER == "hg" and
+ $root.services[.].environment.HG_SERVER_INIT_STORE_ENABLED ==
+ "false" and
+ $root.services[.].environment.PASSWORD ==
+ "ci-test-password" and
+ $root.services[.].environment.HG_SERVER_AUTH_TOKEN_SECRET ==
+ "ci-test-token-secret-32-bytes-long") and
+ (.services.server0.healthcheck.test[1] |
+ contains("= 401") and (contains("PASSWORD") | not)) and
+ .services.server0.environment.HG_SERVER_REST_URL ==
+ "http://server0:8080" and
+ .services.server1.environment.HG_SERVER_REST_URL ==
+ "http://server1:8080" and
+ .services.server2.environment.HG_SERVER_REST_URL ==
+ "http://server2:8080" and
+ (.services.hubble | has("depends_on") | not) and
+ .services.hubble.pull_policy == "always" and
+ .volumes."hg-hubble-db".name == "hugegraph-hubble-db" and
+ .volumes."hg-hubble-db".external == true and
+ .volumes."hg-hubble-upload-files".name ==
+ "hugegraph-hubble-upload-files" and
+ .volumes."hg-hubble-upload-files".external == true and
+ .services.hubble.environment.SPRING_DATASOURCE_URL ==
+ "jdbc:h2:file:/hubble/db/hubble;DB_CLOSE_ON_EXIT=FALSE" 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") and
+ all(["pd0", "pd1", "pd2", "store0", "store1", "store2",
+ "server0", "server1", "server2"][];
+ all($root.services[.].ports[]; .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"
+ assert_props "pd.enabled=true" "keeps Hubble in PD mode"
+ assert_props "auth.enabled=true" "keeps Hubble in authenticated
mode"
+ assert_props "pd.peers=${pd_peers}" "matches HG_SERVER_PD_PEERS"
+ assert_props "pd.server=${pd_rest}" "names a real PD REST endpoint"
+ assert_props "operations.store.allowed_targets=${store_targets}" \
+ "lists every Store REST endpoint"
+
+ # The documented opt-out must actually opt out: no admin password,
+ # no shared token secret, and a healthcheck that does not demand
+ # the 401/200 pair the authenticated stack requires. A silent
+ # failure here would leave users on an unauthenticated cluster
+ # that still advertises itself as authenticated.
+ # Use the exact placeholders the README documents, including the
+ # deliberately short token: it must never reach a container, and
+ # the assertions below are what prove the override drops it.
+ local non_auth="docker/docker-compose-3x3.non-auth.yml"
+ HUGEGRAPH_ADMIN_PASSWORD=non-auth-placeholder \
+ HUGEGRAPH_AUTH_TOKEN_SECRET=non-auth-placeholder \
+ docker compose --env-file /dev/null -f "$cluster" -f "$non_auth"
\
+ config --format json > "$rendered"
+ jq -e '
+ . as $root |
+ all(["server0", "server1", "server2"][];
+ ($root.services[.].environment | has("PASSWORD") | not) and
+ ($root.services[.].environment
+ | has("HG_SERVER_AUTH_TOKEN_SECRET") | not) and
+ ($root.services[.].healthcheck.test[1]
+ | contains("/versions") and (contains("401") | not)))
+ ' "$rendered" >/dev/null
+ # The non-auth properties file is never booted by CI, so without
+ # this its topology can drift from the compose files unnoticed and
+ # users become the detection mechanism. Same contract, same source
+ # of truth, only the auth mode differs.
+ props="docker/hugegraph-hubble-3x3.non-auth.properties"
+ assert_props "auth.enabled=false" "runs Hubble without a login"
+ assert_props "cluster=${cluster_name}" "matches HG_SERVER_CLUSTER"
+ assert_props "pd.enabled=true" "keeps Hubble in PD mode"
+ assert_props "pd.peers=${pd_peers}" "matches HG_SERVER_PD_PEERS"
+ assert_props "pd.server=${pd_rest}" "names a real PD REST endpoint"
+ assert_props "operations.store.allowed_targets=${store_targets}" \
+ "lists every Store REST endpoint"
+ }
+
+ check_cluster_compose
+
+ # Only runs when this contract changes. The step boots the published
+ # images to prove the Compose wiring end to end, so leaving it on every
+ # build would turn a slow or rate-limited registry into a red run for
+ # pull requests that touch none of these files.
+ - name: Check whether the Compose contract changed
+ if: ${{ env.BACKEND == 'rocksdb' }}
+ id: compose_changed
+ run: |
+ set -euo pipefail
+ base="${{ github.event.pull_request.base.sha }}"
+ changed=true
+ if [ -n "$base" ] && git cat-file -e "$base^{commit}" 2>/dev/null;
then
+ if git diff --quiet "$base" HEAD -- docker/
.github/workflows/server-ci.yml; then
+ changed=false
+ fi
+ fi
+ echo "changed=$changed" >> "$GITHUB_OUTPUT"
+
+ - name: Run distributed Compose auth and attach smoke test
+ if: ${{ env.BACKEND == 'rocksdb' &&
steps.compose_changed.outputs.changed == 'true' }}
+ run: |
+ set -euo pipefail
+ command -v docker >/dev/null 2>&1
+ docker compose version >/dev/null
+ docker info >/dev/null
+
+ cluster="docker/docker-compose-3pd-3store-3server.yml"
+ addon="docker/docker-compose-hubble.yml"
+ run_id="${GITHUB_RUN_ID:-local}"
+ network="hugegraph-ci-${run_id}"
+ db_volume="hugegraph-ci-${run_id}-db"
+ upload_volume="hugegraph-ci-${run_id}-uploads"
+ admin_password='ci-smoke-admin-password'
+ token_secret='ci-smoke-token-secret-32-bytes-long'
+
+ # Every Compose call needs the same environment, and cleanup must run
+ # even when an assertion below fails.
+ export HUGEGRAPH_NETWORK="$network"
+ export HUGEGRAPH_ADMIN_PASSWORD="$admin_password"
+ export HUGEGRAPH_AUTH_TOKEN_SECRET="$token_secret"
+ export HUBBLE_DB_VOLUME="$db_volume"
+ export HUBBLE_UPLOAD_VOLUME="$upload_volume"
+
+ cleanup() {
+ docker compose --env-file /dev/null -f "$cluster" -f "$addon" \
+ down -v --remove-orphans >/dev/null 2>&1 || true
+ docker compose --env-file /dev/null -p
"hugegraph-ci-attach-${run_id}" \
+ -f "$addon" down -v >/dev/null 2>&1 || true
+ docker network rm "$network" >/dev/null 2>&1 || true
+ docker volume rm "$db_volume" "$upload_volume" >/dev/null 2>&1 ||
true
+ }
+ trap cleanup EXIT
+ docker network create "$network"
+ # Both Hubble volumes are external, so nothing creates them
implicitly.
+ docker volume create "$db_volume" >/dev/null
+ docker volume create "$upload_volume" >/dev/null
+
+ # The cluster must come up healthy on its own. Its Server healthcheck
+ # already requires an unauthenticated graph request to return 401, so
+ # --wait failing here means the images do not enforce the
+ # authentication this stack configures. The authenticated half is
+ # asserted below, where the credentials are known to be current.
+ docker compose --env-file /dev/null -f "$cluster" up -d --wait \
+ --wait-timeout 420
+ cluster_ids_before="$(docker compose --env-file /dev/null -f
"$cluster" \
+ ps -q pd0 pd1 pd2 store0 store1 store2 server0 server1 server2 |
sort)"
+ for port in 8080 8081 8082; do
+ code="$(curl --retry 30 --retry-delay 2 --retry-all-errors -s -o
/dev/null \
+ -w '%{http_code}'
"http://127.0.0.1:${port}/graphs/hugegraph/schema/vertexlabels")"
+ test "$code" = 401
+ code="$(curl --retry 30 --retry-delay 2 --retry-all-errors -s -o
/dev/null \
+ -w '%{http_code}' -u "admin:${admin_password}" \
+ "http://127.0.0.1:${port}/graphs/hugegraph/schema/vertexlabels")"
+ test "$code" = 200
+ done
+
+ # The distributed topology must really be distributed: three PD peers
+ # and three Stores registered and Up. A partial cluster otherwise
+ # still passes a container-level health check. PD requires an
+ # Authorization header whose user is an internal service name; it
+ # never validates the password, hence the empty one.
+ pd_nodes="$(curl -fsS -u 'hubble:'
"http://127.0.0.1:8620/v1/cluster" \
+ | grep -o '"restUrl":"pd[0-2]:8620"' | sort -u | wc -l)"
+ test "$pd_nodes" -eq 3
+ stores_up="$(curl -fsS -u 'hubble:'
"http://127.0.0.1:8620/v1/stores" \
+ | grep -o '"state":"Up"' | wc -l)"
+ test "$stores_up" -eq 3
+
+ # Attaching Hubble must not recreate any cluster container.
+ docker compose --env-file /dev/null -p
"hugegraph-ci-attach-${run_id}" \
+ -f "$addon" up -d --wait --wait-timeout 180
+ curl --retry 30 --retry-delay 2 --retry-all-errors -fsS \
+ http://127.0.0.1:8088/about | grep -q '"name":"hugegraph-hubble"'
+ cluster_ids_after="$(docker compose --env-file /dev/null -f
"$cluster" \
+ ps -q pd0 pd1 pd2 store0 store1 store2 server0 server1 server2 |
sort)"
+ test "$cluster_ids_before" = "$cluster_ids_after"
Review Comment:
⚠️ This assertion cannot fail, and the flow where it would matter is not
checked.
The attach `up` two lines above runs under `-p
hugegraph-ci-attach-${run_id}` (`:475-476`), a different Compose project from
the cluster's own `hugegraph-3x3` (`docker-compose-3pd-3store-3server.yml:18`).
Compose does not touch containers outside the project it was invoked for, so
`cluster_ids_after` equals `cluster_ids_before` no matter what the add-on
declares — this comparison would still pass if `docker-compose-hubble.yml` grew
a `depends_on` on the cluster services tomorrow, which is exactly the property
the comment above it claims to be proving.
The flow where the no-`depends_on` design actually decides the outcome is
the combined one at `:494-495`, which merges the add-on into the cluster's own
project, and no id comparison is taken around it. Since the PR description
lists "attaches Hubble and confirms no cluster container is recreated" as what
closes the render-only CI finding, the check should sit where it can observe a
regression.
Requested change: re-capture the nine ids after the combined `up -d --wait`
at `:495` and compare them against `cluster_ids_before` too. Keeping the
attach-flow comparison is harmless, but it should not be the only one.
--
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]