20260713_2 落地升级环境变量优先级契约(#114 主工作树收口)
This commit is contained in:
@@ -6,6 +6,11 @@ ENV_FILE="${ENV_FILE:-$INSTALLER_ROOT/.env}"
|
||||
ENV_EXAMPLE="$INSTALLER_ROOT/.env.example"
|
||||
COMPOSE_FILE="$INSTALLER_ROOT/compose/docker-compose.yml"
|
||||
MANIFEST_FILE="${MANIFEST_FILE:-$INSTALLER_ROOT/PACKAGE-MANIFEST.env}"
|
||||
declare -a ENV_TEMPLATE_KEYS=()
|
||||
declare -A ENV_TEMPLATE_KEY_SET=()
|
||||
declare -A INVOCATION_ENV_OVERRIDES=()
|
||||
declare -A REUSED_ENV_KEYS=()
|
||||
declare -A ENV_PARSED_VALUES=()
|
||||
|
||||
log() {
|
||||
printf '[ours-rp-installer] %s\n' "$*"
|
||||
@@ -48,20 +53,246 @@ env_file_has_key() {
|
||||
[[ -f "$env_path" ]] && grep -Eq "^${key}=" "$env_path"
|
||||
}
|
||||
|
||||
persist_env_default() {
|
||||
load_env_template_keys() {
|
||||
local env_path="$1"
|
||||
local line
|
||||
local key
|
||||
|
||||
[[ -f "$env_path" ]] || die "missing environment template: $env_path"
|
||||
ENV_TEMPLATE_KEYS=()
|
||||
ENV_TEMPLATE_KEY_SET=()
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
[[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]] || die "invalid environment assignment in $env_path: $line"
|
||||
key="${BASH_REMATCH[1]}"
|
||||
[[ -z "${ENV_TEMPLATE_KEY_SET[$key]+x}" ]] || die "duplicate environment key in $env_path: $key"
|
||||
ENV_TEMPLATE_KEYS+=("$key")
|
||||
ENV_TEMPLATE_KEY_SET["$key"]=1
|
||||
done < "$env_path"
|
||||
|
||||
(( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "no environment assignments found in $env_path"
|
||||
}
|
||||
|
||||
template_has_env_key() {
|
||||
local key="$1"
|
||||
[[ -n "${ENV_TEMPLATE_KEY_SET[$key]+x}" ]]
|
||||
}
|
||||
|
||||
env_file_assignment_keys() {
|
||||
local env_path="$1"
|
||||
local line
|
||||
|
||||
[[ -f "$env_path" ]] || die "missing environment file: $env_path"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]] || continue
|
||||
printf '%s\n' "${BASH_REMATCH[1]}"
|
||||
done < "$env_path"
|
||||
}
|
||||
|
||||
dotenv_decode_value() {
|
||||
local raw_value="$1"
|
||||
local quote
|
||||
local body
|
||||
local decoded=""
|
||||
local character
|
||||
local next_character
|
||||
local index=0
|
||||
|
||||
[[ -n "$raw_value" ]] || {
|
||||
printf '\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
quote="${raw_value:0:1}"
|
||||
case "$quote" in
|
||||
"'")
|
||||
[[ "${raw_value: -1}" == "'" && ${#raw_value} -ge 2 ]] || return 1
|
||||
body="${raw_value:1:${#raw_value}-2}"
|
||||
while (( index < ${#body} )); do
|
||||
character="${body:index:1}"
|
||||
if [[ "$character" == "\\" && $((index + 1)) -lt ${#body} ]]; then
|
||||
next_character="${body:index + 1:1}"
|
||||
case "$next_character" in
|
||||
"'")
|
||||
decoded+="$next_character"
|
||||
((index += 2))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
decoded+="$character"
|
||||
((index += 1))
|
||||
done
|
||||
;;
|
||||
'"')
|
||||
[[ "${raw_value: -1}" == '"' && ${#raw_value} -ge 2 ]] || return 1
|
||||
body="${raw_value:1:${#raw_value}-2}"
|
||||
while (( index < ${#body} )); do
|
||||
character="${body:index:1}"
|
||||
if [[ "$character" == '$' && $((index + 1)) -lt ${#body} && "${body:index + 1:1}" == '$' ]]; then
|
||||
decoded+='$'
|
||||
((index += 2))
|
||||
continue
|
||||
fi
|
||||
if [[ "$character" == "\\" && $((index + 1)) -lt ${#body} ]]; then
|
||||
next_character="${body:index + 1:1}"
|
||||
case "$next_character" in
|
||||
'"'|\\|'$'|'`')
|
||||
decoded+="$next_character"
|
||||
((index += 2))
|
||||
continue
|
||||
;;
|
||||
n)
|
||||
decoded+=$'\n'
|
||||
((index += 2))
|
||||
continue
|
||||
;;
|
||||
r)
|
||||
decoded+=$'\r'
|
||||
((index += 2))
|
||||
continue
|
||||
;;
|
||||
t)
|
||||
decoded+=$'\t'
|
||||
((index += 2))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
decoded+="$character"
|
||||
((index += 1))
|
||||
done
|
||||
;;
|
||||
*)
|
||||
decoded="$raw_value"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s' "$decoded"
|
||||
}
|
||||
|
||||
parse_env_file() {
|
||||
local env_path="$1"
|
||||
local line
|
||||
local parsed_key
|
||||
local raw_value
|
||||
local parsed_value
|
||||
|
||||
[[ -f "$env_path" ]] || die "missing environment file: $env_path"
|
||||
ENV_PARSED_VALUES=()
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
[[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || return 2
|
||||
parsed_key="${BASH_REMATCH[1]}"
|
||||
raw_value="${BASH_REMATCH[2]}"
|
||||
parsed_value="$(dotenv_decode_value "$raw_value")" || return 2
|
||||
ENV_PARSED_VALUES["$parsed_key"]="$parsed_value"
|
||||
done < "$env_path"
|
||||
}
|
||||
|
||||
env_file_value() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
|
||||
parse_env_file "$env_path"
|
||||
|
||||
[[ -n "${ENV_PARSED_VALUES[$key]+x}" ]] || return 3
|
||||
printf '%s' "${ENV_PARSED_VALUES[$key]}"
|
||||
}
|
||||
|
||||
env_file_set_value() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_env
|
||||
if env_file_has_key "$env_path" "$key"; then
|
||||
local encoded_value
|
||||
|
||||
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value for $key contains an unsupported newline"
|
||||
encoded_value="$(env_file_encode_value "$value")"
|
||||
if ! env_file_has_key "$env_path" "$key"; then
|
||||
printf '%s=%s\n' "$key" "$encoded_value" >> "$env_path"
|
||||
return 0
|
||||
fi
|
||||
tmp_env="$(mktemp)"
|
||||
cat "$env_path" > "$tmp_env"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$tmp_env"
|
||||
ENV_FILE_REPLACEMENT="$key=$encoded_value" awk -v key="$key" '
|
||||
BEGIN { done=0 }
|
||||
$0 ~ "^" key "=" { print ENVIRON["ENV_FILE_REPLACEMENT"]; done=1; next }
|
||||
{ print }
|
||||
END { if (!done) print ENVIRON["ENV_FILE_REPLACEMENT"] }
|
||||
' "$env_path" > "$tmp_env"
|
||||
mv "$tmp_env" "$env_path"
|
||||
}
|
||||
|
||||
env_file_encode_value() {
|
||||
local value="$1"
|
||||
local escaped="$value"
|
||||
|
||||
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "environment value contains an unsupported newline"
|
||||
escaped="${escaped//\\/\\\\}"
|
||||
escaped="${escaped//\"/\\\"}"
|
||||
escaped="${escaped//\$/\$\$}"
|
||||
printf '"%s"' "$escaped"
|
||||
}
|
||||
|
||||
load_env_file() {
|
||||
local env_path="$1"
|
||||
local key
|
||||
|
||||
parse_env_file "$env_path" || die "unable to parse environment file: $env_path"
|
||||
for key in "${!ENV_PARSED_VALUES[@]}"; do
|
||||
printf -v "$key" '%s' "${ENV_PARSED_VALUES[$key]}"
|
||||
export "$key"
|
||||
done
|
||||
}
|
||||
|
||||
validate_env_file_contract() {
|
||||
local env_path="$1"
|
||||
local invalid_keys=""
|
||||
local key
|
||||
|
||||
[[ -f "$env_path" ]] || die "missing environment file: $env_path"
|
||||
(( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "environment template keys were not loaded"
|
||||
|
||||
if ! parse_env_file "$env_path"; then
|
||||
die "unable to parse environment file: $env_path"
|
||||
fi
|
||||
|
||||
for key in "${ENV_TEMPLATE_KEYS[@]}"; do
|
||||
if [[ -z "${ENV_PARSED_VALUES[$key]+x}" || -z "${ENV_PARSED_VALUES[$key]}" ]]; then
|
||||
invalid_keys+="$key"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -z "$invalid_keys" ]] || die "environment contract requires non-empty values in $env_path: ${invalid_keys//$'\n'/,}"
|
||||
}
|
||||
|
||||
capture_invocation_env_overrides() {
|
||||
local key
|
||||
|
||||
(( ${#ENV_TEMPLATE_KEYS[@]} > 0 )) || die "environment template keys were not loaded"
|
||||
INVOCATION_ENV_OVERRIDES=()
|
||||
for key in "${ENV_TEMPLATE_KEYS[@]}"; do
|
||||
if [[ -v "$key" ]]; then
|
||||
INVOCATION_ENV_OVERRIDES["$key"]="${!key}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
log_env_key_group() {
|
||||
local group="$1"
|
||||
shift
|
||||
local rendered="none"
|
||||
if (( $# > 0 )); then
|
||||
local IFS=,
|
||||
rendered="$*"
|
||||
fi
|
||||
log "$group=$rendered"
|
||||
}
|
||||
|
||||
env_flag_enabled() {
|
||||
case "${1:-0}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
@@ -156,15 +387,15 @@ assert_arch_compatibility() {
|
||||
|
||||
load_env() {
|
||||
load_manifest
|
||||
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
|
||||
load_env_template_keys "$ENV_EXAMPLE"
|
||||
validate_env_file_contract "$ENV_EXAMPLE"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
|
||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||
log "created .env from .env.example"
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
validate_env_file_contract "$ENV_FILE"
|
||||
load_env_file "$ENV_FILE"
|
||||
|
||||
if [[ -n "${MANIFEST_PACKAGE_ARCH_RAW:-}" ]]; then
|
||||
manifest_package_arch="$(normalize_arch "$MANIFEST_PACKAGE_ARCH_RAW")" || die "unsupported manifest package arch: $MANIFEST_PACKAGE_ARCH_RAW"
|
||||
@@ -190,27 +421,6 @@ load_env() {
|
||||
PACKAGE_PLATFORM="${PACKAGE_PLATFORM:-$(platform_for_arch "$PACKAGE_ARCH")}"
|
||||
fi
|
||||
|
||||
HOST_DATA_DIR="${HOST_DATA_DIR:-/var/lib/ours-rp-${PACKAGE_ARCH}-installer}"
|
||||
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ours-rp-${PACKAGE_ARCH}-installer}"
|
||||
RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
|
||||
RPKI_PLATFORM="${RPKI_PLATFORM:-$PACKAGE_PLATFORM}"
|
||||
METRICS_IMAGE="${METRICS_IMAGE:-ours-rp-metrics-${PACKAGE_ARCH}:${source_commit_short:-unknown}}"
|
||||
METRICS_PLATFORM="${METRICS_PLATFORM:-$PACKAGE_PLATFORM}"
|
||||
RTR_REPORT_DIR="${RTR_REPORT_DIR:-$HOST_DATA_DIR/empty-rtr-report}"
|
||||
RTR_REPORT_CONTAINER_DIR="${RTR_REPORT_CONTAINER_DIR:-/var/lib/ours-rp/rtr-report}"
|
||||
MONITOR_PLATFORM="${MONITOR_PLATFORM:-$PACKAGE_PLATFORM}"
|
||||
PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}"
|
||||
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
|
||||
METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}"
|
||||
ALLOW_CROSS_ARCH="${ALLOW_CROSS_ARCH:-0}"
|
||||
RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}"
|
||||
FIRST_RUN_WAIT_TIMEOUT_SECS="${FIRST_RUN_WAIT_TIMEOUT_SECS:-7200}"
|
||||
|
||||
persist_env_default "$ENV_FILE" "METRICS_IMAGE" "$METRICS_IMAGE"
|
||||
persist_env_default "$ENV_FILE" "METRICS_PLATFORM" "$METRICS_PLATFORM"
|
||||
persist_env_default "$ENV_FILE" "RTR_REPORT_DIR" "$RTR_REPORT_DIR"
|
||||
persist_env_default "$ENV_FILE" "RTR_REPORT_CONTAINER_DIR" "$RTR_REPORT_CONTAINER_DIR"
|
||||
persist_env_default "$ENV_FILE" "RESOURCE_VALIDATION_MODE" "$RESOURCE_VALIDATION_MODE"
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
|
||||
@@ -13,6 +13,8 @@ Usage: ./scripts/upgrade.sh [--reuse-env-from /path/to/.env] [--keep-reused-imag
|
||||
|
||||
By default, --reuse-env-from creates the new .env from this package's
|
||||
.env.example first, then overlays existing user settings from the old .env.
|
||||
For declared configuration keys, explicit invocation variables have highest
|
||||
priority, for example: INTERVAL_SECS=300 ./scripts/upgrade.sh ...
|
||||
Image tags are intentionally kept from the new package so the upgraded service
|
||||
actually runs the new packaged runtime and monitor images. Use
|
||||
--keep-reused-image only when you intentionally want to keep previous image tags.
|
||||
@@ -39,56 +41,80 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
env_get_key() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
awk -F= -v key="$key" '$1 == key {sub(/^[^=]*=/, ""); print; exit}' "$env_path"
|
||||
}
|
||||
|
||||
env_set_key() {
|
||||
local env_path="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_env
|
||||
if grep -q "^${key}=" "$env_path"; then
|
||||
tmp_env="$(mktemp)"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { done=0 }
|
||||
$0 ~ "^" key "=" { print key "=" value; done=1; next }
|
||||
{ print }
|
||||
END { if (!done) print key "=" value }
|
||||
' "$env_path" > "$tmp_env"
|
||||
mv "$tmp_env" "$env_path"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >> "$env_path"
|
||||
fi
|
||||
}
|
||||
|
||||
overlay_reused_env() {
|
||||
local source_env="$1"
|
||||
local target_env="$2"
|
||||
local key
|
||||
local value
|
||||
while IFS='=' read -r key _; do
|
||||
[[ -n "$key" ]] || continue
|
||||
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
||||
local -a reused_keys=()
|
||||
local -a ignored_removed_keys=()
|
||||
local -a preserved_package_keys=()
|
||||
|
||||
REUSED_ENV_KEYS=()
|
||||
parse_env_file "$source_env" || die "unable to parse reused environment: $source_env"
|
||||
while IFS= read -r key; do
|
||||
if ! template_has_env_key "$key"; then
|
||||
ignored_removed_keys+=("$key")
|
||||
continue
|
||||
fi
|
||||
case "$key" in
|
||||
PACKAGE_ARCH|PACKAGE_PLATFORM|RPKI_PLATFORM|METRICS_PLATFORM|MONITOR_PLATFORM)
|
||||
preserved_package_keys+=("$key")
|
||||
continue
|
||||
;;
|
||||
RPKI_IMAGE|METRICS_IMAGE|PROMETHEUS_IMAGE|GRAFANA_IMAGE)
|
||||
[[ "$UPDATE_PACKAGE_IMAGE" == "0" ]] || continue
|
||||
if [[ "$UPDATE_PACKAGE_IMAGE" != "0" ]]; then
|
||||
preserved_package_keys+=("$key")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
value="$(env_get_key "$source_env" "$key")"
|
||||
env_set_key "$target_env" "$key" "$value"
|
||||
done < <(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$source_env" || true)
|
||||
[[ -n "${ENV_PARSED_VALUES[$key]+x}" ]] || die "unable to read $key from reused environment: $source_env"
|
||||
value="${ENV_PARSED_VALUES[$key]}"
|
||||
env_file_set_value "$target_env" "$key" "$value"
|
||||
REUSED_ENV_KEYS["$key"]=1
|
||||
reused_keys+=("$key")
|
||||
done < <(env_file_assignment_keys "$source_env")
|
||||
|
||||
log_env_key_group "reused_keys" "${reused_keys[@]}"
|
||||
log_env_key_group "ignored_removed_old_keys" "${ignored_removed_keys[@]}"
|
||||
log_env_key_group "preserved_package_keys" "${preserved_package_keys[@]}"
|
||||
}
|
||||
|
||||
apply_invocation_env_overrides() {
|
||||
local target_env="$1"
|
||||
local key
|
||||
local -a overridden_keys=()
|
||||
|
||||
for key in "${ENV_TEMPLATE_KEYS[@]}"; do
|
||||
[[ -n "${INVOCATION_ENV_OVERRIDES[$key]+x}" ]] || continue
|
||||
env_file_set_value "$target_env" "$key" "${INVOCATION_ENV_OVERRIDES[$key]}"
|
||||
overridden_keys+=("$key")
|
||||
done
|
||||
|
||||
log_env_key_group "command_override_keys" "${overridden_keys[@]}"
|
||||
}
|
||||
|
||||
log_template_only_keys() {
|
||||
local key
|
||||
local -a template_only_keys=()
|
||||
|
||||
for key in "${ENV_TEMPLATE_KEYS[@]}"; do
|
||||
[[ -n "${REUSED_ENV_KEYS[$key]+x}" ]] && continue
|
||||
[[ -n "${INVOCATION_ENV_OVERRIDES[$key]+x}" ]] && continue
|
||||
template_only_keys+=("$key")
|
||||
done
|
||||
log_env_key_group "template_only_keys" "${template_only_keys[@]}"
|
||||
}
|
||||
|
||||
load_env_template_keys "$ENV_EXAMPLE"
|
||||
validate_env_file_contract "$ENV_EXAMPLE"
|
||||
capture_invocation_env_overrides
|
||||
REUSED_ENV_KEYS=()
|
||||
|
||||
if [[ -n "$REUSE_ENV_FROM" ]]; then
|
||||
[[ -f "$REUSE_ENV_FROM" ]] || die "missing reuse env file: $REUSE_ENV_FROM"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
|
||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||
overlay_reused_env "$REUSE_ENV_FROM" "$ENV_FILE"
|
||||
log "created new package env from .env.example and overlaid user settings from $REUSE_ENV_FROM"
|
||||
@@ -97,6 +123,13 @@ if [[ -n "$REUSE_ENV_FROM" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||
log "created new package env from .env.example"
|
||||
fi
|
||||
apply_invocation_env_overrides "$ENV_FILE"
|
||||
log_template_only_keys
|
||||
validate_env_file_contract "$ENV_FILE"
|
||||
load_env
|
||||
assert_arch_compatibility
|
||||
create_data_dirs
|
||||
|
||||
Reference in New Issue
Block a user