diff --git a/README.md b/README.md index 349688c..4340167 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,8 @@ dokku google-auth:allow my-app alice@signal.org # …or just on one app dokku google-auth:deny --global former@signal.org # cut someone off dokku google-auth:disable my-app # turn SSO off for an app dokku google-auth:logs -t # follow auth service logs (sign-ins, denials) -dokku google-auth:restart # restart the auth service +dokku google-auth:restart # restart the auth service (rebuilds if the source moved) +dokku google-auth:rebuild # force a rebuild when the source looks unchanged ``` Users can check who they're signed in as at `https:///_google-auth/` @@ -315,12 +316,18 @@ and sign out at `https:///_google-auth/logout`. - **Plugin updates:** `sudo dokku plugin:update google-auth` rebuilds the service image and recreates the container with freshly written settings. Editing the plugin directory in place works too: the image is tagged with a - fingerprint of its source, so `google-auth:restart` (and anything else that - starts the service) rebuilds when the Go code has moved rather than reusing - the binary that happens to be tagged. This matters because the shell half of - the plugin takes effect the moment the files change while the binary does - not, and a container running an older build accepts the mount, accepts - `GOOGLE_AUTH_APP_CONFIG_DIR`, and ignores both. + fingerprint of `Dockerfile`, `go.mod`, `cmd/`, and `internal/`, so + `google-auth:restart` (and anything else that starts the service) rebuilds + when the Go code has moved rather than reusing the binary that happens to be + tagged. This matters because the shell half of the plugin takes effect the + moment the files change while the binary does not, and a container running an + older build accepts the mount, accepts `GOOGLE_AUTH_APP_CONFIG_DIR`, and + ignores both. + That fingerprint covers this plugin's own source and nothing else, so when + the inputs it cannot see change — a patched `golang:1.24-alpine`, a build that + died halfway — force the rebuild: `dokku google-auth:rebuild`, or + `dokku google-auth:rebuild --no-cache` to also discard the layer cache and + re-pull the base images. `dokku google-auth:report` names the running service's state — it asks the service itself rather than trusting Docker's metadata — and if it ever says it is ignoring per-app lists, `dokku google-auth:restart` recreates it. diff --git a/commands b/commands index 879c001..9d22def 100755 --- a/commands +++ b/commands @@ -18,7 +18,8 @@ case "$1" in google-auth:report [app], Show global and per-app google-auth status google-auth:start, Start the shared auth service container google-auth:stop, Stop the shared auth service container - google-auth:restart, Restart the shared auth service container (picks up config changes) + google-auth:restart, Restart the shared auth service container (rebuilds the image if the plugin source changed) + google-auth:rebuild [--no-cache], Force a rebuild of the service image even if the source looks unchanged google-auth:logs [--tail|-t], Show logs from the shared auth service container help_content } diff --git a/functions b/functions index e34590d..a4956dd 100644 --- a/functions +++ b/functions @@ -637,11 +637,19 @@ fn-ga-image-current() { } fn-ga-build-image() { + declare NO_CACHE="${1:-false}" 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 \ - --label "google-auth.source-hash=$(fn-ga-source-hash)" \ - -t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR" + local args=(image build) + if [[ "$NO_CACHE" == "true" ]]; then + # --pull too: the source hash cannot see the base images, so a rebuild that + # keeps a cached golang:1.24-alpine is not actually a fresh build. + args+=(--no-cache --pull) + dokku_log_info1 "Building $GOOGLE_AUTH_IMAGE from scratch (ignoring the layer cache, re-pulling base images)" + else + dokku_log_info1 "Building $GOOGLE_AUTH_IMAGE (first build downloads the Go toolchain image; this can take a few minutes)" + fi + args+=(--label "google-auth.source-hash=$(fn-ga-source-hash)" -t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR") + docker "${args[@]}" } fn-ga-service-running() { @@ -739,7 +747,7 @@ fn-ga-service-start() { # 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-image-current || fn-ga-build-image false fn-ga-write-env-file local port port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")" diff --git a/install b/install index 8cfb3d5..ce4d013 100755 --- a/install +++ b/install @@ -9,9 +9,15 @@ DATA_ROOT="$DOKKU_LIB_ROOT/data/google-auth" mkdir -p "$DATA_ROOT/global" "$DATA_ROOT/apps" +# shellcheck disable=SC1091 +source "$PLUGIN_DIR/functions" + if command -v docker >/dev/null 2>&1; then - echo "-----> Building dokku-google-auth service image (first build can take a few minutes)" - docker image build -t dokku-google-auth:latest "$PLUGIN_DIR" + # Build through fn-ga-build-image rather than calling docker directly, so the + # image carries the source-hash label. An unlabelled image reads as stale + # forever: every service start would rebuild it, and the report would claim + # the binary is older than the plugin right after an upgrade installed it. + fn-ga-build-image false # Pick up the new image if the service is already running. This goes through # fn-ga-service-start rather than its own `docker run` so an upgrade always # rewrites service.env first: recreating the container from the file an older @@ -20,8 +26,6 @@ if command -v docker >/dev/null 2>&1; then # access list and fall back to the global one. if [[ "$(docker container inspect -f '{{.State.Running}}' dokku-google-auth 2>/dev/null)" == "true" ]]; then echo "-----> Restarting google-auth service with the new image" - # shellcheck disable=SC1091 - source "$PLUGIN_DIR/functions" if fn-ga-configured; then fn-ga-service-start else diff --git a/mise.toml b/mise.toml index 4077121..d2f1797 100644 --- a/mise.toml +++ b/mise.toml @@ -39,8 +39,10 @@ description = "Check the help output shapes and that every subcommand is documen run = "test/help-test.sh" [tasks.docker-build] -description = "Build the auth service docker image" -run = "docker build -t dokku-google-auth:latest ." +description = "Build the auth service docker image, labelled the way the plugin does" +# Via fn-ga-build-image so the image carries its source-hash label. A build +# without it reads as permanently stale to the plugin. +run = "bash -c 'source ./functions && fn-ga-build-image'" [tasks.check] description = "Run everything CI would run" diff --git a/subcommands/rebuild b/subcommands/rebuild new file mode 100755 index 0000000..79e8663 --- /dev/null +++ b/subcommands/rebuild @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$(dirname "$(dirname "${BASH_SOURCE[0]}")")/functions" + +cmd-google-auth-rebuild() { + declare desc="rebuild the auth service image from the plugin source and restart" + local cmd="google-auth:rebuild" + [[ "$1" == "$cmd" ]] && shift 1 + + # Starting the service already rebuilds when the Go source has moved, so this + # exists for the case that check cannot see: the source is unchanged but the + # build inputs are not — a patched base image, a half-written image, a build + # that failed partway. + local no_cache=false + while [[ $# -gt 0 ]]; do + case "$1" in + --no-cache) no_cache=true ;; + *) dokku_log_fail "usage: dokku $cmd [--no-cache]" ;; + esac + shift + done + + fn-ga-build-image "$no_cache" + if fn-ga-service-running; then + # The image is current now, so this recreates the container without + # building a second time. + fn-ga-service-start + else + dokku_log_verbose "auth service is not running; the new image is used when it starts" + fi +} + +cmd-google-auth-rebuild "$@" diff --git a/test/access-list-test.sh b/test/access-list-test.sh index 9489aa8..b3580b8 100755 --- a/test/access-list-test.sh +++ b/test/access-list-test.sh @@ -238,6 +238,7 @@ cat >"$WORK/bin-docker/docker" <<'EOF' case "$1 $2" in "container inspect") cat "$DOCKER_INSPECT_FIXTURE" ;; "image inspect") printf '%s\n' "$DOCKER_IMAGE_FIXTURE" ;; + "image build") printf '%s\n' "$*" >"$DOCKER_ARGS_FIXTURE" ;; esac exit 0 EOF @@ -308,6 +309,33 @@ image_current "$SOURCE_HASH" || fail "an image labelled with the current source image_current "0000000000000000" && fail "an image built from other source is not current" image_current "" && fail "an unlabelled image (built before this check) is not current" image_current "" && fail "an image with no label value is not current" + +# Every build must apply the label, or the image it produces is stale on +# arrival — the plugin would rebuild on every start and the report would call a +# freshly installed binary out of date. +# shellcheck disable=SC2030,SC2031 # the subshell is what keeps the stubs local +build_args() ( + export DOCKER_ARGS_FIXTURE="$WORK/build-args" + PATH="$WORK/bin-docker:$PATH" + fn-ga-build-image "$1" >/dev/null + cat "$DOCKER_ARGS_FIXTURE" +) + +DEFAULT_BUILD="$(build_args false)" +grep -q -- "--label google-auth.source-hash=$SOURCE_HASH" <<<"$DEFAULT_BUILD" || + fail "a build must label the image with the source hash, got: $DEFAULT_BUILD" +grep -q -- "-t dokku-google-auth:latest" <<<"$DEFAULT_BUILD" || + fail "a build must tag the service image, got: $DEFAULT_BUILD" +grep -q -- "--no-cache" <<<"$DEFAULT_BUILD" && + fail "an ordinary build should use the layer cache" + +FORCED_BUILD="$(build_args true)" +grep -q -- "--no-cache" <<<"$FORCED_BUILD" || fail "a forced build should ignore the layer cache" +# The source hash covers this plugin's files, not the images the Dockerfile +# pulls, so a forced build that reuses a cached base image is not a fresh one. +grep -q -- "--pull" <<<"$FORCED_BUILD" || fail "a forced build should re-pull the base images" +grep -q -- "--label google-auth.source-hash=$SOURCE_HASH" <<<"$FORCED_BUILD" || + fail "a forced build must still label the image, got: $FORCED_BUILD" echo "ok: service image tracks the plugin source" # --- lifecycle triggers carry per-app lists ---