Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d209fc3222 | ||
|
|
d7fe56f6fe | ||
|
|
d1986678bc | ||
|
|
6af2633936 |
+13
-3
@@ -256,12 +256,22 @@ steps:
|
|||||||
# MCPO_API_KEY, OAUTH_CLIENT_ID, WEBUI_URL, and other non-secret config — are
|
# MCPO_API_KEY, OAUTH_CLIENT_ID, WEBUI_URL, and other non-secret config — are
|
||||||
# left completely untouched. MCPO_API_KEY migration is deferred to a
|
# left completely untouched. MCPO_API_KEY migration is deferred to a
|
||||||
# follow-up; this step never reads or writes it.
|
# follow-up; this step never reads or writes it.
|
||||||
|
#
|
||||||
|
# IMPORTANT: ai.yaml's litellm service references ${AI_AWS_ACCESS_KEY_ID} /
|
||||||
|
# ${AI_AWS_SECRET_ACCESS_KEY} (AI_-prefixed) and renders them into the
|
||||||
|
# container as plain AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (see commit
|
||||||
|
# 37ed671a, "Change AWS keys to use Woodpecker Secrets"). So ai.env must be
|
||||||
|
# written with the AI_-prefixed key names, NOT the plain ones — writing
|
||||||
|
# plain AWS_ACCESS_KEY_ID here would leave ${AI_AWS_ACCESS_KEY_ID}
|
||||||
|
# unresolved at compose-render time (empty), silently breaking Bedrock auth.
|
||||||
|
# All other migrated vars in ai.yaml use plain (unprefixed) names, so only
|
||||||
|
# these two lines need the AI_ prefix.
|
||||||
ssh -o StrictHostKeyChecking=no root@$${SWARM_MANAGER_IP} "FILE=/volume1/docker/compose-files/ai/ai.env
|
ssh -o StrictHostKeyChecking=no root@$${SWARM_MANAGER_IP} "FILE=/volume1/docker/compose-files/ai/ai.env
|
||||||
TMP=\$FILE.tmp.\$\$
|
TMP=\$FILE.tmp.\$\$
|
||||||
grep -vE '^(AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|LITELLM_MASTER_KEY|LITELLM_SALT_KEY|POSTGRES_PASSWORD|DATABASE_URL|WEBUI_SECRET_KEY|OPEN_WEBUI_DATABASE_URL|OAUTH_CLIENT_SECRET)=' \$FILE > \$TMP 2>/dev/null || touch \$TMP
|
grep -vE '^(AI_AWS_ACCESS_KEY_ID|AI_AWS_SECRET_ACCESS_KEY|LITELLM_MASTER_KEY|LITELLM_SALT_KEY|POSTGRES_PASSWORD|DATABASE_URL|WEBUI_SECRET_KEY|OPEN_WEBUI_DATABASE_URL|OAUTH_CLIENT_SECRET)=' \$FILE > \$TMP 2>/dev/null || touch \$TMP
|
||||||
{ cat \$TMP
|
{ cat \$TMP
|
||||||
printf 'AWS_ACCESS_KEY_ID=%s\n' '$${AI_AWS_ACCESS_KEY_ID}'
|
printf 'AI_AWS_ACCESS_KEY_ID=%s\n' '$${AI_AWS_ACCESS_KEY_ID}'
|
||||||
printf 'AWS_SECRET_ACCESS_KEY=%s\n' '$${AI_AWS_SECRET_ACCESS_KEY}'
|
printf 'AI_AWS_SECRET_ACCESS_KEY=%s\n' '$${AI_AWS_SECRET_ACCESS_KEY}'
|
||||||
printf 'LITELLM_MASTER_KEY=%s\n' '$${AI_LITELLM_MASTER_KEY}'
|
printf 'LITELLM_MASTER_KEY=%s\n' '$${AI_LITELLM_MASTER_KEY}'
|
||||||
printf 'LITELLM_SALT_KEY=%s\n' '$${AI_LITELLM_SALT_KEY}'
|
printf 'LITELLM_SALT_KEY=%s\n' '$${AI_LITELLM_SALT_KEY}'
|
||||||
printf 'POSTGRES_PASSWORD=%s\n' '$${AI_LITELLM_DB_PASSWORD}'
|
printf 'POSTGRES_PASSWORD=%s\n' '$${AI_LITELLM_DB_PASSWORD}'
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# NOTE: litellm's AWS/DB/master-key secrets, open-webui's secret key/DB URL,
|
||||||
|
# and the OAuth client secret are provisioned into ai/ai.env at deploy time
|
||||||
|
# from Woodpecker secrets (see PRs #4, #6, #7) -- not committed here.
|
||||||
|
# MCPO_API_KEY (used by mcpo and mcpo-critical) is NOT yet migrated; it
|
||||||
|
# remains manually managed in ai/ai.env by design (see PR #4 discussion).
|
||||||
services:
|
services:
|
||||||
open-webui:
|
open-webui:
|
||||||
image: ghcr.io/open-webui/open-webui:0.11.1
|
image: ghcr.io/open-webui/open-webui:0.11.1
|
||||||
|
|||||||
+46
-2
@@ -22,14 +22,58 @@ def merge_envs(base_path, override_path):
|
|||||||
merged = {**base, **override}
|
merged = {**base, **override}
|
||||||
return list(merged.items())
|
return list(merged.items())
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 2026-08-26 FIX — double-interpolation truncation bug (Pattern B stacks):
|
||||||
|
#
|
||||||
|
# stack-deploy.sh's single-file/no-extras path is:
|
||||||
|
# envsubst "$VARS" < ai.yaml | docker stack deploy -c - ai
|
||||||
|
#
|
||||||
|
# envsubst substitutes ${VAR} placeholders in the compose YAML with the
|
||||||
|
# literal, raw value of each shell-exported variable. If that raw value
|
||||||
|
# itself contains a literal '$' followed by word characters (e.g. a
|
||||||
|
# randomly-generated secret like "...i*Edu$RyAVYTqr4yzSS##..."), the
|
||||||
|
# resulting YAML text now contains what LOOKS like a second variable
|
||||||
|
# reference. `docker stack deploy -c -` runs Compose's own interpolation
|
||||||
|
# pass on that YAML text before creating the service — and Compose sees
|
||||||
|
# that leftover "$RyAVYTqr4yzSS", finds no such env var, and silently
|
||||||
|
# substitutes empty string. The secret gets truncated in the running
|
||||||
|
# container with NO error or warning.
|
||||||
|
#
|
||||||
|
# Confirmed impact (2026-08-26): LITELLM_MASTER_KEY and LITELLM_SALT_KEY
|
||||||
|
# in the `ai` stack were both truncated at their first literal '$' after
|
||||||
|
# a real deploy — 87-char secret arrived in the container as 73 chars.
|
||||||
|
#
|
||||||
|
# This affects every stack using Pattern B (host .env + envsubst, not
|
||||||
|
# native Docker secrets): ai, maintenance, media, unifi, guacamole,
|
||||||
|
# security, auth, traefik, meshcentral, ddm — any of them could have a
|
||||||
|
# '$'-containing value silently truncating right now without detection,
|
||||||
|
# since the failure is silent and only visible by diffing the source
|
||||||
|
# value against the live container env.
|
||||||
|
#
|
||||||
|
# Fix: escape every literal '$' in a value as '$$' at export time, BEFORE
|
||||||
|
# envsubst ever sees it. envsubst does not interpret '$' in the
|
||||||
|
# replacement text (only in the template), so the doubled dollar survives
|
||||||
|
# envsubst untouched. Compose's interpolation pass then consumes exactly
|
||||||
|
# one level of escaping ('$$' -> literal '$'), landing on the correct
|
||||||
|
# original single '$' with no leftover variable-reference lookalike.
|
||||||
|
#
|
||||||
|
# Only applied in export/export_merged (which feed `eval` to set the
|
||||||
|
# actual values envsubst reads) — NOT in vars/vars_merged, which just
|
||||||
|
# build envsubst's space-separated $VARNAME allowlist string and have
|
||||||
|
# nothing to do with actual values.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def escape_dollar(v):
|
||||||
|
return v.replace('$', '$$')
|
||||||
|
|
||||||
mode = sys.argv[1]
|
mode = sys.argv[1]
|
||||||
if mode == 'export':
|
if mode == 'export':
|
||||||
for k, v in parse_env(sys.argv[2]):
|
for k, v in parse_env(sys.argv[2]):
|
||||||
print('export {}={}'.format(k, repr(v)))
|
print('export {}={}'.format(k, repr(escape_dollar(v))))
|
||||||
elif mode == 'export_merged':
|
elif mode == 'export_merged':
|
||||||
# export_merged <global.env> <stack.env>
|
# export_merged <global.env> <stack.env>
|
||||||
for k, v in merge_envs(sys.argv[2], sys.argv[3]):
|
for k, v in merge_envs(sys.argv[2], sys.argv[3]):
|
||||||
print('export {}={}'.format(k, repr(v)))
|
print('export {}={}'.format(k, repr(escape_dollar(v))))
|
||||||
elif mode == 'vars':
|
elif mode == 'vars':
|
||||||
print(' '.join('$' + k for k, v in parse_env(sys.argv[2])))
|
print(' '.join('$' + k for k, v in parse_env(sys.argv[2])))
|
||||||
elif mode == 'vars_merged':
|
elif mode == 'vars_merged':
|
||||||
|
|||||||
Reference in New Issue
Block a user