From eb36ab4ec4613d294898fb75a3ba8166197f7a81 Mon Sep 17 00:00:00 2001 From: libops-agent Date: Sun, 2 Aug 2026 16:14:28 +0000 Subject: [PATCH] [minor] Centralize Compose runtime tooling --- .github/workflows/push.yml | 20 +- .github/workflows/template-contracts.yml | 54 + Makefile | 28 +- docker-bake.hcl | 992 +----------------- images/archivesspace/Dockerfile | 2 +- images/base/Dockerfile | 3 - images/base/README.md | 4 + .../rootfs/usr/local/bin/generate-certs.sh | 42 + .../usr/local/bin/generate-compose-secrets.sh | 54 + .../rootfs/usr/local/bin/init-database.sh | 110 ++ images/nginx/Dockerfile | 2 +- images/omeka-classic/Dockerfile | 2 +- images/omeka-s/Dockerfile | 2 +- images/wp/Dockerfile | 2 +- .../ServiceHealthcheck/docker-compose.yml | 4 + internal/buildkit/cli.go | 3 + internal/buildkit/metadata_test.go | 49 + internal/buildkit/template_test_runner.go | 136 +++ .../buildkit/template_test_runner_test.go | 58 + 19 files changed, 582 insertions(+), 985 deletions(-) create mode 100644 .github/workflows/template-contracts.yml create mode 100755 images/base/rootfs/usr/local/bin/generate-certs.sh create mode 100755 images/base/rootfs/usr/local/bin/generate-compose-secrets.sh create mode 100755 images/base/rootfs/usr/local/bin/init-database.sh create mode 100644 internal/buildkit/template_test_runner.go create mode 100644 internal/buildkit/template_test_runner_test.go diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index ed88dbc0..3cd5bcf7 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -214,7 +214,7 @@ jobs: sitectl_drupal_create_args="$(coordinated_template_args https://github.com/libops/drupal)" sitectl_isle_template_args="$(coordinated_template_args https://github.com/libops/isle)" if [ -z "${sitectl_isle_template_args}" ]; then - sitectl_isle_template_args="--template-repo https://github.com/libops/isle" + sitectl_isle_template_args="--template-repo https://github.com/libops/isle --template-branch main" fi sitectl_isle_create_args="${sitectl_isle_template_args} --fcrepo off --blazegraph off --isle-file-system-uri private --iiif triplet --iiif-topology disabled --bot-mitigation off" sitectl_repository="" @@ -440,7 +440,7 @@ jobs: contents: read packages: read - test-level-4: + test-level-4-matrix: if: ${{ always() && needs.plan.outputs.test_level4 != '[]' && !cancelled() && !failure() }} needs: [plan, build-level-4] strategy: @@ -458,6 +458,20 @@ jobs: contents: read packages: read + test-level-4: + if: ${{ always() }} + needs: [test-level-4-matrix] + runs-on: ubuntu-24.04 + steps: + - name: Verify level-4 matrix + env: + RESULT: ${{ needs.test-level-4-matrix.result }} + run: | + if [[ "${RESULT}" != "success" && "${RESULT}" != "skipped" ]]; then + echo "Level-4 matrix result: ${RESULT}" >&2 + exit 1 + fi + sitectl-create-smoke-test: if: ${{ always() && needs.plan.outputs.sitectl_create_smoke_matrix != '[]' && !cancelled() && !failure() }} needs: [plan, test-level-0, test-level-1, test-level-2, test-level-3, test-level-4] @@ -470,7 +484,7 @@ jobs: with: plugin: ${{ matrix.plugin }} create-definition: default - create-args: ${{ matrix.create-args }} + create-args: --yolo ${{ matrix.create-args }} checkout-source: template project-path: generated-${{ matrix.plugin }} image-tags: ${{ matrix.image-tags }} diff --git a/.github/workflows/template-contracts.yml b/.github/workflows/template-contracts.yml new file mode 100644 index 00000000..88271720 --- /dev/null +++ b/.github/workflows/template-contracts.yml @@ -0,0 +1,54 @@ +name: Template contracts + +on: + workflow_dispatch: + inputs: + smoke: + description: Run each template's retained smoke test after contract checks + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + templates: + runs-on: ubuntu-24.04 + steps: + - name: Checkout buildkit + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + path: buildkit + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: buildkit/go.mod + + - name: Checkout Compose templates + shell: bash + run: | + set -euo pipefail + for repository in archivesspace drupal isle ojs omeka-classic omeka-s wp; do + git clone --depth 1 "https://github.com/libops/${repository}.git" "templates/${repository}" + done + + - name: Check contracts and image pins + working-directory: buildkit + run: >- + go run ./cmd/buildkit template-test --check-only + --root ../templates/archivesspace --root ../templates/drupal + --root ../templates/isle --root ../templates/ojs + --root ../templates/omeka-classic --root ../templates/omeka-s + --root ../templates/wp + + - name: Run template smoke tests + if: ${{ github.event_name == 'workflow_dispatch' && inputs.smoke }} + working-directory: buildkit + run: >- + go run ./cmd/buildkit template-test --keep-going + --root ../templates/archivesspace --root ../templates/drupal + --root ../templates/isle --root ../templates/ojs + --root ../templates/omeka-classic --root ../templates/omeka-s + --root ../templates/wp diff --git a/Makefile b/Makefile index 35400312..1d2d0717 100644 --- a/Makefile +++ b/Makefile @@ -74,14 +74,15 @@ TARGET_IMAGES = $(shell docker buildx bake --print $(TARGET) 2>/dev/null | jq -r TEST_IMAGE ?= $(if $(filter default,$(TARGET)),,$(TARGET)) TEST_MODE ?= fallback TEST_ARGS ?= +TEMPLATE_ROOTS ?= build: mkdir -p build -# This is a catch all target that is used to check for existance of an -# executable when declared as a dependency. -.PHONY: % -%: +# Executable prerequisites are explicit so a misspelled target retains Make's +# useful "No rule to make target" error. +.PHONY: docker jq login +docker jq login: $(call executable-exists,$@) # Prior to building, all folders which might be copied into Docker images must @@ -96,8 +97,8 @@ folder-permissions: # have the executable bit set for all users. So that they can be executed by # the users we create like 'nginx'. We can not insure this via Git as it does # not track executable permissions for "groups" or "others". -.PHONY: executable-permissons -executable-permissons: +.PHONY: executable-permissions +executable-permissions: find images -type f \ \( \ -name "*.sh" \ @@ -126,7 +127,7 @@ docker-buildx: | docker # Despite being a real target we make it PHONY so it is run everytime as $(TARGET) can change. .PHONY: build/bake.json .SILENT: build/bake.json -build/bake.json: | docker-buildx jq build folder-permissions executable-permissons +build/bake.json: | docker-buildx jq build folder-permissions executable-permissions set -x; \ BRANCH="$(BRANCH)" \ CACHE_FROM_REPOSITORY=$(CACHE_FROM_REPOSITORY) \ @@ -181,6 +182,19 @@ list-tests: $(if $(TEST_IMAGE),--image $(TEST_IMAGE),) \ $(if $(TEST),--test $(TEST),) +.PHONY: template-test +## Runs retained smoke tests from one or more Compose template checkouts. +template-test: + @if [ -z "$(GO)" ]; then printf "Go is required to run template tests.\n"; exit 127; fi + @if [ -z "$(strip $(TEMPLATE_ROOTS))" ]; then printf "Set TEMPLATE_ROOTS to one or more template checkout paths.\n"; exit 2; fi + $(GO) run ./cmd/buildkit template-test $(foreach root,$(TEMPLATE_ROOTS),--root "$(root)") --keep-going + +.PHONY: template-check +## Checks canonical template filenames and cross-template image digest parity. +template-check: + @if [ -z "$(strip $(TEMPLATE_ROOTS))" ]; then printf "Set TEMPLATE_ROOTS to one or more template checkout paths.\n"; exit 2; fi + $(GO) run ./cmd/buildkit template-test $(foreach root,$(TEMPLATE_ROOTS),--root "$(root)") --check-only + .PHONY: push ## Builds and pushes the target(s) into remote repository. push: build/bake.json login diff --git a/docker-bake.hcl b/docker-bake.hcl index 82af7837..ec9c8fbe 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -2,7 +2,32 @@ ARCHES = [ "amd64", "arm64", ] +############################################################################### +# Matrix-derived local and architecture targets. +############################################################################### +target "image" { + matrix = { + image = IMAGES + } + name = image + inherits = ["${image}-common"] + contexts = dependencies(image, "") + cache-from = cacheFrom(image, hostArch()) + tags = tags(image, "") +} +target "image-arch" { + matrix = { + image = IMAGES + arch = ARCHES + } + name = "${image}-${arch}" + inherits = ["${image}-common", "${arch}-common"] + contexts = dependencies(image, arch) + cache-from = cacheFrom(image, arch) + cache-to = cacheTo(image, arch) + tags = tags(image, arch) +} IMAGES = [ "activemq5", "activemq6", @@ -587,970 +612,3 @@ target "wp-php84-common" { } ############################################################################### -# Default Image targets for local builds. -############################################################################### -target "activemq5" { - inherits = ["activemq5-common"] - contexts = dependencies("activemq5", "") - cache-from = cacheFrom("activemq5", hostArch()) - tags = tags("activemq5", "") -} - -target "activemq6" { - inherits = ["activemq6-common"] - contexts = dependencies("activemq6", "") - cache-from = cacheFrom("activemq6", hostArch()) - tags = tags("activemq6", "") -} - -target "alpaca" { - inherits = ["alpaca-common"] - contexts = dependencies("alpaca", "") - cache-from = cacheFrom("alpaca", hostArch()) - tags = tags("alpaca", "") -} - -target "archivesspace" { - inherits = ["archivesspace-common"] - contexts = dependencies("archivesspace", "") - cache-from = cacheFrom("archivesspace", hostArch()) - tags = tags("archivesspace", "") -} - -target "archivesspace-solr" { - inherits = ["archivesspace-solr-common"] - contexts = dependencies("archivesspace-solr", "") - cache-from = cacheFrom("archivesspace-solr", hostArch()) - tags = tags("archivesspace-solr", "") -} - -target "base" { - inherits = ["base-common"] - cache-from = cacheFrom("base", hostArch()) - tags = tags("base", "") -} - -target "blazegraph" { - inherits = ["blazegraph-common"] - contexts = dependencies("blazegraph", "") - cache-from = cacheFrom("blazegraph", hostArch()) - tags = tags("blazegraph", "") -} - -target "crayfits" { - inherits = ["crayfits-common"] - contexts = dependencies("crayfits", "") - cache-from = cacheFrom("crayfits", hostArch()) - tags = tags("crayfits", "") -} - -target "drupal-php83" { - inherits = ["drupal-php83-common"] - contexts = dependencies("drupal-php83", "") - cache-from = cacheFrom("drupal-php83", hostArch()) - tags = tags("drupal-php83", "") -} - -target "drupal-php84" { - inherits = ["drupal-php84-common"] - contexts = dependencies("drupal-php84", "") - cache-from = cacheFrom("drupal-php84", hostArch()) - tags = tags("drupal-php84", "") -} - -target "fcrepo6" { - inherits = ["fcrepo6-common"] - contexts = dependencies("fcrepo6", "") - cache-from = cacheFrom("fcrepo6", hostArch()) - tags = tags("fcrepo6", "") -} - -target "fcrepo7" { - inherits = ["fcrepo7-common"] - contexts = dependencies("fcrepo7", "") - cache-from = cacheFrom("fcrepo7", hostArch()) - tags = tags("fcrepo7", "") -} - -target "fits" { - inherits = ["fits-common"] - contexts = dependencies("fits", "") - cache-from = cacheFrom("fits", hostArch()) - tags = tags("fits", "") -} - -target "go1-26" { - inherits = ["go1-26-common"] - contexts = dependencies("go1-26", "") - cache-from = cacheFrom("go1-26", hostArch()) - tags = tags("go1-26", "") -} - -target "homarus" { - inherits = ["homarus-common"] - contexts = dependencies("homarus", "") - cache-from = cacheFrom("homarus", hostArch()) - tags = tags("homarus", "") -} - -target "houdini" { - inherits = ["houdini-common"] - contexts = dependencies("houdini", "") - cache-from = cacheFrom("houdini", hostArch()) - tags = tags("houdini", "") -} - -target "hypercube" { - inherits = ["hypercube-common"] - contexts = dependencies("hypercube", "") - cache-from = cacheFrom("hypercube", hostArch()) - tags = tags("hypercube", "") -} - -target "islandora-php83" { - inherits = ["islandora-php83-common"] - contexts = dependencies("islandora-php83", "") - cache-from = cacheFrom("islandora-php83", hostArch()) - tags = tags("islandora-php83", "") -} - -target "islandora-php84" { - inherits = ["islandora-php84-common"] - contexts = dependencies("islandora-php84", "") - cache-from = cacheFrom("islandora-php84", hostArch()) - tags = tags("islandora-php84", "") -} - -target "java17" { - inherits = ["java17-common"] - contexts = dependencies("java17", "") - cache-from = cacheFrom("java17", hostArch()) - tags = tags("java17", "") -} - -target "java21" { - inherits = ["java21-common"] - contexts = dependencies("java21", "") - cache-from = cacheFrom("java21", hostArch()) - tags = tags("java21", "") -} - -target "java25" { - inherits = ["java25-common"] - contexts = dependencies("java25", "") - cache-from = cacheFrom("java25", hostArch()) - tags = tags("java25", "") -} - -target "leptonica" { - inherits = ["leptonica-common"] - cache-from = cacheFrom("leptonica", hostArch()) - tags = tags("leptonica", "") -} - -target "mariadb11" { - inherits = ["mariadb11-common"] - contexts = dependencies("mariadb11", "") - cache-from = cacheFrom("mariadb11", hostArch()) - tags = tags("mariadb11", "") -} - -target "mergepdf" { - inherits = ["mergepdf-common"] - contexts = dependencies("mergepdf", "") - cache-from = cacheFrom("mergepdf", hostArch()) - tags = tags("mergepdf", "") -} - -target "nginx-php83" { - inherits = ["nginx-php83-common"] - contexts = dependencies("nginx-php83", "") - cache-from = cacheFrom("nginx-php83", hostArch()) - tags = tags("nginx-php83", "") -} - -target "nginx-php84" { - inherits = ["nginx-php84-common"] - contexts = dependencies("nginx-php84", "") - cache-from = cacheFrom("nginx-php84", hostArch()) - tags = tags("nginx-php84", "") -} - -target "ojs-php83" { - inherits = ["ojs-php83-common"] - contexts = dependencies("ojs-php83", "") - cache-from = cacheFrom("ojs-php83", hostArch()) - tags = tags("ojs-php83", "") -} - -target "ojs-php84" { - inherits = ["ojs-php84-common"] - contexts = dependencies("ojs-php84", "") - cache-from = cacheFrom("ojs-php84", hostArch()) - tags = tags("ojs-php84", "") -} - -target "omeka-classic-php83" { - inherits = ["omeka-classic-php83-common"] - contexts = dependencies("omeka-classic-php83", "") - cache-from = cacheFrom("omeka-classic-php83", hostArch()) - tags = tags("omeka-classic-php83", "") -} - -target "omeka-classic-php84" { - inherits = ["omeka-classic-php84-common"] - contexts = dependencies("omeka-classic-php84", "") - cache-from = cacheFrom("omeka-classic-php84", hostArch()) - tags = tags("omeka-classic-php84", "") -} - -target "omeka-s-php83" { - inherits = ["omeka-s-php83-common"] - contexts = dependencies("omeka-s-php83", "") - cache-from = cacheFrom("omeka-s-php83", hostArch()) - tags = tags("omeka-s-php83", "") -} - -target "omeka-s-php84" { - inherits = ["omeka-s-php84-common"] - contexts = dependencies("omeka-s-php84", "") - cache-from = cacheFrom("omeka-s-php84", hostArch()) - tags = tags("omeka-s-php84", "") -} - -target "php83" { - inherits = ["php83-common"] - contexts = dependencies("php83", "") - cache-from = cacheFrom("php83", hostArch()) - tags = tags("php83", "") -} - -target "php84" { - inherits = ["php84-common"] - contexts = dependencies("php84", "") - cache-from = cacheFrom("php84", hostArch()) - tags = tags("php84", "") -} - -target "scyllaridae" { - inherits = ["scyllaridae-common"] - contexts = dependencies("scyllaridae", "") - cache-from = cacheFrom("scyllaridae", hostArch()) - tags = tags("scyllaridae", "") -} - -target "solr9" { - inherits = ["solr9-common"] - contexts = dependencies("solr9", "") - cache-from = cacheFrom("solr9", hostArch()) - tags = tags("solr9", "") -} - -target "solr10" { - inherits = ["solr10-common"] - contexts = dependencies("solr10", "") - cache-from = cacheFrom("solr10", hostArch()) - tags = tags("solr10", "") -} - -target "tomcat9" { - inherits = ["tomcat9-common"] - contexts = dependencies("tomcat9", "") - cache-from = cacheFrom("tomcat9", hostArch()) - tags = tags("tomcat9", "") -} - -target "tomcat11" { - inherits = ["tomcat11-common"] - contexts = dependencies("tomcat11", "") - cache-from = cacheFrom("tomcat11", hostArch()) - tags = tags("tomcat11", "") -} - -target "wp-php83" { - inherits = ["wp-php83-common"] - contexts = dependencies("wp-php83", "") - cache-from = cacheFrom("wp-php83", hostArch()) - tags = tags("wp-php83", "") -} - -target "wp-php84" { - inherits = ["wp-php84-common"] - contexts = dependencies("wp-php84", "") - cache-from = cacheFrom("wp-php84", hostArch()) - tags = tags("wp-php84", "") -} - -############################################################################### -# linux/amd64 targets. -############################################################################### -target "activemq5-amd64" { - inherits = ["activemq5-common", "amd64-common"] - contexts = dependencies("activemq5", "amd64") - cache-from = cacheFrom("activemq5", "amd64") - cache-to = cacheTo("activemq5", "amd64") - tags = tags("activemq5", "amd64") -} - -target "activemq6-amd64" { - inherits = ["activemq6-common", "amd64-common"] - contexts = dependencies("activemq6", "amd64") - cache-from = cacheFrom("activemq6", "amd64") - cache-to = cacheTo("activemq6", "amd64") - tags = tags("activemq6", "amd64") -} - -target "alpaca-amd64" { - inherits = ["alpaca-common", "amd64-common"] - contexts = dependencies("alpaca", "amd64") - cache-from = cacheFrom("alpaca", "amd64") - cache-to = cacheTo("alpaca", "amd64") - tags = tags("alpaca", "amd64") -} - -target "archivesspace-amd64" { - inherits = ["archivesspace-common", "amd64-common"] - contexts = dependencies("archivesspace", "amd64") - cache-from = cacheFrom("archivesspace", "amd64") - cache-to = cacheTo("archivesspace", "amd64") - tags = tags("archivesspace", "amd64") -} - -target "archivesspace-solr-amd64" { - inherits = ["archivesspace-solr-common", "amd64-common"] - contexts = dependencies("archivesspace-solr", "amd64") - cache-from = cacheFrom("archivesspace-solr", "amd64") - cache-to = cacheTo("archivesspace-solr", "amd64") - tags = tags("archivesspace-solr", "amd64") -} - -target "base-amd64" { - inherits = ["base-common", "amd64-common"] - cache-from = cacheFrom("base", "amd64") - cache-to = cacheTo("base", "amd64") - tags = tags("base", "amd64") -} - -target "blazegraph-amd64" { - inherits = ["blazegraph-common", "amd64-common"] - contexts = dependencies("blazegraph", "amd64") - cache-from = cacheFrom("blazegraph", "amd64") - cache-to = cacheTo("blazegraph", "amd64") - tags = tags("blazegraph", "amd64") -} - -target "crayfits-amd64" { - inherits = ["crayfits-common", "amd64-common"] - contexts = dependencies("crayfits", "amd64") - cache-from = cacheFrom("crayfits", "amd64") - cache-to = cacheTo("crayfits", "amd64") - tags = tags("crayfits", "amd64") -} - -target "drupal-php83-amd64" { - inherits = ["drupal-php83-common", "amd64-common"] - contexts = dependencies("drupal-php83", "amd64") - cache-from = cacheFrom("drupal-php83", "amd64") - cache-to = cacheTo("drupal-php83", "amd64") - tags = tags("drupal-php83", "amd64") -} - -target "drupal-php84-amd64" { - inherits = ["drupal-php84-common", "amd64-common"] - contexts = dependencies("drupal-php84", "amd64") - cache-from = cacheFrom("drupal-php84", "amd64") - cache-to = cacheTo("drupal-php84", "amd64") - tags = tags("drupal-php84", "amd64") -} - -target "fcrepo6-amd64" { - inherits = ["fcrepo6-common", "amd64-common"] - contexts = dependencies("fcrepo6", "amd64") - cache-from = cacheFrom("fcrepo6", "amd64") - cache-to = cacheTo("fcrepo6", "amd64") - tags = tags("fcrepo6", "amd64") -} - -target "fcrepo7-amd64" { - inherits = ["fcrepo7-common", "amd64-common"] - contexts = dependencies("fcrepo7", "amd64") - cache-from = cacheFrom("fcrepo7", "amd64") - cache-to = cacheTo("fcrepo7", "amd64") - tags = tags("fcrepo7", "amd64") -} - -target "fits-amd64" { - inherits = ["fits-common", "amd64-common"] - contexts = dependencies("fits", "amd64") - cache-from = cacheFrom("fits", "amd64") - cache-to = cacheTo("fits", "amd64") - tags = tags("fits", "amd64") -} - -target "go1-26-amd64" { - inherits = ["go1-26-common", "amd64-common"] - contexts = dependencies("go1-26", "amd64") - cache-from = cacheFrom("go1-26", "amd64") - cache-to = cacheTo("go1-26", "amd64") - tags = tags("go1-26", "amd64") -} - -target "homarus-amd64" { - inherits = ["homarus-common", "amd64-common"] - contexts = dependencies("homarus", "amd64") - cache-from = cacheFrom("homarus", "amd64") - cache-to = cacheTo("homarus", "amd64") - tags = tags("homarus", "amd64") -} - -target "houdini-amd64" { - inherits = ["houdini-common", "amd64-common"] - contexts = dependencies("houdini", "amd64") - cache-from = cacheFrom("houdini", "amd64") - cache-to = cacheTo("houdini", "amd64") - tags = tags("houdini", "amd64") -} - -target "hypercube-amd64" { - inherits = ["hypercube-common", "amd64-common"] - contexts = dependencies("hypercube", "amd64") - cache-from = cacheFrom("hypercube", "amd64") - cache-to = cacheTo("hypercube", "amd64") - tags = tags("hypercube", "amd64") -} - -target "islandora-php83-amd64" { - inherits = ["islandora-php83-common", "amd64-common"] - contexts = dependencies("islandora-php83", "amd64") - cache-from = cacheFrom("islandora-php83", "amd64") - cache-to = cacheTo("islandora-php83", "amd64") - tags = tags("islandora-php83", "amd64") -} - -target "islandora-php84-amd64" { - inherits = ["islandora-php84-common", "amd64-common"] - contexts = dependencies("islandora-php84", "amd64") - cache-from = cacheFrom("islandora-php84", "amd64") - cache-to = cacheTo("islandora-php84", "amd64") - tags = tags("islandora-php84", "amd64") -} - -target "java17-amd64" { - inherits = ["java17-common", "amd64-common"] - contexts = dependencies("java17", "amd64") - cache-from = cacheFrom("java17", "amd64") - cache-to = cacheTo("java17", "amd64") - tags = tags("java17", "amd64") -} - -target "java21-amd64" { - inherits = ["java21-common", "amd64-common"] - contexts = dependencies("java21", "amd64") - cache-from = cacheFrom("java21", "amd64") - cache-to = cacheTo("java21", "amd64") - tags = tags("java21", "amd64") -} - -target "java25-amd64" { - inherits = ["java25-common", "amd64-common"] - contexts = dependencies("java25", "amd64") - cache-from = cacheFrom("java25", "amd64") - cache-to = cacheTo("java25", "amd64") - tags = tags("java25", "amd64") -} - -target "leptonica-amd64" { - inherits = ["leptonica-common", "amd64-common"] - cache-from = cacheFrom("leptonica", "amd64") - cache-to = cacheTo("leptonica", "amd64") - tags = tags("leptonica", "amd64") -} - -target "mariadb11-amd64" { - inherits = ["mariadb11-common", "amd64-common"] - contexts = dependencies("mariadb11", "amd64") - cache-from = cacheFrom("mariadb11", "amd64") - cache-to = cacheTo("mariadb11", "amd64") - tags = tags("mariadb11", "amd64") -} - -target "mergepdf-amd64" { - inherits = ["mergepdf-common", "amd64-common"] - contexts = dependencies("mergepdf", "amd64") - cache-from = cacheFrom("mergepdf", "amd64") - cache-to = cacheTo("mergepdf", "amd64") - tags = tags("mergepdf", "amd64") -} - -target "nginx-php83-amd64" { - inherits = ["nginx-php83-common", "amd64-common"] - contexts = dependencies("nginx-php83", "amd64") - cache-from = cacheFrom("nginx-php83", "amd64") - cache-to = cacheTo("nginx-php83", "amd64") - tags = tags("nginx-php83", "amd64") -} - -target "nginx-php84-amd64" { - inherits = ["nginx-php84-common", "amd64-common"] - contexts = dependencies("nginx-php84", "amd64") - cache-from = cacheFrom("nginx-php84", "amd64") - cache-to = cacheTo("nginx-php84", "amd64") - tags = tags("nginx-php84", "amd64") -} - -target "ojs-php83-amd64" { - inherits = ["ojs-php83-common", "amd64-common"] - contexts = dependencies("ojs-php83", "amd64") - cache-from = cacheFrom("ojs-php83", "amd64") - cache-to = cacheTo("ojs-php83", "amd64") - tags = tags("ojs-php83", "amd64") -} - -target "ojs-php84-amd64" { - inherits = ["ojs-php84-common", "amd64-common"] - contexts = dependencies("ojs-php84", "amd64") - cache-from = cacheFrom("ojs-php84", "amd64") - cache-to = cacheTo("ojs-php84", "amd64") - tags = tags("ojs-php84", "amd64") -} - -target "omeka-classic-php83-amd64" { - inherits = ["omeka-classic-php83-common", "amd64-common"] - contexts = dependencies("omeka-classic-php83", "amd64") - cache-from = cacheFrom("omeka-classic-php83", "amd64") - cache-to = cacheTo("omeka-classic-php83", "amd64") - tags = tags("omeka-classic-php83", "amd64") -} - -target "omeka-classic-php84-amd64" { - inherits = ["omeka-classic-php84-common", "amd64-common"] - contexts = dependencies("omeka-classic-php84", "amd64") - cache-from = cacheFrom("omeka-classic-php84", "amd64") - cache-to = cacheTo("omeka-classic-php84", "amd64") - tags = tags("omeka-classic-php84", "amd64") -} - -target "omeka-s-php83-amd64" { - inherits = ["omeka-s-php83-common", "amd64-common"] - contexts = dependencies("omeka-s-php83", "amd64") - cache-from = cacheFrom("omeka-s-php83", "amd64") - cache-to = cacheTo("omeka-s-php83", "amd64") - tags = tags("omeka-s-php83", "amd64") -} - -target "omeka-s-php84-amd64" { - inherits = ["omeka-s-php84-common", "amd64-common"] - contexts = dependencies("omeka-s-php84", "amd64") - cache-from = cacheFrom("omeka-s-php84", "amd64") - cache-to = cacheTo("omeka-s-php84", "amd64") - tags = tags("omeka-s-php84", "amd64") -} - -target "php83-amd64" { - inherits = ["php83-common", "amd64-common"] - contexts = dependencies("php83", "amd64") - cache-from = cacheFrom("php83", "amd64") - cache-to = cacheTo("php83", "amd64") - tags = tags("php83", "amd64") -} - -target "php84-amd64" { - inherits = ["php84-common", "amd64-common"] - contexts = dependencies("php84", "amd64") - cache-from = cacheFrom("php84", "amd64") - cache-to = cacheTo("php84", "amd64") - tags = tags("php84", "amd64") -} - -target "scyllaridae-amd64" { - inherits = ["scyllaridae-common", "amd64-common"] - contexts = dependencies("scyllaridae", "amd64") - cache-from = cacheFrom("scyllaridae", "amd64") - cache-to = cacheTo("scyllaridae", "amd64") - tags = tags("scyllaridae", "amd64") -} - -target "solr9-amd64" { - inherits = ["solr9-common", "amd64-common"] - contexts = dependencies("solr9", "amd64") - cache-from = cacheFrom("solr9", "amd64") - cache-to = cacheTo("solr9", "amd64") - tags = tags("solr9", "amd64") -} - -target "solr10-amd64" { - inherits = ["solr10-common", "amd64-common"] - contexts = dependencies("solr10", "amd64") - cache-from = cacheFrom("solr10", "amd64") - cache-to = cacheTo("solr10", "amd64") - tags = tags("solr10", "amd64") -} - -target "tomcat9-amd64" { - inherits = ["tomcat9-common", "amd64-common"] - contexts = dependencies("tomcat9", "amd64") - cache-from = cacheFrom("tomcat9", "amd64") - cache-to = cacheTo("tomcat9", "amd64") - tags = tags("tomcat9", "amd64") -} - -target "tomcat11-amd64" { - inherits = ["tomcat11-common", "amd64-common"] - contexts = dependencies("tomcat11", "amd64") - cache-from = cacheFrom("tomcat11", "amd64") - cache-to = cacheTo("tomcat11", "amd64") - tags = tags("tomcat11", "amd64") -} - -target "wp-php83-amd64" { - inherits = ["wp-php83-common", "amd64-common"] - contexts = dependencies("wp-php83", "amd64") - cache-from = cacheFrom("wp-php83", "amd64") - cache-to = cacheTo("wp-php83", "amd64") - tags = tags("wp-php83", "amd64") -} - -target "wp-php84-amd64" { - inherits = ["wp-php84-common", "amd64-common"] - contexts = dependencies("wp-php84", "amd64") - cache-from = cacheFrom("wp-php84", "amd64") - cache-to = cacheTo("wp-php84", "amd64") - tags = tags("wp-php84", "amd64") -} - -############################################################################### -# linux/arm64 targets. -############################################################################### -target "activemq5-arm64" { - inherits = ["activemq5-common", "arm64-common"] - contexts = dependencies("activemq5", "arm64") - cache-from = cacheFrom("activemq5", "arm64") - cache-to = cacheTo("activemq5", "arm64") - tags = tags("activemq5", "arm64") -} - -target "activemq6-arm64" { - inherits = ["activemq6-common", "arm64-common"] - contexts = dependencies("activemq6", "arm64") - cache-from = cacheFrom("activemq6", "arm64") - cache-to = cacheTo("activemq6", "arm64") - tags = tags("activemq6", "arm64") -} - -target "alpaca-arm64" { - inherits = ["alpaca-common", "arm64-common"] - contexts = dependencies("alpaca", "arm64") - cache-from = cacheFrom("alpaca", "arm64") - cache-to = cacheTo("alpaca", "arm64") - tags = tags("alpaca", "arm64") -} - -target "archivesspace-arm64" { - inherits = ["archivesspace-common", "arm64-common"] - contexts = dependencies("archivesspace", "arm64") - cache-from = cacheFrom("archivesspace", "arm64") - cache-to = cacheTo("archivesspace", "arm64") - tags = tags("archivesspace", "arm64") -} - -target "archivesspace-solr-arm64" { - inherits = ["archivesspace-solr-common", "arm64-common"] - contexts = dependencies("archivesspace-solr", "arm64") - cache-from = cacheFrom("archivesspace-solr", "arm64") - cache-to = cacheTo("archivesspace-solr", "arm64") - tags = tags("archivesspace-solr", "arm64") -} - -target "base-arm64" { - inherits = ["base-common", "arm64-common"] - cache-from = cacheFrom("base", "arm64") - cache-to = cacheTo("base", "arm64") - tags = tags("base", "arm64") -} - -target "blazegraph-arm64" { - inherits = ["blazegraph-common", "arm64-common"] - contexts = dependencies("blazegraph", "arm64") - cache-from = cacheFrom("blazegraph", "arm64") - cache-to = cacheTo("blazegraph", "arm64") - tags = tags("blazegraph", "arm64") -} - -target "crayfits-arm64" { - inherits = ["crayfits-common", "arm64-common"] - contexts = dependencies("crayfits", "arm64") - cache-from = cacheFrom("crayfits", "arm64") - cache-to = cacheTo("crayfits", "arm64") - tags = tags("crayfits", "arm64") -} - -target "drupal-php83-arm64" { - inherits = ["drupal-php83-common", "arm64-common"] - contexts = dependencies("drupal-php83", "arm64") - cache-from = cacheFrom("drupal-php83", "arm64") - cache-to = cacheTo("drupal-php83", "arm64") - tags = tags("drupal-php83", "arm64") -} - -target "drupal-php84-arm64" { - inherits = ["drupal-php84-common", "arm64-common"] - contexts = dependencies("drupal-php84", "arm64") - cache-from = cacheFrom("drupal-php84", "arm64") - cache-to = cacheTo("drupal-php84", "arm64") - tags = tags("drupal-php84", "arm64") -} - -target "fcrepo6-arm64" { - inherits = ["fcrepo6-common", "arm64-common"] - contexts = dependencies("fcrepo6", "arm64") - cache-from = cacheFrom("fcrepo6", "arm64") - cache-to = cacheTo("fcrepo6", "arm64") - tags = tags("fcrepo6", "arm64") -} - -target "fcrepo7-arm64" { - inherits = ["fcrepo7-common", "arm64-common"] - contexts = dependencies("fcrepo7", "arm64") - cache-from = cacheFrom("fcrepo7", "arm64") - cache-to = cacheTo("fcrepo7", "arm64") - tags = tags("fcrepo7", "arm64") -} - -target "fits-arm64" { - inherits = ["fits-common", "arm64-common"] - contexts = dependencies("fits", "arm64") - cache-from = cacheFrom("fits", "arm64") - cache-to = cacheTo("fits", "arm64") - tags = tags("fits", "arm64") -} - -target "go1-26-arm64" { - inherits = ["go1-26-common", "arm64-common"] - contexts = dependencies("go1-26", "arm64") - cache-from = cacheFrom("go1-26", "arm64") - cache-to = cacheTo("go1-26", "arm64") - tags = tags("go1-26", "arm64") -} - -target "homarus-arm64" { - inherits = ["homarus-common", "arm64-common"] - contexts = dependencies("homarus", "arm64") - cache-from = cacheFrom("homarus", "arm64") - cache-to = cacheTo("homarus", "arm64") - tags = tags("homarus", "arm64") -} - -target "houdini-arm64" { - inherits = ["houdini-common", "arm64-common"] - contexts = dependencies("houdini", "arm64") - cache-from = cacheFrom("houdini", "arm64") - cache-to = cacheTo("houdini", "arm64") - tags = tags("houdini", "arm64") -} - -target "hypercube-arm64" { - inherits = ["hypercube-common", "arm64-common"] - contexts = dependencies("hypercube", "arm64") - cache-from = cacheFrom("hypercube", "arm64") - cache-to = cacheTo("hypercube", "arm64") - tags = tags("hypercube", "arm64") -} - -target "islandora-php83-arm64" { - inherits = ["islandora-php83-common", "arm64-common"] - contexts = dependencies("islandora-php83", "arm64") - cache-from = cacheFrom("islandora-php83", "arm64") - cache-to = cacheTo("islandora-php83", "arm64") - tags = tags("islandora-php83", "arm64") -} - -target "islandora-php84-arm64" { - inherits = ["islandora-php84-common", "arm64-common"] - contexts = dependencies("islandora-php84", "arm64") - cache-from = cacheFrom("islandora-php84", "arm64") - cache-to = cacheTo("islandora-php84", "arm64") - tags = tags("islandora-php84", "arm64") -} - -target "java17-arm64" { - inherits = ["java17-common", "arm64-common"] - contexts = dependencies("java17", "arm64") - cache-from = cacheFrom("java17", "arm64") - cache-to = cacheTo("java17", "arm64") - tags = tags("java17", "arm64") -} - -target "java21-arm64" { - inherits = ["java21-common", "arm64-common"] - contexts = dependencies("java21", "arm64") - cache-from = cacheFrom("java21", "arm64") - cache-to = cacheTo("java21", "arm64") - tags = tags("java21", "arm64") -} - -target "java25-arm64" { - inherits = ["java25-common", "arm64-common"] - contexts = dependencies("java25", "arm64") - cache-from = cacheFrom("java25", "arm64") - cache-to = cacheTo("java25", "arm64") - tags = tags("java25", "arm64") -} - -target "leptonica-arm64" { - inherits = ["leptonica-common", "arm64-common"] - cache-from = cacheFrom("leptonica", "arm64") - cache-to = cacheTo("leptonica", "arm64") - tags = tags("leptonica", "arm64") -} - -target "mariadb11-arm64" { - inherits = ["mariadb11-common", "arm64-common"] - contexts = dependencies("mariadb11", "arm64") - cache-from = cacheFrom("mariadb11", "arm64") - cache-to = cacheTo("mariadb11", "arm64") - tags = tags("mariadb11", "arm64") -} - -target "mergepdf-arm64" { - inherits = ["mergepdf-common", "arm64-common"] - contexts = dependencies("mergepdf", "arm64") - cache-from = cacheFrom("mergepdf", "arm64") - cache-to = cacheTo("mergepdf", "arm64") - tags = tags("mergepdf", "arm64") -} - -target "nginx-php83-arm64" { - inherits = ["nginx-php83-common", "arm64-common"] - contexts = dependencies("nginx-php83", "arm64") - cache-from = cacheFrom("nginx-php83", "arm64") - cache-to = cacheTo("nginx-php83", "arm64") - tags = tags("nginx-php83", "arm64") -} - -target "nginx-php84-arm64" { - inherits = ["nginx-php84-common", "arm64-common"] - contexts = dependencies("nginx-php84", "arm64") - cache-from = cacheFrom("nginx-php84", "arm64") - cache-to = cacheTo("nginx-php84", "arm64") - tags = tags("nginx-php84", "arm64") -} - -target "ojs-php83-arm64" { - inherits = ["ojs-php83-common", "arm64-common"] - contexts = dependencies("ojs-php83", "arm64") - cache-from = cacheFrom("ojs-php83", "arm64") - cache-to = cacheTo("ojs-php83", "arm64") - tags = tags("ojs-php83", "arm64") -} - -target "ojs-php84-arm64" { - inherits = ["ojs-php84-common", "arm64-common"] - contexts = dependencies("ojs-php84", "arm64") - cache-from = cacheFrom("ojs-php84", "arm64") - cache-to = cacheTo("ojs-php84", "arm64") - tags = tags("ojs-php84", "arm64") -} - -target "omeka-classic-php83-arm64" { - inherits = ["omeka-classic-php83-common", "arm64-common"] - contexts = dependencies("omeka-classic-php83", "arm64") - cache-from = cacheFrom("omeka-classic-php83", "arm64") - cache-to = cacheTo("omeka-classic-php83", "arm64") - tags = tags("omeka-classic-php83", "arm64") -} - -target "omeka-classic-php84-arm64" { - inherits = ["omeka-classic-php84-common", "arm64-common"] - contexts = dependencies("omeka-classic-php84", "arm64") - cache-from = cacheFrom("omeka-classic-php84", "arm64") - cache-to = cacheTo("omeka-classic-php84", "arm64") - tags = tags("omeka-classic-php84", "arm64") -} - -target "omeka-s-php83-arm64" { - inherits = ["omeka-s-php83-common", "arm64-common"] - contexts = dependencies("omeka-s-php83", "arm64") - cache-from = cacheFrom("omeka-s-php83", "arm64") - cache-to = cacheTo("omeka-s-php83", "arm64") - tags = tags("omeka-s-php83", "arm64") -} - -target "omeka-s-php84-arm64" { - inherits = ["omeka-s-php84-common", "arm64-common"] - contexts = dependencies("omeka-s-php84", "arm64") - cache-from = cacheFrom("omeka-s-php84", "arm64") - cache-to = cacheTo("omeka-s-php84", "arm64") - tags = tags("omeka-s-php84", "arm64") -} - -target "php83-arm64" { - inherits = ["php83-common", "arm64-common"] - contexts = dependencies("php83", "arm64") - cache-from = cacheFrom("php83", "arm64") - cache-to = cacheTo("php83", "arm64") - tags = tags("php83", "arm64") -} - -target "php84-arm64" { - inherits = ["php84-common", "arm64-common"] - contexts = dependencies("php84", "arm64") - cache-from = cacheFrom("php84", "arm64") - cache-to = cacheTo("php84", "arm64") - tags = tags("php84", "arm64") -} - -target "scyllaridae-arm64" { - inherits = ["scyllaridae-common", "arm64-common"] - contexts = dependencies("scyllaridae", "arm64") - cache-from = cacheFrom("scyllaridae", "arm64") - cache-to = cacheTo("scyllaridae", "arm64") - tags = tags("scyllaridae", "arm64") -} - -target "solr9-arm64" { - inherits = ["solr9-common", "arm64-common"] - contexts = dependencies("solr9", "arm64") - cache-from = cacheFrom("solr9", "arm64") - cache-to = cacheTo("solr9", "arm64") - tags = tags("solr9", "arm64") -} - -target "solr10-arm64" { - inherits = ["solr10-common", "arm64-common"] - contexts = dependencies("solr10", "arm64") - cache-from = cacheFrom("solr10", "arm64") - cache-to = cacheTo("solr10", "arm64") - tags = tags("solr10", "arm64") -} - -target "tomcat9-arm64" { - inherits = ["tomcat9-common", "arm64-common"] - contexts = dependencies("tomcat9", "arm64") - cache-from = cacheFrom("tomcat9", "arm64") - cache-to = cacheTo("tomcat9", "arm64") - tags = tags("tomcat9", "arm64") -} - -target "tomcat11-arm64" { - inherits = ["tomcat11-common", "arm64-common"] - contexts = dependencies("tomcat11", "arm64") - cache-from = cacheFrom("tomcat11", "arm64") - cache-to = cacheTo("tomcat11", "arm64") - tags = tags("tomcat11", "arm64") -} - -target "wp-php83-arm64" { - inherits = ["wp-php83-common", "arm64-common"] - contexts = dependencies("wp-php83", "arm64") - cache-from = cacheFrom("wp-php83", "arm64") - cache-to = cacheTo("wp-php83", "arm64") - tags = tags("wp-php83", "arm64") -} - -target "wp-php84-arm64" { - inherits = ["wp-php84-common", "arm64-common"] - contexts = dependencies("wp-php84", "arm64") - cache-from = cacheFrom("wp-php84", "arm64") - cache-to = cacheTo("wp-php84", "arm64") - tags = tags("wp-php84", "arm64") -} diff --git a/images/archivesspace/Dockerfile b/images/archivesspace/Dockerfile index 1cbd4089..4d6a9cb5 100644 --- a/images/archivesspace/Dockerfile +++ b/images/archivesspace/Dockerfile @@ -17,7 +17,7 @@ ARG \ # renovate: datasource=repology depName=alpine_3_24/jemalloc JEMALLOC_VERSION=5.3.0-r6 \ # renovate: datasource=repology depName=alpine_3_24/nodejs - NODEJS_VERSION=24.17.0-r0 \ + NODEJS_VERSION=24.18.1-r0 \ # renovate: datasource=repology depName=alpine_3_24/shared-mime-info SHARED_MIME_INFO_VERSION=2.4-r7 \ # renovate: datasource=repology depName=alpine_3_24/unzip diff --git a/images/base/Dockerfile b/images/base/Dockerfile index b9d4af37..0c2132da 100644 --- a/images/base/Dockerfile +++ b/images/base/Dockerfile @@ -37,8 +37,6 @@ ARG \ JQ_VERSION=1.8.1-r0 \ # renovate: datasource=repology depName=alpine_3_24/mariadb-client MARIADB_CLIENT_VERSION=11.8.8-r0 \ - # renovate: datasource=repology depName=alpine_3_24/mysql-client - MYSQL_CLIENT_VERSION=11.8.8-r0 \ # renovate: datasource=repology depName=alpine_3_24/netcat-openbsd NETCAT_OPENBSD_VERSION=1.234.1-r0 \ # renovate: datasource=repology depName=alpine_3_24/openssl @@ -69,7 +67,6 @@ RUN --mount=type=cache,id=base-apk-${TARGETARCH},sharing=locked,target=/var/cach gzip=="${GZIP_VERSION}" \ jq=="${JQ_VERSION}" \ mariadb-client=="${MARIADB_CLIENT_VERSION}" \ - mysql-client=="${MYSQL_CLIENT_VERSION}" \ netcat-openbsd=="${NETCAT_OPENBSD_VERSION}" \ openssl=="${OPENSSL_VERSION}" \ patch=="${PATCH_VERSION}" \ diff --git a/images/base/README.md b/images/base/README.md index 725753e9..b11364d5 100644 --- a/images/base/README.md +++ b/images/base/README.md @@ -11,6 +11,10 @@ It's based off off [Alpine Linux], and includes [s6 overlay] and [confd]. Requires `alpine` version is set in [docker-bake.hcl](../docker-bake.hcl). +The `libops/base` image tag follows the bundled s6-overlay release (for example, +`3.2.2.0`). It identifies the base process/runtime contract, not an application +version; consumers should continue pinning the image digest. + ## Settings ### Confd Settings diff --git a/images/base/rootfs/usr/local/bin/generate-certs.sh b/images/base/rootfs/usr/local/bin/generate-certs.sh new file mode 100755 index 00000000..08e888fa --- /dev/null +++ b/images/base/rootfs/usr/local/bin/generate-certs.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# shellcheck shell=bash + +set -euo pipefail + +CERT_DIR="${CERT_DIR:-./certs}" +CA_SUBJECT="${CA_SUBJECT:-/CN=LibOps Local Development CA}" +LEAF_SUBJECT="${LEAF_SUBJECT:-/CN=localhost}" +SUBJECT_ALT_NAMES="${SUBJECT_ALT_NAMES:-DNS:localhost,IP:127.0.0.1,IP:::1}" +CA_KEY="${CERT_DIR}/rootCA-key.pem"; CA_CERT="${CERT_DIR}/rootCA.pem" +LEAF_KEY="${CERT_DIR}/privkey.pem"; LEAF_CERT="${CERT_DIR}/cert.pem" +readonly CERT_DIR CA_SUBJECT LEAF_SUBJECT SUBJECT_ALT_NAMES CA_KEY CA_CERT LEAF_KEY LEAF_CERT + +install -d -m 0700 "${CERT_DIR}" +[ ! -s "${CA_CERT}" ] || [ -s "${CA_KEY}" ] || { echo "CA key is missing for ${CA_CERT}" >&2; exit 1; } +[ ! -s "${LEAF_CERT}" ] || [ -s "${LEAF_KEY}" ] || { echo "Leaf key is missing for ${LEAF_CERT}" >&2; exit 1; } +if [ ! -s "${CA_KEY}" ]; then umask 077; openssl genrsa -out "${CA_KEY}" 4096; fi +chmod 0600 "${CA_KEY}" +if [ ! -s "${CA_CERT}" ]; then + openssl req -x509 -new -sha256 -key "${CA_KEY}" -out "${CA_CERT}" -days 3650 -subj "${CA_SUBJECT}" \ + -addext 'subjectKeyIdentifier=hash' -addext 'authorityKeyIdentifier=keyid:always,issuer' \ + -addext 'basicConstraints=critical,CA:TRUE' -addext 'keyUsage=critical,keyCertSign,cRLSign' +fi +chmod 0644 "${CA_CERT}" +if [ ! -s "${LEAF_KEY}" ]; then umask 077; openssl genrsa -out "${LEAF_KEY}" 2048; fi +chmod 0600 "${LEAF_KEY}" +if [ ! -s "${LEAF_CERT}" ]; then + workdir="$(mktemp -d)"; trap 'rm -rf -- "${workdir}"' EXIT + openssl req -new -sha256 -key "${LEAF_KEY}" -out "${workdir}/leaf.csr" -subj "${LEAF_SUBJECT}" + printf '%s\n' 'authorityKeyIdentifier=keyid,issuer' 'basicConstraints=critical,CA:FALSE' \ + 'keyUsage=critical,digitalSignature,keyEncipherment' 'extendedKeyUsage=serverAuth' \ + "subjectAltName=${SUBJECT_ALT_NAMES}" >"${workdir}/leaf.ext" + openssl x509 -req -sha256 -in "${workdir}/leaf.csr" -CA "${CA_CERT}" -CAkey "${CA_KEY}" \ + -set_serial "0x$(openssl rand -hex 16)" -out "${LEAF_CERT}" -days 825 -extfile "${workdir}/leaf.ext" +fi +chmod 0644 "${LEAF_CERT}" +if [ -n "${HOST_UID:-}" ]; then + printf '%s\n' "${HOST_UID}" >"${CERT_DIR}/UID" + chmod 0644 "${CERT_DIR}/UID" + chown -R "${HOST_UID}:${HOST_GID:-${HOST_UID}}" "${CERT_DIR}" +fi +echo "Development certificates are ready in ${CERT_DIR}." diff --git a/images/base/rootfs/usr/local/bin/generate-compose-secrets.sh b/images/base/rootfs/usr/local/bin/generate-compose-secrets.sh new file mode 100755 index 00000000..1eab23e8 --- /dev/null +++ b/images/base/rootfs/usr/local/bin/generate-compose-secrets.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# shellcheck shell=bash + +set -euo pipefail + +COMPOSE_FILE="${COMPOSE_FILE:-compose.yaml}" +SECRETS_ROOT="${SECRETS_ROOT:-./secrets}" +readonly COMPOSE_FILE SECRETS_ROOT + +format_secret() { + local name="$1" variable="SECRET_FORMAT_${1//[^A-Za-z0-9_]/_}" format + format="${!variable:-hex32}" + case "${format}" in + hex32) openssl rand -hex 32 ;; + base64-32) openssl rand -base64 32 | tr -d '\n' ;; + laravel-base64) printf 'base64:'; openssl rand -base64 32 | tr -d '\n' ;; + salt74) + value="$(openssl rand -base64 96 | tr -dc 'A-Za-z0-9_-')" + printf '%s' "${value:0:74}" + ;; + *) echo "Unsupported secret format ${format} for ${name}" >&2; return 1 ;; + esac +} + +resolve_path() { + local declared="$1" + case "${declared}" in + ./*) printf '%s/%s' "${PWD}" "${declared#./}" ;; + /*) printf '%s' "${declared}" ;; + *) printf '%s/%s' "${SECRETS_ROOT%/}" "${declared##*/}" ;; + esac +} + +[ -f "${COMPOSE_FILE}" ] || { echo "Compose file not found: ${COMPOSE_FILE}" >&2; exit 1; } +umask 077 +while IFS=$'\t' read -r name declared; do + if [ -z "${name}" ] || [ -z "${declared}" ]; then + continue + fi + path="$(resolve_path "${declared}")" + case "${path}" in + */certs/*) continue ;; + esac + install -d -m 0700 "$(dirname -- "${path}")" + if [ ! -s "${path}" ]; then + echo "Creating: ${path}" >&2 + format_secret "${name}" >"${path}" + fi + chmod 0600 "${path}" +done < <(yq -r '(.secrets // {}) | to_entries[] | select(.value.file != null) | [.key, .value.file] | @tsv' "${COMPOSE_FILE}") + +if [ -n "${HOST_UID:-}" ]; then + chown -R "${HOST_UID}:${HOST_GID:-${HOST_UID}}" "${SECRETS_ROOT}" +fi diff --git a/images/base/rootfs/usr/local/bin/init-database.sh b/images/base/rootfs/usr/local/bin/init-database.sh new file mode 100755 index 00000000..b61217c2 --- /dev/null +++ b/images/base/rootfs/usr/local/bin/init-database.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# shellcheck shell=bash + +set -euo pipefail + +DB_HOST="${DB_HOST:-mariadb}" +DB_PORT="${DB_PORT:-3306}" +DB_ROOT_USER="${DB_ROOT_USER:-root}" +DB_CHARACTER_SET="${DB_CHARACTER_SET:-utf8mb4}" +DB_COLLATION="${DB_COLLATION:-utf8mb4_unicode_ci}" +DB_ROOT_PASSWORD_FILE="${DB_ROOT_PASSWORD_FILE:-/run/secrets/DB_ROOT_PASSWORD}" +DB_PASSWORD_FILE="${DB_PASSWORD_FILE:-/run/secrets/DB_PASSWORD}" +readonly DB_HOST DB_PORT DB_ROOT_USER DB_CHARACTER_SET DB_COLLATION +readonly DB_ROOT_PASSWORD_FILE DB_PASSWORD_FILE + +: "${DB_NAME:?DB_NAME is required}" +: "${DB_USER:?DB_USER is required}" + +validate_identifier() { + local name="$1" value="$2" + if [[ ! "${value}" =~ ^[A-Za-z0-9_]+$ ]]; then + echo "${name} must contain only letters, numbers, and underscores" >&2 + return 1 + fi +} + +read_secret() { + local name="$1" path="$2" value + if [ ! -s "${path}" ]; then + echo "${name} secret is missing or empty at ${path}" >&2 + return 1 + fi + value="$(cat -- "${path}")" + if [ -z "${value}" ] || [[ "${value}" == *$'\n'* ]] || [[ "${value}" == *$'\r'* ]]; then + echo "${name} must be a non-empty single-line secret" >&2 + return 1 + fi + printf '%s' "${value}" +} + +escape_option_value() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '%s' "${value}" +} + +escape_sql_literal() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\'/\'\'}" + printf '%s' "${value}" +} + +validate_identifier DB_ROOT_USER "${DB_ROOT_USER}" +validate_identifier DB_NAME "${DB_NAME}" +validate_identifier DB_USER "${DB_USER}" +validate_identifier DB_CHARACTER_SET "${DB_CHARACTER_SET}" +validate_identifier DB_COLLATION "${DB_COLLATION}" +if [[ ! "${DB_HOST}" =~ ^[A-Za-z0-9._:-]+$ ]]; then + echo "DB_HOST contains unsupported characters" >&2 + exit 1 +fi +if [[ ! "${DB_PORT}" =~ ^[0-9]+$ ]] || [ "${DB_PORT}" -lt 1 ] || [ "${DB_PORT}" -gt 65535 ]; then + echo "DB_PORT must be an integer from 1 through 65535" >&2 + exit 1 +fi + +root_password="$(read_secret DB_ROOT_PASSWORD "${DB_ROOT_PASSWORD_FILE}")" +db_password="$(read_secret DB_PASSWORD "${DB_PASSWORD_FILE}")" +root_password_option="$(escape_option_value "${root_password}")" +db_user_sql="$(escape_sql_literal "${DB_USER}")" +db_password_sql="$(escape_sql_literal "${db_password}")" +readonly root_password db_password root_password_option db_user_sql db_password_sql + +credentials_dir="$(mktemp -d)" +credentials_file="${credentials_dir}/client.cnf" +cleanup() { rm -rf -- "${credentials_dir}"; } +trap cleanup EXIT +umask 077 +cat >"${credentials_file}" < [args...] buildkit test [flags] + buildkit template-test --root PATH [--root PATH ...] Metadata commands match ci/image-metadata.sh: list diff --git a/internal/buildkit/metadata_test.go b/internal/buildkit/metadata_test.go index 1ae7c378..39286ef1 100644 --- a/internal/buildkit/metadata_test.go +++ b/internal/buildkit/metadata_test.go @@ -898,6 +898,55 @@ func TestPublishedBaseImageRefreshesPackageMetadata(t *testing.T) { } } +func TestPushWorkflowPublishesRequiredLevelFourGate(t *testing.T) { + root := repoRoot(t) + workflowPath := filepath.Join(root, ".github", "workflows", "push.yml") + workflow, err := os.ReadFile(workflowPath) + if err != nil { + t.Fatalf("read %s: %v", workflowPath, err) + } + for _, required := range []string{ + " test-level-4-matrix:\n", + " test-level-4:\n", + " needs: [test-level-4-matrix]\n", + " RESULT: ${{ needs.test-level-4-matrix.result }}\n", + } { + if !strings.Contains(string(workflow), required) { + t.Errorf("%s must contain required level-4 gate contract %q", workflowPath, required) + } + } +} + +func TestBaseOwnsSharedComposeInitializationTools(t *testing.T) { + root := repoRoot(t) + for _, name := range []string{"init-database.sh", "generate-compose-secrets.sh", "generate-certs.sh"} { + path := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", name) + info, err := os.Stat(path) + if err != nil { + t.Fatalf("shared initialization tool %s is missing: %v", name, err) + } + if info.Mode().Perm()&0o111 == 0 { + t.Errorf("shared initialization tool %s is not executable", name) + } + contents, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read shared initialization tool %s: %v", name, err) + } + if !strings.HasPrefix(string(contents), "#!/usr/bin/env bash\n") { + t.Errorf("shared initialization tool %s must run without an initialized s6 environment", name) + } + } + secrets, err := os.ReadFile(filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "generate-compose-secrets.sh")) + if err != nil { + t.Fatal(err) + } + for _, required := range []string{".secrets // {}", "SECRET_FORMAT_", "laravel-base64", "salt74", "openssl rand"} { + if !strings.Contains(string(secrets), required) { + t.Errorf("canonical secret generator must contain %q", required) + } + } +} + func TestMarketedApplicationImagesDoNotShipKnownCredentials(t *testing.T) { root := repoRoot(t) images := []string{"archivesspace", "drupal", "ojs", "omeka-s", "omeka-classic", "wp"} diff --git a/internal/buildkit/template_test_runner.go b/internal/buildkit/template_test_runner.go new file mode 100644 index 00000000..c38ffa2a --- /dev/null +++ b/internal/buildkit/template_test_runner.go @@ -0,0 +1,136 @@ +package buildkit + +import ( + "bytes" + "context" + "flag" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "time" +) + +// RunTemplateTests executes each Compose template's retained smoke-test hook +// with the same bounded, keep-going presentation as image tests. +func RunTemplateTests(args []string, stdout, stderr io.Writer) int { + var roots stringSliceFlag + flags := flag.NewFlagSet("template-test", flag.ContinueOnError) + flags.SetOutput(stderr) + timeout := flags.Duration("timeout", 20*time.Minute, "maximum runtime for each template smoke test") + keepGoing := flags.Bool("keep-going", false, "continue after a template smoke test fails") + checkOnly := flags.Bool("check-only", false, "validate cross-template image pins without running smoke tests") + flags.Var(&roots, "root", "template checkout containing scripts/test.sh; may be repeated") + if err := flags.Parse(args); err != nil { + return 2 + } + roots = append(roots, flags.Args()...) + if len(roots) == 0 { + fmt.Fprintln(stderr, "at least one --root is required") + return 2 + } + if err := verifyTemplateImagePins(roots); err != nil { + fmt.Fprintf(stderr, "template image pin drift: %v\n", err) + return 1 + } + if *checkOnly { + fmt.Fprintf(stdout, "%d template contract(s) passed\n", len(roots)) + return 0 + } + failed := 0 + for _, root := range roots { + name := filepath.Base(filepath.Clean(root)) + fmt.Fprintf(stdout, "\n=== RUN template/%s\n", name) + script := filepath.Join(root, "scripts", "test.sh") + info, err := os.Stat(script) + if err != nil || info.IsDir() { + fmt.Fprintf(stderr, "--- FAIL: template/%s: scripts/test.sh is missing\n", name) + failed++ + if !*keepGoing { + break + } + continue + } + ctx, cancel := context.WithTimeout(context.Background(), *timeout) + command := exec.CommandContext(ctx, "bash", script) + command.Dir = root + var output bytes.Buffer + command.Stdout = io.MultiWriter(stdout, &output) + command.Stderr = io.MultiWriter(stderr, &output) + started := time.Now() + err = command.Run() + cancel() + if ctx.Err() == context.DeadlineExceeded { + err = fmt.Errorf("timed out after %s", *timeout) + } + if err != nil { + fmt.Fprintf(stderr, "--- FAIL: template/%s (%s): %v\n", name, time.Since(started).Round(time.Millisecond), err) + failed++ + if !*keepGoing { + break + } + continue + } + fmt.Fprintf(stdout, "--- PASS: template/%s (%s)\n", name, time.Since(started).Round(time.Millisecond)) + } + if failed > 0 { + fmt.Fprintf(stderr, "\n%d template test(s) failed\n", failed) + return 1 + } + fmt.Fprintf(stdout, "\n%d template test(s) passed\n", len(roots)) + return 0 +} + +var templateImagePattern = regexp.MustCompile(`(?m)^\s*image:\s*([^\s@]+)@sha256:([a-f0-9]{64})\s*$`) + +func verifyTemplateImagePins(roots []string) error { + pins := map[string]map[string][]string{} + for _, root := range roots { + composePath, err := templateComposePath(root) + if err != nil { + return err + } + data, err := os.ReadFile(composePath) // #nosec G304 -- explicit operator-selected template checkout. + if err != nil { + return fmt.Errorf("read %s: %w", composePath, err) + } + for _, match := range templateImagePattern.FindAllStringSubmatch(string(data), -1) { + if pins[match[1]] == nil { + pins[match[1]] = map[string][]string{} + } + pins[match[1]][match[2]] = append(pins[match[1]][match[2]], filepath.Base(filepath.Clean(root))) + } + } + var drift []string + for image, digests := range pins { + if len(digests) < 2 { + continue + } + var values []string + for digest, repos := range digests { + sort.Strings(repos) + values = append(values, fmt.Sprintf("%s in %s", digest[:12], strings.Join(repos, ","))) + } + sort.Strings(values) + drift = append(drift, fmt.Sprintf("%s: %s", image, strings.Join(values, "; "))) + } + sort.Strings(drift) + if len(drift) > 0 { + return fmt.Errorf("same image tag resolves to different digests: %s", strings.Join(drift, " | ")) + } + return nil +} + +func templateComposePath(root string) (string, error) { + for _, name := range []string{"compose.yaml", "docker-compose.yaml", "docker-compose.yml"} { + path := filepath.Join(root, name) + if info, err := os.Stat(path); err == nil && !info.IsDir() { + return path, nil + } + } + return "", fmt.Errorf("no Compose project file found in %s", root) +} diff --git a/internal/buildkit/template_test_runner_test.go b/internal/buildkit/template_test_runner_test.go new file mode 100644 index 00000000..5b38e699 --- /dev/null +++ b/internal/buildkit/template_test_runner_test.go @@ -0,0 +1,58 @@ +package buildkit + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRunTemplateTests(t *testing.T) { + root := t.TempDir() + scripts := filepath.Join(root, "scripts") + if err := os.MkdirAll(scripts, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(scripts, "test.sh"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "compose.yaml"), []byte("services: {}\n"), 0o644); err != nil { + t.Fatal(err) + } + var stdout, stderr bytes.Buffer + if code := RunTemplateTests([]string{"--root", root}, &stdout, &stderr); code != 0 { + t.Fatalf("code=%d stderr=%s", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "PASS") { + t.Fatalf("output=%s", stdout.String()) + } +} + +func TestVerifyTemplateImagePinsRejectsDrift(t *testing.T) { + roots := []string{filepath.Join(t.TempDir(), "one"), filepath.Join(t.TempDir(), "two")} + for index, root := range roots { + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatal(err) + } + digest := strings.Repeat(string(rune('a'+index)), 64) + contents := "services:\n app:\n image: libops/base:1@sha256:" + digest + "\n" + if err := os.WriteFile(filepath.Join(root, "compose.yaml"), []byte(contents), 0o644); err != nil { + t.Fatal(err) + } + } + if err := verifyTemplateImagePins(roots); err == nil { + t.Fatal("digest drift was accepted") + } +} + +func TestVerifyTemplateImagePinsAcceptsLegacyComposeFilename(t *testing.T) { + root := t.TempDir() + contents := "services:\n app:\n image: libops/base:1@sha256:" + strings.Repeat("a", 64) + "\n" + if err := os.WriteFile(filepath.Join(root, "docker-compose.yml"), []byte(contents), 0o644); err != nil { + t.Fatal(err) + } + if err := verifyTemplateImagePins([]string{root}); err != nil { + t.Fatalf("legacy Compose filename rejected: %v", err) + } +}