Skip to content
Merged
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
24 changes: 22 additions & 2 deletions .github/actions/build-and-scan/action.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
name: Build and scan runtime images
description: Build both runtime images for amd64 and arm64, then scan each image
name: Build, scan, and validate runtime images
description: Build and scan both runtime images, then run app image acceptance tests
inputs:
nextcloud_version:
description: Nextcloud version passed to the app image build
required: true
runs:
using: composite
steps:
- name: Setup Bats
uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0
with:
support-install: false
assert-install: false
detik-install: false
file-install: false

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
Expand Down Expand Up @@ -73,3 +81,15 @@ runs:
'app@linux/arm64=scan/app:arm64' \
'web@linux/amd64=scan/web:amd64' \
'web@linux/arm64=scan/web:arm64'

- name: Runtime acceptance app image (linux/amd64)
shell: bash
env:
APP_IMAGE: scan/app:amd64
run: bats tests/app-image.bats

- name: Runtime acceptance app image (linux/arm64)
shell: bash
env:
APP_IMAGE: scan/app:arm64
run: bats tests/app-image.bats
15 changes: 14 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
COMPOSE ?= docker compose
GARAGES3_COMPOSE_FILE ?= docker-compose-garages3.yml
APP_TEST_IMAGE ?= nextcloud-app:acceptance

.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images test-ncdd scan-images
.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images test-ncdd test-app-image test-current-app-image scan-images

up-garages3:
$(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d garage
Expand Down Expand Up @@ -35,6 +36,18 @@ test-scan-images:
test-ncdd:
bats tests/ncdd.bats

test-app-image:
@test -n "$(APP_IMAGE)" || { echo 'Usage: make test-app-image APP_IMAGE=<local-image>' >&2; exit 2; }
APP_IMAGE="$(APP_IMAGE)" bats tests/app-image.bats

test-current-app-image:
@set -e; \
version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \
test -n "$$version" || { echo 'NEXTCLOUD_VERSION is missing from .env.example' >&2; exit 1; }; \
docker buildx build --platform linux/amd64 --load --tag "$(APP_TEST_IMAGE)" \
--build-arg "NEXTCLOUD_VERSION=$$version" --file .docker/app/Dockerfile .docker/app; \
$(MAKE) test-app-image APP_IMAGE="$(APP_TEST_IMAGE)"

scan-images:
@set -e; \
version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \
Expand Down
20 changes: 20 additions & 0 deletions docs/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ Immutable web image:
ghcr.io/librecodecoop/nextcloud-docker-web@sha256:<web-image-digest>
```

## Runtime acceptance

The app image has a runtime acceptance test based on the same behavioral checks used by the official Nextcloud container projects.

The test operates on an already-built local image. It does not rebuild the image and does not use the repository deployment Compose files:

```bash
make test-app-image APP_IMAGE=scan/app:amd64
```

For a local build of the current app image followed by the same acceptance test:

```bash
make test-current-app-image
```

The Bats acceptance test creates an isolated Docker network and PostgreSQL container, starts the image with Nextcloud autoinstall variables, waits for the installation, runs `occ status` and `occ check`, and sends a FastCGI request through the FPM runtime. Test-created resources are removed on success and failure.

CI must run this test against the exact locally loaded app images produced by the build step. Runtime acceptance is a publication gate alongside vulnerability scanning; a separate deployment-stack test is not required for this contract.

## Implementation boundary

This document defines the target contract. It does not by itself migrate the current Dockerfiles, workflows, or historical tags.
Expand Down
204 changes: 204 additions & 0 deletions tests/app-image.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env bats

setup() {
: "${APP_IMAGE:?APP_IMAGE must reference an already-built local image}"

command -v docker >/dev/null 2>&1 || {
echo "Docker is required to run the app image acceptance test." >&2
return 127
}

docker image inspect "$APP_IMAGE" >/dev/null 2>&1 || {
echo "Image not found locally: $APP_IMAGE" >&2
return 2
}

POSTGRES_IMAGE=${NEXTCLOUD_IMAGE_TEST_POSTGRES_IMAGE:-postgres:18-alpine}
TIMEOUT_SECONDS=${NEXTCLOUD_IMAGE_TEST_TIMEOUT:-300}
RUNTIME_USER=${NEXTCLOUD_IMAGE_TEST_RUNTIME_USER:-www-data}

IMAGE_ARCH=$(docker image inspect --format '{{.Architecture}}' "$APP_IMAGE")
case "$IMAGE_ARCH" in
amd64|arm64) IMAGE_PLATFORM="linux/$IMAGE_ARCH" ;;
*)
echo "Unsupported image architecture: $IMAGE_ARCH" >&2
return 2
;;
esac

TEST_ID="nextcloud-app-test-$$-${BATS_TEST_NUMBER}-${RANDOM}"
NETWORK_NAME="$TEST_ID"
DB_CONTAINER="${TEST_ID}-db"
APP_CONTAINER="${TEST_ID}-app"
FCGI_CLIENT_IMAGE="nextcloud-app-test-fcgi-client"

DB_USER=nextcloud
DB_DATABASE=nextcloud
DB_PASSWORD="test-${RANDOM}-${RANDOM}-password"
ADMIN_USER=test_admin
ADMIN_PASSWORD="test-${RANDOM}-${RANDOM}-admin-password"

NETWORK_CREATED=false
DB_CREATED=false
APP_CREATED=false
}

teardown() {
if $APP_CREATED; then
docker rm -f "$APP_CONTAINER" >/dev/null 2>&1 || true
fi
if $DB_CREATED; then
docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
fi
if $NETWORK_CREATED; then
docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true
fi
}

diagnostics() {
echo
echo "=== app container logs ===" >&2
if $APP_CREATED; then
docker logs "$APP_CONTAINER" >&2 || true
else
echo "app container was not created" >&2
fi

echo
echo "=== postgres container logs ===" >&2
if $DB_CREATED; then
docker logs "$DB_CONTAINER" >&2 || true
else
echo "postgres container was not created" >&2
fi
}

fail_with_diagnostics() {
local message=$1
diagnostics
echo "$message" >&2
return 1
}

wait_until() {
local description=$1
shift

local started now
started=$(date +%s)

while ! "$@"; do
now=$(date +%s)
if [ $((now - started)) -ge "$TIMEOUT_SECONDS" ]; then
fail_with_diagnostics "Timed out after ${TIMEOUT_SECONDS}s waiting for $description."
return 1
fi
sleep 2
done
}

postgres_ready() {
docker exec "$DB_CONTAINER" pg_isready -U "$DB_USER" -d "$DB_DATABASE" >/dev/null 2>&1
}

nextcloud_installed() {
local status
status=$(docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ status --output=json 2>/dev/null) || return 1
grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$status"
}

ensure_fcgi_client() {
if docker image inspect "$FCGI_CLIENT_IMAGE" >/dev/null 2>&1; then
return
fi

docker build -q -t "$FCGI_CLIENT_IMAGE" - <<'EOF' >/dev/null
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends libfcgi-bin \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["cgi-fcgi"]
EOF
}

fcgi_status() {
docker run --rm -i \
--network "$NETWORK_NAME" \
-e REQUEST_METHOD=GET \
-e SCRIPT_NAME=/status.php \
-e SCRIPT_FILENAME=/var/www/html/status.php \
"$FCGI_CLIENT_IMAGE" \
-bind -connect app:9000 2>/dev/null
}

fpm_ready() {
local response
response=$(fcgi_status) || return 1
grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$response"
}

@test "app image installs Nextcloud and serves it through FPM" {
echo "Testing $APP_IMAGE ($IMAGE_PLATFORM)"

run docker network create "$NETWORK_NAME"
[ "$status" -eq 0 ]
NETWORK_CREATED=true

run docker run -d \
--name "$DB_CONTAINER" \
--network "$NETWORK_NAME" \
--network-alias db \
-e "POSTGRES_USER=$DB_USER" \
-e "POSTGRES_PASSWORD=$DB_PASSWORD" \
-e "POSTGRES_DB=$DB_DATABASE" \
"$POSTGRES_IMAGE"
[ "$status" -eq 0 ]
DB_CREATED=true

wait_until "PostgreSQL readiness" postgres_ready

run docker run -d \
--name "$APP_CONTAINER" \
--network "$NETWORK_NAME" \
--network-alias app \
--platform "$IMAGE_PLATFORM" \
-e POSTGRES_HOST=db \
-e "POSTGRES_USER=$DB_USER" \
-e "POSTGRES_PASSWORD=$DB_PASSWORD" \
-e "POSTGRES_DB=$DB_DATABASE" \
-e "NEXTCLOUD_ADMIN_USER=$ADMIN_USER" \
-e "NEXTCLOUD_ADMIN_PASSWORD=$ADMIN_PASSWORD" \
-e NEXTCLOUD_TRUSTED_DOMAINS=localhost \
"$APP_IMAGE"
[ "$status" -eq 0 ]
APP_CREATED=true

wait_until "Nextcloud installation" nextcloud_installed

run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ status --output=json
if [ "$status" -ne 0 ]; then
fail_with_diagnostics "occ status failed."
fi
if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$output"; then
printf '%s\n' "$output" >&2
fail_with_diagnostics "occ status did not report installed: true."
fi

run docker exec -u "$RUNTIME_USER" "$APP_CONTAINER" php occ check
if [ "$status" -ne 0 ]; then
fail_with_diagnostics "occ check failed."
fi

ensure_fcgi_client

wait_until "FPM readiness" fpm_ready

run fcgi_status
if [ "$status" -ne 0 ]; then
fail_with_diagnostics "FPM did not accept the FastCGI request after becoming ready."
fi
if ! grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' <<<"$output"; then
printf '%s\n' "$output" >&2
fail_with_diagnostics "FPM response did not report installed: true."
fi
}
Loading