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
3 changes: 3 additions & 0 deletions hugegraph-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ RUN sed -i "s/^restserver.url.*$/restserver.url=http:\/\/0.0.0.0:8080/g" ./conf/
COPY hugegraph-server/hugegraph-dist/docker/scripts/remote-connect.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/scripts/detect-storage.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh .
# props.awk needs no COPY: it ships in the assembly bin/ above, which is also
# where bin/enable-auth.sh finds it. yamlscan.awk serves only the entrypoint.
COPY hugegraph-server/hugegraph-dist/docker/yamlscan.awk .
RUN chmod 755 ./docker-entrypoint.sh

EXPOSE 8080
Expand Down
3 changes: 3 additions & 0 deletions hugegraph-server/Dockerfile-hstore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ RUN cd /hugegraph-server/conf/graphs \
COPY hugegraph-server/hugegraph-dist/docker/scripts/remote-connect.groovy ./scripts
#COPY hugegraph-server/hugegraph-dist/docker/scripts/detect-storage.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh .
# props.awk needs no COPY: it ships in the assembly bin/ above, which is also
# where bin/enable-auth.sh finds it. yamlscan.awk serves only the entrypoint.
COPY hugegraph-server/hugegraph-dist/docker/yamlscan.awk .
RUN chmod 755 ./docker-entrypoint.sh

EXPOSE 8080
Expand Down
96 changes: 96 additions & 0 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ trap 'rm -rf "${TEST_HOME}"' EXIT

mkdir -p "${TEST_HOME}/bin" "${TEST_HOME}/conf/graphs" "${TEST_HOME}/docker"
cp "${SCRIPT_DIR}/docker-entrypoint.sh" "${TEST_HOME}/docker-entrypoint.sh"
# props.awk is packaged in the release bin/; the image gets it from there, and
# the entrypoint accepts it beside itself so this harness can stage either.
cp "${SCRIPT_DIR}/../src/assembly/static/bin/props.awk" "${TEST_HOME}/props.awk"
cp "${SCRIPT_DIR}/yamlscan.awk" "${TEST_HOME}/yamlscan.awk"
touch "${TEST_HOME}/docker/init_complete"

cat > "${TEST_HOME}/conf/rest-server.properties" <<'EOF'
Expand Down Expand Up @@ -54,6 +58,7 @@ printf 'called\n' >> ./docker/enable-auth-calls
EOF
cat > "${TEST_HOME}/bin/wait-partition.sh" <<'EOF'
#!/usr/bin/env bash
printf 'called\n' >> ./docker/wait-partition-calls
exit 0
EOF
cat > "${TEST_HOME}/bin/wait-storage.sh" <<'EOF'
Expand Down Expand Up @@ -324,4 +329,95 @@ done
)
assert_start_timeout 120

# A mounted rest-server.properties that already carries auth.authenticator,
# with no matching yaml mapping and no PASSWORD given, used to start without a
# word: the parity check ran only inside the PASSWORD branch, so nothing ever
# compared the two sides and the server came up with REST enforcing and Gremlin
# on AllowAllAuthenticator. The check now runs on every start, and a refusal
# has to come before anything touches the backend.
printf '%s\n' 'host: 8182' > "${TEST_HOME}/conf/gremlin-server.yaml"
grep -qx 'auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator' \
"${TEST_HOME}/conf/rest-server.properties" ||
printf '%s\n' \
'auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator' \
>> "${TEST_HOME}/conf/rest-server.properties"
rm -f "${TEST_HOME}/docker/init_complete"
before_calls="$(wc -l < "${TEST_HOME}/docker/init-store-calls")"
before_auth="$(wc -l < "${TEST_HOME}/docker/enable-auth-calls")"
status=0
(
cd "${TEST_HOME}"
bash ./docker-entrypoint.sh
) || status=$?
if (( status == 0 )); then
echo "entrypoint must refuse a mounted REST-only authenticator with no PASSWORD" >&2
exit 1
fi
if [[ "$(wc -l < "${TEST_HOME}/docker/init-store-calls")" != "${before_calls}" ]]; then
echo "the refusal must happen before init-store runs" >&2
exit 1
fi
if [[ "$(wc -l < "${TEST_HOME}/docker/enable-auth-calls")" != "${before_auth}" ]]; then
echo "a refused start must not run enable-auth.sh" >&2
exit 1
fi

# The same start is accepted once both sides agree, so the check above is a
# parity decision and not a blanket refusal to run without PASSWORD.
printf '%s\n' \
'authentication: {' \
' authenticator: org.apache.hugegraph.auth.StandardAuthenticator,' \
' config: {tokens: conf/rest-server.properties}' \
'}' > "${TEST_HOME}/conf/gremlin-server.yaml"
rm -f "${TEST_HOME}/docker/init_complete"
(
cd "${TEST_HOME}"
bash ./docker-entrypoint.sh
)

# ── The stabilization check follows the backend the JVM actually loaded ──
# ACTUAL_BACKEND is compared against a literal, so it has to be the decoded
# value. A mounted hugegraph.properties may spell the word with a unicode
# escape for the s, which java.util.Properties hands the server as hstore;
# reading the on-disk escaping instead compared something else to hstore,
# skipped wait-partition.sh, and let startup continue before the partitions
# were assigned. bs is the backslash, taken from its code point rather than
# written here: printf '%c' 92 hands back the digit 9, which would have built a
# fixture holding a different word than the one being decoded.
bs=$(awk 'BEGIN { printf "%c", 92 }')
if [[ "${#bs}" != 1 || "$(printf '%d' "'${bs}")" != 92 ]]; then
echo "this host did not yield a backslash for code point 92" >&2
exit 1
fi
touch "${TEST_HOME}/docker/init_complete"
rm -f "${TEST_HOME}/docker/wait-partition-calls"
printf '%s\n' "backend=h${bs}u0073tore" 'pd.peers=pd:8686' \
> "${TEST_HOME}/conf/graphs/hugegraph.properties"
if [[ "$(head -n 1 "${TEST_HOME}/conf/graphs/hugegraph.properties")" != \
"backend=h${bs}u0073tore" ]]; then
echo "the fixture has to hold the escaped bytes, not the decoded word" >&2
exit 1
fi
(
cd "${TEST_HOME}"
bash ./docker-entrypoint.sh
)
if [[ ! -s "${TEST_HOME}/docker/wait-partition-calls" ]]; then
echo "an escaped hstore backend must still reach wait-partition.sh" >&2
exit 1
fi
# The other half: this is a read that follows the server, not a switch that
# simply always waits.
rm -f "${TEST_HOME}/docker/wait-partition-calls"
printf '%s\n' 'backend=rocksdb' 'pd.peers=pd:8686' \
> "${TEST_HOME}/conf/graphs/hugegraph.properties"
(
cd "${TEST_HOME}"
bash ./docker-entrypoint.sh
)
if [[ -e "${TEST_HOME}/docker/wait-partition-calls" ]]; then
echo "wait-partition.sh ran for a rocksdb backend" >&2
exit 1
fi

echo "PASS: Docker entrypoint configures HStore discovery and authentication"
141 changes: 124 additions & 17 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@ mkdir -p "${DOCKER_FOLDER}"

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

# Property reading/writing goes through props.awk, which implements the
# java.util.Properties grammar HugeConfig applies (escapes, `:`/whitespace
# separators, CR/CRLF/LF line terminators, continuations, first-definition-wins
# duplicates). grep/sed rewrites disagree with it on mounted or upgraded
# configs, silently producing two definitions of one key. Values move through
# environment variables rather than argv so a PASSWORD never shows up in `ps`
# output.
#
# props.awk lives in the packaged bin/ directory because bin/enable-auth.sh
# reads properties with it too, and that assembly fileSet is what both the
# release tarball and this image are built from. Beside the entrypoint is only
# where the source tree and the tests put it.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
props_from_env="${PROPS_AWK:-}"
yaml_from_env="${YAMLSCAN_AWK:-}"
PROPS_AWK=""
for candidate in "${props_from_env}" "${HERE}/props.awk" "${HERE}/bin/props.awk"; do
if [[ -n "${candidate}" && -f "${candidate}" ]]; then
PROPS_AWK="${candidate}"
break
fi
done
if [[ -z "${PROPS_AWK}" ]]; then
log "ERROR: props.awk not found beside the entrypoint or in bin/"
exit 1
fi

YAMLSCAN=""
for candidate in "${yaml_from_env}" "${HERE}/yamlscan.awk"; do
if [[ -n "${candidate}" && -f "${candidate}" ]]; then
YAMLSCAN="${candidate}"
break
fi
done
if [[ -z "${YAMLSCAN}" ]]; then
log "ERROR: yamlscan.awk not found beside the entrypoint"
exit 1
fi

encode_prop_value() {
local value="$1" encoded="" char
local i
Expand All @@ -48,18 +87,10 @@ encode_prop_value() {

set_prop_encoded() {
local key="$1" encoded_val="$2" file="$3"
local esc_key esc_val key_re

esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
esc_val=$(printf '%s' "$encoded_val" | sed -e 's/[&|\\~]/\\&/g')
key_re="^[[:space:]]*${esc_key}([[:space:]]*[:=]|[[:space:]]+|[[:space:]]*$)"

if grep -qE "${key_re}" "${file}"; then
sed -ri "0,/${key_re}/!{/${key_re}/d;}" "${file}"
sed -ri "0,/${key_re}/s~${key_re}.*~${key}=${esc_val}~" "${file}"
else
printf '%s=%s\n' "$key" "$encoded_val" >> "${file}"
fi
PROPS_MODE=set PROPS_KEY="${key}" \
PROPS_VALUE_ENCODED="${encoded_val}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

set_prop() {
Expand All @@ -70,12 +101,72 @@ set_prop() {

get_prop_encoded() {
local key="$1" file="$2"
local esc_key

esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
sed -nE \
"s~^[[:space:]]*${esc_key}([[:space:]]*[:=][[:space:]]*|[[:space:]]+)(.*)$~\\2~p" \
"${file}" | head -n 1
PROPS_MODE=get PROPS_KEY="${key}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

# The value as java.util.Properties hands it to the server, escapes resolved.
# Compare against this, not the on-disk bytes: `backend=h\u0073tore` is a legal
# spelling of hstore that the JVM reads as hstore and a string compare against
# the raw text does not.
get_prop_decoded() {
local key="$1" file="$2"

PROPS_MODE=get PROPS_DECODED=1 PROPS_KEY="${key}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

# What the top-level authentication mapping of gremlin-server.yaml says about
# authentication, as one of three states: none, named, nameless.
#
# The question and its answer live in yamlscan.awk, which reads the mapping the
# way snakeyaml presents it to the server: only a column-0 `authentication`
# mapping counts, only its direct `authenticator` child names a class, comment
# text never counts as content, and a nested `config.authenticator` belongs to
# the config map rather than to the server. Those distinctions are the whole
# decision -- an earlier grep-shaped version of this function reported `named`
# for `authentication: {} # authenticator: X` and for a class nested under
# `config:`, which passed the REST/Gremlin parity check while Gremlin was
# running on AllowAllAuthenticator.
yaml_auth_state() {
local yaml="./conf/gremlin-server.yaml"

[[ -f "${yaml}" ]] || { echo "none"; return 0; }
awk -f "${YAMLSCAN}" "${yaml}"
}

# Authentication has to be configured on both sides or on neither. A mounted
# config carrying only one is refused rather than completed: the entrypoint
# cannot know which class the operator means, and finishing the other side from
# a guessed default is how Gremlin ends up on AllowAllAuthenticator while REST
# enforces StandardAuthenticator. A mapping that names no authenticator is
# refused by itself, because enable-auth.sh guards on the presence of that
# mapping and would otherwise write only the REST side.
check_auth_sides() {
local rest=0 yaml=0 state

state=$(yaml_auth_state)
if [[ "${state}" == "nameless" ]]; then
log "ERROR: gremlin-server.yaml carries a top-level authentication" \
"mapping that names no authenticator; add an authenticator entry" \
"to it or remove the mapping, then restart."
return 1
fi
if [[ -n "$(get_prop_encoded "auth.authenticator" "${REST_SERVER_CONF}")" ]]; then
rest=1
fi
if [[ "${state}" == "named" ]]; then
yaml=1
fi
if (( rest == yaml )); then
return 0
fi
log "ERROR: authentication is configured in only one of" \
"rest-server.properties (auth.authenticator) and" \
"gremlin-server.yaml (authentication.authenticator);" \
"configure both or neither, then restart."
return 1
}

migrate_env() {
Expand Down Expand Up @@ -168,6 +259,14 @@ elif [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then
set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \
"${GRAPH_CONF}"
fi
# Both sides have to agree whether authentication is on, whatever the reason
# the container was started for. Running this only inside the PASSWORD branch
# below left a mounted rest-server.properties that carried auth.authenticator
# with no matching yaml mapping completely unvalidated: with no PASSWORD the
# entrypoint skipped the check, enable-auth.sh never ran, and the server came
# up with REST enforcing and Gremlin open. A refusal exits under set -e.
check_auth_sides

if [[ -n "${PASSWORD:-}" ]]; then
set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"
# This script is idempotent and must run outside the initialization guard:
Expand Down Expand Up @@ -243,7 +342,15 @@ fi
./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t "${SERVER_STARTUP_TIMEOUT_S}"

# Post-startup cluster stabilization check (hstore only — rocksdb has no partitions)
ACTUAL_BACKEND=$(grep -E '^[[:space:]]*backend[[:space:]]*=' "${GRAPH_CONF}" | head -n 1 | sed 's/.*=//' | tr -d '[:space:]' || true)
# Read through props.awk so a mounted config using the `:` or bare-whitespace
# separator is seen at all, and first-definition-wins matches HugeConfig; the
# grep this replaces only ever accepted `=`. Decoded, because this is compared
# against a literal: the JVM reads `backend=h\u0073tore` as hstore while the
# on-disk bytes are not that string, and the comparison deciding to skip
# wait-partition.sh is how startup continued before partitions were assigned.
# Trailing whitespace is dropped here rather than in the reader, which reports
# the value verbatim apart from the escapes java.util.Properties resolves.
ACTUAL_BACKEND=$(get_prop_decoded "backend" "${GRAPH_CONF}" | tr -d '[:space:]' || true)
if [[ "${ACTUAL_BACKEND}" == "hstore" ]]; then
STORE_REST="${STORE_REST:-store:8520}"
export STORE_REST
Expand Down
Loading