Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/docker-build-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@ on:
paths:
- '**/Dockerfile*'
- '.dockerignore'
- 'hugegraph-server/hugegraph-dist/docker/**'
- '**/docker/docker-entrypoint.sh'

jobs:
entrypoint-test:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Server entrypoint smoke tests
run: hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh

docker-build:
runs-on: ubuntu-24.04
strategy:
Expand Down
19 changes: 19 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,25 @@ Configuration is injected via environment variables. The old `docker/configs/app
| `HG_SERVER_PD_PEERS` | Yes | — | `pd.peers` | PD cluster addresses (e.g. `pd0:8686,pd1:8686,pd2:8686`) |
| `STORE_REST` | No | — | Used by `wait-partition.sh` | Store REST endpoint for partition verification (e.g. `store0:8520`) |
| `PASSWORD` | No | — | Enables auth mode | Optional authentication password |
| `HG_SERVER_INIT_STORE_ENABLED` | No | `true` | `init_store.enabled` in `rest-server.properties` | Set `false` in PD/HStore deployments so init-store skips local backend and admin initialization |

> **Auth with `HG_SERVER_INIT_STORE_ENABLED=false` requires `usePD=true`.**
> With init-store skipped, the only component that creates the built-in admin
> account is the PD metadata path, which the server takes only when `usePD` is
> true. Enabling auth without it would start a server that enforces
> authentication while no account exists, so the entrypoint refuses that
> combination and exits with an error rather than starting.
>
> When the combination is valid, a `PASSWORD` is written to `auth.admin_pa`
> instead of being passed to `init-store.sh`, because the admin account is then
> created on server startup. Two consequences worth knowing:
>
> - The password is stored in `conf/rest-server.properties`, so anyone who can
> read that config volume can read it. With init-store enabled the password
> only travels over stdin and is never written to disk.
> - `auth.admin_pa` takes effect only when the admin account is first created.
> Changing `PASSWORD` on a later restart does not rotate an existing admin
> password, and does not report an error.

**Deprecated aliases** (still work but log a warning):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,21 @@ public class ServerOptions extends OptionHolder {
"./conf/graphs"
);

public static final ConfigOption<Boolean> INIT_STORE_ENABLED =
new ConfigOption<>(
"init_store.enabled",
"Whether init-store initializes the local backend stores " +
"and the built-in admin account. Set false in distributed " +
"deployments (PD/HStore) where the storage side already " +
"owns the metadata. Note that setting it false also means " +
"the admin account is not created here, and the only " +
"other component that creates it requires 'usePD' to be " +
"true, so enabling auth without that would leave no " +
"account to authenticate against.",
disallowEmpty(),
true
);

public static final ConfigOption<Boolean> SERVER_START_IGNORE_SINGLE_GRAPH_ERROR =
new ConfigOption<>(
"server.start_ignore_single_graph_error",
Expand Down Expand Up @@ -418,6 +433,13 @@ public class ServerOptions extends OptionHolder {
nonNegativeInt(),
0);

// TODO: these keys are camelCase but conf/rest-server.properties ships them
// as arthas.telnet_port / arthas.http_port / arthas.disabled_commands, so
// nothing matches and operator values are silently replaced by the defaults
// below. Only arthas.ip works. Unnoticed so far because each shipped value
// equals its default, but e.g. arthas.disabled_commands=jad,exec is dropped
// and exec stays enabled. Rename these to snake_case (pre-existing, tracked
// separately from this change).
public static final ConfigOption<String> ARTHAS_TELNET_PORT =
new ConfigOption<>(
"arthas.telnetPort",
Expand Down
143 changes: 134 additions & 9 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,74 @@ set -euo pipefail
DOCKER_FOLDER="./docker"
INIT_FLAG_FILE="init_complete"
GRAPH_CONF="./conf/graphs/hugegraph.properties"
REST_SERVER_CONF="./conf/rest-server.properties"

mkdir -p "${DOCKER_FOLDER}"

log() { echo "[hugegraph-server-entrypoint] $*"; }

# Sets a property to exactly one canonical `key=value` line. Existing
# definitions are matched on any separator a properties file allows (`=`, `:`
# or whitespace) and collapsed into that single line, because leaving a second
# definition behind would make the parser expose the key as a list and a scalar
# read of it would then fail. Comment lines are left alone. Matching is literal,
# so no regex escaping of the key or value is needed.
set_prop() {
local key="$1" val="$2" file="$3"
local esc_key esc_val

SET_PROP_KEY="$key" SET_PROP_VAL="$val" awk '
BEGIN { key = ENVIRON["SET_PROP_KEY"]; val = ENVIRON["SET_PROP_VAL"] }
{
line = $0
probe = line
sub(/^[[:space:]]+/, "", probe)
if (index(probe, key) == 1) {
rest = substr(probe, length(key) + 1)
if (rest ~ /^[[:space:]]*[=:]/ || rest ~ /^[[:space:]]+/) {
if (!done) { print key "=" val; done = 1 }
next
}
}
print line
}
END { if (!done) print key "=" val }
' "${file}" > "${file}.tmp" && mv "${file}.tmp" "${file}"
}

# Escapes a value for Java-properties serialization. Backslashes must be
# doubled or the parser consumes them, so a password like `abc\def` would
# otherwise be read back as `abcdef`; a leading space would be stripped.
props_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/^ /\\ /'
}

# Echoes the value of a property, or nothing when the key or the file is
# absent, so callers apply their own default. Accepts the `=`, `:` and
# whitespace separators that properties files allow. On duplicate keys the last
# one wins, matching how the properties parser reads the same file.
get_prop() {
local key="$1" file="$2"
local esc_key

[[ -f "${file}" ]] || return 0
esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
esc_val=$(printf '%s' "$val" | sed -e 's/[&|\\]/\\&/g')
# '#' delimits the s command because the pattern itself contains '|'
sed -rn "s#^[[:space:]]*${esc_key}([[:space:]]*[=:]|[[:space:]]+)[[:space:]]*(.*)\$#\\2#p" \
"${file}" | tail -n 1 | tr -d '[:space:]'
}

if grep -qE "^[[:space:]]*${esc_key}[[:space:]]*=" "${file}"; then
sed -ri "s|^([[:space:]]*${esc_key}[[:space:]]*=).*|\\1${esc_val}|" "${file}"
else
printf '%s=%s\n' "$key" "$val" >> "${file}"
fi
# Canonicalizes a boolean the way the server does. HugeConfig parses these
# options through commons-configuration2 PropertyConverter.toBoolean, i.e.
# BooleanUtils, which is case-insensitive and accepts y/t/on/yes/true and
# n/f/no/off/false. The shell must agree with it, or the two layers can
# disagree about whether to skip: `FALSE` once meant "skip" to Java and "run"
# to this script. Unrecognized values fail here, as they do in the server.
to_bool() {
case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
y|t|on|yes|true) echo "true" ;;
n|f|no|off|false) echo "false" ;;
*) return 1 ;;
esac
}

migrate_env() {
Expand All @@ -54,19 +105,89 @@ migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"
# ── Map env → properties file ─────────────────────────────────────────
[[ -n "${HG_SERVER_BACKEND:-}" ]] && set_prop "backend" "${HG_SERVER_BACKEND}" "${GRAPH_CONF}"
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}"
if [[ -n "${HG_SERVER_INIT_STORE_ENABLED:-}" ]]; then
# Canonicalize before writing, so the property file only ever holds `true`
# or `false` and cannot be read differently by the shell and the server
if ! HG_SERVER_INIT_STORE_ENABLED=$(to_bool "${HG_SERVER_INIT_STORE_ENABLED}"); then
log "ERROR: HG_SERVER_INIT_STORE_ENABLED must be a boolean, got '${HG_SERVER_INIT_STORE_ENABLED}'"
exit 1
fi
set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}" "${REST_SERVER_CONF}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ get_prop() accepts the =, :, and whitespace separators, but set_prop() only replaces key=value. With a mounted init_store.enabled:false and HG_SERVER_INIT_STORE_ENABLED=true, this appends a second key instead of overriding the first. Commons PropertiesConfiguration then exposes multiple values for this scalar Boolean, so HugeConfig.get(INIT_STORE_ENABLED) no longer receives a Boolean and init-store/server startup fails; the same issue affects a colon-form auth.admin_pa overridden by PASSWORD. Please make set_prop() recognize the same separators and normalize all existing definitions to one canonical key, then add colon/whitespace env-override tests that load the result through HugeConfig.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 3cb4fda. set_prop now matches the same =, : and whitespace separators as get_prop and collapses every existing definition into one canonical key=value line, leaving comments alone. Rewrote it in awk so the key and value are matched and emitted literally, with no regex escaping.

Worth noting it is worse than a stale read: a duplicated key fails the scalar type check while the config is still loading, so new HugeConfig(...) throws Invalid value for key 'init_store.enabled': '[false, true]' and both init-store and server startup fail outright. Pinned that with a test that loads a duplicated key through HugeConfig.

Entrypoint coverage added for a colon-form override, a whitespace-form override, a colon-form auth.admin_pa overridden by PASSWORD, and that commented defaults are not counted as definitions.

fi

# ── Build wait-storage env ─────────────────────────────────────────────
WAIT_ENV=()
[[ -n "${HG_SERVER_BACKEND:-}" ]] && WAIT_ENV+=("hugegraph.backend=${HG_SERVER_BACKEND}")
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] && WAIT_ENV+=("hugegraph.pd.peers=${HG_SERVER_PD_PEERS}")

# ── Init store (once) ─────────────────────────────────────────────────
if [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
wait_storage() {
if (( ${#WAIT_ENV[@]} > 0 )); then
env "${WAIT_ENV[@]}" ./bin/wait-storage.sh
else
./bin/wait-storage.sh
fi
}

# ── Init store (once) ─────────────────────────────────────────────────
# With `init_store.enabled=false` (distributed PD/HStore) init-store is a no-op:
# storage owns the metadata and the admin account is created on server startup
# from `auth.admin_pa`. A requested PASSWORD is therefore written to that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

‼️ The claim that server startup creates the admin is not true for the shipped HStore Docker configurations. GraphManager calls initAdminUserIfNeeded() only inside usePD=true, but ServerOptions.USE_PD defaults to false and the repository's HStore compose files set only the HStore backend and PD peers. With this skip option plus PASSWORD, InitStore no longer creates the admin and normal startup does not take the PD metadata path, so authentication can start without the requested admin account. Please either make bootstrap work for every advertised HStore skip configuration or reject/avoid this mode unless usePD=true, and add an integration test that authenticates with the supplied password.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and worse than a mismatch: with usePD false nothing creates the admin at all. StandardAuthenticator.initAdminUserIfNeeded is called only from InitStore, and GraphManager.initAdminUserIfNeeded only from loadMetaFromPD(), which is gated on usePD.

Took the reject option in a62f6ff. The entrypoint refuses skip + auth unless usePD=true and exits with the reason; InitStore logs the same for non-Docker installs. Skip without auth is unchanged. usePD=true is already the pairing in the cluster-test rest-server.properties.template.

The shipped compose files not setting usePD is pre-existing and left alone, since enabling it also moves graph loading to PD metadata.

# property rather than piped into init-store.sh, where it would be read and
# discarded without creating the account.
#
# The value is read back from the config file rather than from the env var, so
# that a rest-server.properties mounted with the property already set behaves
# the same as `HG_SERVER_INIT_STORE_ENABLED` (the env mapping above has already
# been applied, so env still wins).
INIT_STORE_ENABLED=$(get_prop "init_store.enabled" "${REST_SERVER_CONF}")
if [[ -n "${INIT_STORE_ENABLED}" ]]; then
if ! INIT_STORE_ENABLED=$(to_bool "${INIT_STORE_ENABLED}"); then
log "ERROR: init_store.enabled in ${REST_SERVER_CONF} must be a boolean," \
"got '${INIT_STORE_ENABLED}'"
exit 1
fi
fi
if [[ "${INIT_STORE_ENABLED:-true}" == "false" ]]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Shell and Java use different Boolean semantics here. The shell skips only for exact lowercase false, while HugeConfig accepts case-insensitive false values (and Java-properties syntax also permits separators other than =). With HG_SERVER_INIT_STORE_ENABLED=FALSE and PASSWORD, the shell pipes the password to init-store.sh and later writes init_complete, but Java parses the option as false and returns before creating the admin. A focused exact-head test reproduced this with four failed assertions. Please canonicalize or validate the value consistently across both layers and cover uppercase and mounted-property variants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in a62f6ff. to_bool now mirrors PropertyConverter.toBoolean (BooleanUtils: y/t/on/yes/true, n/f/no/off/false, case-insensitive), canonicalizes before writing so the file only ever holds true or false, and exits non-zero on anything else instead of diverging silently.

get_prop also accepts the : and whitespace separators a properties file allows, not just =. Covered for FALSE, off, no, a non-boolean, and a mounted : separator.

log "init-store disabled, skipping local backend/admin init"

# With init-store skipped, nothing creates the built-in admin account
# unless the server takes the PD metadata path, which it only does when
# `usePD=true`. Enabling auth without that combination starts a server
# that enforces authentication while no account exists, so refuse it here
# rather than fail every request later.
AUTH_REQUESTED=""
[[ -n "${PASSWORD:-}" ]] && AUTH_REQUESTED=1
[[ -n "$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")" ]] && AUTH_REQUESTED=1
if [[ -n "${AUTH_REQUESTED}" ]]; then
USE_PD=$(to_bool "$(get_prop "usePD" "${REST_SERVER_CONF}")" 2>/dev/null || echo "false")
if [[ "${USE_PD}" != "true" ]]; then
log "ERROR: auth is enabled and init_store.enabled=false, but usePD is not true."
log "ERROR: With init-store skipped the admin account is only created on the PD"
log "ERROR: metadata path, so this combination would start a server that nobody"
log "ERROR: can authenticate against."
log "ERROR: Set usePD=true in ${REST_SERVER_CONF}, or leave init-store enabled"
log "ERROR: so that it can create the admin account locally."
exit 1
fi
fi

# Still wait: the server needs the storage side reachable at startup even
# though nothing is initialized here
wait_storage

if [[ -n "${PASSWORD:-}" ]]; then
log "enabling auth mode, admin password applied via auth.admin_pa"
./bin/enable-auth.sh
# TODO: auth.admin_pa only applies when the admin account is first
# created, so changing PASSWORD on a later restart silently keeps the
# old one. It also leaves the password at rest in rest-server.properties,
# unlike the enabled path where it only travels over stdin.
set_prop "auth.admin_pa" "$(props_escape "${PASSWORD}")" "${REST_SERVER_CONF}"
fi
# No init flag is written here: nothing was initialized, so a later run
# with init-store enabled must still perform the real initialization.
elif [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
wait_storage

if [[ -z "${PASSWORD:-}" ]]; then
log "init hugegraph with non-auth mode"
Expand All @@ -76,6 +197,10 @@ if [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
./bin/enable-auth.sh
echo "${PASSWORD}" | ./bin/init-store.sh
fi
# TODO: this flag only tracks "init has run", not what it ran with. On a
# persisted volume it survives env changes, so flipping HG_SERVER_* on a
# later start will not re-init. It also does not survive a Kubernetes pod
# restart, which is why init_store.enabled exists rather than a flag file.
touch "${DOCKER_FOLDER}/${INIT_FLAG_FILE}"
else
log "HugeGraph initialization already done. Skipping re-init..."
Expand Down
Loading
Loading