Fix plugin updates.

This commit is contained in:
Greyson Parrelli
2026-08-06 22:52:15 -04:00
parent 0f43f6c1f2
commit 3406c622c5
6 changed files with 224 additions and 35 deletions
+98 -15
View File
@@ -210,7 +210,7 @@ fn-ga-apply-list-change() {
return 0
fi
if ! fn-ga-service-reads-app-lists; then
dokku_log_info1 "recreating the auth service so it can read per-app lists"
dokku_log_info1 "recreating the auth service so it can read per-app lists (this rebuilds the image if the plugin was updated)"
fn-ga-service-start
return 0
fi
@@ -275,7 +275,7 @@ fn-ga-app-set-enabled() {
local dir
dir="$(fn-ga-app-dir "$APP")"
if [[ "$ENABLED" == "true" ]]; then
mkdir -p "$dir"
dir="$(fn-ga-app-dir-ensure "$APP")"
touch "$dir/enabled"
else
rm -f "$dir/enabled"
@@ -326,9 +326,8 @@ fn-ga-validate-pattern() {
fn-ga-exclude-add() {
declare APP="$1" PATTERN="$2"
local dir file
dir="$(fn-ga-app-dir "$APP")"
dir="$(fn-ga-app-dir-ensure "$APP")"
file="$dir/excludes"
mkdir -p "$dir"
touch "$file"
grep -qxF "$PATTERN" "$file" || printf '%s\n' "$PATTERN" >>"$file"
}
@@ -605,29 +604,103 @@ fn-ga-image-exists() {
docker image inspect "$GOOGLE_AUTH_IMAGE" >/dev/null 2>&1
}
# Fingerprint of everything that ends up in the service image. The image is
# labelled with it so an upgraded plugin rebuilds instead of reusing a binary
# built from older source. Without this the shell half of the plugin upgrades
# the moment the files change — new commands, new nginx config, new env var —
# while the binary enforcing them stays whatever was built first, and the
# mismatch is invisible from the outside.
fn-ga-source-hash() {
local dir="$GOOGLE_AUTH_PLUGIN_DIR" file
{
while IFS= read -r file; do
printf '%s ' "${file#"$dir/"}"
sha256sum "$file" | awk '{print $1}'
done < <(
{
printf '%s\n' "$dir/Dockerfile" "$dir/go.mod"
find "$dir/cmd" "$dir/internal" -type f 2>/dev/null
} | LC_ALL=C sort
)
} | sha256sum | awk '{print $1}'
}
# True when the built image matches the plugin's current source.
fn-ga-image-current() {
fn-ga-image-exists || return 1
local labelled
labelled="$(docker image inspect -f '{{index .Config.Labels "google-auth.source-hash"}}' "$GOOGLE_AUTH_IMAGE" 2>/dev/null)"
# Images built before this label existed report an empty value or "<no value>",
# and are stale by definition.
[[ -n "$labelled" && "$labelled" != "<no value>" ]] || return 1
[[ "$labelled" == "$(fn-ga-source-hash)" ]]
}
fn-ga-build-image() {
command -v docker >/dev/null 2>&1 || dokku_log_fail "docker is required to build the google-auth service image"
dokku_log_info1 "Building $GOOGLE_AUTH_IMAGE (first build downloads the Go toolchain image; this can take a few minutes)"
docker image build -t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR"
docker image build \
--label "google-auth.source-hash=$(fn-ga-source-hash)" \
-t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR"
}
fn-ga-service-running() {
[[ "$(docker container inspect -f '{{.State.Running}}' "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" == "true" ]]
}
# True when the running container can actually read per-app lists. That takes
# both halves: the bind mount, and the environment variable pointing at it.
# Either one alone makes the service ignore every per-app list and fall back to
# the global one — silently, and in the permissive direction — so both are
# checked. A container started by an older version of this plugin has neither;
# one recreated from a stale service.env has the mount without the variable.
# Asks the running service which per-app config directory it is using, and
# prints it. This is the only check that sees the binary rather than the
# container around it; a build from before per-app lists existed answers with a
# bare "ok" and names no directory. Fails if the service is unreachable or too
# old to answer.
fn-ga-service-app-config-dir() {
command -v curl >/dev/null 2>&1 || return 1
local port body dir
port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
body="$(curl -fsS --max-time 3 "http://127.0.0.1:${port}${GOOGLE_AUTH_ROUTE_PREFIX}/healthz" 2>/dev/null)" || return 1
dir="$(sed -n 's/.*"app_config_dir":"\([^"]*\)".*/\1/p' <<<"$body")"
[[ -n "$dir" ]] || return 1
printf '%s' "$dir"
}
# True when the running service can actually read per-app lists. That takes
# three things, and missing any of them makes the service ignore every per-app
# list and fall back to the global one — silently, and in the permissive
# direction — so all three are checked:
#
# the bind mount — a container from an older plugin has none;
# the env var naming it — one recreated from a stale service.env has the
# mount without the variable;
# a binary that reads both — one recreated from a stale *image* has the
# mount and the variable and ignores them, which
# no amount of docker metadata can reveal. Only
# the service's own answer distinguishes it.
fn-ga-service-reads-app-lists() {
local inspected
inspected="$(docker container inspect \
-f '{{range .Mounts}}mount={{println .Destination}}{{end}}{{range .Config.Env}}env={{println .}}{{end}}' \
"$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" || return 1
grep -qxF "mount=$GOOGLE_AUTH_APP_CONFIG_MOUNT" <<<"$inspected" || return 1
grep -qxF "env=GOOGLE_AUTH_APP_CONFIG_DIR=$GOOGLE_AUTH_APP_CONFIG_MOUNT" <<<"$inspected"
grep -qxF "env=GOOGLE_AUTH_APP_CONFIG_DIR=$GOOGLE_AUTH_APP_CONFIG_MOUNT" <<<"$inspected" || return 1
local reported
if reported="$(fn-ga-service-app-config-dir)"; then
[[ "$reported" == "$GOOGLE_AUTH_APP_CONFIG_MOUNT" ]]
return $?
fi
# No answer (curl missing, or the service is not reachable on the loopback
# port): fall back to checking that the image was built from this plugin's
# current source and that the container is running that image.
fn-ga-image-current && fn-ga-container-runs-current-image
}
# True when the running container was created from the current service image,
# rather than from an earlier build still tagged over.
fn-ga-container-runs-current-image() {
local running current
running="$(docker container inspect -f '{{.Image}}' "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" || return 1
current="$(docker image inspect -f '{{.Id}}' "$GOOGLE_AUTH_IMAGE" 2>/dev/null)" || return 1
[[ -n "$running" && "$running" == "$current" ]]
}
fn-ga-write-env-file() {
@@ -637,8 +710,13 @@ fn-ga-write-env-file() {
emails="$(fn-ga-global-get-list allowed-emails | paste -sd, -)"
denied="$(fn-ga-global-get-list denied-emails | paste -sd, -)"
mkdir -p "$GOOGLE_AUTH_DATA_ROOT"
umask 077
cat >"$envfile" <<EOF
# The umask stays inside this subshell. Leaking it left every directory
# created later in the same command at 0700 — including an app's config
# directory during google-auth:enable, which the service then could not
# traverse to read that app's lists.
(
umask 077
cat >"$envfile" <<EOF
GOOGLE_AUTH_CLIENT_ID=$(fn-ga-global-get client-id)
GOOGLE_AUTH_CLIENT_SECRET=$(fn-ga-global-get client-secret)
GOOGLE_AUTH_COOKIE_SECRET=$(fn-ga-global-get cookie-secret)
@@ -652,11 +730,16 @@ GOOGLE_AUTH_SESSION_TTL=$(fn-ga-global-get session-ttl 24h)
GOOGLE_AUTH_ALLOW_INSECURE=$(fn-ga-global-get allow-insecure false)
GOOGLE_AUTH_LISTEN=:2999
EOF
)
}
fn-ga-service-start() {
fn-ga-configured || dokku_log_fail "google-auth is not fully configured; run: dokku google-auth:configure"
fn-ga-image-exists || fn-ga-build-image
# Rebuild whenever the source moved, not merely when the image is missing.
# Starting the service is how an operator expects to pick up an upgraded
# plugin, and reusing the old binary there is what makes a new feature look
# broken rather than absent.
fn-ga-image-current || fn-ga-build-image
fn-ga-write-env-file
local port
port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"