diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 13fb012a..0f74187d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,6 +34,9 @@ on:
jobs:
build:
+ env:
+ # renovate: datasource=docker depName=moby/buildkit
+ BUILDKIT_IMAGE: moby/buildkit:v0.31.0@sha256:a095b3d11ce1a9a05b6064ef515dfca0291ec5bcf2ea8178da8f6461924294e1
strategy:
matrix:
runner: [ubuntu-24.04, ubuntu-24.04-arm]
@@ -138,14 +141,14 @@ jobs:
done
}
- retry_with_backoff docker pull moby/buildkit:v0.31.0
+ retry_with_backoff docker pull "${BUILDKIT_IMAGE}"
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
driver-opts: |
- image=moby/buildkit:v0.31.0
+ image=${{ env.BUILDKIT_IMAGE }}
network=host
- id: build
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index ac1c93cc..2a494d60 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -1,9 +1,17 @@
name: Build and Push Docker Images
on:
push:
- branches: ["*"]
+ branches: ["**"]
paths:
+ - ".github/workflows/**"
+ - "ci/**"
+ - "cmd/**"
+ - "go.mod"
+ - "go.sum"
- "images/**"
+ - "internal/**"
+ - "docker-bake.hcl"
+ - "Makefile"
workflow_dispatch:
inputs:
image:
@@ -35,7 +43,9 @@ jobs:
- uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6
- name: lint
- run: shellcheck **/*.sh
+ run: |
+ mapfile -d '' scripts < <(git grep -Ilz -E '^#!.*(ba)?sh')
+ shellcheck "${scripts[@]}"
plan:
runs-on: ubuntu-24.04
@@ -63,6 +73,8 @@ jobs:
sitectl_isle_libops_image_tags: ${{ steps.plan.outputs.sitectl_isle_libops_image_tags }}
sitectl_isle_images: ${{ steps.plan.outputs.sitectl_isle_images }}
sitectl_create_smoke_matrix: ${{ steps.plan.outputs.sitectl_create_smoke_matrix }}
+ sitectl_repository: ${{ steps.plan.outputs.sitectl_repository }}
+ sitectl_ref: ${{ steps.plan.outputs.sitectl_ref }}
steps:
- uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6
with:
@@ -73,6 +85,9 @@ jobs:
with:
go-version-file: go.mod
+ - name: Test planner and repository contracts
+ run: go test -count=1 ./...
+
- id: plan
name: Plan
env:
@@ -124,6 +139,9 @@ jobs:
local create_args="$3"
local image_tags="$4"
local images="$5"
+ local local_plugin_repository="${6:-}"
+ local local_plugin_ref="${7:-main}"
+ local local_plugin_path="${8:-}"
if [ -z "${image_tags// }" ]; then
return
fi
@@ -134,16 +152,38 @@ jobs:
--arg create_args "$create_args" \
--arg image_tags "$image_tags" \
--arg images "$images" \
+ --arg local_plugin_repository "$local_plugin_repository" \
+ --arg local_plugin_ref "$local_plugin_ref" \
+ --arg local_plugin_path "$local_plugin_path" \
'. + [{
"plugin": $plugin,
"packages": $packages,
"create-args": $create_args,
"image-tags": $image_tags,
- "images": $images
+ "images": $images,
+ "local-plugin-repository": $local_plugin_repository,
+ "local-plugin-ref": $local_plugin_ref,
+ "local-plugin-path": $local_plugin_path
}]' <<<"$smoke_matrix"
)"
}
+ coordinated_template_args() {
+ local repository="$1"
+ local branch="${GITHUB_REF_NAME}"
+
+ if [ "${branch}" = "main" ]; then
+ return
+ fi
+ if [[ ! "${branch}" =~ ^[A-Za-z0-9._/-]+$ ]]; then
+ echo "Skipping coordinated template branch with unsafe ref name: ${branch}" >&2
+ return
+ fi
+ if git ls-remote --exit-code --heads "${repository}" "refs/heads/${branch}" >/dev/null 2>&1; then
+ printf '%s' "--template-repo ${repository} --template-branch ${branch}"
+ fi
+ }
+
sitectl_wp_image_tags="$(planned_tag_assignment wp wp-php84)"
sitectl_omeka_s_image_tags="$(planned_tag_assignment omeka-s omeka-s-php84)"
sitectl_omeka_classic_image_tags="$(planned_tag_assignment omeka-classic omeka-classic-php84)"
@@ -151,14 +191,40 @@ jobs:
sitectl_drupal_image_tags="$(planned_tag_assignment drupal drupal-php84)"
sitectl_isle_libops_image_tags="$(planned_tag_assignment drupal islandora-php84)"
sitectl_isle_images=""
+ sitectl_wp_create_args="$(coordinated_template_args https://github.com/libops/wp)"
+ sitectl_omeka_s_create_args="$(coordinated_template_args https://github.com/libops/omeka-s)"
+ sitectl_omeka_classic_create_args="$(coordinated_template_args https://github.com/libops/omeka-classic)"
+ sitectl_ojs_create_args="$(coordinated_template_args https://github.com/libops/ojs)"
+ 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"
+ 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=""
+ sitectl_ref="main"
+ if [ -n "$(coordinated_template_args https://github.com/libops/sitectl)" ]; then
+ sitectl_repository="libops/sitectl"
+ sitectl_ref="${GITHUB_REF_NAME}"
+ fi
+ sitectl_isle_packages="sitectl sitectl-isle sitectl-drupal"
+ sitectl_isle_plugin_repository=""
+ sitectl_isle_plugin_ref="main"
+ sitectl_isle_plugin_path=""
+ if [ -n "$(coordinated_template_args https://github.com/libops/sitectl-isle)" ]; then
+ sitectl_isle_packages="sitectl sitectl-drupal"
+ sitectl_isle_plugin_repository="libops/sitectl-isle"
+ sitectl_isle_plugin_ref="${GITHUB_REF_NAME}"
+ sitectl_isle_plugin_path="sitectl-isle-source"
+ fi
smoke_matrix='[]'
- add_smoke_row "wp" "sitectl sitectl-wp" "" "$sitectl_wp_image_tags" ""
- add_smoke_row "omeka-s" "sitectl sitectl-omeka-s" "" "$sitectl_omeka_s_image_tags" ""
- add_smoke_row "omeka-classic" "sitectl sitectl-omeka-classic" "" "$sitectl_omeka_classic_image_tags" ""
- add_smoke_row "ojs" "sitectl sitectl-ojs" "" "$sitectl_ojs_image_tags" ""
- add_smoke_row "drupal" "sitectl sitectl-drupal" "" "$sitectl_drupal_image_tags" ""
- add_smoke_row "isle" "sitectl sitectl-isle sitectl-drupal" "--template-repo https://github.com/libops/isle --fcrepo off --blazegraph off --isle-file-system-uri private --iiif triplet --iiif-topology disabled --bot-mitigation off" "$sitectl_isle_libops_image_tags" "$sitectl_isle_images"
+ add_smoke_row "wp" "sitectl sitectl-wp" "$sitectl_wp_create_args" "$sitectl_wp_image_tags" ""
+ add_smoke_row "omeka-s" "sitectl sitectl-omeka-s" "$sitectl_omeka_s_create_args" "$sitectl_omeka_s_image_tags" ""
+ add_smoke_row "omeka-classic" "sitectl sitectl-omeka-classic" "$sitectl_omeka_classic_create_args" "$sitectl_omeka_classic_image_tags" ""
+ add_smoke_row "ojs" "sitectl sitectl-ojs" "$sitectl_ojs_create_args" "$sitectl_ojs_image_tags" ""
+ add_smoke_row "drupal" "sitectl sitectl-drupal" "$sitectl_drupal_create_args" "$sitectl_drupal_image_tags" ""
+ add_smoke_row "isle" "$sitectl_isle_packages" "$sitectl_isle_create_args" "$sitectl_isle_libops_image_tags" "$sitectl_isle_images" "$sitectl_isle_plugin_repository" "$sitectl_isle_plugin_ref" "$sitectl_isle_plugin_path"
{
echo "sitectl_wp_image_tags=${sitectl_wp_image_tags}"
@@ -169,6 +235,8 @@ jobs:
echo "sitectl_isle_libops_image_tags=${sitectl_isle_libops_image_tags}"
echo "sitectl_isle_images=${sitectl_isle_images}"
echo "sitectl_create_smoke_matrix=${smoke_matrix}"
+ echo "sitectl_repository=${sitectl_repository}"
+ echo "sitectl_ref=${sitectl_ref}"
} >> "$GITHUB_OUTPUT"
- name: Summary
@@ -375,13 +443,13 @@ jobs:
sitectl-create-smoke-test:
if: ${{ always() && needs.plan.outputs.sitectl_create_smoke_matrix != '[]' && !cancelled() && !failure() }}
- needs: [plan, test-level-4]
+ needs: [plan, test-level-0, test-level-1, test-level-2, test-level-3, test-level-4]
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.sitectl_create_smoke_matrix) }}
- uses: libops/.github/.github/workflows/sitectl-create-smoke-test.yaml@main
+ uses: libops/.github/.github/workflows/sitectl-create-smoke-test.yaml@e366f7422565ea5ee71c0c626b740e03f2e2ffcd # main
with:
plugin: ${{ matrix.plugin }}
create-definition: default
@@ -391,5 +459,10 @@ jobs:
image-tags: ${{ matrix.image-tags }}
images: ${{ matrix.images }}
packages: ${{ matrix.packages }}
+ local-plugin-repository: ${{ matrix.local-plugin-repository }}
+ local-plugin-ref: ${{ matrix.local-plugin-ref }}
+ local-plugin-path: ${{ matrix.local-plugin-path }}
+ sitectl-repository: ${{ needs.plan.outputs.sitectl_repository }}
+ sitectl-ref: ${{ needs.plan.outputs.sitectl_ref }}
permissions:
contents: read
diff --git a/Makefile b/Makefile
index 5e8b642a..535c0955 100644
--- a/Makefile
+++ b/Makefile
@@ -31,9 +31,12 @@ endef
# Used to include host-platform specific docker compose files.
OS := $(shell uname -s | tr A-Z a-z)
-# Used to determine set TAGS when no explicit value provided,
-# as well as to fetch branch specific remote caches when building.
-BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
+# Used to set TAGS when no explicit value is provided and to fetch branch-specific
+# remote caches. Keep it aligned with the metadata helper's Docker-tag fallback
+# normalization so names such as feature/foo produce valid cache references.
+# Pipe the ref directly into the normalizer so branch text is never re-evaluated
+# as part of a generated shell command.
+BRANCH = $(shell git -c safe.directory='$(CURDIR)' rev-parse --abbrev-ref HEAD | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^-+//; s/-+$$//')
# The buildkit builder to use.
BUILDER ?= default
@@ -121,7 +124,7 @@ docker-buildx: | docker
.SILENT: build/bake.json
build/bake.json: | docker-buildx jq build folder-permissions executable-permissons
set -x; \
- BRANCH=$(BRANCH) \
+ BRANCH="$(BRANCH)" \
CACHE_FROM_REPOSITORY=$(CACHE_FROM_REPOSITORY) \
CACHE_TO_REPOSITORY=$(CACHE_TO_REPOSITORY) \
REPOSITORY=$(REPOSITORY) \
diff --git a/README.md b/README.md
index 2321efa4..f92d9f57 100644
--- a/README.md
+++ b/README.md
@@ -32,9 +32,26 @@ Changes in this fork:
- Dropped fcrepo file persistence
- Added ArchivesSpace, OJS, Omeka, and Wordpress images.
- Moved Islandora buildkit's `drupal` image to `islandora` and made a drupal-only image
- - All of these images are intended to be infra scaffolding and app-specific codebase dropped into the container in local builds
+ - ArchivesSpace, OJS, Omeka S, and Omeka Classic include checksum-verified upstream application releases.
+ - Drupal and Wordpress remain infrastructure scaffolding for Composer-managed application code in downstream builds.
- Tags are based on software versions
- Multiple versions of the same software/image can coexist (e.g. java, solr, fcrepo, php, etc.)
- `base` environment variables (e.g. `DB_NAME`) are overriden instead of `IMAGE_NAME_*` env vars
- Removed Islandora multisite support
- Added optional Vault-backed secret bootstrap
+
+## OJS dataset initialization
+
+Fresh OJS installations defer the install-time ROR registry and IP geolocation
+downloads so that container initialization is bounded and does not require
+outbound network access. OJS's built-in scheduler remains enabled and updates
+the ROR registry monthly on the first day of the month and the IP database
+monthly on the tenth.
+
+To populate either dataset immediately after installation, run the corresponding
+OJS scheduler task from `/var/www/ojs` as the application user:
+
+```sh
+php lib/pkp/tools/scheduler.php test --name='PKP\task\UpdateRorRegistryDataset'
+php lib/pkp/tools/scheduler.php test --name='PKP\task\UpdateIPGeoDB'
+```
diff --git a/ci/tests/update-sha-readme.sh b/ci/tests/update-sha-readme.sh
new file mode 100755
index 00000000..953b8c3a
--- /dev/null
+++ b/ci/tests/update-sha-readme.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
+work_dir=$(mktemp -d)
+trap 'rm -rf "${work_dir}"' EXIT
+
+# shellcheck disable=SC1090
+UPDATE_SHA_LIBRARY_ONLY=true source "${root_dir}/ci/update-sha.sh" test 1.2.3 2.0.0 ignored
+
+readme="${work_dir}/README.md"
+printf '%s\n' 'Docker image version 1.2.3.' >"${readme}"
+update_readme "${readme}" 1.2.3 2.0.0
+grep -qx 'Docker image version 2.0.0.' "${readme}"
+
+printf '%s\n' 'Docker image version 9.9.9.' >"${readme}"
+if update_readme "${readme}" 1.2.3 2.0.0; then
+ echo "Missing README source version unexpectedly succeeded" >&2
+ exit 1
+fi
+
+printf '%s\n' 'First 1.2.3.' 'Second 1.2.3.' >"${readme}"
+if update_readme "${readme}" 1.2.3 2.0.0; then
+ echo "Ambiguous README source version unexpectedly succeeded" >&2
+ exit 1
+fi
diff --git a/ci/update-sha.sh b/ci/update-sha.sh
index fd8a26f6..4e13299b 100755
--- a/ci/update-sha.sh
+++ b/ci/update-sha.sh
@@ -19,37 +19,124 @@ update_dockerfile_sha() {
local ARG="$2"
local DOCKERFILES=("${@:3}")
local SHA
- curl -fLs "$URL" -o curl.resp || echo "Request failed with exit code $?"
- SHA=$(shasum -a 256 curl.resp | awk '{print $1}')
+ local DOWNLOAD
+ local dockerfile
+ DOWNLOAD=$(mktemp)
+
+ if ! curl \
+ --fail \
+ --location \
+ --retry 3 \
+ --retry-all-errors \
+ --connect-timeout 20 \
+ --max-time 600 \
+ --silent \
+ --show-error \
+ --output "$DOWNLOAD" \
+ "$URL"; then
+ rm -f "$DOWNLOAD"
+ echo "Failed to download release artifact: $URL" >&2
+ return 1
+ fi
- if [[ "$OSTYPE" == "darwin"* ]]; then
- sed -i '' 's|^ARG '"$ARG"'=.*|ARG '"$ARG"'="'"$SHA"'"|g' "${DOCKERFILES[@]}"
- else
- sed -i 's|^ARG '"$ARG"'=.*|ARG '"$ARG"'="'"$SHA"'"|g' "${DOCKERFILES[@]}"
+ SHA=$(shasum -a 256 "$DOWNLOAD" | awk '{print $1}')
+ if [[ ! "$SHA" =~ ^[0-9a-f]{64}$ ]]; then
+ rm -f "$DOWNLOAD"
+ echo "Failed to calculate SHA256 for release artifact: $URL" >&2
+ return 1
fi
- rm curl.resp
+
+ for dockerfile in "${DOCKERFILES[@]}"; do
+ if [[ $(grep -Ec "^[[:space:]]*(ARG[[:space:]]+)?${ARG}=\"?[[:xdigit:]]{64}\"?" "${dockerfile}") -ne 1 ]]; then
+ rm -f "$DOWNLOAD"
+ echo "Expected exactly one existing ${ARG} checksum in ${dockerfile}" >&2
+ return 1
+ fi
+
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -E -i '' "s|^([[:space:]]*(ARG[[:space:]]+)?${ARG}=)\"?[[:xdigit:]]{64}\"?(.*)$|\\1\"${SHA}\"\\3|" "${dockerfile}"
+ else
+ sed -E -i "s|^([[:space:]]*(ARG[[:space:]]+)?${ARG}=)\"?[[:xdigit:]]{64}\"?(.*)$|\\1\"${SHA}\"\\3|" "${dockerfile}"
+ fi
+
+ if ! grep -Eq "^[[:space:]]*(ARG[[:space:]]+)?${ARG}=\"${SHA}\"" "${dockerfile}"; then
+ rm -f "$DOWNLOAD"
+ echo "Failed to update ${ARG} in ${dockerfile}" >&2
+ return 1
+ fi
+ done
+ rm -f "$DOWNLOAD"
}
update_readme() {
local README="$1"
local OLD_VERSION="$2"
local NEW_VERSION="$3"
- # update the README to specify the new version
- if [ "$README" != "" ]; then
- if [[ "$OSTYPE" == "darwin"* ]]; then
- sed -i '' "s/${OLD_VERSION}\.$/${NEW_VERSION}\./" "$README"
- else
- sed -i "s/${OLD_VERSION}\.$/${NEW_VERSION}\./" "$README"
- fi
+ local match_count
+ local new_match_count
+ local replacement
+
+ if [ -z "$README" ]; then
+ return 0
+ fi
+ if [ ! -f "$README" ]; then
+ echo "Configured README does not exist: $README" >&2
+ return 1
+ fi
+
+ match_count=$(awk -v suffix="${OLD_VERSION}." '
+ length($0) >= length(suffix) && substr($0, length($0) - length(suffix) + 1) == suffix { count++ }
+ END { print count + 0 }
+ ' "$README")
+ if [ "$match_count" -ne 1 ]; then
+ echo "Expected exactly one README line ending in ${OLD_VERSION}. in ${README}; found ${match_count}" >&2
+ return 1
+ fi
+
+ replacement=$(mktemp)
+ if ! awk -v old_suffix="${OLD_VERSION}." -v new_suffix="${NEW_VERSION}." '
+ length($0) >= length(old_suffix) && substr($0, length($0) - length(old_suffix) + 1) == old_suffix {
+ print substr($0, 1, length($0) - length(old_suffix)) new_suffix
+ next
+ }
+ { print }
+ ' "$README" >"$replacement"; then
+ rm -f "$replacement"
+ echo "Failed to prepare README update for ${README}" >&2
+ return 1
+ fi
+ if cmp -s "$README" "$replacement"; then
+ rm -f "$replacement"
+ echo "README replacement did not change ${README}" >&2
+ return 1
+ fi
+ command cat "$replacement" >"$README"
+ rm -f "$replacement"
+
+ new_match_count=$(awk -v suffix="${NEW_VERSION}." '
+ length($0) >= length(suffix) && substr($0, length($0) - length(suffix) + 1) == suffix { count++ }
+ END { print count + 0 }
+ ' "$README")
+ if [ "$new_match_count" -ne 1 ]; then
+ echo "Failed to verify README version ${NEW_VERSION} in ${README}" >&2
+ return 1
fi
}
+if [ "${UPDATE_SHA_LIBRARY_ONLY:-false}" = "true" ]; then
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ exit 0
+ fi
+ return 0
+fi
+
echo "Updating SHA for $DEP@$NEW_VERSION"
if [ "$DEP" = "alpaca" ] ; then
URL="https://github.com/islandora/alpaca/archive/refs/tags/${NEW_VERSION}.tar.gz"
ARG=ALPACA_SHA256
DOCKERFILES=("images/alpaca/Dockerfile")
+ README="images/alpaca/README.md"
elif [ "$DEP" = "apache-tomcat9" ]; then
URL="https://downloads.apache.org/tomcat/tomcat-9/v$NEW_VERSION/bin/apache-tomcat-$NEW_VERSION.tar.gz"
@@ -109,10 +196,19 @@ elif [ "$DEP" = "fcrepo7" ]; then
DOCKERFILES=("images/fcrepo7/Dockerfile")
README="images/fcrepo7/README.md"
-elif [ "$DEP" = "islandora-syn" ]; then
+elif [ "$DEP" = "islandora-syn-fcrepo6" ]; then
URL="https://github.com/Islandora/Syn/releases/download/v${NEW_VERSION}/islandora-syn-${NEW_VERSION}-all.jar"
ARG="SYN_SHA256"
- DOCKERFILES=("images/fcrepo6/Dockerfile" "images/fcrepo7/Dockerfile")
+ DOCKERFILES=("images/fcrepo6/Dockerfile")
+
+elif [ "$DEP" = "islandora-syn-fcrepo7" ]; then
+ URL="https://github.com/Islandora/Syn/releases/download/v${NEW_VERSION}/islandora-syn-${NEW_VERSION}-all.jar"
+ ARG="SYN_SHA256"
+ DOCKERFILES=("images/fcrepo7/Dockerfile")
+
+elif [ "$DEP" = "islandora-syn" ]; then
+ echo "Use islandora-syn-fcrepo6 or islandora-syn-fcrepo7 so Syn major lines remain independent" >&2
+ exit 1
elif [ "$DEP" = "fcrepo-import-export" ]; then
URL="https://github.com/fcrepo-exts/fcrepo-import-export/releases/download/fcrepo-import-export-${NEW_VERSION}/fcrepo-import-export-${NEW_VERSION}.jar"
@@ -214,8 +310,8 @@ elif [ "$DEP" = "s6-overlay" ]; then
exit 0
else
- echo "DEP not found"
- exit 0
+ echo "Unsupported dependency: $DEP" >&2
+ exit 1
fi
update_dockerfile_sha "$URL" "$ARG" "${DOCKERFILES[@]}"
diff --git a/docker-bake.hcl b/docker-bake.hcl
index 68fa4e62..ab9969f7 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -97,8 +97,8 @@ LOCAL_TAG_SUFFIXES = {
"activemq5" = "5"
"activemq6" = "6"
"alpaca" = ""
- "archivesspace" = "4.2.0"
- "archivesspace-solr" = "4.2.0"
+ "archivesspace" = ""
+ "archivesspace-solr" = ""
"base" = ""
"blazegraph" = ""
"crayfits" = ""
@@ -248,14 +248,19 @@ function "tags" {
result = equal("", suffix) ? [for tag in split(" ", TAGS): "${REPOSITORY}/${PUBLISHED_IMAGES[image]}:${tagName(image, tag)}"] : [for tag in split(" ", TAGS): "${REPOSITORY}/${PUBLISHED_IMAGES[image]}:${tagName(image, tag)}-${suffix}"]
}
+function "normalizeTag" {
+ params = [value]
+ result = trim(regex_replace(value, "[^A-Za-z0-9_.-]+", "-"), "-")
+}
+
function "cacheFrom" {
params = [image, arch]
- result = equal("", arch) ? [] : ["type=registry,ref=${CACHE_FROM_REPOSITORY}/cache:${image}-main-${arch}", notequal("", BRANCH) ? "type=registry,ref=${CACHE_FROM_REPOSITORY}/cache:${image}-${BRANCH}-${arch}" : ""]
+ result = equal("", arch) ? [] : ["type=registry,ref=${CACHE_FROM_REPOSITORY}/cache:${image}-main-${arch}", notequal("", normalizeTag(BRANCH)) ? "type=registry,ref=${CACHE_FROM_REPOSITORY}/cache:${image}-${normalizeTag(BRANCH)}-${arch}" : ""]
}
function "cacheTo" {
params = [image, arch]
- result = [notequal("", BRANCH) ? "type=registry,oci-mediatypes=true,mode=max,compression=estargz,compression-level=5,ref=${CACHE_TO_REPOSITORY}/cache:${image}-${BRANCH}-${arch}" : ""]
+ result = [notequal("", normalizeTag(BRANCH)) ? "type=registry,oci-mediatypes=true,mode=max,compression=estargz,compression-level=5,ref=${CACHE_TO_REPOSITORY}/cache:${image}-${normalizeTag(BRANCH)}-${arch}" : ""]
}
function "context" {
diff --git a/images/activemq5/Dockerfile b/images/activemq5/Dockerfile
index a0b73b3b..be9facff 100644
--- a/images/activemq5/Dockerfile
+++ b/images/activemq5/Dockerfile
@@ -27,10 +27,10 @@ RUN --mount=type=cache,id=activemq-downloads-${TARGETARCH},sharing=locked,target
ENV \
ACTIVEMQ_AUDIT_LOG_LEVEL=INFO \
ACTIVEMQ_LOG_LEVEL=INFO \
- ACTIVEMQ_PASSWORD=password \
+ ACTIVEMQ_PASSWORD= \
ACTIVEMQ_USER=admin \
ACTIVEMQ_WEB_ADMIN_NAME=admin \
- ACTIVEMQ_WEB_ADMIN_PASSWORD=password \
+ ACTIVEMQ_WEB_ADMIN_PASSWORD= \
ACTIVEMQ_WEB_ADMIN_ROLES=admin
COPY --link rootfs /
@@ -38,8 +38,4 @@ COPY --link rootfs /
RUN create-service-user.sh --name activemq && \
cleanup.sh
-HEALTHCHECK CMD curl -s \
- -u admin:"$(cat /var/run/s6/container_environment/ACTIVEMQ_WEB_ADMIN_PASSWORD)" \
- -H origin:localhost \
- "http://localhost:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost,service=Health/CurrentStatus" \
- | jq .value | grep -q Good
+HEALTHCHECK CMD activemq-healthcheck.sh
diff --git a/images/activemq5/README.md b/images/activemq5/README.md
index 63fb4164..416740a2 100644
--- a/images/activemq5/README.md
+++ b/images/activemq5/README.md
@@ -6,16 +6,21 @@ Built from [libops/isle-buildkit activemq](https://github.com/libops/buildkit/tr
Please refer to the [ActiveMQ Documentation] for more in-depth information.
-As a quick example this will bring up an instance of ActiveMQ, and allow you to
-log into the [WebConsole] on `http://localhost:8161` as the user `admin` with
-the password `password`.
+As a quick example this will generate separate broker and web-console
+credentials, bring up ActiveMQ, and allow you to log into the [WebConsole] on
+`http://localhost:8161` as `admin` using the value of
+`ACTIVEMQ_WEB_ADMIN_PASSWORD`.
```bash
-docker run --rm -ti -p 8161:8161 libops/activemq
+export ACTIVEMQ_PASSWORD="$(openssl rand -hex 32)"
+export ACTIVEMQ_WEB_ADMIN_PASSWORD="$(openssl rand -hex 32)"
+docker run --rm -ti -p 8161:8161 \
+ --env ACTIVEMQ_PASSWORD \
+ --env ACTIVEMQ_WEB_ADMIN_PASSWORD \
+ libops/activemq:5
```
-> N.B. if no credentials are given you will not be able to log in via the
-[WebConsole].
+Both credentials are required; the container fails closed when either is empty.
## Dependencies
@@ -46,10 +51,10 @@ additional settings, volumes, ports, etc.
| :-------------------------- | :------- | :----------------------------------------------------------------------------- |
| ACTIVEMQ_AUDIT_LOG_LEVEL | INFO | Log level. Possible Values: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE or ALL |
| ACTIVEMQ_LOG_LEVEL | INFO | Log level. Possible Values: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE or ALL |
-| ACTIVEMQ_PASSWORD | password | See [Security]: credentials.properties |
+| ACTIVEMQ_PASSWORD | | See [Security]: credentials.properties; required at startup |
| ACTIVEMQ_USER | admin | See [Security]: credentials.properties |
| ACTIVEMQ_WEB_ADMIN_NAME | admin | See [WebConsole]: jetty-realm.properties |
-| ACTIVEMQ_WEB_ADMIN_PASSWORD | password | See [WebConsole]: jetty-realm.properties |
+| ACTIVEMQ_WEB_ADMIN_PASSWORD | | See [WebConsole]: jetty-realm.properties; required at startup |
| ACTIVEMQ_WEB_ADMIN_ROLES | admin | See [WebConsole]: jetty-realm.properties |
Additional users/groups/etc can be defined by adding more environment variables,
diff --git a/images/activemq5/rootfs/etc/confd/templates/credentials.properties.tmpl b/images/activemq5/rootfs/etc/confd/templates/credentials.properties.tmpl
index 4952c702..86ddec53 100644
--- a/images/activemq5/rootfs/etc/confd/templates/credentials.properties.tmpl
+++ b/images/activemq5/rootfs/etc/confd/templates/credentials.properties.tmpl
@@ -1,3 +1,10 @@
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines credentials that will be used by components (like web console) to access the broker
-activemq.username={{ getenv "ACTIVEMQ_USER" }}
-activemq.password={{ getenv "ACTIVEMQ_PASSWORD" }}
+activemq.username={{ template "propertiesValueEscape" (getenv "ACTIVEMQ_USER") }}
+activemq.password={{ template "propertiesValueEscape" (getenv "ACTIVEMQ_PASSWORD") }}
diff --git a/images/activemq5/rootfs/etc/confd/templates/groups.properties.tmpl b/images/activemq5/rootfs/etc/confd/templates/groups.properties.tmpl
index 8e26887d..470cb092 100644
--- a/images/activemq5/rootfs/etc/confd/templates/groups.properties.tmpl
+++ b/images/activemq5/rootfs/etc/confd/templates/groups.properties.tmpl
@@ -1,4 +1,22 @@
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines groups and the users that belong to them.
# group=user[,user ...]
-{{ range $dir := lsdir "/activemq/group" }}{{ getv (printf "/activemq/group/%s/name" $dir) }}={{ getv (printf "/activemq/group/%s/members" $dir) }}
+{{ range $dir := lsdir "/activemq/group" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/group/%s/name" $dir)) }}={{ template "propertiesValueEscape" (getv (printf "/activemq/group/%s/members" $dir)) }}
{{ end }}
diff --git a/images/activemq5/rootfs/etc/confd/templates/jetty-realm.properties.tmpl b/images/activemq5/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
index 31a932a4..524995b7 100644
--- a/images/activemq5/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
+++ b/images/activemq5/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
@@ -1,5 +1,23 @@
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines users that can access the web (console, demo, etc.)
# username: password [,rolename ...]
-{{ getenv "ACTIVEMQ_WEB_ADMIN_NAME" }}: {{ getenv "ACTIVEMQ_WEB_ADMIN_PASSWORD" }}, {{ getenv "ACTIVEMQ_WEB_ADMIN_ROLES" }}
-{{ range $dir := lsdir "/activemq/web/user" }}{{ getv (printf "/activemq/web/user/%s/name" $dir) }}: {{ getv (printf "/activemq/web/user/%s/password" $dir) }}, {{ getv (printf "/activemq/web/user/%s/roles" $dir) }}
+{{ template "propertiesKeyEscape" (getenv "ACTIVEMQ_WEB_ADMIN_NAME") }}: {{ template "propertiesValueEscape" (getenv "ACTIVEMQ_WEB_ADMIN_PASSWORD") }}, {{ template "propertiesValueEscape" (getenv "ACTIVEMQ_WEB_ADMIN_ROLES") }}
+{{ range $dir := lsdir "/activemq/web/user" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/web/user/%s/name" $dir)) }}: {{ template "propertiesValueEscape" (getv (printf "/activemq/web/user/%s/password" $dir)) }}, {{ template "propertiesValueEscape" (getv (printf "/activemq/web/user/%s/roles" $dir)) }}
{{ end }}
diff --git a/images/activemq5/rootfs/etc/confd/templates/users.properties.tmpl b/images/activemq5/rootfs/etc/confd/templates/users.properties.tmpl
index 669a832d..cc1d1d95 100644
--- a/images/activemq5/rootfs/etc/confd/templates/users.properties.tmpl
+++ b/images/activemq5/rootfs/etc/confd/templates/users.properties.tmpl
@@ -1,2 +1,20 @@
-{{ range $dir := lsdir "/activemq/user" }}{{ getv (printf "/activemq/user/%s/name" $dir) }}={{ getv (printf "/activemq/user/%s/password" $dir) }}
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
+{{ range $dir := lsdir "/activemq/user" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/user/%s/name" $dir)) }}={{ template "propertiesValueEscape" (getv (printf "/activemq/user/%s/password" $dir)) }}
{{ end }}
diff --git a/images/activemq5/rootfs/etc/s6-overlay/s6-rc.d/activemq/run b/images/activemq5/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
index 9ea0dc80..9ec0b40a 100755
--- a/images/activemq5/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
+++ b/images/activemq5/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
@@ -1,7 +1,16 @@
-#!/usr/bin/env bash
+#!/command/with-contenv bash
+# shellcheck shell=bash
set -e
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
+require_environment_variables ACTIVEMQ_USER ACTIVEMQ_PASSWORD \
+ ACTIVEMQ_WEB_ADMIN_NAME ACTIVEMQ_WEB_ADMIN_PASSWORD
+
# When bind mounting we need to ensure that we
# actually can write to the folder.
chown activemq:activemq /opt/activemq/data
+unset ACTIVEMQ_PASSWORD ACTIVEMQ_WEB_ADMIN_PASSWORD
+unset DB_ROOT_PASSWORD
exec s6-setuidgid activemq /opt/activemq/bin/activemq console
diff --git a/images/activemq5/rootfs/usr/local/bin/activemq-healthcheck.sh b/images/activemq5/rootfs/usr/local/bin/activemq-healthcheck.sh
new file mode 100755
index 00000000..92a25067
--- /dev/null
+++ b/images/activemq5/rootfs/usr/local/bin/activemq-healthcheck.sh
@@ -0,0 +1,25 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -euo pipefail
+
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+require_environment_variables ACTIVEMQ_WEB_ADMIN_NAME ACTIVEMQ_WEB_ADMIN_PASSWORD
+
+umask 077
+curl_config=$(mktemp -t activemq-healthcheck.XXXXXXXXXX)
+trap 'rm -f -- "${curl_config}"' EXIT
+chmod 0600 "${curl_config}"
+
+# Keep the credential out of curl's argv. The temporary curl configuration is
+# root-only and contains only a derived Basic authorization value.
+authorization=$(printf '%s:%s' "${ACTIVEMQ_WEB_ADMIN_NAME}" "${ACTIVEMQ_WEB_ADMIN_PASSWORD}" | base64 | tr -d '\n')
+{
+ printf '%s\n' 'fail' 'silent' 'show-error'
+ printf 'header = "Origin: localhost"\n'
+ printf 'header = "Authorization: Basic %s"\n' "${authorization}"
+ printf 'url = "%s"\n' 'http://localhost:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost,service=Health/CurrentStatus'
+} >"${curl_config}"
+
+curl --config "${curl_config}" | jq -e '.value == "Good"' >/dev/null
diff --git a/images/activemq5/tests/ServiceHealthcheck/docker-compose.yml b/images/activemq5/tests/ServiceHealthcheck/docker-compose.yml
index b72dd41d..7d3d45d9 100644
--- a/images/activemq5/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/activemq5/tests/ServiceHealthcheck/docker-compose.yml
@@ -8,6 +8,14 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_USER_ESCAPE_NAME: 'broker:user=name'
+ ACTIVEMQ_USER_ESCAPE_PASSWORD: ' broker:p\a=ss:word'
+ ACTIVEMQ_GROUP_ESCAPE_NAME: 'operators:admin=team'
+ ACTIVEMQ_GROUP_ESCAPE_MEMBERS: 'broker:user=name'
+ ACTIVEMQ_WEB_ADMIN_NAME: healthcheck-user
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: 'web:p"a\ss&+=word'
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/activemq5/tests/ServiceHealthcheck/test.sh b/images/activemq5/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..a7317ec5 100755
--- a/images/activemq5/tests/ServiceHealthcheck/test.sh
+++ b/images/activemq5/tests/ServiceHealthcheck/test.sh
@@ -2,13 +2,16 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
}
trap 'on_terminate' SIGTERM
+grep -Fqx 'broker\:user\=name=\ broker:p\\a=ss:word' /opt/activemq/conf/users.properties
+grep -Fqx 'operators\:admin\=team=broker:user=name' /opt/activemq/conf/groups.properties
+
sleep 60
# The kotlin check should be stopping this container
diff --git a/images/activemq5/tests/ServiceStartsWithDefaults/docker-compose.yml b/images/activemq5/tests/ServiceStartsWithDefaults/docker-compose.yml
index ebca7ded..652d1963 100644
--- a/images/activemq5/tests/ServiceStartsWithDefaults/docker-compose.yml
+++ b/images/activemq5/tests/ServiceStartsWithDefaults/docker-compose.yml
@@ -11,6 +11,9 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/activemq6/Dockerfile b/images/activemq6/Dockerfile
index e7303298..6e8364c7 100644
--- a/images/activemq6/Dockerfile
+++ b/images/activemq6/Dockerfile
@@ -27,10 +27,10 @@ RUN --mount=type=cache,id=activemq-downloads-${TARGETARCH},sharing=locked,target
ENV \
ACTIVEMQ_AUDIT_LOG_LEVEL=INFO \
ACTIVEMQ_LOG_LEVEL=INFO \
- ACTIVEMQ_PASSWORD=password \
+ ACTIVEMQ_PASSWORD= \
ACTIVEMQ_USER=admin \
ACTIVEMQ_WEB_ADMIN_NAME=admin \
- ACTIVEMQ_WEB_ADMIN_PASSWORD=password \
+ ACTIVEMQ_WEB_ADMIN_PASSWORD= \
ACTIVEMQ_WEB_ADMIN_ROLES=admin
COPY --link rootfs /
@@ -38,8 +38,4 @@ COPY --link rootfs /
RUN create-service-user.sh --name activemq && \
cleanup.sh
-HEALTHCHECK CMD curl -s \
- -u admin:"$(cat /var/run/s6/container_environment/ACTIVEMQ_WEB_ADMIN_PASSWORD)" \
- -H origin:localhost \
- "http://localhost:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost,service=Health/CurrentStatus" \
- | jq .value | grep -q Good
+HEALTHCHECK CMD activemq-healthcheck.sh
diff --git a/images/activemq6/README.md b/images/activemq6/README.md
index 2053d037..3d8ce17b 100644
--- a/images/activemq6/README.md
+++ b/images/activemq6/README.md
@@ -6,16 +6,21 @@ Built from [libops/isle-buildkit activemq7](https://github.com/libops/buildkit/t
Please refer to the [ActiveMQ Documentation] for more in-depth information.
-As a quick example this will bring up an instance of ActiveMQ, and allow you to
-log into the [WebConsole] on `http://localhost:8161` as the user `admin` with
-the password `password`.
+As a quick example this will generate separate broker and web-console
+credentials, bring up ActiveMQ, and allow you to log into the [WebConsole] on
+`http://localhost:8161` as `admin` using the value of
+`ACTIVEMQ_WEB_ADMIN_PASSWORD`.
```bash
-docker run --rm -ti -p 8161:8161 libops/activemq
+export ACTIVEMQ_PASSWORD="$(openssl rand -hex 32)"
+export ACTIVEMQ_WEB_ADMIN_PASSWORD="$(openssl rand -hex 32)"
+docker run --rm -ti -p 8161:8161 \
+ --env ACTIVEMQ_PASSWORD \
+ --env ACTIVEMQ_WEB_ADMIN_PASSWORD \
+ libops/activemq:6
```
-> N.B. if no credentials are given you will not be able to log in via the
-[WebConsole].
+Both credentials are required; the container fails closed when either is empty.
## Dependencies
@@ -46,10 +51,10 @@ additional settings, volumes, ports, etc.
| :-------------------------- | :------- | :----------------------------------------------------------------------------- |
| ACTIVEMQ_AUDIT_LOG_LEVEL | INFO | Log level. Possible Values: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE or ALL |
| ACTIVEMQ_LOG_LEVEL | INFO | Log level. Possible Values: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE or ALL |
-| ACTIVEMQ_PASSWORD | password | See [Security]: credentials.properties |
+| ACTIVEMQ_PASSWORD | | See [Security]: credentials.properties; required at startup |
| ACTIVEMQ_USER | admin | See [Security]: credentials.properties |
| ACTIVEMQ_WEB_ADMIN_NAME | admin | See [WebConsole]: jetty-realm.properties |
-| ACTIVEMQ_WEB_ADMIN_PASSWORD | password | See [WebConsole]: jetty-realm.properties |
+| ACTIVEMQ_WEB_ADMIN_PASSWORD | | See [WebConsole]: jetty-realm.properties; required at startup |
| ACTIVEMQ_WEB_ADMIN_ROLES | admin | See [WebConsole]: jetty-realm.properties |
Additional users/groups/etc can be defined by adding more environment variables,
diff --git a/images/activemq6/rootfs/etc/confd/templates/credentials.properties.tmpl b/images/activemq6/rootfs/etc/confd/templates/credentials.properties.tmpl
index 4952c702..86ddec53 100644
--- a/images/activemq6/rootfs/etc/confd/templates/credentials.properties.tmpl
+++ b/images/activemq6/rootfs/etc/confd/templates/credentials.properties.tmpl
@@ -1,3 +1,10 @@
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines credentials that will be used by components (like web console) to access the broker
-activemq.username={{ getenv "ACTIVEMQ_USER" }}
-activemq.password={{ getenv "ACTIVEMQ_PASSWORD" }}
+activemq.username={{ template "propertiesValueEscape" (getenv "ACTIVEMQ_USER") }}
+activemq.password={{ template "propertiesValueEscape" (getenv "ACTIVEMQ_PASSWORD") }}
diff --git a/images/activemq6/rootfs/etc/confd/templates/groups.properties.tmpl b/images/activemq6/rootfs/etc/confd/templates/groups.properties.tmpl
index 8e26887d..470cb092 100644
--- a/images/activemq6/rootfs/etc/confd/templates/groups.properties.tmpl
+++ b/images/activemq6/rootfs/etc/confd/templates/groups.properties.tmpl
@@ -1,4 +1,22 @@
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines groups and the users that belong to them.
# group=user[,user ...]
-{{ range $dir := lsdir "/activemq/group" }}{{ getv (printf "/activemq/group/%s/name" $dir) }}={{ getv (printf "/activemq/group/%s/members" $dir) }}
+{{ range $dir := lsdir "/activemq/group" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/group/%s/name" $dir)) }}={{ template "propertiesValueEscape" (getv (printf "/activemq/group/%s/members" $dir)) }}
{{ end }}
diff --git a/images/activemq6/rootfs/etc/confd/templates/jetty-realm.properties.tmpl b/images/activemq6/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
index 31a932a4..524995b7 100644
--- a/images/activemq6/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
+++ b/images/activemq6/rootfs/etc/confd/templates/jetty-realm.properties.tmpl
@@ -1,5 +1,23 @@
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
# Defines users that can access the web (console, demo, etc.)
# username: password [,rolename ...]
-{{ getenv "ACTIVEMQ_WEB_ADMIN_NAME" }}: {{ getenv "ACTIVEMQ_WEB_ADMIN_PASSWORD" }}, {{ getenv "ACTIVEMQ_WEB_ADMIN_ROLES" }}
-{{ range $dir := lsdir "/activemq/web/user" }}{{ getv (printf "/activemq/web/user/%s/name" $dir) }}: {{ getv (printf "/activemq/web/user/%s/password" $dir) }}, {{ getv (printf "/activemq/web/user/%s/roles" $dir) }}
+{{ template "propertiesKeyEscape" (getenv "ACTIVEMQ_WEB_ADMIN_NAME") }}: {{ template "propertiesValueEscape" (getenv "ACTIVEMQ_WEB_ADMIN_PASSWORD") }}, {{ template "propertiesValueEscape" (getenv "ACTIVEMQ_WEB_ADMIN_ROLES") }}
+{{ range $dir := lsdir "/activemq/web/user" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/web/user/%s/name" $dir)) }}: {{ template "propertiesValueEscape" (getv (printf "/activemq/web/user/%s/password" $dir)) }}, {{ template "propertiesValueEscape" (getv (printf "/activemq/web/user/%s/roles" $dir)) }}
{{ end }}
diff --git a/images/activemq6/rootfs/etc/confd/templates/users.properties.tmpl b/images/activemq6/rootfs/etc/confd/templates/users.properties.tmpl
index 669a832d..cc1d1d95 100644
--- a/images/activemq6/rootfs/etc/confd/templates/users.properties.tmpl
+++ b/images/activemq6/rootfs/etc/confd/templates/users.properties.tmpl
@@ -1,2 +1,20 @@
-{{ range $dir := lsdir "/activemq/user" }}{{ getv (printf "/activemq/user/%s/name" $dir) }}={{ getv (printf "/activemq/user/%s/password" $dir) }}
+{{- define "propertiesKeyEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- $spaces := replace $tabs " " "\\ " -1 -}}
+{{- $colons := replace $spaces ":" "\\:" -1 -}}
+{{- $equals := replace $colons "=" "\\=" -1 -}}
+{{- $hashes := replace $equals "#" "\\#" -1 -}}
+{{- replace $hashes "!" "\\!" -1 -}}
+{{- end -}}
+{{- define "propertiesValueEscape" -}}
+{{- $backslashes := replace . "\\" "\\\\" -1 -}}
+{{- $carriageReturns := replace $backslashes "\r" "\\r" -1 -}}
+{{- $newlines := replace $carriageReturns "\n" "\\n" -1 -}}
+{{- $tabs := replace $newlines "\t" "\\t" -1 -}}
+{{- replace $tabs " " "\\ " -1 -}}
+{{- end -}}
+{{ range $dir := lsdir "/activemq/user" }}{{ template "propertiesKeyEscape" (getv (printf "/activemq/user/%s/name" $dir)) }}={{ template "propertiesValueEscape" (getv (printf "/activemq/user/%s/password" $dir)) }}
{{ end }}
diff --git a/images/activemq6/rootfs/etc/s6-overlay/s6-rc.d/activemq/run b/images/activemq6/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
index 9ea0dc80..9ec0b40a 100755
--- a/images/activemq6/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
+++ b/images/activemq6/rootfs/etc/s6-overlay/s6-rc.d/activemq/run
@@ -1,7 +1,16 @@
-#!/usr/bin/env bash
+#!/command/with-contenv bash
+# shellcheck shell=bash
set -e
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
+require_environment_variables ACTIVEMQ_USER ACTIVEMQ_PASSWORD \
+ ACTIVEMQ_WEB_ADMIN_NAME ACTIVEMQ_WEB_ADMIN_PASSWORD
+
# When bind mounting we need to ensure that we
# actually can write to the folder.
chown activemq:activemq /opt/activemq/data
+unset ACTIVEMQ_PASSWORD ACTIVEMQ_WEB_ADMIN_PASSWORD
+unset DB_ROOT_PASSWORD
exec s6-setuidgid activemq /opt/activemq/bin/activemq console
diff --git a/images/activemq6/rootfs/usr/local/bin/activemq-healthcheck.sh b/images/activemq6/rootfs/usr/local/bin/activemq-healthcheck.sh
new file mode 100755
index 00000000..92a25067
--- /dev/null
+++ b/images/activemq6/rootfs/usr/local/bin/activemq-healthcheck.sh
@@ -0,0 +1,25 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -euo pipefail
+
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+require_environment_variables ACTIVEMQ_WEB_ADMIN_NAME ACTIVEMQ_WEB_ADMIN_PASSWORD
+
+umask 077
+curl_config=$(mktemp -t activemq-healthcheck.XXXXXXXXXX)
+trap 'rm -f -- "${curl_config}"' EXIT
+chmod 0600 "${curl_config}"
+
+# Keep the credential out of curl's argv. The temporary curl configuration is
+# root-only and contains only a derived Basic authorization value.
+authorization=$(printf '%s:%s' "${ACTIVEMQ_WEB_ADMIN_NAME}" "${ACTIVEMQ_WEB_ADMIN_PASSWORD}" | base64 | tr -d '\n')
+{
+ printf '%s\n' 'fail' 'silent' 'show-error'
+ printf 'header = "Origin: localhost"\n'
+ printf 'header = "Authorization: Basic %s"\n' "${authorization}"
+ printf 'url = "%s"\n' 'http://localhost:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=localhost,service=Health/CurrentStatus'
+} >"${curl_config}"
+
+curl --config "${curl_config}" | jq -e '.value == "Good"' >/dev/null
diff --git a/images/activemq6/tests/ServiceHealthcheck/docker-compose.yml b/images/activemq6/tests/ServiceHealthcheck/docker-compose.yml
index b72dd41d..7d3d45d9 100644
--- a/images/activemq6/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/activemq6/tests/ServiceHealthcheck/docker-compose.yml
@@ -8,6 +8,14 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_USER_ESCAPE_NAME: 'broker:user=name'
+ ACTIVEMQ_USER_ESCAPE_PASSWORD: ' broker:p\a=ss:word'
+ ACTIVEMQ_GROUP_ESCAPE_NAME: 'operators:admin=team'
+ ACTIVEMQ_GROUP_ESCAPE_MEMBERS: 'broker:user=name'
+ ACTIVEMQ_WEB_ADMIN_NAME: healthcheck-user
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: 'web:p"a\ss&+=word'
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/activemq6/tests/ServiceHealthcheck/test.sh b/images/activemq6/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..a7317ec5 100755
--- a/images/activemq6/tests/ServiceHealthcheck/test.sh
+++ b/images/activemq6/tests/ServiceHealthcheck/test.sh
@@ -2,13 +2,16 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
}
trap 'on_terminate' SIGTERM
+grep -Fqx 'broker\:user\=name=\ broker:p\\a=ss:word' /opt/activemq/conf/users.properties
+grep -Fqx 'operators\:admin\=team=broker:user=name' /opt/activemq/conf/groups.properties
+
sleep 60
# The kotlin check should be stopping this container
diff --git a/images/activemq6/tests/ServiceStartsWithDefaults/docker-compose.yml b/images/activemq6/tests/ServiceStartsWithDefaults/docker-compose.yml
index ebca7ded..652d1963 100644
--- a/images/activemq6/tests/ServiceStartsWithDefaults/docker-compose.yml
+++ b/images/activemq6/tests/ServiceStartsWithDefaults/docker-compose.yml
@@ -11,6 +11,9 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/alpaca/Dockerfile b/images/alpaca/Dockerfile
index deb46686..26056c00 100644
--- a/images/alpaca/Dockerfile
+++ b/images/alpaca/Dockerfile
@@ -90,7 +90,7 @@ ENV \
ALPACA_JAVA_OPTS= \
ALPACA_JMS_CONNECTIONS=10 \
ALPACA_JMS_CONSUMERS=1 \
- ALPACA_JMS_PASSWORD=password \
+ ALPACA_JMS_PASSWORD= \
ALPACA_JMS_URL=tcp://activemq:61616 \
ALPACA_JMS_USER=admin \
ALPACA_MAX_REDELIVERIES=5 \
diff --git a/images/alpaca/README.md b/images/alpaca/README.md
index 65a6a4b5..83bc8de4 100644
--- a/images/alpaca/README.md
+++ b/images/alpaca/README.md
@@ -1,6 +1,6 @@
# Alpaca
-Docker image for [Alpaca] version 2.2.0.
+Docker image for [Alpaca] version 2.4.0.
Built from [libops/isle-buildkit alpaca](https://github.com/libops/buildkit/tree/main/images/alpaca)
@@ -64,7 +64,7 @@ additional settings, volumes, ports, etc.
| ALPACA_JAVA_OPTS | | |
| ALPACA_JMS_CONNECTIONS | 10 | |
| ALPACA_JMS_CONSUMERS | 1 | |
-| ALPACA_JMS_PASSWORD | password | Password to authenticate with |
+| ALPACA_JMS_PASSWORD | | Password to authenticate with; required at startup |
| ALPACA_JMS_URL | tcp://activemq:61616 | The url for connecting to the ActiveMQ broker, shared by all components |
| ALPACA_JMS_USER | admin | User to authenticate as |
| ALPACA_MAX_REDELIVERIES | 5 | Number of attempts to redeliver if an exception occurs |
diff --git a/images/alpaca/rootfs/etc/confd/conf.d/alpaca.properties.toml b/images/alpaca/rootfs/etc/confd/conf.d/alpaca.properties.toml
index 3958ca18..2d84a064 100644
--- a/images/alpaca/rootfs/etc/confd/conf.d/alpaca.properties.toml
+++ b/images/alpaca/rootfs/etc/confd/conf.d/alpaca.properties.toml
@@ -3,5 +3,5 @@ src = "alpaca.properties.tmpl"
dest = "/opt/alpaca/alpaca.properties"
uid = 100
gid = 1000
-mode = "0644"
+mode = "0640"
keys = [ "/" ]
diff --git a/images/alpaca/rootfs/etc/s6-overlay/s6-rc.d/alpaca/run b/images/alpaca/rootfs/etc/s6-overlay/s6-rc.d/alpaca/run
index 20904782..9bfc18b7 100755
--- a/images/alpaca/rootfs/etc/s6-overlay/s6-rc.d/alpaca/run
+++ b/images/alpaca/rootfs/etc/s6-overlay/s6-rc.d/alpaca/run
@@ -2,12 +2,19 @@
# shellcheck shell=bash
set -e
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
+require_environment_variables ALPACA_JMS_USER ALPACA_JMS_PASSWORD
+
alpaca_java_opts=()
if [ -n "${ALPACA_JAVA_OPTS:-}" ]; then
read -r -a alpaca_java_opts <<<"$ALPACA_JAVA_OPTS"
fi
+unset ALPACA_JMS_PASSWORD
+unset DB_ROOT_PASSWORD
exec s6-setuidgid alpaca java "${alpaca_java_opts[@]}" \
-jar /opt/alpaca/alpaca.jar \
-c /opt/alpaca/alpaca.properties
diff --git a/images/alpaca/tests/ServiceStartsWithDefaults/docker-compose.yml b/images/alpaca/tests/ServiceStartsWithDefaults/docker-compose.yml
index e6619be0..d6d016e4 100644
--- a/images/alpaca/tests/ServiceStartsWithDefaults/docker-compose.yml
+++ b/images/alpaca/tests/ServiceStartsWithDefaults/docker-compose.yml
@@ -8,8 +8,13 @@ services:
alpaca:
<<: *common
image: ${ALPACA:-libops/alpaca:local}
+ environment:
+ ALPACA_JMS_PASSWORD: test-broker-password
depends_on:
- activemq
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
diff --git a/images/archivesspace-solr/Dockerfile b/images/archivesspace-solr/Dockerfile
index 92616191..a1f6b296 100644
--- a/images/archivesspace-solr/Dockerfile
+++ b/images/archivesspace-solr/Dockerfile
@@ -1,6 +1,8 @@
# syntax=docker/dockerfile:1.20.0
FROM solr9
+LABEL org.opencontainers.image.licenses="Apache-2.0 AND ECL-2.0"
+
ARG TARGETARCH
# renovate: datasource=github-releases depName=archivesspace packageName=archivesspace/archivesspace
@@ -17,6 +19,8 @@ RUN --mount=type=cache,id=archivesspace-solr-downloads-${TARGETARCH},sharing=loc
&& \
mkdir -p /opt/solr/server/solr/configsets/archivesspace/conf && \
cp -R /tmp/archivesspace-release/archivesspace/solr/. /opt/solr/server/solr/configsets/archivesspace/conf/ && \
+ mkdir -p /usr/share/licenses/archivesspace-solr && \
+ cp /tmp/archivesspace-release/archivesspace/COPYING /usr/share/licenses/archivesspace-solr/COPYING && \
mkdir -p /opt/solr/server/solr/archivesspace && \
cp -R /opt/solr/server/solr/configsets/archivesspace/. /opt/solr/server/solr/archivesspace/ && \
printf 'name=archivesspace\n' > /opt/solr/server/solr/archivesspace/core.properties && \
diff --git a/images/archivesspace/Dockerfile b/images/archivesspace/Dockerfile
index cec599c4..e051cd0f 100644
--- a/images/archivesspace/Dockerfile
+++ b/images/archivesspace/Dockerfile
@@ -1,6 +1,8 @@
# syntax=docker/dockerfile:1.20.0
FROM java17
+LABEL org.opencontainers.image.licenses="ECL-2.0"
+
ARG TARGETARCH
ARG \
@@ -62,6 +64,8 @@ RUN --mount=type=cache,id=archivesspace-downloads-${TARGETARCH},sharing=locked,t
--sha256 "${MYSQL_CONNECTOR_SHA256}" \
--dest /archivesspace/lib \
&& \
+ mkdir -p /usr/share/licenses/archivesspace && \
+ cp /archivesspace/COPYING /usr/share/licenses/archivesspace/COPYING && \
chmod +x /archivesspace/archivesspace.sh /archivesspace/scripts/*.sh && \
chown -R archivesspace:archivesspace /archivesspace && \
cleanup.sh
@@ -74,7 +78,7 @@ ENV \
ASPACE_JAVA_XMX=-Xmx2048m \
DB_HOST=mariadb \
DB_NAME=archivesspace \
- DB_PASSWORD=changeme \
+ DB_PASSWORD= \
DB_PORT=3306 \
DB_USER=archivesspace \
INGRESS_HOSTNAMES=localhost \
diff --git a/images/archivesspace/rootfs/etc/confd/templates/archivesspace.config.rb.tmpl b/images/archivesspace/rootfs/etc/confd/templates/archivesspace.config.rb.tmpl
index eec1bd6f..b541d5c7 100644
--- a/images/archivesspace/rootfs/etc/confd/templates/archivesspace.config.rb.tmpl
+++ b/images/archivesspace/rootfs/etc/confd/templates/archivesspace.config.rb.tmpl
@@ -10,7 +10,11 @@
{{ $appConfigSolrURL := getenv "APPCONFIG_SOLR_URL" }}
{{ $smtpFrom := getenv "SMTP_FROM" }}
{{ $smtpHost := getenv "SMTP_HOST" }}
-AppConfig[:db_url] = 'jdbc:mysql://{{ template "sqlEscape" $dbHost }}:{{ template "sqlEscape" $dbPort }}/{{ template "sqlEscape" $dbName }}?useUnicode=true&characterEncoding=UTF-8&user={{ template "sqlEscape" $dbUser }}&password={{ template "sqlEscape" $dbPassword }}&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC'
+require 'uri'
+
+db_user = URI.encode_www_form_component('{{ template "sqlEscape" $dbUser }}')
+db_password = URI.encode_www_form_component('{{ template "sqlEscape" $dbPassword }}')
+AppConfig[:db_url] = 'jdbc:mysql://{{ template "sqlEscape" $dbHost }}:{{ template "sqlEscape" $dbPort }}/{{ template "sqlEscape" $dbName }}?useUnicode=true&characterEncoding=UTF-8&user=' + db_user + '&password=' + db_password + '&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC'
{{ if ne (getenv "APPCONFIG_SOLR_URL") "" -}}
AppConfig[:solr_url] = '{{ template "sqlEscape" $appConfigSolrURL }}'
{{ else -}}
diff --git a/images/archivesspace/rootfs/etc/s6-overlay/s6-rc.d/archivesspace/run b/images/archivesspace/rootfs/etc/s6-overlay/s6-rc.d/archivesspace/run
index b2d1e63a..f754f7cb 100755
--- a/images/archivesspace/rootfs/etc/s6-overlay/s6-rc.d/archivesspace/run
+++ b/images/archivesspace/rootfs/etc/s6-overlay/s6-rc.d/archivesspace/run
@@ -16,4 +16,5 @@ trap terminate TERM
setsid /usr/local/bin/archivesspace-startup.sh &
child=$!
+unset DB_ROOT_PASSWORD
wait "${child}"
diff --git a/images/archivesspace/rootfs/usr/local/bin/archivesspace-startup.sh b/images/archivesspace/rootfs/usr/local/bin/archivesspace-startup.sh
index 15d41b48..e7b73616 100755
--- a/images/archivesspace/rootfs/usr/local/bin/archivesspace-startup.sh
+++ b/images/archivesspace/rootfs/usr/local/bin/archivesspace-startup.sh
@@ -2,26 +2,21 @@
# shellcheck shell=bash
set -euo pipefail
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
+require_environment_variables DB_HOST DB_NAME DB_USER DB_PASSWORD
+
data_dir="${APPCONFIG_DATA_DIR:-/archivesspace/data}"
mkdir -p "${data_dir}/tmp"
chown -R archivesspace:archivesspace "${data_dir}" /archivesspace/config
-function root_password {
- printf '%s' "${DB_ROOT_PASSWORD:-}"
-}
-
function create_archivesspace_database {
- local root_password_value
- root_password_value="$(root_password)"
- cat <<-SQL | DB_ROOT_PASSWORD="${root_password_value}" create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
-
-SET PASSWORD FOR '${DB_USER}'@'%' = PASSWORD('${DB_PASSWORD}');
-SQL
+ DB_CHARACTER_SET=utf8mb4 \
+ DB_COLLATION=utf8mb4_unicode_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
if [ -n "${ASPACE_INITIALIZE_PLUGINS:-}" ]; then
@@ -34,10 +29,9 @@ fi
rm -rf "${data_dir:?}/tmp"/*
if [ "${ASPACE_DB_MIGRATE:-true}" = "true" ]; then
- if [ -n "$(root_password)" ]; then
- create_archivesspace_database
- fi
+ database_bootstrap_if_enabled create_archivesspace_database
s6-setuidgid archivesspace /archivesspace/scripts/setup-database.sh
fi
+unset DB_ROOT_PASSWORD
exec s6-setuidgid archivesspace /archivesspace/archivesspace.sh
diff --git a/images/archivesspace/tests/ServiceHealthcheck/docker-compose.yml b/images/archivesspace/tests/ServiceHealthcheck/docker-compose.yml
index bdac72ec..6410d5ff 100644
--- a/images/archivesspace/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/archivesspace/tests/ServiceHealthcheck/docker-compose.yml
@@ -5,8 +5,7 @@ services:
image: ${ARCHIVESSPACE:-libops/archivesspace:local}
environment:
ASPACE_DB_MIGRATE: "true"
- DB_ROOT_PASSWORD: root
- DB_PASSWORD: password
+ DB_PASSWORD: "archive&=space%#password"
APPCONFIG_SOLR_URL: http://solr:8983/solr/archivesspace
depends_on:
mariadb:
@@ -19,6 +18,6 @@ services:
DB_ROOT_PASSWORD: root
DB_NAME: archivesspace
DB_USER: archivesspace
- DB_PASSWORD: password
+ DB_PASSWORD: "archive&=space%#password"
solr:
image: ${ARCHIVESSPACE_SOLR:-libops/archivesspace-solr:local}
diff --git a/images/base/Dockerfile b/images/base/Dockerfile
index 5c4c1203..113412d7 100644
--- a/images/base/Dockerfile
+++ b/images/base/Dockerfile
@@ -140,15 +140,16 @@ ENV \
# environment variables via ENV.
ENV \
APP_UID=100 \
+ DB_BOOTSTRAP_ENABLED=false \
DB_HOST=mariadb \
DB_NAME=default \
- DB_PASSWORD=password \
+ DB_PASSWORD= \
DB_PORT=3306 \
- DB_ROOT_PASSWORD=password \
+ DB_ROOT_PASSWORD= \
DB_ROOT_USER=root \
DB_USER=default \
DEVELOPMENT_ENVIRONMENT=false \
- JWT_ADMIN_TOKEN=islandora \
+ JWT_ADMIN_TOKEN= \
LIBOPS_PROJECT_ID= \
LIBOPS_SITE_ID= \
UID= \
diff --git a/images/base/README.md b/images/base/README.md
index a1bd09c1..97abbe8e 100644
--- a/images/base/README.md
+++ b/images/base/README.md
@@ -69,7 +69,7 @@ duplication.
| Environment Variable | Default | Description |
| :------------------- | :-------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
-| JWT_ADMIN_TOKEN | islandora | Used for [bearer authentication] (Only use with HTTPS or over private networks) |
+| JWT_ADMIN_TOKEN | | Used for [bearer authentication]; supply a secret for services that enable Syn |
| JWT_PRIVATE_KEY | @see base/rootfs/etc/defaults/JWT_PRIVATE_KEY | Private key for JWT authentication, RSA PEM Format is expected (should only be used in the Drupal container) |
| JWT_PUBLIC_KEY | @see base/rootfs/etc/defaults/JWT_PUBLIC_KEY | Public key for JWT authentication |
@@ -91,15 +91,16 @@ add `jwt` as a secondary group to your service user.
Database settings target MariaDB.
-| Environment Variable | Default | Description |
-| :------------------- | :--------- | :---------------------------------------------------------------------------------------------- |
-| DB_HOST | mariadb | The database host to use |
-| DB_NAME | default | The name of the default database if no other is specified |
-| DB_PASSWORD | password | The password of the user used by the service (e.g. Drupal) to connect to the database |
-| DB_PORT | 3306 | The database port to use |
-| DB_ROOT_PASSWORD | password | The root user password |
-| DB_ROOT_USER | root | The root user, which is used only on startup to create database / user in the chosen backend |
-| DB_USER | default | The user used by the service (e.g. Drupal) to connect to the database |
+| Environment Variable | Default | Description |
+| :-------------------- | :------ | :----------------------------------------------------------------------------------------------------------- |
+| DB_BOOTSTRAP_ENABLED | false | Explicitly allow an application setup service to create its database and user with root credentials |
+| DB_HOST | mariadb | The database host to use |
+| DB_NAME | default | The name of the default database if no other is specified |
+| DB_PASSWORD | | The password of the scoped user used by the service; required by database-backed applications |
+| DB_PORT | 3306 | The database port to use |
+| DB_ROOT_PASSWORD | | The root password; required only when `DB_BOOTSTRAP_ENABLED=true` and preferably supplied as a file secret |
+| DB_ROOT_USER | root | The root user, used only by an explicitly enabled database bootstrap |
+| DB_USER | default | The user used by the service (e.g. Drupal) to connect to the database |
### Development Settings
diff --git a/images/base/rootfs/etc/s6-overlay/s6-rc.d/confd/run b/images/base/rootfs/etc/s6-overlay/s6-rc.d/confd/run
index 7c942dd3..79896310 100755
--- a/images/base/rootfs/etc/s6-overlay/s6-rc.d/confd/run
+++ b/images/base/rootfs/etc/s6-overlay/s6-rc.d/confd/run
@@ -2,6 +2,10 @@
# shellcheck shell=bash
set -e
+# This long-running renderer only needs the scoped application environment.
+# Do not retain the database administrator credential after initialization.
+unset DB_ROOT_PASSWORD
+
# Only run the service if explicitly told to do so.
if [[ "${CONFD_ENABLE_SERVICE}" == "true" ]]; then
exec confd-render-templates.sh -- -interval "${CONFD_POLLING_INTERVAL}"
diff --git a/images/base/rootfs/etc/s6-overlay/scripts/container-environment.sh b/images/base/rootfs/etc/s6-overlay/scripts/container-environment.sh
index 94984d63..3bce05ea 100755
--- a/images/base/rootfs/etc/s6-overlay/scripts/container-environment.sh
+++ b/images/base/rootfs/etc/s6-overlay/scripts/container-environment.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -e
+umask 077
# Sets container environment variables in order of precedence depending on the
# source:
@@ -63,9 +64,17 @@ fi
# Normalize the container environment through confd's env backend. This can only
# import values that are already defined in the environment.
{
+ # shellcheck disable=SC2016 # Literal Go template; shell expansion is unwanted.
+ printf '%s\n' '{{ define "execlineEscape" -}}{{- $escaped := replace . "\\" "\\\\" -1 -}}{{- replace $escaped "\"" "\\\"" -1 -}}{{- end -}}'
for file in /var/run/s6/container_environment/*; do
VAR=$(basename "${file}")
KEY=$(echo "${VAR}" | tr '[:upper:]' '[:lower:]' | tr '_' '/')
- echo "${VAR}=\"{{ getv \"/${KEY}\" (getenv \"${VAR}\") }}\""
+ echo "${VAR}=\"{{ template \"execlineEscape\" (getv \"/${KEY}\" (getenv \"${VAR}\")) }}\""
done
} | /usr/local/bin/confd-import-environment.sh
+
+# These files can include database administrator and application credentials.
+# Startup scripts run as root and import them before dropping privileges, so
+# they do not need to be readable by application service users.
+chmod 0700 /var/run/s6/container_environment
+find /var/run/s6/container_environment -maxdepth 1 -type f -exec chmod 0600 {} +
diff --git a/images/base/rootfs/etc/s6-overlay/scripts/vault-secrets.sh b/images/base/rootfs/etc/s6-overlay/scripts/vault-secrets.sh
index ef07f8bb..f228381b 100755
--- a/images/base/rootfs/etc/s6-overlay/scripts/vault-secrets.sh
+++ b/images/base/rootfs/etc/s6-overlay/scripts/vault-secrets.sh
@@ -341,7 +341,11 @@ write_secret_file() {
chmod 0400 "${tmp}"
if [[ "$(id -u)" = "0" ]]; then
- chown "${app_uid}:0" "${tmp}"
+ if [[ "${name}" = "DB_ROOT_PASSWORD" ]]; then
+ chown 0:0 "${tmp}"
+ else
+ chown "${app_uid}:0" "${tmp}"
+ fi
fi
mv -f "${tmp}" "${target}"
}
diff --git a/images/base/rootfs/usr/local/bin/cleanup.sh b/images/base/rootfs/usr/local/bin/cleanup.sh
index 27c1a9c7..7557c474 100755
--- a/images/base/rootfs/usr/local/bin/cleanup.sh
+++ b/images/base/rootfs/usr/local/bin/cleanup.sh
@@ -22,36 +22,25 @@ EOF
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "hx" OPTION; do
- case $OPTION in
- h)
+ while (($# > 0)); do
+ case "$1" in
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
+ ;;
+ --)
+ shift
+ if (($# > 0)); then
+ echo "cleanup.sh does not accept positional arguments" >&2
+ exit 1
+ fi
;;
*)
- echo "Invalid Option: $OPTION" >&2
- usage
+ echo "Invalid option or argument: $1" >&2
exit 1
;;
esac
@@ -63,7 +52,9 @@ function cmdline {
function main {
cmdline "${ARGS[@]}"
for file in /etc/cleanup.d/*; do
- $file
+ if [[ -f "${file}" && -x "${file}" ]]; then
+ "${file}"
+ fi
done
}
main
diff --git a/images/base/rootfs/usr/local/bin/confd-import-environment.sh b/images/base/rootfs/usr/local/bin/confd-import-environment.sh
index 7782a049..a06c3ee7 100755
--- a/images/base/rootfs/usr/local/bin/confd-import-environment.sh
+++ b/images/base/rootfs/usr/local/bin/confd-import-environment.sh
@@ -29,36 +29,25 @@ EOF
}
function cmdline() {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "hx" OPTION; do
- case $OPTION in
- h)
+ while (($# > 0)); do
+ case "$1" in
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
+ ;;
+ --)
+ shift
+ if (($# > 0)); then
+ echo "confd-import-environment.sh does not accept positional arguments" >&2
+ exit 1
+ fi
;;
*)
- echo "Invalid Option: $OPTION" >&2
- usage
+ echo "Invalid option or argument: $1" >&2
exit 1
;;
esac
@@ -68,12 +57,12 @@ function cmdline() {
}
function main {
- local tmp_dir
cmdline "${ARGS[@]}"
# Temporary directory to deposit generated confd configuration templates and
# output, etc.
tmp_dir="$(mktemp -d -t confd-XXXXXXXXXX)"
+ trap 'rm -rf -- "${tmp_dir}"' EXIT HUP INT TERM
mkdir -p "${tmp_dir}/conf.d" "${tmp_dir}/templates" "${tmp_dir}/out"
# Generate template script that will update the container environment with
@@ -104,7 +93,5 @@ EOF
# Import the variables from confd.
execlineb -P "${tmp_dir}/import.sh"
- # Remove temporary files.
- rm -fr "${tmp_dir}"
}
main
diff --git a/images/base/rootfs/usr/local/bin/confd-render-templates.sh b/images/base/rootfs/usr/local/bin/confd-render-templates.sh
index 7c8baf0e..902478d7 100755
--- a/images/base/rootfs/usr/local/bin/confd-render-templates.sh
+++ b/images/base/rootfs/usr/local/bin/confd-render-templates.sh
@@ -26,50 +26,31 @@ EOF
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
+ OPTIONS=()
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "hx" OPTION; do
- case $OPTION in
- h)
+ while (($# > 0)); do
+ case "$1" in
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
+ ;;
+ --)
+ shift
+ OPTIONS+=("$@")
+ break
;;
*)
- echo "Invalid Option: $OPTION" >&2
- usage
- exit 1
+ OPTIONS+=("$1")
+ shift
;;
esac
done
- shift $((OPTIND - 1))
-
- # Remaining options to be passed onto the client, preceeded by '--'.
- if [ "$#" -gt 0 ]; then
- readonly OPTIONS=("${@}")
- shift $#
- else
- readonly OPTIONS=()
- fi
+ readonly OPTIONS
return 0
}
diff --git a/images/base/rootfs/usr/local/bin/create-database.sh b/images/base/rootfs/usr/local/bin/create-database.sh
index 0a957259..c62646db 100755
--- a/images/base/rootfs/usr/local/bin/create-database.sh
+++ b/images/base/rootfs/usr/local/bin/create-database.sh
@@ -51,104 +51,95 @@ function fallback {
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --host) args="${args}-b " ;;
- --port) args="${args}-c " ;;
- --user) args="${args}-d " ;;
- --password) args="${args}-e " ;;
- --database) args="${args}-f " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "b:c:d:e:f:hx" OPTION; do
- case $OPTION in
- b)
- readonly HOST=${OPTARG}
- ;;
- c)
- readonly PORT=${OPTARG}
- ;;
- d)
- readonly USER=${OPTARG}
+ HOST=
+ PORT=
+ USER=
+ PASSWORD=
+ DATABASE_NAME=
+
+ while (($# > 0)); do
+ case "$1" in
+ --host|-b|--port|-c|--user|-d|--password|-e|--database|-f)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ --host|-b) HOST=$2 ;;
+ --port|-c) PORT=$2 ;;
+ --user|-d) USER=$2 ;;
+ --password|-e) PASSWORD=$2 ;;
+ --database|-f) DATABASE_NAME=$2 ;;
+ esac
+ shift 2
;;
- e)
- readonly PASSWORD=${OPTARG}
- ;;
- f)
- readonly DATABASE_NAME=${OPTARG}
- ;;
- h)
+ --host=*) HOST=${1#*=}; shift ;;
+ --port=*) PORT=${1#*=}; shift ;;
+ --user=*) USER=${1#*=}; shift ;;
+ --password=*) PASSWORD=${1#*=}; shift ;;
+ --database=*) DATABASE_NAME=${1#*=}; shift ;;
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
+ --) shift; break ;;
+ -*)
+ echo "Invalid option: $1" >&2
usage
exit 1
;;
+ *)
+ break
+ ;;
esac
done
if fallback "--database" "DATABASE_NAME" "DB_NAME"; then
- readonly DATABASE_NAME=${DB_NAME}
+ DATABASE_NAME=${DB_NAME}
fi
if fallback "--user" "USER" "DB_ROOT_USER"; then
- readonly USER=${DB_ROOT_USER}
+ USER=${DB_ROOT_USER}
fi
if fallback "--password" "PASSWORD" "DB_ROOT_PASSWORD"; then
- readonly PASSWORD=${DB_ROOT_PASSWORD}
+ PASSWORD=${DB_ROOT_PASSWORD}
fi
if fallback "--host" "HOST" "DB_HOST"; then
- readonly HOST=${DB_HOST}
+ HOST=${DB_HOST}
fi
if fallback "--port" "PORT" "DB_PORT"; then
- readonly PORT=${DB_PORT}
+ PORT=${DB_PORT}
fi
- shift $((OPTIND - 1))
-
# Allow either passing in a file/pipe or reading from stdin by specifiying "-" or
# ommiting completely.
- if [[ -f "${1}" || -p "${1}" ]]; then
- readonly FILE="${1}"
+ if [[ -n "${1:-}" && ( -f "$1" || -p "$1" ) ]]; then
+ FILE=$1
shift
- elif [[ "${1}" == "-" ]]; then
- readonly FILE=/dev/stdin
+ elif [[ "${1:-}" == "-" ]]; then
+ FILE=/dev/stdin
shift
else
- readonly FILE=/dev/stdin
+ FILE=/dev/stdin
fi
+ readonly HOST PORT USER PASSWORD DATABASE_NAME FILE
+
return 0
}
function execute_sql_file {
- execute-sql-file.sh \
+ LIBOPS_DATABASE_PASSWORD="${PASSWORD}" execute-sql-file.sh \
--host "${HOST}" \
--port "${PORT}" \
--user "${USER}" \
- --password "${PASSWORD}" \
"${@}"
}
diff --git a/images/base/rootfs/usr/local/bin/create-service-user.sh b/images/base/rootfs/usr/local/bin/create-service-user.sh
index fe8e39d8..712b9e3f 100755
--- a/images/base/rootfs/usr/local/bin/create-service-user.sh
+++ b/images/base/rootfs/usr/local/bin/create-service-user.sh
@@ -28,58 +28,55 @@ EOF
}
function cmdline() {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --name) args="${args}-n " ;;
- --group) args="${args}-g " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
+ NAME=
+ GROUP=
+ DIRECTORIES=()
- while getopts "n:g:hx" OPTION; do
- case $OPTION in
- n)
- readonly NAME=${OPTARG}
- ;;
- g)
- readonly GROUP=${OPTARG}
+ while (($# > 0)); do
+ case "$1" in
+ -n|--name|-g|--group)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ -n|--name) NAME=$2 ;;
+ -g|--group) GROUP=$2 ;;
+ esac
+ shift 2
;;
- h)
+ --name=*) NAME=${1#*=}; shift ;;
+ --group=*) GROUP=${1#*=}; shift ;;
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
- usage
+ --)
+ shift
+ DIRECTORIES+=("$@")
+ break
+ ;;
+ -*)
+ echo "Invalid option: $1" >&2
exit 1
;;
+ *)
+ DIRECTORIES+=("$1")
+ shift
+ ;;
esac
done
- if [[ ! -v NAME ]]; then
+ if [[ -z ${NAME} ]]; then
echo "Missing one or more required options: --name" >&2
exit 1
fi
- # All remaning parameters are directories to be created.
- shift $((OPTIND - 1))
- DIRECTORIES=("$@")
- readonly DIRECTORIES
+ readonly NAME GROUP DIRECTORIES
return 0
}
@@ -104,7 +101,7 @@ function main {
addgroup "${user}" tty
fi
# Optional secondary group.
- if [[ -v GROUP ]]; then
+ if [[ -n "${GROUP}" ]]; then
if ! id -nG "${user}" | tr ' ' '\n' | grep -qx "${GROUP}"; then
addgroup "${NAME}" "${GROUP}"
fi
diff --git a/images/base/rootfs/usr/local/bin/download.sh b/images/base/rootfs/usr/local/bin/download.sh
index 413a1bed..f6a5317a 100755
--- a/images/base/rootfs/usr/local/bin/download.sh
+++ b/images/base/rootfs/usr/local/bin/download.sh
@@ -34,73 +34,73 @@ EOF
}
function cmdline {
- local arg=
- local args=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --url) args="${args}-u " ;;
- --sha256) args="${args}-c " ;;
- --dest) args="${args}-d " ;;
- --strip) args="${args}-s " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "u:c:d:shx" OPTION; do
- case $OPTION in
- u)
- readonly URL=${OPTARG}
- ;;
- c)
- readonly CHECKSUM=${OPTARG}
+ URL=
+ CHECKSUM=
+ DEST=
+ STRIP=false
+ REMOVE=()
+
+ while (($# > 0)); do
+ case "$1" in
+ -u|--url|-c|--sha256|-d|--dest)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ -u|--url) URL=$2 ;;
+ -c|--sha256) CHECKSUM=$2 ;;
+ -d|--dest) DEST=$2 ;;
+ esac
+ shift 2
;;
- d)
- readonly DEST=${OPTARG}
+ --url=*) URL=${1#*=}; shift ;;
+ --sha256=*) CHECKSUM=${1#*=}; shift ;;
+ --dest=*) DEST=${1#*=}; shift ;;
+ -s|--strip)
+ STRIP=true
+ shift
;;
- s)
- readonly STRIP=true
- ;;
- h)
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
- usage
+ --)
+ shift
+ REMOVE+=("$@")
+ break
+ ;;
+ -*)
+ echo "Invalid option: $1" >&2
exit 1
;;
+ *)
+ REMOVE+=("$1")
+ shift
+ ;;
esac
done
- if [[ -z $URL || -z $CHECKSUM ]]; then
- echo "Missing one or more required options: --url --sha256"
+ if [[ -z ${URL} || -z ${CHECKSUM} ]]; then
+ echo "Missing one or more required options: --url --sha256" >&2
exit 1
fi
- # All remaning parameters are files to be removed from the installation.
- shift $((OPTIND-1))
- readonly REMOVE=("$@")
+ readonly URL CHECKSUM DEST STRIP REMOVE
return 0
}
function validate {
local file=${1}
- sha256sum "${file}" | cut -f1 -d' ' | xargs test "${CHECKSUM}" ==
+ local actual
+ actual=$(sha256sum <"${file}")
+ actual=${actual%% *}
+ [[ "${actual}" == "${CHECKSUM}" ]]
}
function unpack {
@@ -109,7 +109,7 @@ function unpack {
local args=()
local filename=
mkdir -p "${dest}"
- if [[ -v STRIP ]]; then
+ if [[ "${STRIP}" == "true" ]]; then
args+=("--strip-components" "1")
fi
filename=$(basename "${file}")
@@ -124,15 +124,22 @@ function unpack {
gunzip "${file}" -f -c > "${dest}/${filename%.*}"
;;
*.zip | *.war)
- if [[ -v STRIP ]]; then
- mkdir -p /tmp/unpack
- unzip "${file}" -d /tmp/unpack
- local unpack_root
- unpack_root="$(find /tmp/unpack/ -type d -mindepth 1 -maxdepth 1)"
- shopt -s dotglob nullglob
- mv "${unpack_root}"/* "${dest}"
- shopt -u dotglob nullglob
- rm -fr /tmp/unpack
+ if [[ "${STRIP}" == "true" ]]; then
+ (
+ local unpack_dir unpack_root
+ local top_level=()
+ unpack_dir=$(mktemp -d -t download-unpack.XXXXXXXXXX)
+ trap 'rm -rf -- "${unpack_dir}"' EXIT HUP INT TERM
+ unzip "${file}" -d "${unpack_dir}"
+ mapfile -d '' top_level < <(find "${unpack_dir}" -mindepth 1 -maxdepth 1 -print0)
+ if ((${#top_level[@]} != 1)) || [[ ! -d "${top_level[0]}" ]]; then
+ echo "ZIP strip requires exactly one top-level directory: ${file}" >&2
+ exit 1
+ fi
+ unpack_root=${top_level[0]}
+ shopt -s dotglob nullglob
+ mv "${unpack_root}"/* "${dest}"
+ )
else
unzip "${file}" -d "${dest}"
fi
@@ -163,8 +170,11 @@ function main {
wget -N -P "${DOWNLOAD_CACHE_DIRECTORY}" "${URL}"
# Return non-zero if the checksum does not match the downloaded file.
validate "${file}"
- if [[ -v DEST ]]; then
+ if [[ -n "${DEST}" ]]; then
unpack "${file}" "${DEST}"
fi
}
-main
+
+if [[ "${DOWNLOAD_LIBRARY_ONLY:-false}" != "true" ]]; then
+ main
+fi
diff --git a/images/base/rootfs/usr/local/bin/execute-sql-file.sh b/images/base/rootfs/usr/local/bin/execute-sql-file.sh
index dc7dcad2..3e19b479 100755
--- a/images/base/rootfs/usr/local/bin/execute-sql-file.sh
+++ b/images/base/rootfs/usr/local/bin/execute-sql-file.sh
@@ -4,6 +4,12 @@ ARGS=("$@")
PROGNAME=$(basename "$0")
readonly ARGS PROGNAME
+# shellcheck source=images/base/rootfs/usr/local/share/libops/database.sh
+source "${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}"
+
+CLIENT_DEFAULTS_FILE=
+REMOVE_CLIENT_DEFAULTS_FILE=false
+
function usage {
cat <<-EOF
usage: $PROGNAME options FILE
@@ -23,6 +29,7 @@ function usage {
--port The database port.
--user The user to connect as.
--password The password to use for the user.
+ --defaults-extra-file Existing MariaDB client option file containing the password.
--database The database to run the sql command against. (Optional)
-h --help Show this help.
-x --debug Debug this script.
@@ -54,58 +61,54 @@ function fallback {
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --host) args="${args}-b " ;;
- --port) args="${args}-c " ;;
- --user) args="${args}-d " ;;
- --password) args="${args}-e " ;;
- --database) args="${args}-f " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
+ HOST=
+ PORT=
+ USER=
+ PASSWORD=
+ DATABASE=
+ DEFAULTS_EXTRA_FILE=
+
+ while (($# > 0)); do
+ case "$1" in
+ --host|-b|--port|-c|--user|-d|--password|-e|--database|-f|--defaults-extra-file)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ --host|-b) HOST=$2 ;;
+ --port|-c) PORT=$2 ;;
+ --user|-d) USER=$2 ;;
+ --password|-e) PASSWORD=$2 ;;
+ --database|-f) DATABASE=$2 ;;
+ --defaults-extra-file) DEFAULTS_EXTRA_FILE=$2 ;;
+ esac
+ shift 2
;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "b:c:d:e:f:hx" OPTION; do
- case $OPTION in
- b)
- HOST=${OPTARG}
- ;;
- c)
- PORT=${OPTARG}
- ;;
- d)
- USER=${OPTARG}
- ;;
- e)
- PASSWORD=${OPTARG}
- ;;
- f)
- DATABASE=${OPTARG}
- ;;
- h)
+ --host=*) HOST=${1#*=}; shift ;;
+ --port=*) PORT=${1#*=}; shift ;;
+ --user=*) USER=${1#*=}; shift ;;
+ --password=*) PASSWORD=${1#*=}; shift ;;
+ --database=*) DATABASE=${1#*=}; shift ;;
+ --defaults-extra-file=*) DEFAULTS_EXTRA_FILE=${1#*=}; shift ;;
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
+ --) shift; break ;;
+ -) break ;;
+ -*)
+ echo "Invalid option: $1" >&2
usage
exit 1
;;
+ *)
+ break
+ ;;
esac
done
@@ -113,7 +116,9 @@ function cmdline {
USER=${DB_ROOT_USER}
fi
- if fallback "--password" "PASSWORD" "DB_ROOT_PASSWORD"; then
+ if [[ -z "${PASSWORD}" && -n "${LIBOPS_DATABASE_PASSWORD:-}" ]]; then
+ PASSWORD=${LIBOPS_DATABASE_PASSWORD}
+ elif [[ -z "${PASSWORD}" && -z "${DEFAULTS_EXTRA_FILE}" ]] && fallback "--password" "PASSWORD" "DB_ROOT_PASSWORD"; then
PASSWORD=${DB_ROOT_PASSWORD}
fi
@@ -125,14 +130,12 @@ function cmdline {
PORT=${DB_PORT}
fi
- shift $((OPTIND - 1))
-
# Allow either passing in a file or reading from stdin by specifiying "-" or
# ommiting completely.
- if [[ -f "${1}" || -p "${1}" ]]; then
- FILE="${1}"
+ if [[ -n "${1:-}" && ( -f "$1" || -p "$1" ) ]]; then
+ FILE=$1
shift
- elif [[ "${1}" == "-" ]]; then
+ elif [[ "${1:-}" == "-" ]]; then
FILE=/dev/stdin
shift
else
@@ -140,7 +143,7 @@ function cmdline {
fi
# Remaining options to be passed onto the client, preceeded by '--'.
- if [[ "${1}" == "--" ]]; then
+ if [[ "${1:-}" == "--" ]]; then
shift
fi
@@ -151,7 +154,12 @@ function cmdline {
OPTIONS=()
fi
- readonly HOST PORT USER PASSWORD DATABASE FILE OPTIONS
+ if [[ -n "${DEFAULTS_EXTRA_FILE}" && ! -r "${DEFAULTS_EXTRA_FILE}" ]]; then
+ echo "MariaDB client option file is not readable: ${DEFAULTS_EXTRA_FILE}" >&2
+ exit 1
+ fi
+
+ readonly HOST PORT USER PASSWORD DATABASE DEFAULTS_EXTRA_FILE FILE OPTIONS
return 0
}
@@ -163,33 +171,52 @@ function wait_for_access {
--host "${HOST}" \
--port "${PORT}" \
--user "${USER}" \
- --password "${PASSWORD}" >&2
+ --defaults-extra-file "${CLIENT_DEFAULTS_FILE}" >&2
}
function mysql_execute_sql_file {
- local database_arg=
+ local database_args=()
if [[ -n "${DATABASE}" ]]; then
- database_arg="--database=${DATABASE}"
+ database_args+=("--database=${DATABASE}")
fi
mariadb \
+ --defaults-extra-file="${CLIENT_DEFAULTS_FILE}" \
--host="${HOST}" \
--port="${PORT}" \
--user="${USER}" \
- --password="${PASSWORD}" \
--protocol=tcp \
- "${database_arg}" \
+ "${database_args[@]}" \
"${OPTIONS[@]}" \
<"${FILE}"
}
+function prepare_client_defaults_file {
+ if [[ -n "${DEFAULTS_EXTRA_FILE}" ]]; then
+ CLIENT_DEFAULTS_FILE=${DEFAULTS_EXTRA_FILE}
+ return 0
+ fi
+
+ CLIENT_DEFAULTS_FILE=$(mktemp -t mariadb-client.XXXXXXXXXX)
+ database_write_client_defaults_file "${CLIENT_DEFAULTS_FILE}" "${PASSWORD}"
+ REMOVE_CLIENT_DEFAULTS_FILE=true
+}
+
+function cleanup_client_defaults_file {
+ if [[ "${REMOVE_CLIENT_DEFAULTS_FILE}" = "true" && -n "${CLIENT_DEFAULTS_FILE}" ]]; then
+ rm -f -- "${CLIENT_DEFAULTS_FILE}"
+ fi
+}
+
function execute_sql_file {
mysql_execute_sql_file
}
function main {
cmdline "${ARGS[@]}"
+ prepare_client_defaults_file
+ trap cleanup_client_defaults_file EXIT
wait_for_access
execute_sql_file
}
diff --git a/images/base/rootfs/usr/local/bin/render-database-bootstrap-sql.sh b/images/base/rootfs/usr/local/bin/render-database-bootstrap-sql.sh
new file mode 100755
index 00000000..0a1aaf8d
--- /dev/null
+++ b/images/base/rootfs/usr/local/bin/render-database-bootstrap-sql.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# shellcheck source=images/base/rootfs/usr/local/share/libops/database.sh
+source "${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}"
+
+: "${DB_NAME:?DB_NAME is required}"
+: "${DB_USER:?DB_USER is required}"
+: "${DB_PASSWORD:?DB_PASSWORD is required}"
+
+DB_CHARACTER_SET=${DB_CHARACTER_SET:-utf8mb4}
+DB_COLLATION=${DB_COLLATION:-utf8mb4_unicode_ci}
+
+database_validate_identifier DB_NAME "${DB_NAME}"
+database_validate_identifier DB_USER "${DB_USER}"
+database_validate_identifier DB_CHARACTER_SET "${DB_CHARACTER_SET}"
+database_validate_identifier DB_COLLATION "${DB_COLLATION}"
+
+db_user_sql=$(database_escape_sql_literal "${DB_USER}")
+db_password_sql=$(database_escape_sql_literal "${DB_PASSWORD}")
+
+cat <<-SQL
+CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET ${DB_CHARACTER_SET} COLLATE ${DB_COLLATION};
+CREATE USER IF NOT EXISTS '${db_user_sql}'@'%' IDENTIFIED BY '${db_password_sql}';
+GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${db_user_sql}'@'%';
+SET PASSWORD FOR '${db_user_sql}'@'%' = PASSWORD('${db_password_sql}');
+FLUSH PRIVILEGES;
+SQL
diff --git a/images/base/rootfs/usr/local/bin/wait-for-database.sh b/images/base/rootfs/usr/local/bin/wait-for-database.sh
index 70120f14..353c1c91 100755
--- a/images/base/rootfs/usr/local/bin/wait-for-database.sh
+++ b/images/base/rootfs/usr/local/bin/wait-for-database.sh
@@ -5,6 +5,12 @@ ARGS=("$@")
PROGNAME=$(basename "$0")
readonly ARGS PROGNAME
+# shellcheck source=images/base/rootfs/usr/local/share/libops/database.sh
+source "${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}"
+
+CLIENT_DEFAULTS_FILE=
+REMOVE_CLIENT_DEFAULTS_FILE=false
+
function usage {
cat <<-EOF
usage: $PROGNAME options
@@ -19,6 +25,7 @@ function usage {
--port The database port.
--user The user to connect as.
--password The password to use for the user.
+ --defaults-extra-file Existing MariaDB client option file containing the password.
--timeout Time to wait for a connection to the database, defaults to 5 minutes. (Optional)
-h --help Show this help.
-x --debug Debug this script.
@@ -34,77 +41,89 @@ EOF
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --host) args="${args}-b " ;;
- --port) args="${args}-c " ;;
- --user) args="${args}-d " ;;
- --password) args="${args}-e " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "b:c:d:e:hx" OPTION; do
- case $OPTION in
- b)
- readonly HOST=${OPTARG}
+ HOST=
+ PORT=
+ USER=
+ PASSWORD=
+ DEFAULTS_EXTRA_FILE=
+ TIMEOUT=300
+
+ while (($# > 0)); do
+ case "$1" in
+ --host|-b|--port|-c|--user|-d|--password|-e|--timeout|-t|--defaults-extra-file)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ --host|-b) HOST=$2 ;;
+ --port|-c) PORT=$2 ;;
+ --user|-d) USER=$2 ;;
+ --password|-e) PASSWORD=$2 ;;
+ --timeout|-t) TIMEOUT=$2 ;;
+ --defaults-extra-file) DEFAULTS_EXTRA_FILE=$2 ;;
+ esac
+ shift 2
;;
- c)
- readonly PORT=${OPTARG}
- ;;
- d)
- readonly USER=${OPTARG}
- ;;
- e)
- readonly PASSWORD=${OPTARG}
- ;;
- h)
+ --host=*) HOST=${1#*=}; shift ;;
+ --port=*) PORT=${1#*=}; shift ;;
+ --user=*) USER=${1#*=}; shift ;;
+ --password=*) PASSWORD=${1#*=}; shift ;;
+ --timeout=*) TIMEOUT=${1#*=}; shift ;;
+ --defaults-extra-file=*) DEFAULTS_EXTRA_FILE=${1#*=}; shift ;;
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
+ --) shift; break ;;
+ -*)
+ echo "Invalid option: $1" >&2
usage
exit 1
;;
+ *)
+ break
+ ;;
esac
done
- if [[ -z $HOST || -z $PORT || -z $USER || -z $PASSWORD ]]; then
- echo "Missing one of required options: --host --port --user --password" >&2
+ if [[ -z "${PASSWORD}" && -n "${LIBOPS_DATABASE_PASSWORD:-}" ]]; then
+ PASSWORD=${LIBOPS_DATABASE_PASSWORD}
+ elif [[ -z "${PASSWORD}" && -n "${DB_ROOT_PASSWORD:-}" ]]; then
+ PASSWORD=${DB_ROOT_PASSWORD}
+ fi
+
+ if [[ -z $HOST || -z $PORT || -z $USER || ( -z $PASSWORD && -z $DEFAULTS_EXTRA_FILE ) ]]; then
+ echo "Missing one of required options: --host --port --user and --password/--defaults-extra-file" >&2
+ exit 1
+ fi
+
+ if [[ -n "${DEFAULTS_EXTRA_FILE}" && ! -r "${DEFAULTS_EXTRA_FILE}" ]]; then
+ echo "MariaDB client option file is not readable: ${DEFAULTS_EXTRA_FILE}" >&2
exit 1
fi
+ readonly HOST PORT USER PASSWORD DEFAULTS_EXTRA_FILE TIMEOUT
+
return 0
}
function wait_for_connection {
- local duration=${TIMEOUT:-300}
+ local duration=${TIMEOUT}
echo "Waiting for up to ${duration} seconds to connect to Database ${HOST}:${PORT}" >&2
timeout "${duration}" wait-for-open-port.sh "${HOST}" "${PORT}"
}
function mysql_validate_credentials {
mariadb \
+ --defaults-extra-file="${CLIENT_DEFAULTS_FILE}" \
--batch \
--skip-column-names \
--user="${USER}" \
- --password="${PASSWORD}" \
--host="${HOST}" \
--port="${PORT}" \
--protocol=tcp \
@@ -112,6 +131,23 @@ function mysql_validate_credentials {
>/dev/null
}
+function prepare_client_defaults_file {
+ if [[ -n "${DEFAULTS_EXTRA_FILE}" ]]; then
+ CLIENT_DEFAULTS_FILE=${DEFAULTS_EXTRA_FILE}
+ return 0
+ fi
+
+ CLIENT_DEFAULTS_FILE=$(mktemp -t mariadb-client.XXXXXXXXXX)
+ database_write_client_defaults_file "${CLIENT_DEFAULTS_FILE}" "${PASSWORD}"
+ REMOVE_CLIENT_DEFAULTS_FILE=true
+}
+
+function cleanup_client_defaults_file {
+ if [[ "${REMOVE_CLIENT_DEFAULTS_FILE}" = "true" && -n "${CLIENT_DEFAULTS_FILE}" ]]; then
+ rm -f -- "${CLIENT_DEFAULTS_FILE}"
+ fi
+}
+
function validate_credentials {
echo "Validating Database credentials" >&2
mysql_validate_credentials
@@ -119,20 +155,22 @@ function validate_credentials {
function main {
cmdline "${ARGS[@]}"
+ prepare_client_defaults_file
+ trap cleanup_client_defaults_file EXIT
if wait_for_connection; then
echo "Database found" >&2
else
echo "Timed out waiting for database connection" >&2
- exit 1
+ return 1
fi
if validate_credentials; then
echo "Credentials are valid" >&2
- exit 0
+ return 0
else
echo "Credentials are invalid" >&2
- exit 1
+ return 1
fi
}
main
diff --git a/images/base/rootfs/usr/local/bin/wait-for-open-port.sh b/images/base/rootfs/usr/local/bin/wait-for-open-port.sh
index ee427c55..26c32b76 100755
--- a/images/base/rootfs/usr/local/bin/wait-for-open-port.sh
+++ b/images/base/rootfs/usr/local/bin/wait-for-open-port.sh
@@ -24,52 +24,43 @@ EOF
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
+ local positional=()
- while getopts "hx" OPTION; do
- case $OPTION in
- h)
+ while (($# > 0)); do
+ case "$1" in
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
;;
- *)
- echo "Invalid Option: $OPTION" >&2
- usage
+ --)
+ shift
+ positional+=("$@")
+ break
+ ;;
+ -*)
+ echo "Invalid option: $1" >&2
exit 1
;;
+ *)
+ positional+=("$1")
+ shift
+ ;;
esac
done
- shift $((OPTIND - 1))
-
- if [ "$#" -ne 2 ]; then
+ if [ "${#positional[@]}" -ne 2 ]; then
echo "Illegal number of parameters" >&2
usage
return 1
fi
- readonly HOST=${1}
- shift
- readonly PORT=${1}
+ HOST=${positional[0]}
+ PORT=${positional[1]}
+ readonly HOST PORT
return 0
}
diff --git a/images/base/rootfs/usr/local/share/libops/database.sh b/images/base/rootfs/usr/local/share/libops/database.sh
new file mode 100755
index 00000000..b4f0e52f
--- /dev/null
+++ b/images/base/rootfs/usr/local/share/libops/database.sh
@@ -0,0 +1,106 @@
+#!/usr/bin/env bash
+# shellcheck shell=bash
+
+function database_validate_identifier {
+ local name=$1
+ local value=$2
+
+ if [[ ! "${value}" =~ ^[A-Za-z0-9_]+$ ]]; then
+ echo "${name} must contain only letters, numbers, and underscores" >&2
+ return 1
+ fi
+}
+
+function database_escape_sql_literal {
+ local value=$1
+
+ value=${value//\\/\\\\}
+ value=${value//\'/\'\'}
+ printf '%s' "${value}"
+}
+
+function database_escape_option_file_value {
+ local value=$1
+
+ value=${value//\\/\\\\}
+ value=${value//$'\n'/\\n}
+ value=${value//$'\r'/\\r}
+ value=${value//$'\t'/\\t}
+ value=${value//\"/\\\"}
+ printf '%s' "${value}"
+}
+
+function database_write_client_defaults_file {
+ local file=$1
+ local password=$2
+ local password_escaped
+
+ password_escaped=$(database_escape_option_file_value "${password}")
+ (
+ umask 077
+ printf '[client]\npassword="%s"\n' "${password_escaped}" >"${file}"
+ chmod 0600 "${file}"
+ )
+}
+
+function database_mariadb_with_password {
+ local password=$1
+ shift
+
+ (
+ local client_defaults_file
+ client_defaults_file=$(mktemp -t mariadb-client.XXXXXXXXXX)
+ trap 'rm -f -- "${client_defaults_file}"' EXIT
+ database_write_client_defaults_file "${client_defaults_file}" "${password}"
+ command mariadb --defaults-extra-file="${client_defaults_file}" "$@"
+ )
+}
+
+function database_require_root_credentials {
+ if [ -z "${DB_ROOT_USER:-}" ]; then
+ echo "DB_ROOT_USER must not be empty" >&2
+ return 1
+ fi
+ if [ -z "${DB_ROOT_PASSWORD:-}" ]; then
+ echo "DB_ROOT_PASSWORD must not be empty" >&2
+ return 1
+ fi
+}
+
+# Return 0 when application-side database bootstrap was explicitly enabled,
+# 1 when it is disabled, and 2 for an unsafe or invalid configuration.
+function database_bootstrap_enabled {
+ case "${DB_BOOTSTRAP_ENABLED:-false}" in
+ 1 | [Tt][Rr][Uu][Ee] | [Yy][Ee][Ss] | [Oo][Nn])
+ if ! database_require_root_credentials; then
+ echo "Root credentials are required when DB_BOOTSTRAP_ENABLED=true" >&2
+ return 2
+ fi
+ return 0
+ ;;
+ '' | 0 | [Ff][Aa][Ll][Ss][Ee] | [Nn][Oo] | [Oo][Ff][Ff])
+ return 1
+ ;;
+ *)
+ echo "DB_BOOTSTRAP_ENABLED must be true or false" >&2
+ return 2
+ ;;
+ esac
+}
+
+function database_bootstrap_if_enabled {
+ local status=0
+
+ database_bootstrap_enabled || status=$?
+ case "${status}" in
+ 0)
+ "$@"
+ ;;
+ 1)
+ return 0
+ ;;
+ *)
+ return "${status}"
+ ;;
+ esac
+}
diff --git a/images/base/rootfs/usr/local/share/libops/environment.sh b/images/base/rootfs/usr/local/share/libops/environment.sh
new file mode 100755
index 00000000..d5505902
--- /dev/null
+++ b/images/base/rootfs/usr/local/share/libops/environment.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# shellcheck shell=bash
+
+function require_environment_variables {
+ local name value missing=false
+
+ for name in "$@"; do
+ value=${!name-}
+ if [ -z "${value}" ]; then
+ echo "${name} must not be empty" >&2
+ missing=true
+ fi
+ done
+ if [ "${missing}" = "true" ]; then
+ return 1
+ fi
+}
diff --git a/images/base/tests/DatabaseHelperArgumentSafety/docker-compose.yml b/images/base/tests/DatabaseHelperArgumentSafety/docker-compose.yml
new file mode 100644
index 00000000..581a6f2b
--- /dev/null
+++ b/images/base/tests/DatabaseHelperArgumentSafety/docker-compose.yml
@@ -0,0 +1,9 @@
+---
+name: base-databasehelperargumentsafety
+services:
+ base:
+ image: ${BASE:-libops/base:local}
+ volumes:
+ - ./test.sh:/test.sh:ro
+ command:
+ - /test.sh
diff --git a/images/base/tests/DatabaseHelperArgumentSafety/test.sh b/images/base/tests/DatabaseHelperArgumentSafety/test.sh
new file mode 100755
index 00000000..76fd5b48
--- /dev/null
+++ b/images/base/tests/DatabaseHelperArgumentSafety/test.sh
@@ -0,0 +1,239 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -euo pipefail
+
+stub_dir="$(mktemp -d)"
+capture="$(mktemp)"
+marker=/tmp/database-helper-injected
+# shellcheck disable=SC2016 # Literal command substitution is an injection payload.
+password='space "$(touch /tmp/database-helper-injected)" quote'"'"' semicolon; back\slash end'
+
+cleanup() {
+ rm -rf "${stub_dir}" "${capture}" "${marker}"
+}
+trap cleanup EXIT
+
+cat >"${stub_dir}/mariadb" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+for argument in "$@"; do
+ if [[ "${argument}" == --password* ]]; then
+ echo "Password was exposed in MariaDB argv" >&2
+ exit 1
+ fi
+ if [[ "${argument}" == --defaults-extra-file=* ]]; then
+ defaults_file=${argument#*=}
+ test -r "${defaults_file}"
+ test "$(stat -c '%a' "${defaults_file}")" = 600
+ fi
+done
+printf '%s\n' "$@" >>"${DATABASE_HELPER_CAPTURE:?}"
+EOF
+
+cat >"${stub_dir}/wait-for-open-port.sh" <<'EOF'
+#!/usr/bin/env bash
+exit 0
+EOF
+
+cat >"${stub_dir}/wget" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+destination=
+url=${!#}
+while (($# > 0)); do
+ case "$1" in
+ -P) destination=$2; shift 2 ;;
+ *) shift ;;
+ esac
+done
+printf '%s' "${url}" >"${DOWNLOAD_ARGUMENT_CAPTURE:?}"
+mkdir -p "${destination}"
+: >"${destination}/$(basename "${url}")"
+EOF
+
+cat >"${stub_dir}/nc" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+printf '%s\n' "$@" >"${PORT_ARGUMENT_CAPTURE:?}"
+EOF
+
+cat >"${stub_dir}/confd" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+printf '%s\n' "$@" >"${CONFD_ARGUMENT_CAPTURE:?}"
+EOF
+
+chmod +x "${stub_dir}/mariadb" "${stub_dir}/wait-for-open-port.sh" \
+ "${stub_dir}/wget" "${stub_dir}/nc" "${stub_dir}/confd"
+export DATABASE_HELPER_CAPTURE="${capture}"
+export PATH="${stub_dir}:${PATH}"
+export LIBOPS_DATABASE_LIBRARY=${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}
+export LIBOPS_BIN_DIRECTORY=${LIBOPS_BIN_DIRECTORY:-/usr/local/bin}
+# shellcheck disable=SC1090
+source "${LIBOPS_DATABASE_LIBRARY}"
+# shellcheck disable=SC1090
+source "${LIBOPS_ENVIRONMENT_LIBRARY:-/usr/local/share/libops/environment.sh}"
+
+client_defaults_file=$(mktemp)
+database_write_client_defaults_file "${client_defaults_file}" "${password}"
+test "$(stat -c '%a' "${client_defaults_file}")" = 600
+expected_client_defaults=$(cat <<'EOF'
+[client]
+password="space \"$(touch /tmp/database-helper-injected)\" quote' semicolon; back\\slash end"
+EOF
+)
+test "$(cat "${client_defaults_file}")" = "${expected_client_defaults}"
+rm -f "${client_defaults_file}"
+
+if REQUIRED_TEST_VALUE='' require_environment_variables REQUIRED_TEST_VALUE 2>/dev/null; then
+ echo "Empty required environment value was accepted" >&2
+ exit 1
+fi
+REQUIRED_TEST_VALUE=present require_environment_variables REQUIRED_TEST_VALUE
+
+if DB_ROOT_USER=root DB_ROOT_PASSWORD='' database_require_root_credentials 2>/dev/null; then
+ echo "Empty database root password was accepted" >&2
+ exit 1
+fi
+if DB_ROOT_USER='' DB_ROOT_PASSWORD=root database_require_root_credentials 2>/dev/null; then
+ echo "Empty database root user was accepted" >&2
+ exit 1
+fi
+
+bootstrap_marker="${stub_dir}/bootstrap-called"
+export DB_ROOT_USER=root
+record_bootstrap() {
+ touch "${bootstrap_marker}"
+}
+
+DB_BOOTSTRAP_ENABLED=false \
+ DB_ROOT_PASSWORD=password \
+ database_bootstrap_if_enabled record_bootstrap
+test ! -e "${bootstrap_marker}"
+
+if DB_BOOTSTRAP_ENABLED=true \
+ DB_ROOT_PASSWORD='' \
+ database_bootstrap_if_enabled record_bootstrap 2>/dev/null; then
+ echo "Database bootstrap accepted an empty root password" >&2
+ exit 1
+fi
+test ! -e "${bootstrap_marker}"
+
+if DB_BOOTSTRAP_ENABLED=invalid \
+ DB_ROOT_PASSWORD=root \
+ database_bootstrap_if_enabled record_bootstrap 2>/dev/null; then
+ echo "Database bootstrap accepted an invalid enable flag" >&2
+ exit 1
+fi
+test ! -e "${bootstrap_marker}"
+
+DB_BOOTSTRAP_ENABLED=true \
+ DB_ROOT_PASSWORD=root \
+ database_bootstrap_if_enabled record_bootstrap
+test -e "${bootstrap_marker}"
+rm "${bootstrap_marker}"
+
+# shellcheck disable=SC2016 # Literal command substitution is an injection payload.
+malicious_argument='space "$(touch /tmp/database-helper-injected)" quote'"'"' and back\slash'
+download_capture="${stub_dir}/download-argument"
+port_capture="${stub_dir}/port-argument"
+confd_capture="${stub_dir}/confd-argument"
+export DOWNLOAD_ARGUMENT_CAPTURE="${download_capture}"
+export PORT_ARGUMENT_CAPTURE="${port_capture}"
+export CONFD_ARGUMENT_CAPTURE="${confd_capture}"
+
+DOWNLOAD_CACHE_DIRECTORY="${stub_dir}/downloads" \
+ download.sh \
+ --url "https://example.invalid/${malicious_argument}" \
+ --sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+test "$(cat "${download_capture}")" = "https://example.invalid/${malicious_argument}"
+
+"${LIBOPS_BIN_DIRECTORY}/wait-for-open-port.sh" "${malicious_argument}" "33 06"
+mapfile -t port_args <"${port_capture}"
+test "${port_args[2]}" = "${malicious_argument}"
+test "${port_args[3]}" = "33 06"
+
+CONFD_LOG_LEVEL=error confd-render-templates.sh -- -config-file "${malicious_argument}"
+mapfile -t confd_args <"${confd_capture}"
+test "${confd_args[5]}" = "${malicious_argument}"
+
+for helper in create-service-user.sh cleanup.sh confd-import-environment.sh; do
+ "${helper}" --help "${malicious_argument}" >/dev/null
+done
+test ! -e "${marker}"
+
+sql=$(
+ DB_NAME="database_name" \
+ DB_USER="database_user" \
+ DB_PASSWORD="${password}" \
+ render-database-bootstrap-sql.sh
+)
+expected_sql=$(cat <<'EOF'
+CREATE DATABASE IF NOT EXISTS `database_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+CREATE USER IF NOT EXISTS 'database_user'@'%' IDENTIFIED BY 'space "$(touch /tmp/database-helper-injected)" quote'' semicolon; back\\slash end';
+GRANT ALL PRIVILEGES ON `database_name`.* TO 'database_user'@'%';
+SET PASSWORD FOR 'database_user'@'%' = PASSWORD('space "$(touch /tmp/database-helper-injected)" quote'' semicolon; back\\slash end');
+FLUSH PRIVILEGES;
+EOF
+)
+test "${sql}" = "${expected_sql}"
+
+if DB_NAME="invalid-name" \
+ DB_USER="database_user" \
+ DB_PASSWORD="${password}" \
+ render-database-bootstrap-sql.sh >/dev/null 2>&1; then
+ echo "Invalid database identifier was accepted" >&2
+ exit 1
+fi
+
+wait-for-database.sh \
+ --host "database host" \
+ --port 3306 \
+ --user "database user" \
+ --password "${password}" \
+ --timeout 1
+
+grep -Fqx -- "--host=database host" "${capture}"
+grep -Fqx -- "--user=database user" "${capture}"
+if grep -Eq '^--password(=|$)' "${capture}"; then
+ echo "Database password was exposed in client argv" >&2
+ exit 1
+fi
+
+printf 'SELECT 1;\n' | execute-sql-file.sh \
+ --host "database host" \
+ --port 3306 \
+ --user "database user" \
+ --password "${password}" \
+ --database "database name" \
+ -- - --skip-column-names
+
+grep -Fqx -- "--database=database name" "${capture}"
+grep -Fqx -- "--skip-column-names" "${capture}"
+
+# A lone dash is also the documented FILE argument and must not be rejected as
+# an unknown option when callers do not use an option delimiter before it.
+printf 'SELECT 1;\n' | execute-sql-file.sh \
+ --host "database host" \
+ --port 3306 \
+ --user "database user" \
+ --password "${password}" \
+ --database "database name" \
+ - -- --batch
+
+grep -Fqx -- "--batch" "${capture}"
+
+printf 'SELECT 1;\n' | \
+ DB_HOST="database host" \
+ DB_PORT=3306 \
+ DB_NAME="database name" \
+ DB_ROOT_USER="database user" \
+ DB_ROOT_PASSWORD="${password}" \
+ create-database.sh
+
+if grep -Eq '^--password(=|$)' "${capture}"; then
+ echo "Database password was exposed in client argv" >&2
+ exit 1
+fi
+test ! -e "${marker}"
diff --git a/images/base/tests/EnvironmentPrecedence/secrets/DB_PASSWORD b/images/base/tests/EnvironmentPrecedence/secrets/DB_PASSWORD
index ab0bcb16..1fe4d989 100644
--- a/images/base/tests/EnvironmentPrecedence/secrets/DB_PASSWORD
+++ b/images/base/tests/EnvironmentPrecedence/secrets/DB_PASSWORD
@@ -1 +1 @@
-DB_PASSWORD secret value
\ No newline at end of file
+DB_PASSWORD secret "quote" \ value
diff --git a/images/base/tests/EnvironmentPrecedence/test.sh b/images/base/tests/EnvironmentPrecedence/test.sh
index d10163ad..46fe7a96 100755
--- a/images/base/tests/EnvironmentPrecedence/test.sh
+++ b/images/base/tests/EnvironmentPrecedence/test.sh
@@ -18,7 +18,7 @@ source /usr/local/share/isle/utilities.sh
# For ease of reading overridden values follow the format:
# ENV_VAR_NAME="ENV_VAR_NAME SOURCE value"
expect "JWT_ADMIN_TOKEN" "JWT_ADMIN_TOKEN secret value" # Secret should take precedence
-expect "DB_PASSWORD" "DB_PASSWORD secret value" # Secret should take precedence
+expect "DB_PASSWORD" 'DB_PASSWORD secret "quote" \ value' # Secret should take precedence and preserve special characters
expect "GOOGLE_APPLICATION_CREDENTIALS" "/run/secrets/GOOGLE_APPLICATION_CREDENTIALS" # Google credentials should remain file based
expect "DB_NAME" "DB_NAME passed in value" # Environment passed into the container should take precedence
expect "DB_USER" "default" # Environment variables defined in Dockerfile should take precedence
diff --git a/images/drupal/Dockerfile b/images/drupal/Dockerfile
index 58b053d6..611489a1 100644
--- a/images/drupal/Dockerfile
+++ b/images/drupal/Dockerfile
@@ -2,6 +2,8 @@
ARG PHP_BASE=nginx-php83
FROM ${PHP_BASE}
+LABEL org.opencontainers.image.licenses="GPL-2.0-or-later"
+
ARG PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock
EXPOSE 80
@@ -28,17 +30,17 @@ ENV \
DB_PORT=3306 \
DB_NAME=drupal \
DB_USER=drupal \
- DB_PASSWORD=changeme \
+ DB_PASSWORD= \
DRUPAL_DEFAULT_ACCOUNT_EMAIL=webmaster@localhost.com \
DRUPAL_DEFAULT_ACCOUNT_NAME=admin \
- DRUPAL_DEFAULT_ACCOUNT_PASSWORD=password \
+ DRUPAL_DEFAULT_ACCOUNT_PASSWORD= \
DRUPAL_DEFAULT_CONFIGDIR=/var/www/drupal/config/sync \
DRUPAL_DEFAULT_INSTALL_EXISTING_CONFIG=false \
DRUPAL_DEFAULT_EMAIL=webmaster@localhost.com \
DRUPAL_DEFAULT_LOCALE=en \
DRUPAL_DEFAULT_NAME=Drupal \
DRUPAL_DEFAULT_PROFILE=standard \
- DRUPAL_DEFAULT_SALT=9PPaL0CxZAIcq6h5wxgDGlCZrp7JdT_x7v9gVzpdbUjMt1PqDz3uD0Zy-i16DuJ1-Htfshhqeg \
+ DRUPAL_DEFAULT_SALT= \
DRUPAL_DEFAULT_SOLR_CORE=default \
DRUPAL_DEFAULT_SOLR_HOST=solr \
DRUPAL_DEFAULT_SOLR_PORT=8983 \
@@ -78,4 +80,4 @@ COPY --link rootfs /
RUN chown -R nginx:nginx /var/www/drupal
-HEALTHCHECK CMD { [ ! -f /var/www/drupal/web/core/lib/Drupal.php ] || [ -f /installed ]; } && curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD { [ ! -f /var/www/drupal/web/core/lib/Drupal.php ] || [ -f /installed ]; } && curl -fsS http://localhost/status | grep -q pool
diff --git a/images/drupal/rootfs/etc/confd/conf.d/drupal.libops.settings.toml b/images/drupal/rootfs/etc/confd/conf.d/drupal.libops.settings.toml
index f9aa0770..b69bee30 100644
--- a/images/drupal/rootfs/etc/confd/conf.d/drupal.libops.settings.toml
+++ b/images/drupal/rootfs/etc/confd/conf.d/drupal.libops.settings.toml
@@ -2,6 +2,6 @@
src = "drupal.libops.settings.php.tmpl"
dest = "/etc/drupal/libops.settings.php"
uid = 0
-gid = 0
-mode = "0644"
+gid = 101
+mode = "0640"
keys = [ "/" ]
diff --git a/images/drupal/rootfs/etc/s6-overlay/scripts/install.sh b/images/drupal/rootfs/etc/s6-overlay/scripts/install.sh
index fbd4305c..ef3a774b 100755
--- a/images/drupal/rootfs/etc/s6-overlay/scripts/install.sh
+++ b/images/drupal/rootfs/etc/s6-overlay/scripts/install.sh
@@ -4,29 +4,33 @@ set -euo pipefail
export PATH="/var/www/drupal/vendor/bin:${PATH}"
-function mysql_create_database {
- cat <<-SQL | create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
+# shellcheck source=images/base/rootfs/usr/local/share/libops/database.sh
+source "${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}"
+# shellcheck source=images/base/rootfs/usr/local/share/libops/environment.sh
+source "${LIBOPS_ENVIRONMENT_LIBRARY:-/usr/local/share/libops/environment.sh}"
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}');
-SQL
+function mysql_create_database {
+ DB_CHARACTER_SET=utf8mb4 \
+ DB_COLLATION=utf8mb4_unicode_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function mysql_count_query {
cat <<-SQL
SELECT COUNT(DISTINCT table_name)
FROM information_schema.columns
-WHERE table_schema = '${DB_NAME}';
+WHERE table_schema = DATABASE();
SQL
}
function installed {
local count
- count=$(execute-sql-file.sh <(mysql_count_query) -- -N 2>/dev/null) || return 1
+ count=$(LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" execute-sql-file.sh \
+ --host "${DB_HOST}" \
+ --port "${DB_PORT}" \
+ --user "${DB_USER}" \
+ --database "${DB_NAME}" \
+ <(mysql_count_query) -- -N 2>/dev/null) || return 1
[[ ${count:-0} -ne 0 ]]
}
@@ -53,7 +57,7 @@ function install_site {
existing_config_arg=("--existing-config")
fi
- drush \
+ DRUSH_COMMAND_SITE_INSTALL_OPTIONS_ACCOUNT_PASS="${DRUPAL_DEFAULT_ACCOUNT_PASSWORD}" drush \
-n \
-r /var/www/drupal/web \
site:install "${DRUPAL_DEFAULT_PROFILE}" \
@@ -62,10 +66,8 @@ function install_site {
--site-name="${DRUPAL_DEFAULT_NAME}" \
--site-mail="${DRUPAL_DEFAULT_EMAIL}" \
--account-name="${DRUPAL_DEFAULT_ACCOUNT_NAME}" \
- --account-pass="${DRUPAL_DEFAULT_ACCOUNT_PASSWORD}" \
--account-mail="${DRUPAL_DEFAULT_ACCOUNT_EMAIL}" \
- --locale="${DRUPAL_DEFAULT_LOCALE}" \
- --db-url="mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
+ --locale="${DRUPAL_DEFAULT_LOCALE}"
}
function run_install_hooks {
@@ -127,14 +129,19 @@ function main {
return 0
fi
+ require_environment_variables DB_HOST DB_NAME DB_USER DB_PASSWORD DRUPAL_DEFAULT_SALT
+
cd /var/www/drupal
drush_cache_setup
setup_directories
- mysql_create_database
+ if [ "${DB_HOST}" = "mariadb" ]; then
+ database_bootstrap_if_enabled mysql_create_database
+ fi
if installed; then
echo "Already Installed"
else
+ require_environment_variables DRUPAL_DEFAULT_ACCOUNT_PASSWORD
echo "Installing"
install_site
run_install_hooks
@@ -142,4 +149,7 @@ function main {
fi
finished
}
-main
+
+if [ "${DRUPAL_SETUP_LIBRARY_ONLY:-false}" != "true" ]; then
+ main
+fi
diff --git a/images/drupal/rootfs/usr/local/bin/install-drupal-site.sh b/images/drupal/rootfs/usr/local/bin/install-drupal-site.sh
index 78c10639..d4b5a273 100755
--- a/images/drupal/rootfs/usr/local/bin/install-drupal-site.sh
+++ b/images/drupal/rootfs/usr/local/bin/install-drupal-site.sh
@@ -14,8 +14,7 @@ function usage {
OPTIONS:
--host The database host.
--port The database port.
- --db-user The user to connect as.
- --db-password The password to use for the user.
+ --db-user The user to connect as.
--db-name The name of the database to install into.
-h --help Show this help.
@@ -23,90 +22,84 @@ function usage {
Examples:
Install default Drupal site:
- $PROGNAME \\
+ LIBOPS_DRUPAL_INSTALL_DB_PASSWORD=password $PROGNAME \\
--host "mariadb" \\
--port "3306" \\
--db-user "root" \\
- --db-password "password" \\
--db-name "drupal_default" \\
standard --sites-subdir=default --site-name=Islandora
EOF
}
function cmdline {
- local arg=
- for arg; do
- local delim=""
- case "$arg" in
- # Translate --gnu-long-options to -g (short options)
- --host) args="${args}-b " ;;
- --port) args="${args}-c " ;;
- --db-user) args="${args}-d " ;;
- --db-password) args="${args}-e " ;;
- --db-name) args="${args}-f " ;;
- --help) args="${args}-h " ;;
- --debug) args="${args}-x " ;;
- # Pass through anything else
- *)
- [[ "${arg:0:1}" == "-" ]] || delim="\""
- args="${args}${delim}${arg}${delim} "
- ;;
- esac
- done
-
- # Reset the positional parameters to the short options
- eval set -- "${args}"
-
- while getopts "b:c:d:e:f:hx" OPTION; do
- case $OPTION in
- b)
- readonly HOST=${OPTARG}
- ;;
- c)
- readonly PORT=${OPTARG}
- ;;
- d)
- readonly DB_USER=${OPTARG}
- ;;
- e)
- readonly DB_PASSWORD=${OPTARG}
+ HOST=
+ PORT=
+ DB_USER=
+ DB_PASSWORD=${LIBOPS_DRUPAL_INSTALL_DB_PASSWORD:-}
+ DB_NAME=
+ DRUSH_ARGS=()
+
+ while (($# > 0)); do
+ case "$1" in
+ -b|--host|-c|--port|-d|--db-user|-f|--db-name)
+ if (($# < 2)); then
+ echo "Option $1 requires a value" >&2
+ exit 1
+ fi
+ case "$1" in
+ -b|--host) HOST=$2 ;;
+ -c|--port) PORT=$2 ;;
+ -d|--db-user) DB_USER=$2 ;;
+ -f|--db-name) DB_NAME=$2 ;;
+ esac
+ shift 2
;;
- f)
- readonly DB_NAME=${OPTARG}
- ;;
- h)
+ --host=*) HOST=${1#*=}; shift ;;
+ --port=*) PORT=${1#*=}; shift ;;
+ --db-user=*) DB_USER=${1#*=}; shift ;;
+ --db-name=*) DB_NAME=${1#*=}; shift ;;
+ -h|--help)
usage
exit 0
;;
- x)
+ -x|--debug)
set -x
+ shift
+ ;;
+ --)
+ shift
+ DRUSH_ARGS+=("$@")
+ break
+ ;;
+ -*)
+ # The first unrecognized option and all following arguments belong
+ # to drush, allowing its own option syntax to pass through intact.
+ DRUSH_ARGS+=("$@")
+ break
;;
*)
- echo "Invalid Option: $OPTION" >&2
- usage
- exit 1
+ DRUSH_ARGS+=("$1")
+ shift
;;
esac
done
if [[ -z $HOST || -z $PORT || -z $DB_USER || -z $DB_PASSWORD || -z $DB_NAME ]]; then
- echo "Missing one of required options: --host --port --db-user --db-password --db-name"
+ echo "Missing one of required settings: --host --port --db-user --db-name LIBOPS_DRUPAL_INSTALL_DB_PASSWORD"
exit 1
fi
- # All remaning parameters are passed to 'drush site-install'.
- shift $((OPTIND - 1))
- readonly DRUSH_ARGS=("$@")
+ readonly HOST PORT DB_USER DB_PASSWORD DB_NAME DRUSH_ARGS
return 0
}
function execute_sql_file {
- execute-sql-file.sh \
+ LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" execute-sql-file.sh \
--host "${HOST}" \
--port "${PORT}" \
--user "${DB_USER}" \
- --password "${DB_PASSWORD}" \
+ --database "${DB_NAME}" \
"${@}"
}
@@ -114,7 +107,7 @@ function mysql_count_query {
cat <<-EOF
SELECT COUNT(DISTINCT table_name)
FROM information_schema.columns
-WHERE table_schema = '${DB_NAME}';
+WHERE table_schema = DATABASE();
EOF
}
@@ -122,6 +115,19 @@ function mysql_count {
execute_sql_file <(mysql_count_query) -- -N 2>/dev/null
}
+function uri_encode {
+ LIBOPS_DRUPAL_URI_COMPONENT="$1" php -r 'echo rawurlencode((string) getenv("LIBOPS_DRUPAL_URI_COMPONENT"));'
+}
+
+function database_url {
+ local encoded_name encoded_password encoded_user
+ encoded_user=$(uri_encode "${DB_USER}")
+ encoded_password=$(uri_encode "${DB_PASSWORD}")
+ encoded_name=$(uri_encode "${DB_NAME}")
+ printf 'mysql://%s:%s@%s:%s/%s' \
+ "${encoded_user}" "${encoded_password}" "${HOST}" "${PORT}" "${encoded_name}"
+}
+
# Check the number of tables to determine if it has already been installed.
function installed {
local count=
@@ -130,15 +136,18 @@ function installed {
}
function main {
+ local drush_database_url
cmdline "${ARGS[@]}"
if installed; then
echo "Site already is installed."
return 0
fi
echo "Installing site."
- drush \
+ drush_database_url=$(database_url)
+ DRUSH_COMMAND_SITE_INSTALL_OPTIONS_ACCOUNT_PASS="${LIBOPS_DRUPAL_INSTALL_ACCOUNT_PASSWORD:-}" \
+ DRUSH_COMMAND_SITE_INSTALL_OPTIONS_DB_URL="${drush_database_url}" \
+ drush \
-n \
- si "${DRUSH_ARGS[@]}" \
- --db-url="mysql://${DB_USER}:${DB_PASSWORD}@${HOST}:${PORT}/${DB_NAME}"
+ si "${DRUSH_ARGS[@]}"
}
main
diff --git a/images/fcrepo6/Dockerfile b/images/fcrepo6/Dockerfile
index e58165c0..eae0882b 100644
--- a/images/fcrepo6/Dockerfile
+++ b/images/fcrepo6/Dockerfile
@@ -8,6 +8,7 @@ ARG FCREPO_FILE=fcrepo-webapp-${SOFTWARE_VERSION}.war
ARG FCREPO_URL=https://github.com/fcrepo/fcrepo/releases/download/fcrepo-${SOFTWARE_VERSION}/${FCREPO_FILE}
ARG FCREPO_SHA256="5091550fe31e7693f36396e0ea3231c990648f14d914597124b84971a9daaae5"
+# renovate: datasource=github-releases depName=islandora-syn-fcrepo6 packageName=Islandora/Syn
ARG SYN_VERSION=1.1.1
ARG SYN_FILE=islandora-syn-${SYN_VERSION}-all.jar
ARG SYN_URL=https://github.com/islandora/Syn/releases/download/v${SYN_VERSION}/${SYN_FILE}
@@ -51,7 +52,7 @@ RUN --mount=type=cache,id=fcrepo6-downloads-${TARGETARCH},sharing=locked,target=
ENV \
DB_NAME=fcrepo \
- DB_PASSWORD=password \
+ DB_PASSWORD= \
DB_USER=fcrepo \
FCREPO_ACTIVEMQ_BROKER=tcp://activemq:61616 \
FCREPO_ACTIVEMQ_QUEUE=fedora \
@@ -64,4 +65,4 @@ COPY --link rootfs /
RUN chown -R tomcat:tomcat /opt/tomcat
-HEALTHCHECK CMD curl -s http://localhost:8080/fcrepo/rest/fcr:systeminfo | jq -e .version
+HEALTHCHECK CMD curl -fsS http://localhost:8080/fcrepo/rest/fcr:systeminfo | jq -e .version
diff --git a/images/fcrepo6/README.md b/images/fcrepo6/README.md
index 7c23bb0d..d44c8b38 100644
--- a/images/fcrepo6/README.md
+++ b/images/fcrepo6/README.md
@@ -74,14 +74,7 @@ Change `SOFTWARE_VERSION` and then generate the `FCREPO_SHA256` with the followi
commands:
```bash
-SOFTWARE_VERSION=$(cat fcrepo6/Dockerfile | grep -o 'SOFTWARE_VERSION=.*' | cut -f2 -d=)
-FCREPO_FILE=$(cat fcrepo6/Dockerfile | grep -o 'FCREPO_FILE=.*' | cut -f2 -d=)
-FCREPO_URL=$(cat fcrepo6/Dockerfile | grep -o 'FCREPO_URL=.*' | cut -f2 -d=)
-FCREPO_FILE=$(eval "echo $FCREPO_FILE")
-FCREPO_URL=$(eval "echo $FCREPO_URL")
-wget --quiet "${FCREPO_URL}"
-shasum -a 256 "${FCREPO_FILE}" | cut -f1 -d' '
-rm "${FCREPO_FILE}"
+./ci/update-sha.sh fcrepo6 OLD_VERSION NEW_VERSION ''
```
You can change the version used for [syn] by modifying the build argument
@@ -91,14 +84,7 @@ Change `SYN_VERSION` and then generate the `SYN_SHA256` with the following
commands:
```bash
-SYN_VERSION=$(cat fcrepo6/Dockerfile | grep -o 'SYN_VERSION=.*' | cut -f2 -d=)
-SYN_FILE=$(cat fcrepo6/Dockerfile | grep -o 'SYN_FILE=.*' | cut -f2 -d=)
-SYN_URL=$(cat fcrepo6/Dockerfile | grep -o 'SYN_URL=.*' | cut -f2 -d=)
-SYN_FILE=$(eval "echo $SYN_FILE")
-SYN_URL=$(eval "echo $SYN_URL")
-wget --quiet "${SYN_URL}"
-shasum -a 256 "${SYN_FILE}" | cut -f1 -d' '
-rm "${SYN_FILE}"
+./ci/update-sha.sh islandora-syn-fcrepo6 OLD_VERSION NEW_VERSION ''
```
You can change the version used for [fcrepo-import-export] by modifying the
@@ -109,14 +95,7 @@ Change `IMPORT_EXPORT_VERSION` and then generate the `IMPORT_EXPORT_SHA256` with
the following commands:
```bash
-IMPORT_EXPORT_VERSION=$(cat fcrepo6/Dockerfile | grep -o 'IMPORT_EXPORT_VERSION=.*' | cut -f2 -d=)
-IMPORT_EXPORT_FILE=$(cat fcrepo6/Dockerfile | grep -o 'IMPORT_EXPORT_FILE=.*' | cut -f2 -d=)
-IMPORT_EXPORT_URL=$(cat fcrepo6/Dockerfile | grep -o 'IMPORT_EXPORT_URL=.*' | cut -f2 -d=)
-IMPORT_EXPORT_FILE=$(eval "echo $IMPORT_EXPORT_FILE")
-IMPORT_EXPORT_URL=$(eval "echo $IMPORT_EXPORT_URL")
-wget --quiet "${IMPORT_EXPORT_URL}"
-shasum -a 256 "${IMPORT_EXPORT_FILE}" | cut -f1 -d' '
-rm "${IMPORT_EXPORT_FILE}"
+./ci/update-sha.sh fcrepo-import-export OLD_VERSION NEW_VERSION ''
```
You can change the version used for [fcrepo-upgrade-utils] by modifying the
@@ -127,14 +106,7 @@ Change `UPGRADE_UTILS_VERSION` and then generate the `UPGRADE_UTILS_SHA256` with
the following commands:
```bash
-UPGRADE_UTILS_VERSION=$(cat fcrepo6/Dockerfile | grep -o 'UPGRADE_UTILS_VERSION=.*' | cut -f2 -d=)
-UPGRADE_UTILS_FILE=$(cat fcrepo6/Dockerfile | grep -o 'UPGRADE_UTILS_FILE=.*' | cut -f2 -d=)
-UPGRADE_UTILS_URL=$(cat fcrepo6/Dockerfile | grep -o 'UPGRADE_UTILS_URL=.*' | cut -f2 -d=)
-UPGRADE_UTILS_FILE=$(eval "echo $UPGRADE_UTILS_FILE")
-UPGRADE_UTILS_URL=$(eval "echo $UPGRADE_UTILS_URL")
-wget --quiet "${UPGRADE_UTILS_URL}"
-shasum -a 256 "${UPGRADE_UTILS_FILE}" | cut -f1 -d' '
-rm "${UPGRADE_UTILS_FILE}"
+./ci/update-sha.sh fcrepo-upgrade-utils OLD_VERSION NEW_VERSION ''
```
[base image]: ../base/README.md
diff --git a/images/fcrepo6/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh b/images/fcrepo6/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
index f35ebb00..abaf0528 100755
--- a/images/fcrepo6/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
+++ b/images/fcrepo6/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
@@ -2,19 +2,15 @@
# shellcheck shell=bash
set -e
-function mysql_create_database {
- cat <<-EOF | create-database.sh
--- Create fcrepo database in mariadb or mysql.
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8 COLLATE utf8_general_ci;
-
--- Create fcrepo user and grant rights.
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
--- Update fcrepo password if changed.
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}')
-EOF
+function mysql_create_database {
+ DB_CHARACTER_SET=utf8 \
+ DB_COLLATION=utf8_general_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function wait_for_broker {
@@ -32,7 +28,14 @@ function wait_for_broker {
}
function main {
- mysql_create_database
+ require_environment_variables DB_HOST DB_NAME DB_USER DB_PASSWORD
+ if [ "${DB_HOST}" = "mariadb" ]; then
+ database_bootstrap_if_enabled mysql_create_database
+ fi
+ LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" wait-for-database.sh \
+ --host "${DB_HOST}" \
+ --port "${DB_PORT}" \
+ --user "${DB_USER}"
# When bind mounting we need to ensure that we
# actually can write to the folder.
chown tomcat:tomcat /data
diff --git a/images/fcrepo6/tests/ServiceHealthcheck/docker-compose.yml b/images/fcrepo6/tests/ServiceHealthcheck/docker-compose.yml
index 3f02fb21..1d4b163d 100644
--- a/images/fcrepo6/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/fcrepo6/tests/ServiceHealthcheck/docker-compose.yml
@@ -8,9 +8,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/fcrepo6/tests/ServiceHealthcheck/test.sh b/images/fcrepo6/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..e84270db 100755
--- a/images/fcrepo6/tests/ServiceHealthcheck/test.sh
+++ b/images/fcrepo6/tests/ServiceHealthcheck/test.sh
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
diff --git a/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/docker-compose.yml b/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
index bd543553..8fe1cb51 100644
--- a/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
+++ b/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
@@ -10,9 +10,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/test.sh b/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/test.sh
index f9ef12dd..14dd561f 100755
--- a/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/test.sh
+++ b/images/fcrepo6/tests/ServiceStartsWithBackendMySQL/test.sh
@@ -5,7 +5,9 @@
source /usr/local/share/isle/utilities.sh
function count {
- cat <<-EOF | execute-sql-file.sh --database "fcrepo" - -- -N 2>/dev/null
+ cat <<-EOF | LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" execute-sql-file.sh \
+ --host "${DB_HOST}" --port "${DB_PORT}" --user "${DB_USER}" \
+ --database "${DB_NAME}" - -- -N 2>/dev/null
SELECT COUNT(*) as count FROM containment;
EOF
}
diff --git a/images/fcrepo6/tests/SynAuthentication/docker-compose.yml b/images/fcrepo6/tests/SynAuthentication/docker-compose.yml
index a43596ad..bd3f90e8 100644
--- a/images/fcrepo6/tests/SynAuthentication/docker-compose.yml
+++ b/images/fcrepo6/tests/SynAuthentication/docker-compose.yml
@@ -8,9 +8,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/fcrepo7/Dockerfile b/images/fcrepo7/Dockerfile
index e6cdbfee..e40e21b2 100644
--- a/images/fcrepo7/Dockerfile
+++ b/images/fcrepo7/Dockerfile
@@ -2,12 +2,13 @@ FROM java21 AS syn-builder
ARG TARGETARCH
-ARG SYN_VERSION=fix-tomcat11-release-2x
-ARG SYN_REF=refs/heads/${SYN_VERSION}
+# renovate: datasource=github-releases depName=islandora-syn-fcrepo7 packageName=Islandora/Syn
+ARG SYN_VERSION=2.0.1
+ARG SYN_REF=refs/tags/v${SYN_VERSION}
ARG SYN_REPOSITORY=https://github.com/Islandora/Syn.git
ARG SYN_FILE=islandora-syn-${SYN_VERSION}-all.jar
ARG SYN_RELEASE_URL=https://github.com/Islandora/Syn/releases/download/v${SYN_VERSION}/${SYN_FILE}
-ARG SYN_SHA256=2ee12833f46aab8980e872a255612865323bf22165f86b8f5208018c1775afa5
+ARG SYN_SHA256=dde30978ca103a78d88f98ee0d2e95b767566377b29b3e8b680831fef8662bd8
WORKDIR /build/syn
@@ -72,7 +73,7 @@ RUN --mount=type=cache,id=fcrepo7-downloads-${TARGETARCH},sharing=locked,target=
ENV \
DB_NAME=fcrepo \
- DB_PASSWORD=password \
+ DB_PASSWORD= \
DB_USER=fcrepo \
FCREPO_ACTIVEMQ_BROKER=tcp://activemq:61616 \
FCREPO_ACTIVEMQ_QUEUE=fedora \
@@ -87,4 +88,4 @@ COPY --link rootfs /
RUN chown -R tomcat:tomcat /opt/tomcat
-HEALTHCHECK CMD curl -s http://localhost:8080/fcrepo/rest/fcr:systeminfo | jq -e .version
+HEALTHCHECK CMD curl -fsS http://localhost:8080/fcrepo/rest/fcr:systeminfo | jq -e .version
diff --git a/images/fcrepo7/README.md b/images/fcrepo7/README.md
index 5b5bc53b..7dbafb38 100644
--- a/images/fcrepo7/README.md
+++ b/images/fcrepo7/README.md
@@ -74,23 +74,22 @@ Change `SOFTWARE_VERSION` and then generate the `FCREPO_SHA256` with the followi
commands:
```bash
-SOFTWARE_VERSION=$(cat fcrepo7/Dockerfile | grep -o 'SOFTWARE_VERSION=.*' | cut -f2 -d=)
-FCREPO_FILE=$(cat fcrepo7/Dockerfile | grep -o 'FCREPO_FILE=.*' | cut -f2 -d=)
-FCREPO_URL=$(cat fcrepo7/Dockerfile | grep -o 'FCREPO_URL=.*' | cut -f2 -d=)
-FCREPO_FILE=$(eval "echo $FCREPO_FILE")
-FCREPO_URL=$(eval "echo $FCREPO_URL")
-wget --quiet "${FCREPO_URL}"
-shasum -a 256 "${FCREPO_FILE}" | cut -f1 -d' '
-rm "${FCREPO_FILE}"
+./ci/update-sha.sh fcrepo7 OLD_VERSION NEW_VERSION ''
```
You can change the version used for [syn] by modifying the build arguments
`SYN_VERSION` and `SYN_REF` in the `Dockerfile`.
-By default, the image fetches `refs/heads/${SYN_VERSION}` from the Syn
-repository and builds it with `./gradlew`. To build a tagged Syn release
-artifact instead, set `SYN_VERSION`, `SYN_REF=refs/tags/v${SYN_VERSION}`, and
-`SYN_SHA256` for that tag's release artifact.
+By default, the image downloads the checksum-pinned `v${SYN_VERSION}` release
+artifact. Change `SYN_VERSION` and then generate `SYN_SHA256` with:
+
+```bash
+./ci/update-sha.sh islandora-syn-fcrepo7 OLD_VERSION NEW_VERSION ''
+```
+
+To test an unreleased Syn branch, set `SYN_VERSION` and
+`SYN_REF=refs/heads/${SYN_VERSION}`. The image then builds Syn with `./gradlew`
+instead of downloading a release artifact.
You can change the version used for [fcrepo-import-export] by modifying the
build argument `IMPORT_EXPORT_VERSION` and `IMPORT_EXPORT_SHA256` in the
@@ -100,14 +99,7 @@ Change `IMPORT_EXPORT_VERSION` and then generate the `IMPORT_EXPORT_SHA256` with
the following commands:
```bash
-IMPORT_EXPORT_VERSION=$(cat fcrepo7/Dockerfile | grep -o 'IMPORT_EXPORT_VERSION=.*' | cut -f2 -d=)
-IMPORT_EXPORT_FILE=$(cat fcrepo7/Dockerfile | grep -o 'IMPORT_EXPORT_FILE=.*' | cut -f2 -d=)
-IMPORT_EXPORT_URL=$(cat fcrepo7/Dockerfile | grep -o 'IMPORT_EXPORT_URL=.*' | cut -f2 -d=)
-IMPORT_EXPORT_FILE=$(eval "echo $IMPORT_EXPORT_FILE")
-IMPORT_EXPORT_URL=$(eval "echo $IMPORT_EXPORT_URL")
-wget --quiet "${IMPORT_EXPORT_URL}"
-shasum -a 256 "${IMPORT_EXPORT_FILE}" | cut -f1 -d' '
-rm "${IMPORT_EXPORT_FILE}"
+./ci/update-sha.sh fcrepo-import-export OLD_VERSION NEW_VERSION ''
```
You can change the version used for [fcrepo-upgrade-utils] by modifying the
@@ -118,14 +110,7 @@ Change `UPGRADE_UTILS_VERSION` and then generate the `UPGRADE_UTILS_SHA256` with
the following commands:
```bash
-UPGRADE_UTILS_VERSION=$(cat fcrepo7/Dockerfile | grep -o 'UPGRADE_UTILS_VERSION=.*' | cut -f2 -d=)
-UPGRADE_UTILS_FILE=$(cat fcrepo7/Dockerfile | grep -o 'UPGRADE_UTILS_FILE=.*' | cut -f2 -d=)
-UPGRADE_UTILS_URL=$(cat fcrepo7/Dockerfile | grep -o 'UPGRADE_UTILS_URL=.*' | cut -f2 -d=)
-UPGRADE_UTILS_FILE=$(eval "echo $UPGRADE_UTILS_FILE")
-UPGRADE_UTILS_URL=$(eval "echo $UPGRADE_UTILS_URL")
-wget --quiet "${UPGRADE_UTILS_URL}"
-shasum -a 256 "${UPGRADE_UTILS_FILE}" | cut -f1 -d' '
-rm "${UPGRADE_UTILS_FILE}"
+./ci/update-sha.sh fcrepo-upgrade-utils OLD_VERSION NEW_VERSION ''
```
[base image]: ../base/README.md
diff --git a/images/fcrepo7/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh b/images/fcrepo7/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
index f35ebb00..abaf0528 100755
--- a/images/fcrepo7/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
+++ b/images/fcrepo7/rootfs/etc/s6-overlay/scripts/fcrepo-setup.sh
@@ -2,19 +2,15 @@
# shellcheck shell=bash
set -e
-function mysql_create_database {
- cat <<-EOF | create-database.sh
--- Create fcrepo database in mariadb or mysql.
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8 COLLATE utf8_general_ci;
-
--- Create fcrepo user and grant rights.
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
--- Update fcrepo password if changed.
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}')
-EOF
+function mysql_create_database {
+ DB_CHARACTER_SET=utf8 \
+ DB_COLLATION=utf8_general_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function wait_for_broker {
@@ -32,7 +28,14 @@ function wait_for_broker {
}
function main {
- mysql_create_database
+ require_environment_variables DB_HOST DB_NAME DB_USER DB_PASSWORD
+ if [ "${DB_HOST}" = "mariadb" ]; then
+ database_bootstrap_if_enabled mysql_create_database
+ fi
+ LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" wait-for-database.sh \
+ --host "${DB_HOST}" \
+ --port "${DB_PORT}" \
+ --user "${DB_USER}"
# When bind mounting we need to ensure that we
# actually can write to the folder.
chown tomcat:tomcat /data
diff --git a/images/fcrepo7/tests/ServiceHealthcheck/docker-compose.yml b/images/fcrepo7/tests/ServiceHealthcheck/docker-compose.yml
index 94254798..4413223e 100644
--- a/images/fcrepo7/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/fcrepo7/tests/ServiceHealthcheck/docker-compose.yml
@@ -8,9 +8,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/fcrepo7/tests/ServiceHealthcheck/test.sh b/images/fcrepo7/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..e84270db 100755
--- a/images/fcrepo7/tests/ServiceHealthcheck/test.sh
+++ b/images/fcrepo7/tests/ServiceHealthcheck/test.sh
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
diff --git a/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/docker-compose.yml b/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
index 690dfaea..c390f8cb 100644
--- a/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
+++ b/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/docker-compose.yml
@@ -10,9 +10,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/test.sh b/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/test.sh
index f9ef12dd..14dd561f 100755
--- a/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/test.sh
+++ b/images/fcrepo7/tests/ServiceStartsWithBackendMySQL/test.sh
@@ -5,7 +5,9 @@
source /usr/local/share/isle/utilities.sh
function count {
- cat <<-EOF | execute-sql-file.sh --database "fcrepo" - -- -N 2>/dev/null
+ cat <<-EOF | LIBOPS_DATABASE_PASSWORD="${DB_PASSWORD}" execute-sql-file.sh \
+ --host "${DB_HOST}" --port "${DB_PORT}" --user "${DB_USER}" \
+ --database "${DB_NAME}" - -- -N 2>/dev/null
SELECT COUNT(*) as count FROM containment;
EOF
}
diff --git a/images/fcrepo7/tests/SynAuthentication/docker-compose.yml b/images/fcrepo7/tests/SynAuthentication/docker-compose.yml
index 2bb8e6ff..3048047b 100644
--- a/images/fcrepo7/tests/SynAuthentication/docker-compose.yml
+++ b/images/fcrepo7/tests/SynAuthentication/docker-compose.yml
@@ -8,9 +8,20 @@ services:
activemq:
<<: *common
image: ${ACTIVEMQ:-libops/activemq:local}
+ environment:
+ ACTIVEMQ_PASSWORD: test-broker-password
+ ACTIVEMQ_WEB_ADMIN_PASSWORD: test-web-password
mariadb:
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: fcrepo
+ DB_USER: fcrepo
+ DB_PASSWORD: test-database-password
fcrepo:
+ environment:
+ DB_PASSWORD: test-database-password
+ JWT_ADMIN_TOKEN: islandora
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/islandora/Dockerfile b/images/islandora/Dockerfile
index 37b50092..f3ef28c5 100644
--- a/images/islandora/Dockerfile
+++ b/images/islandora/Dockerfile
@@ -18,4 +18,4 @@ COPY --link rootfs /
RUN mkdir -p /var/www/drupal && \
chown -R nginx:nginx /var/www/drupal
-HEALTHCHECK CMD { [ ! -f /var/www/drupal/web/core/lib/Drupal.php ] || [ -f /installed ]; } && curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD { [ ! -f /var/www/drupal/web/core/lib/Drupal.php ] || [ -f /installed ]; } && curl -fsS http://localhost/status | grep -q pool
diff --git a/images/islandora/README.md b/images/islandora/README.md
index 7c38e2a1..1aaed527 100644
--- a/images/islandora/README.md
+++ b/images/islandora/README.md
@@ -53,7 +53,7 @@ derivative service and `fcrepo` based containers.
| :------------------------------ | :---------------------- | :------------------------------------------------- |
| DRUPAL_DEFAULT_ACCOUNT_EMAIL | webmaster@localhost.com | The email to use for the admin account |
| DRUPAL_DEFAULT_ACCOUNT_NAME | admin | The Drupal administrator user |
-| DRUPAL_DEFAULT_ACCOUNT_PASSWORD | password | The Drupal administrator user password |
+| DRUPAL_DEFAULT_ACCOUNT_PASSWORD | | The Drupal administrator password; required when installing a site |
| DRUPAL_DEFAULT_EMAIL | webmaster@localhost.com | The Drupal administrators email |
| DRUPAL_DEFAULT_LOCALE | en | The Drupal sites locale |
| DRUPAL_DEFAULT_NAME | default | The Drupal sites name |
@@ -62,7 +62,8 @@ derivative service and `fcrepo` based containers.
| DRUPAL_DEFAULT_CONFIGDIR | | Install using existing config files from directory |
| DRUPAL_DEFAULT_INSTALL | true | Perform install if not already installed |
-Provide your own passwords when running in production.
+The image has no administrator password default and fails closed when
+`DRUPAL_DEFAULT_INSTALL=true` without an explicit password.
[base image]: ../base/README.md
[Drupal]: https://www.drupal.org/
diff --git a/images/islandora/rootfs/etc/islandora/utilities.sh b/images/islandora/rootfs/etc/islandora/utilities.sh
index 7c6eb270..fe1a9af5 100755
--- a/images/islandora/rootfs/etc/islandora/utilities.sh
+++ b/images/islandora/rootfs/etc/islandora/utilities.sh
@@ -100,11 +100,10 @@ function execute_sql_file {
port=$(drupal_site_env "${site}" "DB_PORT")
user=$(drupal_site_env "${site}" "DB_ROOT_USER")
password=$(drupal_site_env "${site}" "DB_ROOT_PASSWORD")
- execute-sql-file.sh \
+ LIBOPS_DATABASE_PASSWORD="${password}" execute-sql-file.sh \
--host "${host}" \
--port "${port}" \
--user "${user}" \
- --password "${password}" \
"${@}"
}
@@ -115,16 +114,12 @@ function mysql_query {
db_name=$(drupal_site_env "${site}" "DB_NAME")
db_user=$(drupal_site_env "${site}" "DB_USER")
db_password=$(drupal_site_env "${site}" "DB_PASSWORD")
- cat <<-EOF
--- Create if does not exist.
-CREATE DATABASE IF NOT EXISTS ${db_name} CHARACTER SET utf8 COLLATE utf8_general_ci;
-CREATE USER IF NOT EXISTS ${db_user}@'%' IDENTIFIED BY "${db_password}";
-GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON ${db_name}.* to ${db_user}@'%' IDENTIFIED BY "${db_password}";
-FLUSH PRIVILEGES;
-
--- Update DB_USER password if changed.
-SET PASSWORD FOR ${db_user}@'%' = PASSWORD('${db_password}');
-EOF
+ DB_NAME="${db_name}" \
+ DB_USER="${db_user}" \
+ DB_PASSWORD="${db_password}" \
+ DB_CHARACTER_SET=utf8 \
+ DB_COLLATION=utf8_general_ci \
+ render-database-bootstrap-sql.sh
}
function mysql_create_database {
@@ -209,16 +204,16 @@ function install_site {
echo "USE_EXISTIG_CONFIG: ${use_existing_config_arg}"
echo "EVERYTHING ELSE: $*"
+ LIBOPS_DRUPAL_INSTALL_ACCOUNT_PASSWORD="${account_password}" \
+ LIBOPS_DRUPAL_INSTALL_DB_PASSWORD="${password}" \
/usr/local/bin/install-drupal-site.sh \
--host "${host}" \
--port "${port}" \
--db-user "${user}" \
- --db-password "${password}" \
--db-name "${db_name}" \
"${profile}" \
--account-mail="${account_email}" \
--account-name="${account_name}" \
- --account-pass="${account_password}" \
--site-mail="${site_email}" \
--locale="${site_locale}" \
--site-name="${site_name}" \
diff --git a/images/islandora/rootfs/etc/s6-overlay/scripts/install.d/50-islandora.sh b/images/islandora/rootfs/etc/s6-overlay/scripts/install.d/50-islandora.sh
index bfae00b7..0719ca00 100755
--- a/images/islandora/rootfs/etc/s6-overlay/scripts/install.d/50-islandora.sh
+++ b/images/islandora/rootfs/etc/s6-overlay/scripts/install.d/50-islandora.sh
@@ -2,8 +2,6 @@
# shellcheck shell=bash
set -euo pipefail
-readonly SITE="default"
-
function ingress_primary_hostname {
local hostnames hostname
hostnames="${INGRESS_HOSTNAMES:-localhost}"
diff --git a/images/java25/README.md b/images/java25/README.md
index a326586c..4d5f5275 100644
--- a/images/java25/README.md
+++ b/images/java25/README.md
@@ -1,8 +1,8 @@
-# Java 21
+# Java 25
-Docker image for [Java] OpenJDK version 21.
+Docker image for [Java] OpenJDK version 25.
-Built from [libops/isle-buildkit java21](https://github.com/libops/buildkit/tree/main/images/java21)
+Built from [libops/buildkit java25](https://github.com/libops/buildkit/tree/main/images/java25)
Please refer to the [Java Documentation] for more in-depth information.
diff --git a/images/mariadb11/README.md b/images/mariadb11/README.md
index eec5cd60..c600cfd4 100644
--- a/images/mariadb11/README.md
+++ b/images/mariadb11/README.md
@@ -1,17 +1,20 @@
# MariaDB 11
-Docker image for [MariaDB] version 11.4.12
+Docker image for [MariaDB] version 11.8.8.
Built from [libops/isle-buildkit mariadb11](https://github.com/libops/buildkit/tree/main/images/mariadb11)
Please refer to the [MariaDB Documentation] for more in-depth information.
-As a quick example this will bring up an instance of MariaDB, and allow you to
-log in with client as the user `root` with the password `password`.
+As a quick example this will generate a root password, bring up MariaDB, and
+connect as `root` using that generated credential.
```bash
-docker run --rm -d -name mariadb libops/mariadb
-docker exec -ti mariadb mysql -u root --password='password'
+export DB_ROOT_PASSWORD="$(openssl rand -hex 32)"
+docker run --rm -d --name mariadb \
+ --env DB_ROOT_PASSWORD \
+ libops/mariadb:11
+docker exec -ti --env MYSQL_PWD="${DB_ROOT_PASSWORD}" mariadb mariadb -u root
```
## Dependencies
@@ -41,8 +44,11 @@ default database connection configuration.
| Environment Variable | Default | Description |
| :------------------- | :------ | :------------------------------------------------------------------------------------ |
-| MYSQL_ROOT_PASSWORD | | The database root user password. Defaults to `DB_ROOT_PASSWORD` |
-| MYSQL_ROOT_USER | | The database root user (used to create the site database). Defaults to `DB_ROOT_USER` |
+| DB_ROOT_PASSWORD | | The database root password; required at startup |
+| DB_ROOT_USER | root | The database root user |
+| DB_NAME | default | Optional scoped database to create when `DB_PASSWORD` is set |
+| DB_USER | default | Optional scoped database user to create when `DB_PASSWORD` is set |
+| DB_PASSWORD | | Scoped user password; leave empty when provisioning through a separate initializer |
| MYSQL_MAX_ALLOWED_PACKET | 16777216 | Max packet length to send to or receive from the server, [documentation](https://mariadb.com/docs/server/ref/mdb/system-variables/max_allowed_packet/)
| MYSQL_TRANSACTION_ISOLATION | READ-COMMITTED | The isolation level for transactions.
diff --git a/images/mariadb11/rootfs/etc/s6-overlay/s6-rc.d/mysqld/run b/images/mariadb11/rootfs/etc/s6-overlay/s6-rc.d/mysqld/run
index 8a29d178..b705d4ef 100755
--- a/images/mariadb11/rootfs/etc/s6-overlay/s6-rc.d/mysqld/run
+++ b/images/mariadb11/rootfs/etc/s6-overlay/s6-rc.d/mysqld/run
@@ -1,3 +1,4 @@
#!/usr/bin/env bash
set -e
+unset DB_ROOT_PASSWORD
exec s6-setuidgid mysql /usr/bin/mariadbd
diff --git a/images/mariadb11/rootfs/etc/s6-overlay/scripts/mysql-setup.sh b/images/mariadb11/rootfs/etc/s6-overlay/scripts/mysql-setup.sh
index 9899706e..72bf06bb 100755
--- a/images/mariadb11/rootfs/etc/s6-overlay/scripts/mysql-setup.sh
+++ b/images/mariadb11/rootfs/etc/s6-overlay/scripts/mysql-setup.sh
@@ -2,6 +2,23 @@
# shellcheck shell=bash
set -e
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
+# Never initialize a network-accessible root account with empty credentials.
+database_require_root_credentials
+
+# A scoped database can be provisioned by the database container for isolated
+# tests or simple deployments. Production templates use a dedicated one-shot
+# initializer and therefore do not pass DB_PASSWORD to this service.
+bootstrap_sql=
+if [ -n "${DB_PASSWORD:-}" ]; then
+ require_environment_variables DB_NAME DB_USER
+ bootstrap_sql=$(render-database-bootstrap-sql.sh)
+fi
+
# Make run directory if it does not exist.
mkdir /run/mysqld &>/dev/null || true
chown mysql:mysql /run/mysqld
@@ -22,12 +39,16 @@ done
# Change the root users password.
echo "Changing the root users (${DB_ROOT_USER}) password."
+database_validate_identifier DB_ROOT_USER "${DB_ROOT_USER}"
+db_root_user_sql=$(database_escape_sql_literal "${DB_ROOT_USER}")
+db_root_password_sql=$(database_escape_sql_literal "${DB_ROOT_PASSWORD}")
cat <<-EOF | mariadb --no-defaults --protocol=socket --user="${DB_ROOT_USER}"
- CREATE USER IF NOT EXISTS '${DB_ROOT_USER}'@'%';
- GRANT ALL PRIVILEGES ON *.* TO '${DB_ROOT_USER}'@'%' WITH GRANT OPTION;
- SET PASSWORD FOR '${DB_ROOT_USER}'@'localhost' = PASSWORD('${DB_ROOT_PASSWORD}');
- SET PASSWORD FOR '${DB_ROOT_USER}'@'%' = PASSWORD('${DB_ROOT_PASSWORD}');
+ CREATE USER IF NOT EXISTS '${db_root_user_sql}'@'%';
+ GRANT ALL PRIVILEGES ON *.* TO '${db_root_user_sql}'@'%' WITH GRANT OPTION;
+ SET PASSWORD FOR '${db_root_user_sql}'@'localhost' = PASSWORD('${db_root_password_sql}');
+ SET PASSWORD FOR '${db_root_user_sql}'@'%' = PASSWORD('${db_root_password_sql}');
FLUSH PRIVILEGES;
+ ${bootstrap_sql}
EOF
# Stop the database.
diff --git a/images/mariadb11/tests/RejectsEmptyRootPassword/docker-compose.yml b/images/mariadb11/tests/RejectsEmptyRootPassword/docker-compose.yml
new file mode 100644
index 00000000..d768c432
--- /dev/null
+++ b/images/mariadb11/tests/RejectsEmptyRootPassword/docker-compose.yml
@@ -0,0 +1,7 @@
+---
+name: mariadb11-rejectsemptyrootpassword
+services:
+ mariadb:
+ image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: ""
diff --git a/images/mariadb11/tests/ServiceHealthcheck/docker-compose.yml b/images/mariadb11/tests/ServiceHealthcheck/docker-compose.yml
index 37a1e4ea..4b792668 100644
--- a/images/mariadb11/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/mariadb11/tests/ServiceHealthcheck/docker-compose.yml
@@ -8,6 +8,8 @@ services:
mariadb:
<<: *common
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/mariadb11/tests/ServiceHealthcheck/test.sh b/images/mariadb11/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..e84270db 100755
--- a/images/mariadb11/tests/ServiceHealthcheck/test.sh
+++ b/images/mariadb11/tests/ServiceHealthcheck/test.sh
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
diff --git a/images/mariadb11/tests/ServiceStartsWithDefaults/docker-compose.yml b/images/mariadb11/tests/ServiceStartsWithDefaults/docker-compose.yml
index 485d12e0..cc527808 100644
--- a/images/mariadb11/tests/ServiceStartsWithDefaults/docker-compose.yml
+++ b/images/mariadb11/tests/ServiceStartsWithDefaults/docker-compose.yml
@@ -8,6 +8,8 @@ services:
mariadb:
<<: *common
image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
volumes:
- ./test.sh:/test.sh # Test to run.
command:
diff --git a/images/nginx/Dockerfile b/images/nginx/Dockerfile
index c3522b99..1f6da414 100644
--- a/images/nginx/Dockerfile
+++ b/images/nginx/Dockerfile
@@ -43,4 +43,4 @@ ENV \
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD curl -fsS http://localhost/status | grep -q pool
diff --git a/images/nginx/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/images/nginx/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
index 65719fcb..bccb8b44 100755
--- a/images/nginx/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
+++ b/images/nginx/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
@@ -1,3 +1,6 @@
#!/usr/bin/env bash
set -e
+unset DB_ROOT_PASSWORD
+unset DRUPAL_DEFAULT_ACCOUNT_PASSWORD OJS_ADMIN_PASSWORD \
+ OMEKA_CLASSIC_ADMIN_PASSWORD OMEKA_S_ADMIN_PASSWORD WORDPRESS_ADMIN_PASSWORD
exec s6-notifyoncheck -t 30000 -n 1 /usr/sbin/nginx
diff --git a/images/ojs/Dockerfile b/images/ojs/Dockerfile
index 184c38b6..cad02818 100644
--- a/images/ojs/Dockerfile
+++ b/images/ojs/Dockerfile
@@ -2,6 +2,8 @@
ARG PHP_BASE=nginx-php83
FROM ${PHP_BASE}
+LABEL org.opencontainers.image.licenses="GPL-3.0-only"
+
ARG TARGETARCH
ARG PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock
ARG PHP_PACKAGE=php83
@@ -14,11 +16,19 @@ ARG \
# renovate: datasource=repology depName=alpine_3_24/npm
NPM_VERSION=11.12.1-r0 \
# renovate: datasource=repology depName=alpine_3_24/poppler-utils
- POPPLER_VERSION=25.12.0-r1
+ POPPLER_VERSION=25.12.0-r1 \
+ # renovate: datasource=custom.ojs depName=ojs
+ SOFTWARE_VERSION=3.5.0-5
+
+ARG FILE=ojs-${SOFTWARE_VERSION}.tar.gz
+ARG URL=https://pkp.sfu.ca/ojs/download/${FILE}
+ARG SHA256="fd59cb1add60ab4e56e40ed843ea43ff84af11f0b64c3940f2e03186ad11445e"
EXPOSE 80
WORKDIR /var/www/ojs
+COPY --link patches /opt/ojs-patches
+
RUN --mount=type=cache,id=ojs-apk-${TARGETARCH},sharing=locked,target=/var/cache/apk \
apk add \
antiword=="${ANTIWORD_VERSION}" \
@@ -29,7 +39,33 @@ RUN --mount=type=cache,id=ojs-apk-${TARGETARCH},sharing=locked,target=/var/cache
"${PHP_PACKAGE}-ftp" \
"${PHP_PACKAGE}-gettext" \
&& \
+ cleanup.sh
+
+# Fresh OJS installations normally fetch mutable ROR and IP geolocation datasets
+# while creating the database. Defer those network-backed tasks to OJS's scheduler
+# so first boot remains bounded and can run without outbound network access. Verify
+# the pinned release still contains exactly the two expected actions before changing
+# its install descriptor; upgrades use a separate descriptor and are unaffected.
+RUN --mount=type=cache,id=ojs-downloads-${TARGETARCH},sharing=locked,target=/opt/downloads \
+ download.sh \
+ --url "${URL}" \
+ --sha256 "${SHA256}" \
+ --strip \
+ --dest /var/www/ojs \
+ && \
+ test "$(grep -Fc '' dbscripts/xml/install.xml)" -eq 1 && \
+ test "$(grep -Fc '' dbscripts/xml/install.xml)" -eq 1 && \
+ patch --forward --strip=1 --input=/opt/ojs-patches/install-cli-database-port.patch && \
+ sed -i \
+ -e '//d' \
+ -e '//d' \
+ dbscripts/xml/install.xml \
+ && \
+ mkdir -p /usr/share/licenses/ojs && \
+ cp docs/COPYING /usr/share/licenses/ojs/COPYING && \
+ rm -rf .github tests docs /opt/ojs-patches && \
mkdir -p /var/www/files /var/www/ojs/cache /var/www/ojs/public && \
+ touch /var/www/ojs/opcache_stat.php && \
chown -R nginx:nginx /var/www/ojs /var/www/files && \
cleanup.sh
@@ -38,13 +74,14 @@ ENV \
DB_PORT=3306 \
DB_NAME=ojs \
DB_USER=ojs \
- DB_PASSWORD=changeme \
- OJS_SALT=changeme \
- OJS_API_KEY_SECRET=changeme \
- OJS_SECRET_KEY=changeme \
+ DB_PASSWORD= \
+ OJS_SALT= \
+ OJS_API_KEY_SECRET= \
+ OJS_SECRET_KEY= \
OJS_ADMIN_USERNAME=admin \
OJS_ADMIN_EMAIL=admin@localhost \
- OJS_ADMIN_PASSWORD=changeme \
+ OJS_ADMIN_PASSWORD= \
+ OJS_INSTALLED=Off \
OJS_LOCALE=en \
OJS_TIMEZONE=UTC \
OJS_FILES_DIR=/var/www/files \
@@ -86,4 +123,4 @@ ENV \
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD test -f /installed && test -f /var/www/ojs/index.php && curl -fsS http://localhost/status | grep -q pool
diff --git a/images/ojs/patches/install-cli-database-port.patch b/images/ojs/patches/install-cli-database-port.patch
new file mode 100644
index 00000000..3e7dfe9f
--- /dev/null
+++ b/images/ojs/patches/install-cli-database-port.patch
@@ -0,0 +1,10 @@
+--- a/lib/pkp/classes/cliTool/InstallTool.php
++++ b/lib/pkp/classes/cliTool/InstallTool.php
+@@ -108,6 +108,7 @@
+ $this->printTitle('installer.databaseSettings');
+ $this->readParamOptions('databaseDriver', 'installer.databaseDriver', $installForm->getDatabaseDriversOptions());
+ $this->readParam('databaseHost', 'installer.databaseHost', '');
++ $this->params['databasePort'] = getenv('DB_PORT') ?: 3306;
+ $this->readParam('databaseUsername', 'installer.databaseUsername', '');
+ $this->readParam('databasePassword', 'installer.databasePassword', '');
+ $this->readParam('databaseName', 'installer.databaseName');
diff --git a/images/ojs/rootfs/etc/confd/templates/config.inc.tmpl b/images/ojs/rootfs/etc/confd/templates/config.inc.tmpl
index 04a0f01f..260d822f 100644
--- a/images/ojs/rootfs/etc/confd/templates/config.inc.tmpl
+++ b/images/ojs/rootfs/etc/confd/templates/config.inc.tmpl
@@ -32,7 +32,7 @@ app_key = "{{ template "doubleQuoteEscape" (getenv "OJS_SECRET_KEY") }}"
; Set this to On once the system has been installed
; (This is generally done automatically by the installer)
-installed = Off
+installed = {{ template "booleanOnOff" (getenv "OJS_INSTALLED") }}
; The canonical URL to the OJS installation (excluding the trailing slash)
{{ $base_url_ingress_hostnames := replace (getenv "INGRESS_HOSTNAMES") " " "" -1 }}
@@ -279,10 +279,10 @@ session_check_ip = On
encryption = sha1
; The unique salt to use for generating password reset hashes
-salt = "{{ getenv "OJS_SALT" }}"
+salt = "{{ template "doubleQuoteEscape" (getenv "OJS_SALT") }}"
; The unique secret used for encoding and decoding API keys
-api_key_secret = "{{ getenv "OJS_API_KEY_SECRET" }}"
+api_key_secret = "{{ template "doubleQuoteEscape" (getenv "OJS_API_KEY_SECRET") }}"
; The number of seconds before a password reset hash expires (defaults to 7200 / 2 hours)
reset_seconds = 7200
diff --git a/images/ojs/rootfs/etc/cont-init.d/01-ojs-permissions b/images/ojs/rootfs/etc/cont-init.d/01-ojs-permissions
index b562a2ef..ce5906ae 100755
--- a/images/ojs/rootfs/etc/cont-init.d/01-ojs-permissions
+++ b/images/ojs/rootfs/etc/cont-init.d/01-ojs-permissions
@@ -1,4 +1,5 @@
#!/command/with-contenv bash
+# shellcheck shell=bash
set -euo pipefail
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd-oneshot/dependencies.d/ojs-install-state b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd-oneshot/dependencies.d/ojs-install-state
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd-oneshot/dependencies.d/ojs-install-state
@@ -0,0 +1 @@
+
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd/dependencies.d/ojs-setup b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd/dependencies.d/ojs-setup
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd/dependencies.d/ojs-setup
@@ -0,0 +1 @@
+
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/dependencies.d/container-environment b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/dependencies.d/container-environment
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/dependencies.d/container-environment
@@ -0,0 +1 @@
+
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/type b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/type
new file mode 100644
index 00000000..bdd22a18
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/type
@@ -0,0 +1 @@
+oneshot
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/up b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/up
new file mode 100644
index 00000000..b86ae318
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-install-state/up
@@ -0,0 +1 @@
+/etc/s6-overlay/scripts/ojs-install-state.sh
diff --git a/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-setup/dependencies.d/ready b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-setup/dependencies.d/ready
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/s6-rc.d/ojs-setup/dependencies.d/ready
@@ -0,0 +1 @@
+
diff --git a/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-install-state.sh b/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-install-state.sh
new file mode 100755
index 00000000..42de37a5
--- /dev/null
+++ b/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-install-state.sh
@@ -0,0 +1,15 @@
+#!/command/with-contenv bash
+# shellcheck shell=bash
+
+set -euo pipefail
+
+state=Off
+if [ -f /installed ]; then
+ state=On
+fi
+
+environment_dir=/var/run/s6/container_environment
+umask 077
+mkdir -p "${environment_dir}"
+printf '%s' "${state}" >"${environment_dir}/OJS_INSTALLED"
+chmod 0600 "${environment_dir}/OJS_INSTALLED"
diff --git a/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-setup.sh b/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-setup.sh
index ebc62727..a1cc54f5 100755
--- a/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-setup.sh
+++ b/images/ojs/rootfs/etc/s6-overlay/scripts/ojs-setup.sh
@@ -3,22 +3,22 @@
set -eou pipefail
-function mysql_create_database {
- cat <<-EOF | create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8 COLLATE utf8_general_ci;
-
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
+# shellcheck source=images/base/rootfs/usr/local/share/libops/database.sh
+source "${LIBOPS_DATABASE_LIBRARY:-/usr/local/share/libops/database.sh}"
+# shellcheck source=images/base/rootfs/usr/local/share/libops/environment.sh
+source "${LIBOPS_ENVIRONMENT_LIBRARY:-/usr/local/share/libops/environment.sh}"
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}');
-EOF
+function mysql_create_database {
+ DB_CHARACTER_SET=utf8 \
+ DB_COLLATION=utf8_general_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function set_ojs_installed {
- sed -i 's/^installed = .*/installed = On/' /var/www/ojs/config.inc.php
- chmod 440 /var/www/ojs/config.inc.php
touch /installed
+ /etc/s6-overlay/scripts/ojs-install-state.sh
+ export OJS_INSTALLED=On
+ render_ojs_config
}
function fix_ojs_writable_permissions {
@@ -35,16 +35,62 @@ function render_ojs_config {
/etc/s6-overlay/scripts/confd-oneshot.sh
}
+function validate_ojs_app_key {
+ local encoded_key key_length
+
+ if [[ "${OJS_SECRET_KEY}" == base64:* ]]; then
+ encoded_key=${OJS_SECRET_KEY#base64:}
+ if [[ ! "${encoded_key}" =~ ^[A-Za-z0-9+/]{43}=$ ]] ||
+ ! key_length=$(printf '%s' "${encoded_key}" | openssl base64 -d -A 2>/dev/null | wc -c); then
+ echo "OJS_SECRET_KEY is not valid base64" >&2
+ return 1
+ fi
+ else
+ key_length=${#OJS_SECRET_KEY}
+ fi
+
+ if [ "${key_length}" -ne 32 ]; then
+ echo "OJS_SECRET_KEY must contain exactly 32 bytes for the default aes-256-cbc cipher" >&2
+ return 1
+ fi
+}
+
+function wait_for_database {
+ local attempts=0
+ until database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute='SELECT 1' \
+ >/dev/null 2>&1; do
+ attempts=$((attempts + 1))
+ if [ "${attempts}" -ge 60 ]; then
+ echo "Database was not ready in time: ${DB_HOST}:${DB_PORT}" >&2
+ return 1
+ fi
+ sleep 2
+ done
+}
+
function check_ojs_installed {
- mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
- -e "SELECT 1 FROM versions WHERE current = 1 AND product_type = 'core' AND product = 'ojs2' LIMIT 1" &>/dev/null
+ database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute="SELECT 1 FROM versions WHERE current = 1 AND product_type = 'core' AND product = 'ojs2' LIMIT 1" \
+ &>/dev/null
return $?
}
function install_ojs {
echo "OJS not installed. Running installation..."
- local enable_beacon=n
+ local enable_beacon=n install_log install_success=
+ umask 077
+ install_log=$(mktemp -t ojs-install.XXXXXXXXXX)
+ trap 'rm -f -- "${install_log}"' EXIT
case "${OJS_ENABLE_BEACON}" in
1|[Oo][Nn]|[Tt][Rr][Uu][Ee]|[Yy]*)
enable_beacon=y
@@ -68,49 +114,60 @@ function install_ojs {
printf '%s\n' "${OJS_OAI_REPOSITORY_ID}"
printf '%s\n' "${enable_beacon}"
printf 'y\n'
- } | php /var/www/ojs/tools/install.php > /tmp/ojs-install.log 2>&1 && install_success=true || install_success=
+ } | php /var/www/ojs/tools/install.php >"${install_log}" 2>&1 && install_success=true || install_success=
- if [ -n "${install_success}" ] && grep -q "Successfully installed version" /tmp/ojs-install.log && check_ojs_installed; then
+ if [ -n "${install_success}" ] && grep -q "Successfully installed version" "${install_log}" && check_ojs_installed; then
echo "=========================================="
echo "OJS Installation Complete!"
echo "=========================================="
- rm /tmp/ojs-install.log
- render_ojs_config
+ rm -f -- "${install_log}"
+ trap - EXIT
set_ojs_installed
fix_ojs_writable_permissions
else
echo "=========================================="
echo "OJS Installation Failed!"
echo "=========================================="
- cat /tmp/ojs-install.log
+ cat "${install_log}"
echo "=========================================="
exit 1
fi
}
+function setup_ojs_database {
+ if [ "${DB_HOST}" = "mariadb" ]; then
+ database_bootstrap_if_enabled mysql_create_database
+ fi
+ wait_for_database
+ if check_ojs_installed; then
+ echo "OJS already installed. Skipping installation."
+ set_ojs_installed
+ fix_ojs_writable_permissions
+ return 0
+ fi
+ require_environment_variables OJS_ADMIN_PASSWORD
+ install_ojs
+}
+
function main {
if [ ! -f /var/www/ojs/index.php ]; then
echo "OJS application files are not present. Skipping OJS setup."
return 0
fi
+ require_environment_variables \
+ DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD \
+ OJS_SALT OJS_API_KEY_SECRET OJS_SECRET_KEY
+ validate_ojs_app_key
+
# wait for nginx
if ! timeout 300 wait-for-open-port.sh localhost 80; then
echo "Could not connect to nginx at localhost:80"
exit 1
fi
- if [ "${DB_HOST}" = "mariadb" ]; then
- mysql_create_database
- if check_ojs_installed; then
- echo "OJS already installed. Skipping installation."
- set_ojs_installed
- fix_ojs_writable_permissions
- exit 0
- fi
- install_ojs
- fi
-
- set_ojs_installed
- fix_ojs_writable_permissions
+ setup_ojs_database
}
-main
+
+if [ "${OJS_SETUP_LIBRARY_ONLY:-false}" != "true" ]; then
+ main
+fi
diff --git a/images/ojs/tests/ContinuousConfdPreservesInstalledState/docker-compose.yml b/images/ojs/tests/ContinuousConfdPreservesInstalledState/docker-compose.yml
new file mode 100644
index 00000000..707a4485
--- /dev/null
+++ b/images/ojs/tests/ContinuousConfdPreservesInstalledState/docker-compose.yml
@@ -0,0 +1,50 @@
+---
+name: ojs-continuousconfdpreservesinstalledstate
+services:
+ ojs:
+ image: ${OJS:-libops/ojs:local-php83}
+ environment:
+ CONFD_ENABLE_SERVICE: "true"
+ CONFD_POLLING_INTERVAL: 1
+ DB_HOST: database
+ DB_PASSWORD: 'p"a\ss''word&+='
+ OJS_SALT: test-salt
+ OJS_API_KEY_SECRET: test-api-key-secret
+ OJS_SECRET_KEY: base64:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=
+ OJS_ADMIN_PASSWORD: 'Adm&in+=p@ss'
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -euo pipefail
+ for attempt in $$(seq 1 300); do
+ if [ -f /installed ] && grep -qx 'installed = On' /var/www/ojs/config.inc.php; then
+ break
+ fi
+ sleep 1
+ done
+ test -f /installed
+ grep -qx 'installed = On' /var/www/ojs/config.inc.php
+ test "$$(cat /var/run/s6/container_environment/OJS_INSTALLED)" = On
+ test "$$(stat -c '%a' /var/run/s6/container_environment/OJS_INSTALLED)" = 600
+ test "$$(stat -c '%a' /var/www/ojs/config.inc.php)" = 640
+ curl -fsS http://localhost/status | grep -q pool
+ curl -fsSI http://localhost/ >/dev/null
+ for attempt in $$(seq 1 10); do
+ sleep 1
+ grep -qx 'installed = On' /var/www/ojs/config.inc.php
+ done
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ mariadb:
+ image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: ojs
+ DB_USER: ojs
+ DB_PASSWORD: 'p"a\ss''word&+='
+ networks:
+ default:
+ aliases:
+ - database
diff --git a/images/ojs/tests/ServiceHealthcheck/docker-compose.yml b/images/ojs/tests/ServiceHealthcheck/docker-compose.yml
index 0a1031c5..6888ea03 100644
--- a/images/ojs/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/ojs/tests/ServiceHealthcheck/docker-compose.yml
@@ -3,3 +3,24 @@ name: ojs-servicehealthcheck
services:
ojs:
image: ${OJS:-libops/ojs:local-php83}
+ environment:
+ DB_HOST: database
+ DB_PASSWORD: 'p"a\ss''word&+='
+ OJS_SALT: test-salt
+ OJS_API_KEY_SECRET: test-api-key-secret
+ OJS_SECRET_KEY: base64:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=
+ OJS_ADMIN_PASSWORD: 'Adm&in+=p@ss'
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ mariadb:
+ image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: ojs
+ DB_USER: ojs
+ DB_PASSWORD: 'p"a\ss''word&+='
+ networks:
+ default:
+ aliases:
+ - database
diff --git a/images/omeka-classic/Dockerfile b/images/omeka-classic/Dockerfile
index c97b5f25..d47c73a1 100644
--- a/images/omeka-classic/Dockerfile
+++ b/images/omeka-classic/Dockerfile
@@ -2,6 +2,8 @@
ARG PHP_BASE=nginx-php83
FROM ${PHP_BASE}
+LABEL org.opencontainers.image.licenses="GPL-3.0-or-later"
+
ARG TARGETARCH
ARG PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock
ARG PHP_PACKAGE=php83
@@ -10,7 +12,15 @@ ARG \
# renovate: datasource=repology depName=alpine_3_24/imagemagick
IMAGEMAGICK_VERSION=7.1.2.24-r0 \
# renovate: datasource=repology depName=alpine_3_24/msmtp
- MSMTP_VERSION=1.8.32-r1
+ MSMTP_VERSION=1.8.32-r1 \
+ # renovate: datasource=repology depName=alpine_3_24/unzip
+ UNZIP_VERSION=6.0-r16 \
+ # renovate: datasource=github-releases depName=omeka-classic packageName=omeka/Omeka
+ SOFTWARE_VERSION=3.2.1
+
+ARG FILE=omeka-${SOFTWARE_VERSION}.zip
+ARG URL=https://github.com/omeka/Omeka/releases/download/v${SOFTWARE_VERSION}/${FILE}
+ARG SHA256="2cb4d65511321cc5c009cb61516d9ed97378a800fc4a26eb46450c3c4ca230c2"
EXPOSE 80
WORKDIR /var/www/omeka-classic
@@ -19,9 +29,20 @@ RUN --mount=type=cache,id=omeka-classic-apk-${TARGETARCH},sharing=locked,target=
apk add \
imagemagick=="${IMAGEMAGICK_VERSION}" \
msmtp=="${MSMTP_VERSION}" \
+ unzip=="${UNZIP_VERSION}" \
"${PHP_PACKAGE}-exif" \
&& \
- mkdir -p /var/www/omeka-classic/files && \
+ cleanup.sh
+
+RUN --mount=type=cache,id=omeka-classic-downloads-${TARGETARCH},sharing=locked,target=/opt/downloads \
+ download.sh \
+ --url "${URL}" \
+ --sha256 "${SHA256}" \
+ --strip \
+ --dest /var/www/omeka-classic \
+ && \
+ mkdir -p /usr/share/licenses/omeka-classic && \
+ cp /var/www/omeka-classic/license.txt /usr/share/licenses/omeka-classic/license.txt && \
chown -R nginx:nginx /var/www/omeka-classic && \
cleanup.sh
@@ -30,10 +51,10 @@ ENV \
DB_PORT=3306 \
DB_NAME=omeka_classic \
DB_USER=omeka_classic \
- DB_PASSWORD=changeme \
+ DB_PASSWORD= \
OMEKA_CLASSIC_ADMIN_USERNAME=admin \
OMEKA_CLASSIC_ADMIN_EMAIL=admin@example.com \
- OMEKA_CLASSIC_ADMIN_PASSWORD=changeme \
+ OMEKA_CLASSIC_ADMIN_PASSWORD= \
OMEKA_CLASSIC_SITE_TITLE="Omeka Classic" \
OMEKA_CLASSIC_TABLE_PREFIX=omeka_ \
INGRESS_HOSTNAMES=localhost \
@@ -65,8 +86,9 @@ ENV \
PHP_MAX_INPUT_TIME=300 \
PHP_DEFAULT_SOCKET_TIMEOUT=300 \
PHP_REQUEST_TERMINATE_TIMEOUT=300 \
- PHP_MEMORY_LIMIT=256M
+ PHP_MEMORY_LIMIT=256M \
+ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=600000
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD test -f /installed && test -f /var/www/omeka-classic/index.php && curl -fsS http://localhost/status | grep -q pool
diff --git a/images/omeka-classic/rootfs/etc/confd/templates/db.ini.tmpl b/images/omeka-classic/rootfs/etc/confd/templates/db.ini.tmpl
index 6a02cd0f..0107a254 100644
--- a/images/omeka-classic/rootfs/etc/confd/templates/db.ini.tmpl
+++ b/images/omeka-classic/rootfs/etc/confd/templates/db.ini.tmpl
@@ -1,9 +1,9 @@
+{{- define "doubleQuoteEscape" -}}{{- $escaped := replace . "\\" "\\\\" -1 -}}{{- replace $escaped "\"" "\\\"" -1 -}}{{- end -}}
[database]
-host = "{{ getenv "DB_HOST" }}"
-username = "{{ getenv "DB_USER" }}"
-password = "{{ getenv "DB_PASSWORD" }}"
-dbname = "{{ getenv "DB_NAME" }}"
-prefix = "{{ getenv "OMEKA_CLASSIC_TABLE_PREFIX" }}"
+host = "{{ template "doubleQuoteEscape" (getenv "DB_HOST") }}"
+username = "{{ template "doubleQuoteEscape" (getenv "DB_USER") }}"
+password = "{{ template "doubleQuoteEscape" (getenv "DB_PASSWORD") }}"
+dbname = "{{ template "doubleQuoteEscape" (getenv "DB_NAME") }}"
+prefix = "{{ template "doubleQuoteEscape" (getenv "OMEKA_CLASSIC_TABLE_PREFIX") }}"
charset = "utf8mb4"
-port = "{{ getenv "DB_PORT" }}"
-
+port = "{{ template "doubleQuoteEscape" (getenv "DB_PORT") }}"
diff --git a/images/omeka-classic/rootfs/etc/s6-overlay/s6-rc.d/omeka-classic-setup/dependencies.d/ready b/images/omeka-classic/rootfs/etc/s6-overlay/s6-rc.d/omeka-classic-setup/dependencies.d/ready
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/omeka-classic/rootfs/etc/s6-overlay/s6-rc.d/omeka-classic-setup/dependencies.d/ready
@@ -0,0 +1 @@
+
diff --git a/images/omeka-classic/rootfs/etc/s6-overlay/scripts/omeka-classic-setup.sh b/images/omeka-classic/rootfs/etc/s6-overlay/scripts/omeka-classic-setup.sh
index 8a1aa800..5029645f 100755
--- a/images/omeka-classic/rootfs/etc/s6-overlay/scripts/omeka-classic-setup.sh
+++ b/images/omeka-classic/rootfs/etc/s6-overlay/scripts/omeka-classic-setup.sh
@@ -3,19 +3,26 @@
set -eou pipefail
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
function mysql_create_database {
- cat <<-SQL | create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}');
-SQL
+ DB_CHARACTER_SET=utf8mb4 \
+ DB_COLLATION=utf8mb4_unicode_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function wait_for_database {
local attempts=0
- until mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" -e 'SELECT 1' >/dev/null 2>&1; do
+ until database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute='SELECT 1' \
+ >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 60 ]; then
echo "Database was not ready in time"
@@ -26,8 +33,14 @@ function wait_for_database {
}
function check_installed {
- mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
- -e "SELECT 1 FROM ${OMEKA_CLASSIC_TABLE_PREFIX}options LIMIT 1" >/dev/null 2>&1
+ database_validate_identifier OMEKA_CLASSIC_TABLE_PREFIX "${OMEKA_CLASSIC_TABLE_PREFIX}"
+ database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute="SELECT 1 FROM ${OMEKA_CLASSIC_TABLE_PREFIX}options LIMIT 1" \
+ >/dev/null 2>&1
}
function install_omeka {
@@ -36,31 +49,59 @@ function install_omeka {
return 0
fi
+ require_environment_variables OMEKA_CLASSIC_ADMIN_PASSWORD
+
timeout 300 wait-for-open-port.sh localhost 80
+
+ local form_dir install_log curl_status=0
+ umask 077
+ form_dir=$(mktemp -d)
+ install_log=$(mktemp -t omeka-classic-install.XXXXXXXXXX)
+ trap 'rm -rf -- "${form_dir}" "${install_log}"' EXIT
+ printf '%s' "${OMEKA_CLASSIC_ADMIN_USERNAME}" >"${form_dir}/admin-username"
+ printf '%s' "${OMEKA_CLASSIC_ADMIN_PASSWORD}" >"${form_dir}/admin-password"
+ printf '%s' "${OMEKA_CLASSIC_ADMIN_EMAIL}" >"${form_dir}/admin-email"
+ printf '%s' "${OMEKA_CLASSIC_SITE_TITLE}" >"${form_dir}/site-title"
+ printf '%s' , >"${form_dir}/tag-delimiter"
+ printf '%s' 800 >"${form_dir}/fullsize-constraint"
+ printf '%s' 200 >"${form_dir}/thumbnail-constraint"
+ printf '%s' 200 >"${form_dir}/square-thumbnail-constraint"
+ printf '%s' 10 >"${form_dir}/per-page-admin"
+ printf '%s' 10 >"${form_dir}/per-page-public"
+ printf '%s' /usr/bin >"${form_dir}/path-to-convert"
+ printf '%s' Install >"${form_dir}/install-submit"
+
curl -fsS \
- --data-urlencode "username=${OMEKA_CLASSIC_ADMIN_USERNAME}" \
- --data-urlencode "password=${OMEKA_CLASSIC_ADMIN_PASSWORD}" \
- --data-urlencode "password_confirm=${OMEKA_CLASSIC_ADMIN_PASSWORD}" \
- --data-urlencode "super_email=${OMEKA_CLASSIC_ADMIN_EMAIL}" \
- --data-urlencode "administrator_email=${OMEKA_CLASSIC_ADMIN_EMAIL}" \
- --data-urlencode "site_title=${OMEKA_CLASSIC_SITE_TITLE}" \
- --data-urlencode "tag_delimiter=," \
- --data-urlencode "fullsize_constraint=800" \
- --data-urlencode "thumbnail_constraint=200" \
- --data-urlencode "square_thumbnail_constraint=200" \
- --data-urlencode "per_page_admin=10" \
- --data-urlencode "per_page_public=10" \
- --data-urlencode "path_to_convert=/usr/bin" \
- --data-urlencode "install_submit=Install" \
- http://localhost/install/install.php >/tmp/omeka-classic-install.log 2>&1 || {
- cat /tmp/omeka-classic-install.log
- exit 1
- }
+ --data-urlencode "username@${form_dir}/admin-username" \
+ --data-urlencode "password@${form_dir}/admin-password" \
+ --data-urlencode "password_confirm@${form_dir}/admin-password" \
+ --data-urlencode "super_email@${form_dir}/admin-email" \
+ --data-urlencode "administrator_email@${form_dir}/admin-email" \
+ --data-urlencode "site_title@${form_dir}/site-title" \
+ --data-urlencode "tag_delimiter@${form_dir}/tag-delimiter" \
+ --data-urlencode "fullsize_constraint@${form_dir}/fullsize-constraint" \
+ --data-urlencode "thumbnail_constraint@${form_dir}/thumbnail-constraint" \
+ --data-urlencode "square_thumbnail_constraint@${form_dir}/square-thumbnail-constraint" \
+ --data-urlencode "per_page_admin@${form_dir}/per-page-admin" \
+ --data-urlencode "per_page_public@${form_dir}/per-page-public" \
+ --data-urlencode "path_to_convert@${form_dir}/path-to-convert" \
+ --data-urlencode "install_submit@${form_dir}/install-submit" \
+ http://localhost/install/install.php >"${install_log}" 2>&1 || curl_status=$?
+ rm -rf -- "${form_dir}"
+
+ if [ "${curl_status}" -ne 0 ]; then
+ cat "${install_log}"
+ rm -f -- "${install_log}"
+ trap - EXIT
+ return "${curl_status}"
+ fi
if ! check_installed; then
echo "Omeka Classic installer response did not create the expected database tables."
- cat /tmp/omeka-classic-install.log
+ cat "${install_log}"
exit 1
fi
+ rm -f -- "${install_log}"
+ trap - EXIT
}
function main {
@@ -69,8 +110,10 @@ function main {
return 0
fi
+ require_environment_variables DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
+
if [ "${DB_HOST}" = "mariadb" ]; then
- mysql_create_database
+ database_bootstrap_if_enabled mysql_create_database
fi
wait_for_database
install_omeka
diff --git a/images/omeka-classic/tests/ServiceHealthcheck/docker-compose.yml b/images/omeka-classic/tests/ServiceHealthcheck/docker-compose.yml
index 7f9b70ed..46b9a42d 100644
--- a/images/omeka-classic/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/omeka-classic/tests/ServiceHealthcheck/docker-compose.yml
@@ -3,3 +3,18 @@ name: omeka-classic-servicehealthcheck
services:
omeka-classic:
image: ${OMEKA_CLASSIC:-libops/omeka-classic:local-php83}
+ environment:
+ DB_PASSWORD: 'p"a\ss''word&+='
+ OMEKA_CLASSIC_ADMIN_EMAIL: admin+test@example.com
+ OMEKA_CLASSIC_ADMIN_PASSWORD: 'Adm&in+=p@ss'
+ OMEKA_CLASSIC_SITE_TITLE: "Omeka Classic & Test = +"
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ mariadb:
+ image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: omeka_classic
+ DB_USER: omeka_classic
+ DB_PASSWORD: 'p"a\ss''word&+='
diff --git a/images/omeka-s/Dockerfile b/images/omeka-s/Dockerfile
index 1b2d1bc7..5c390dc9 100644
--- a/images/omeka-s/Dockerfile
+++ b/images/omeka-s/Dockerfile
@@ -2,6 +2,8 @@
ARG PHP_BASE=nginx-php83
FROM ${PHP_BASE}
+LABEL org.opencontainers.image.licenses="GPL-3.0-only"
+
ARG TARGETARCH
ARG PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock
@@ -9,7 +11,15 @@ ARG \
# renovate: datasource=repology depName=alpine_3_24/imagemagick
IMAGEMAGICK_VERSION=7.1.2.24-r0 \
# renovate: datasource=repology depName=alpine_3_24/msmtp
- MSMTP_VERSION=1.8.32-r1
+ MSMTP_VERSION=1.8.32-r1 \
+ # renovate: datasource=repology depName=alpine_3_24/unzip
+ UNZIP_VERSION=6.0-r16 \
+ # renovate: datasource=github-releases depName=omeka-s packageName=omeka/omeka-s
+ SOFTWARE_VERSION=4.2.1
+
+ARG FILE=omeka-s-${SOFTWARE_VERSION}.zip
+ARG URL=https://github.com/omeka/omeka-s/releases/download/v${SOFTWARE_VERSION}/${FILE}
+ARG SHA256="cc27d1c7aca0209523d19aa285f4a08e29e34950dcc446951a7c1311de348e82"
EXPOSE 80
WORKDIR /var/www/omeka-s
@@ -18,8 +28,19 @@ RUN --mount=type=cache,id=omeka-s-apk-${TARGETARCH},sharing=locked,target=/var/c
apk add \
imagemagick=="${IMAGEMAGICK_VERSION}" \
msmtp=="${MSMTP_VERSION}" \
+ unzip=="${UNZIP_VERSION}" \
+ && \
+ cleanup.sh
+
+RUN --mount=type=cache,id=omeka-s-downloads-${TARGETARCH},sharing=locked,target=/opt/downloads \
+ download.sh \
+ --url "${URL}" \
+ --sha256 "${SHA256}" \
+ --strip \
+ --dest /var/www/omeka-s \
&& \
- mkdir -p /var/www/omeka-s/config /var/www/omeka-s/files && \
+ mkdir -p /usr/share/licenses/omeka-s && \
+ cp /var/www/omeka-s/LICENSE /usr/share/licenses/omeka-s/LICENSE && \
chown -R nginx:nginx /var/www/omeka-s && \
cleanup.sh
@@ -28,10 +49,10 @@ ENV \
DB_PORT=3306 \
DB_NAME=omeka_s \
DB_USER=omeka_s \
- DB_PASSWORD=changeme \
+ DB_PASSWORD= \
OMEKA_S_ADMIN_EMAIL=admin@example.com \
OMEKA_S_ADMIN_NAME=Administrator \
- OMEKA_S_ADMIN_PASSWORD=changeme \
+ OMEKA_S_ADMIN_PASSWORD= \
OMEKA_S_SITE_TITLE="Omeka S" \
OMEKA_S_TIME_ZONE=UTC \
OMEKA_S_LOCALE=en_US \
@@ -64,8 +85,9 @@ ENV \
PHP_MAX_INPUT_TIME=300 \
PHP_DEFAULT_SOCKET_TIMEOUT=300 \
PHP_REQUEST_TERMINATE_TIMEOUT=300 \
- PHP_MEMORY_LIMIT=256M
+ PHP_MEMORY_LIMIT=256M \
+ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=600000
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD test -f /installed && test -f /var/www/omeka-s/index.php && curl -fsS http://localhost/status | grep -q pool
diff --git a/images/omeka-s/rootfs/etc/confd/templates/database.ini.tmpl b/images/omeka-s/rootfs/etc/confd/templates/database.ini.tmpl
index ec6661db..0ffffecd 100644
--- a/images/omeka-s/rootfs/etc/confd/templates/database.ini.tmpl
+++ b/images/omeka-s/rootfs/etc/confd/templates/database.ini.tmpl
@@ -1,6 +1,6 @@
-user = "{{ getenv "DB_USER" }}"
-password = "{{ getenv "DB_PASSWORD" }}"
-dbname = "{{ getenv "DB_NAME" }}"
-host = "{{ getenv "DB_HOST" }}"
-port = "{{ getenv "DB_PORT" }}"
-
+{{- define "doubleQuoteEscape" -}}{{- $escaped := replace . "\\" "\\\\" -1 -}}{{- replace $escaped "\"" "\\\"" -1 -}}{{- end -}}
+user = "{{ template "doubleQuoteEscape" (getenv "DB_USER") }}"
+password = "{{ template "doubleQuoteEscape" (getenv "DB_PASSWORD") }}"
+dbname = "{{ template "doubleQuoteEscape" (getenv "DB_NAME") }}"
+host = "{{ template "doubleQuoteEscape" (getenv "DB_HOST") }}"
+port = "{{ template "doubleQuoteEscape" (getenv "DB_PORT") }}"
diff --git a/images/omeka-s/rootfs/etc/s6-overlay/s6-rc.d/omeka-s-setup/dependencies.d/ready b/images/omeka-s/rootfs/etc/s6-overlay/s6-rc.d/omeka-s-setup/dependencies.d/ready
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/images/omeka-s/rootfs/etc/s6-overlay/s6-rc.d/omeka-s-setup/dependencies.d/ready
@@ -0,0 +1 @@
+
diff --git a/images/omeka-s/rootfs/etc/s6-overlay/scripts/omeka-s-setup.sh b/images/omeka-s/rootfs/etc/s6-overlay/scripts/omeka-s-setup.sh
index 23793a9e..7806d212 100755
--- a/images/omeka-s/rootfs/etc/s6-overlay/scripts/omeka-s-setup.sh
+++ b/images/omeka-s/rootfs/etc/s6-overlay/scripts/omeka-s-setup.sh
@@ -3,19 +3,26 @@
set -eou pipefail
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
+
function mysql_create_database {
- cat <<-SQL | create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}');
-SQL
+ DB_CHARACTER_SET=utf8mb4 \
+ DB_COLLATION=utf8mb4_unicode_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function wait_for_database {
local attempts=0
- until mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" -e 'SELECT 1' >/dev/null 2>&1; do
+ until database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute='SELECT 1' \
+ >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 60 ]; then
echo "Database was not ready in time"
@@ -26,8 +33,13 @@ function wait_for_database {
}
function check_installed {
- mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
- -e "SELECT 1 FROM \`user\` LIMIT 1" >/dev/null 2>&1
+ database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute="SELECT 1 FROM \`user\` LIMIT 1" \
+ >/dev/null 2>&1
}
function install_omeka {
@@ -36,20 +48,49 @@ function install_omeka {
return 0
fi
+ require_environment_variables OMEKA_S_ADMIN_PASSWORD
+
timeout 300 wait-for-open-port.sh localhost 80
- curl -fsS -d "user[email]=${OMEKA_S_ADMIN_EMAIL}" \
- -d "user[email-confirm]=${OMEKA_S_ADMIN_EMAIL}" \
- -d "user[name]=${OMEKA_S_ADMIN_NAME}" \
- -d "user[password-confirm][password]=${OMEKA_S_ADMIN_PASSWORD}" \
- -d "user[password-confirm][password-confirm]=${OMEKA_S_ADMIN_PASSWORD}" \
- -d "settings[installation_title]=${OMEKA_S_SITE_TITLE}" \
- -d "settings[time_zone]=${OMEKA_S_TIME_ZONE}" \
- -d "settings[locale]=${OMEKA_S_LOCALE}" \
- -d "submit=Submit" \
- http://localhost/install >/tmp/omeka-s-install.log 2>&1 || {
- cat /tmp/omeka-s-install.log
- exit 1
- }
+
+ local form_dir install_log curl_status=0
+ umask 077
+ form_dir=$(mktemp -d)
+ install_log=$(mktemp -t omeka-s-install.XXXXXXXXXX)
+ trap 'rm -rf -- "${form_dir}" "${install_log}"' EXIT
+ printf '%s' "${OMEKA_S_ADMIN_EMAIL}" >"${form_dir}/admin-email"
+ printf '%s' "${OMEKA_S_ADMIN_NAME}" >"${form_dir}/admin-name"
+ printf '%s' "${OMEKA_S_ADMIN_PASSWORD}" >"${form_dir}/admin-password"
+ printf '%s' "${OMEKA_S_SITE_TITLE}" >"${form_dir}/site-title"
+ printf '%s' "${OMEKA_S_TIME_ZONE}" >"${form_dir}/time-zone"
+ printf '%s' "${OMEKA_S_LOCALE}" >"${form_dir}/locale"
+ printf '%s' Submit >"${form_dir}/submit"
+
+ curl -fsS \
+ --data-urlencode "user[email]@${form_dir}/admin-email" \
+ --data-urlencode "user[email-confirm]@${form_dir}/admin-email" \
+ --data-urlencode "user[name]@${form_dir}/admin-name" \
+ --data-urlencode "user[password-confirm][password]@${form_dir}/admin-password" \
+ --data-urlencode "user[password-confirm][password-confirm]@${form_dir}/admin-password" \
+ --data-urlencode "settings[installation_title]@${form_dir}/site-title" \
+ --data-urlencode "settings[time_zone]@${form_dir}/time-zone" \
+ --data-urlencode "settings[locale]@${form_dir}/locale" \
+ --data-urlencode "submit@${form_dir}/submit" \
+ http://localhost/install >"${install_log}" 2>&1 || curl_status=$?
+ rm -rf -- "${form_dir}"
+
+ if [ "${curl_status}" -ne 0 ]; then
+ cat "${install_log}"
+ rm -f -- "${install_log}"
+ trap - EXIT
+ return "${curl_status}"
+ fi
+ if ! check_installed; then
+ echo "Omeka S installer response did not create the expected database tables."
+ cat "${install_log}"
+ exit 1
+ fi
+ rm -f -- "${install_log}"
+ trap - EXIT
}
function main {
@@ -58,8 +99,10 @@ function main {
return 0
fi
+ require_environment_variables DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD
+
if [ "${DB_HOST}" = "mariadb" ]; then
- mysql_create_database
+ database_bootstrap_if_enabled mysql_create_database
fi
wait_for_database
install_omeka
diff --git a/images/omeka-s/tests/ServiceHealthcheck/docker-compose.yml b/images/omeka-s/tests/ServiceHealthcheck/docker-compose.yml
index 44871445..6bd1ac7b 100644
--- a/images/omeka-s/tests/ServiceHealthcheck/docker-compose.yml
+++ b/images/omeka-s/tests/ServiceHealthcheck/docker-compose.yml
@@ -3,3 +3,19 @@ name: omeka-s-servicehealthcheck
services:
omeka-s:
image: ${OMEKA_S:-libops/omeka-s:local-php83}
+ environment:
+ DB_PASSWORD: 'p"a\ss''word&+='
+ OMEKA_S_ADMIN_EMAIL: admin+test@example.com
+ OMEKA_S_ADMIN_NAME: "Administrator & Curator"
+ OMEKA_S_ADMIN_PASSWORD: 'Adm&in+=p@ss'
+ OMEKA_S_SITE_TITLE: "Omeka S & Test = +"
+ depends_on:
+ mariadb:
+ condition: service_healthy
+ mariadb:
+ image: ${MARIADB11:-libops/mariadb:local-11}
+ environment:
+ DB_ROOT_PASSWORD: root
+ DB_NAME: omeka_s
+ DB_USER: omeka_s
+ DB_PASSWORD: 'p"a\ss''word&+='
diff --git a/images/php83/Dockerfile b/images/php83/Dockerfile
index de2d88ec..e68acc96 100644
--- a/images/php83/Dockerfile
+++ b/images/php83/Dockerfile
@@ -24,7 +24,7 @@ ARG \
# renovate: datasource=repology depName=alpine_3_24/icu-data-full
ICU_VERSION=78.1-r0 \
# renovate: datasource=repology depName=alpine_3_24/php83
- SOFTWARE_VERSION=8.3.31-r1
+ SOFTWARE_VERSION=8.3.32-r0
# Platform specific does require arch specific identifier.
RUN --mount=type=cache,id=php83-apk-${TARGETARCH},sharing=locked,target=/var/cache/apk \
diff --git a/images/php83/rootfs/etc/s6-overlay/s6-rc.d/fpm/run b/images/php83/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
index 01e7ef9c..83060448 100755
--- a/images/php83/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
+++ b/images/php83/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
set -e
+unset DB_ROOT_PASSWORD
+unset DRUPAL_DEFAULT_ACCOUNT_PASSWORD OJS_ADMIN_PASSWORD \
+ OMEKA_CLASSIC_ADMIN_PASSWORD OMEKA_S_ADMIN_PASSWORD WORDPRESS_ADMIN_PASSWORD
exec s6-notifyoncheck -d -t 30000 -n 1 /usr/sbin/php-fpm83 \
--pid /run/php-fpm/php-fpm.pid \
--prefix /run/php-fpm \
diff --git a/images/php84/Dockerfile b/images/php84/Dockerfile
index 97e62805..bffc0804 100644
--- a/images/php84/Dockerfile
+++ b/images/php84/Dockerfile
@@ -24,7 +24,7 @@ ARG \
# renovate: datasource=repology depName=alpine_3_24/icu-data-full
ICU_VERSION=78.1-r0 \
# renovate: datasource=repology depName=alpine_3_24/php84
- SOFTWARE_VERSION=8.4.22-r0
+ SOFTWARE_VERSION=8.4.23-r0
# Platform specific does require arch specific identifier.
RUN --mount=type=cache,id=php84-apk-${TARGETARCH},sharing=locked,target=/var/cache/apk \
diff --git a/images/php84/rootfs/etc/s6-overlay/s6-rc.d/fpm/run b/images/php84/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
index 9ef7b851..082a8835 100755
--- a/images/php84/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
+++ b/images/php84/rootfs/etc/s6-overlay/s6-rc.d/fpm/run
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
set -e
+unset DB_ROOT_PASSWORD
+unset DRUPAL_DEFAULT_ACCOUNT_PASSWORD OJS_ADMIN_PASSWORD \
+ OMEKA_CLASSIC_ADMIN_PASSWORD OMEKA_S_ADMIN_PASSWORD WORDPRESS_ADMIN_PASSWORD
exec s6-notifyoncheck -d -t 30000 -n 1 /usr/sbin/php-fpm84 \
--pid /run/php-fpm/php-fpm.pid \
--prefix /run/php-fpm \
diff --git a/images/scyllaridae/rootfs/etc/s6-overlay/s6-rc.d/scyllaridae/run b/images/scyllaridae/rootfs/etc/s6-overlay/s6-rc.d/scyllaridae/run
index ad4f6548..6bbb0a4e 100755
--- a/images/scyllaridae/rootfs/etc/s6-overlay/s6-rc.d/scyllaridae/run
+++ b/images/scyllaridae/rootfs/etc/s6-overlay/s6-rc.d/scyllaridae/run
@@ -1,4 +1,5 @@
#!/command/execlineb -P
with-contenv
+unexport DB_ROOT_PASSWORD
exec s6-setuidgid scyllaridae /app/scyllaridae
diff --git a/images/solr10/Dockerfile b/images/solr10/Dockerfile
index c72a136f..f0ba2960 100644
--- a/images/solr10/Dockerfile
+++ b/images/solr10/Dockerfile
@@ -54,4 +54,4 @@ ENV \
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost:8983/solr/admin/info/system?wt=json | jq -r .responseHeader.status | grep -q 0
+HEALTHCHECK CMD curl -fsS http://localhost:8983/solr/admin/info/system?wt=json | jq -r .responseHeader.status | grep -q 0
diff --git a/images/solr10/README.md b/images/solr10/README.md
index 0d373ac1..6a320c45 100644
--- a/images/solr10/README.md
+++ b/images/solr10/README.md
@@ -64,14 +64,7 @@ Change `SOFTWARE_VERSION` and then generate the `SOLR_FILE_SHA256` with the foll
commands:
```bash
-SOFTWARE_VERSION=$(cat solr10/Dockerfile | grep -o 'SOFTWARE_VERSION=.*' | cut -f2 -d=)
-SOLR_FILE=$(cat solr10/Dockerfile | grep -o 'SOLR_FILE=.*' | cut -f2 -d=)
-SOLR_URL=$(cat solr10/Dockerfile | grep -o 'SOLR_URL=.*' | cut -f2 -d=)
-SOLR_FILE=$(eval "echo $SOLR_FILE")
-SOLR_URL=$(eval "echo $SOLR_URL")
-wget --quiet "${SOLR_URL}"
-shasum -a 256 "${SOLR_FILE}" | cut -f1 -d' '
-rm "${SOLR_FILE}"
+./ci/update-sha.sh apache-solr10 OLD_VERSION NEW_VERSION ''
```
[Solr Documentation]: https://lucene.apache.org/solr/guide/7_1/
diff --git a/images/solr10/rootfs/etc/s6-overlay/s6-rc.d/solr/run b/images/solr10/rootfs/etc/s6-overlay/s6-rc.d/solr/run
index e7f87058..617a2d95 100755
--- a/images/solr10/rootfs/etc/s6-overlay/s6-rc.d/solr/run
+++ b/images/solr10/rootfs/etc/s6-overlay/s6-rc.d/solr/run
@@ -15,4 +15,5 @@ fi
# actually can write to the folder.
chown -R solr:solr /opt/solr/server/solr
+unset DB_ROOT_PASSWORD
exec s6-setuidgid solr /opt/solr/bin/solr start -f "${ARGS[@]}"
diff --git a/images/solr10/tests/ServiceHealthcheck/test.sh b/images/solr10/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..e84270db 100755
--- a/images/solr10/tests/ServiceHealthcheck/test.sh
+++ b/images/solr10/tests/ServiceHealthcheck/test.sh
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
diff --git a/images/solr9/Dockerfile b/images/solr9/Dockerfile
index c7cd3082..51f8cba6 100644
--- a/images/solr9/Dockerfile
+++ b/images/solr9/Dockerfile
@@ -54,4 +54,4 @@ ENV \
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost:8983/solr/admin/info/system?wt=json | jq -r .responseHeader.status | grep -q 0
+HEALTHCHECK CMD curl -fsS http://localhost:8983/solr/admin/info/system?wt=json | jq -r .responseHeader.status | grep -q 0
diff --git a/images/solr9/README.md b/images/solr9/README.md
index 5e986cec..a2f52824 100644
--- a/images/solr9/README.md
+++ b/images/solr9/README.md
@@ -64,14 +64,7 @@ Change `SOFTWARE_VERSION` and then generate the `SOLR_FILE_SHA256` with the foll
commands:
```bash
-SOFTWARE_VERSION=$(cat solr9/Dockerfile | grep -o 'SOFTWARE_VERSION=.*' | cut -f2 -d=)
-SOLR_FILE=$(cat solr9/Dockerfile | grep -o 'SOLR_FILE=.*' | cut -f2 -d=)
-SOLR_URL=$(cat solr9/Dockerfile | grep -o 'SOLR_URL=.*' | cut -f2 -d=)
-SOLR_FILE=$(eval "echo $SOLR_FILE")
-SOLR_URL=$(eval "echo $SOLR_URL")
-wget --quiet "${SOLR_URL}"
-shasum -a 256 "${SOLR_FILE}" | cut -f1 -d' '
-rm "${SOLR_FILE}"
+./ci/update-sha.sh apache-solr9 OLD_VERSION NEW_VERSION ''
```
[Solr Documentation]: https://lucene.apache.org/solr/guide/7_1/
diff --git a/images/solr9/rootfs/etc/s6-overlay/s6-rc.d/solr/run b/images/solr9/rootfs/etc/s6-overlay/s6-rc.d/solr/run
index e7f87058..617a2d95 100755
--- a/images/solr9/rootfs/etc/s6-overlay/s6-rc.d/solr/run
+++ b/images/solr9/rootfs/etc/s6-overlay/s6-rc.d/solr/run
@@ -15,4 +15,5 @@ fi
# actually can write to the folder.
chown -R solr:solr /opt/solr/server/solr
+unset DB_ROOT_PASSWORD
exec s6-setuidgid solr /opt/solr/bin/solr start -f "${ARGS[@]}"
diff --git a/images/solr9/tests/ServiceHealthcheck/test.sh b/images/solr9/tests/ServiceHealthcheck/test.sh
index dd1d6bdf..e84270db 100755
--- a/images/solr9/tests/ServiceHealthcheck/test.sh
+++ b/images/solr9/tests/ServiceHealthcheck/test.sh
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# Invoked indirectly by the SIGTERM trap below.
-# shellcheck disable=SC2329
+# shellcheck disable=SC2317,SC2329
on_terminate() {
echo "Termination signal received. Exiting..."
exit 0
diff --git a/images/tomcat11/Dockerfile b/images/tomcat11/Dockerfile
index 9d93893f..4b1dac4e 100644
--- a/images/tomcat11/Dockerfile
+++ b/images/tomcat11/Dockerfile
@@ -31,12 +31,12 @@ RUN create-service-user.sh \
ENV \
TOMCAT_ADMIN_NAME=admin \
- TOMCAT_ADMIN_PASSWORD=password \
+ TOMCAT_ADMIN_PASSWORD= \
TOMCAT_ADMIN_ROLES=manager-gui \
TOMCAT_CONNECTION_TIMEOUT=20000 \
TOMCAT_CATALINA_OPTS= \
TOMCAT_JAVA_OPTS= \
TOMCAT_LOG_LEVEL=INFO \
- TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^.*$
+ TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^(127[.].*|::1)$
COPY --link rootfs /
diff --git a/images/tomcat11/README.md b/images/tomcat11/README.md
index b7d33af0..cb3d439c 100644
--- a/images/tomcat11/README.md
+++ b/images/tomcat11/README.md
@@ -1,24 +1,30 @@
-# Tomcat
+# Tomcat 11
-Docker image for [Tomcat] version 9.0.118.
+Docker image for [Tomcat] version 11.0.22.
-Built from [libops/isle-buildkit tomcat](https://github.com/libops/buildkit/tree/main/images/tomcat)
+Built from [libops/buildkit tomcat11](https://github.com/libops/buildkit/tree/main/images/tomcat11)
Please refer to the [Tomcat Documentation] for more in-depth information.
-As a quick example this will bring up an instance of [Tomcat], and allow you
-to view the manager webapp on .
+As a quick example this will bring up an instance of [Tomcat] and allow you to
+view the manager webapp on as `admin`
+using a generated password. The port is bound to the host loopback interface;
+the manager valve is relaxed only for this local example because Docker bridge
+traffic does not arrive from the container's loopback address.
```bash
+export TOMCAT_ADMIN_PASSWORD="$(openssl rand -hex 32)"
docker run --rm -ti \
- -p 8080:8080 \
- libops/tomcat
+ --env TOMCAT_ADMIN_PASSWORD \
+ --env TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE='.*' \
+ -p 127.0.0.1:8080:8080 \
+ libops/tomcat:11
```
## Dependencies
-Requires `libops/java:17` Docker image to build. Please refer to the
-[Java 17 Image README](../java17/README.md) for additional information including
+Requires `libops/java:25` Docker image to build. Please refer to the
+[Java 25 Image README](../java25/README.md) for additional information including
additional settings, volumes, ports, etc.
## Ports
@@ -35,12 +41,12 @@ additional settings, volumes, ports, etc.
| Environment Variable | Default | Description |
| :---------------------------------- | :---------- | :------------------------------------------------------------------------------------ |
| TOMCAT_ADMIN_NAME | admin | The user name of the manager webapp admin user |
-| TOMCAT_ADMIN_PASSWORD | password | The password for the manager webapp admin user |
+| TOMCAT_ADMIN_PASSWORD | | The password for the manager webapp admin user; an empty value disables the admin user|
| TOMCAT_ADMIN_ROLES | manager-gui | Comma separated list of roles the user has |
| TOMCAT_CATALINA_OPTS | | |
| TOMCAT_JAVA_OPTS | | |
| TOMCAT_LOG_LEVEL | INFO | Log level. Possible Values: SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST or ALL |
-| TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE | ^.*$ | Allows / blocks access to manager app to addresses which match this regex |
+| TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE | `^(127[.].*\|::1)$` | Allows / blocks access to manager app to addresses which match this regex |
Additional users/groups/etc can be defined by adding more environment variables,
following the above conventions:
@@ -66,7 +72,7 @@ For example to add a new user `someone` you would need to define the following:
> become `FCREPO_TOMCAT_CATALINA_OPTS`. This is to allow for different settings
> on a per-service basis when sharing the same confd backend.
-[AJP]: https://tomcat.apache.org/tomcat-9.0-doc/config/ajp.html
-[Tomcat Documentation]: https://tomcat.apache.org/tomcat-9.0-doc/
-[Tomcat Logging]: https://tomcat.apache.org/tomcat-9.0-doc/logging.html
+[AJP]: https://tomcat.apache.org/tomcat-11.0-doc/config/ajp.html
+[Tomcat Documentation]: https://tomcat.apache.org/tomcat-11.0-doc/
+[Tomcat Logging]: https://tomcat.apache.org/tomcat-11.0-doc/logging.html
[Tomcat]: https://tomcat.apache.org/
diff --git a/images/tomcat11/rootfs/etc/confd/conf.d/tomcat-users.toml b/images/tomcat11/rootfs/etc/confd/conf.d/tomcat-users.toml
index f5ebde25..fe990515 100644
--- a/images/tomcat11/rootfs/etc/confd/conf.d/tomcat-users.toml
+++ b/images/tomcat11/rootfs/etc/confd/conf.d/tomcat-users.toml
@@ -3,4 +3,5 @@ src = "tomcat-users.xml.tmpl"
dest = "/opt/tomcat/conf/tomcat-users.xml"
uid = 100
gid = 1000
+mode = "0640"
keys = [ "/" ]
diff --git a/images/tomcat11/rootfs/etc/confd/templates/tomcat-users.xml.tmpl b/images/tomcat11/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
index b137d2cf..4297e20c 100644
--- a/images/tomcat11/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
+++ b/images/tomcat11/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
@@ -1,9 +1,10 @@
+{{- define "xmlAttributeEscape" -}}{{- $ampersands := replace . "&" "&" -1 -}}{{- $quotes := replace $ampersands "\"" """ -1 -}}{{- $lessThan := replace $quotes "<" "<" -1 -}}{{- replace $lessThan ">" ">" -1 -}}{{- end -}}
-
- {{ range $dir := lsdir "/tomcat/user" }}
+ {{ if getenv "TOMCAT_ADMIN_PASSWORD" }}{{ end }}
+ {{ range $dir := lsdir "/tomcat/user" }}
{{ end }}
diff --git a/images/tomcat11/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run b/images/tomcat11/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
index 1ef17ba7..cabfc60b 100755
--- a/images/tomcat11/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
+++ b/images/tomcat11/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
@@ -1,4 +1,5 @@
-#!/usr/bin/env bash
+#!/command/with-contenv bash
+# shellcheck shell=bash
set -e
# Many services deployed into tomcat need somewhere to write data, by convention
@@ -8,4 +9,5 @@ set -e
# actually can write to the folder.
chown tomcat:tomcat /data
-exec with-contenv s6-setuidgid tomcat /opt/tomcat/bin/catalina.sh run
+unset DB_ROOT_PASSWORD
+exec s6-setuidgid tomcat /opt/tomcat/bin/catalina.sh run
diff --git a/images/tomcat9/Dockerfile b/images/tomcat9/Dockerfile
index 893a6b35..b5251adb 100644
--- a/images/tomcat9/Dockerfile
+++ b/images/tomcat9/Dockerfile
@@ -31,12 +31,12 @@ RUN create-service-user.sh \
ENV \
TOMCAT_ADMIN_NAME=admin \
- TOMCAT_ADMIN_PASSWORD=password \
+ TOMCAT_ADMIN_PASSWORD= \
TOMCAT_ADMIN_ROLES=manager-gui \
TOMCAT_CONNECTION_TIMEOUT=20000 \
TOMCAT_CATALINA_OPTS= \
TOMCAT_JAVA_OPTS= \
TOMCAT_LOG_LEVEL=INFO \
- TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^.*$
+ TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^(127[.].*|::1)$
COPY --link rootfs /
diff --git a/images/tomcat9/README.md b/images/tomcat9/README.md
index b7d33af0..e0d54ca0 100644
--- a/images/tomcat9/README.md
+++ b/images/tomcat9/README.md
@@ -6,13 +6,19 @@ Built from [libops/isle-buildkit tomcat](https://github.com/libops/buildkit/tree
Please refer to the [Tomcat Documentation] for more in-depth information.
-As a quick example this will bring up an instance of [Tomcat], and allow you
-to view the manager webapp on .
+As a quick example this will bring up an instance of [Tomcat] and allow you to
+view the manager webapp on as `admin`
+using a generated password. The port is bound to the host loopback interface;
+the manager valve is relaxed only for this local example because Docker bridge
+traffic does not arrive from the container's loopback address.
```bash
+export TOMCAT_ADMIN_PASSWORD="$(openssl rand -hex 32)"
docker run --rm -ti \
- -p 8080:8080 \
- libops/tomcat
+ --env TOMCAT_ADMIN_PASSWORD \
+ --env TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE='.*' \
+ -p 127.0.0.1:8080:8080 \
+ libops/tomcat:9
```
## Dependencies
@@ -35,12 +41,12 @@ additional settings, volumes, ports, etc.
| Environment Variable | Default | Description |
| :---------------------------------- | :---------- | :------------------------------------------------------------------------------------ |
| TOMCAT_ADMIN_NAME | admin | The user name of the manager webapp admin user |
-| TOMCAT_ADMIN_PASSWORD | password | The password for the manager webapp admin user |
+| TOMCAT_ADMIN_PASSWORD | | The password for the manager webapp admin user; an empty value disables the admin user|
| TOMCAT_ADMIN_ROLES | manager-gui | Comma separated list of roles the user has |
| TOMCAT_CATALINA_OPTS | | |
| TOMCAT_JAVA_OPTS | | |
| TOMCAT_LOG_LEVEL | INFO | Log level. Possible Values: SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST or ALL |
-| TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE | ^.*$ | Allows / blocks access to manager app to addresses which match this regex |
+| TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE | `^(127[.].*\|::1)$` | Allows / blocks access to manager app to addresses which match this regex |
Additional users/groups/etc can be defined by adding more environment variables,
following the above conventions:
diff --git a/images/tomcat9/rootfs/etc/confd/conf.d/tomcat-users.toml b/images/tomcat9/rootfs/etc/confd/conf.d/tomcat-users.toml
index f5ebde25..fe990515 100644
--- a/images/tomcat9/rootfs/etc/confd/conf.d/tomcat-users.toml
+++ b/images/tomcat9/rootfs/etc/confd/conf.d/tomcat-users.toml
@@ -3,4 +3,5 @@ src = "tomcat-users.xml.tmpl"
dest = "/opt/tomcat/conf/tomcat-users.xml"
uid = 100
gid = 1000
+mode = "0640"
keys = [ "/" ]
diff --git a/images/tomcat9/rootfs/etc/confd/templates/tomcat-users.xml.tmpl b/images/tomcat9/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
index b137d2cf..4297e20c 100644
--- a/images/tomcat9/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
+++ b/images/tomcat9/rootfs/etc/confd/templates/tomcat-users.xml.tmpl
@@ -1,9 +1,10 @@
+{{- define "xmlAttributeEscape" -}}{{- $ampersands := replace . "&" "&" -1 -}}{{- $quotes := replace $ampersands "\"" """ -1 -}}{{- $lessThan := replace $quotes "<" "<" -1 -}}{{- replace $lessThan ">" ">" -1 -}}{{- end -}}
-
- {{ range $dir := lsdir "/tomcat/user" }}
+ {{ if getenv "TOMCAT_ADMIN_PASSWORD" }}{{ end }}
+ {{ range $dir := lsdir "/tomcat/user" }}
{{ end }}
diff --git a/images/tomcat9/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run b/images/tomcat9/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
index 1ef17ba7..cabfc60b 100755
--- a/images/tomcat9/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
+++ b/images/tomcat9/rootfs/etc/s6-overlay/s6-rc.d/tomcat/run
@@ -1,4 +1,5 @@
-#!/usr/bin/env bash
+#!/command/with-contenv bash
+# shellcheck shell=bash
set -e
# Many services deployed into tomcat need somewhere to write data, by convention
@@ -8,4 +9,5 @@ set -e
# actually can write to the folder.
chown tomcat:tomcat /data
-exec with-contenv s6-setuidgid tomcat /opt/tomcat/bin/catalina.sh run
+unset DB_ROOT_PASSWORD
+exec s6-setuidgid tomcat /opt/tomcat/bin/catalina.sh run
diff --git a/images/wp/Dockerfile b/images/wp/Dockerfile
index 797d2775..b719e3a3 100644
--- a/images/wp/Dockerfile
+++ b/images/wp/Dockerfile
@@ -2,6 +2,8 @@
ARG PHP_BASE=nginx-php83
FROM ${PHP_BASE}
+LABEL org.opencontainers.image.licenses="GPL-2.0-or-later"
+
ARG TARGETARCH
ARG PHP_FPM_SOCKET=/run/php-fpm/php-fpm.sock
@@ -28,24 +30,24 @@ ENV \
DB_PORT=3306 \
DB_NAME=wordpress \
DB_USER=wordpress \
- DB_PASSWORD=changeme \
+ DB_PASSWORD= \
INGRESS_HOSTNAMES=localhost \
INGRESS_SCHEME=http \
WORDPRESS_SITE_TITLE=WordPress \
WORDPRESS_ADMIN_USERNAME=admin \
WORDPRESS_ADMIN_EMAIL=admin@example.com \
- WORDPRESS_ADMIN_PASSWORD=changeme \
+ WORDPRESS_ADMIN_PASSWORD= \
WORDPRESS_LOCALE=en_US \
WORDPRESS_TABLE_PREFIX=wp_ \
WORDPRESS_BLOG_PUBLIC=0 \
- WORDPRESS_AUTH_KEY=changeme \
- WORDPRESS_SECURE_AUTH_KEY=changeme \
- WORDPRESS_LOGGED_IN_KEY=changeme \
- WORDPRESS_NONCE_KEY=changeme \
- WORDPRESS_AUTH_SALT=changeme \
- WORDPRESS_SECURE_AUTH_SALT=changeme \
- WORDPRESS_LOGGED_IN_SALT=changeme \
- WORDPRESS_NONCE_SALT=changeme \
+ WORDPRESS_AUTH_KEY= \
+ WORDPRESS_SECURE_AUTH_KEY= \
+ WORDPRESS_LOGGED_IN_KEY= \
+ WORDPRESS_NONCE_KEY= \
+ WORDPRESS_AUTH_SALT= \
+ WORDPRESS_SECURE_AUTH_SALT= \
+ WORDPRESS_LOGGED_IN_SALT= \
+ WORDPRESS_NONCE_SALT= \
LIBOPS_SMTP_HOST=host.docker.internal \
LIBOPS_SMTP_PORT=25 \
SMTP_FROM= \
@@ -78,4 +80,4 @@ ENV \
COPY --link rootfs /
-HEALTHCHECK CMD curl -s http://localhost/status | grep -q pool
+HEALTHCHECK CMD curl -fsS http://localhost/status | grep -q pool
diff --git a/images/wp/rootfs/etc/confd/conf.d/wordpress.application.toml b/images/wp/rootfs/etc/confd/conf.d/wordpress.application.toml
index 127529bf..a5f69f3c 100644
--- a/images/wp/rootfs/etc/confd/conf.d/wordpress.application.toml
+++ b/images/wp/rootfs/etc/confd/conf.d/wordpress.application.toml
@@ -2,6 +2,6 @@
src = "wordpress.application.php.tmpl"
dest = "/var/www/bedrock/config/libops-runtime.php"
uid = 0
-gid = 0
-mode = "0644"
+gid = 101
+mode = "0640"
keys = [ "/" ]
diff --git a/images/wp/rootfs/etc/cont-init.d/01-wordpress-permissions b/images/wp/rootfs/etc/cont-init.d/01-wordpress-permissions
index f4b29821..0dd17226 100755
--- a/images/wp/rootfs/etc/cont-init.d/01-wordpress-permissions
+++ b/images/wp/rootfs/etc/cont-init.d/01-wordpress-permissions
@@ -1,4 +1,5 @@
#!/command/with-contenv bash
+# shellcheck shell=bash
DIRS=(
/var/www/bedrock/web/app/uploads
diff --git a/images/wp/rootfs/etc/s6-overlay/scripts/wordpress-setup.sh b/images/wp/rootfs/etc/s6-overlay/scripts/wordpress-setup.sh
index c798fa3e..61d054a4 100755
--- a/images/wp/rootfs/etc/s6-overlay/scripts/wordpress-setup.sh
+++ b/images/wp/rootfs/etc/s6-overlay/scripts/wordpress-setup.sh
@@ -3,16 +3,15 @@
set -eou pipefail
-function mysql_create_database {
- cat <<-SQL | create-database.sh
-CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-
-CREATE USER IF NOT EXISTS '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
-GRANT ALL PRIVILEGES ON ${DB_NAME}.* to '${DB_USER}'@'%';
-FLUSH PRIVILEGES;
+# shellcheck disable=SC1091
+source /usr/local/share/libops/database.sh
+# shellcheck disable=SC1091
+source /usr/local/share/libops/environment.sh
-SET PASSWORD FOR ${DB_USER}@'%' = PASSWORD('${DB_PASSWORD}');
-SQL
+function mysql_create_database {
+ DB_CHARACTER_SET=utf8mb4 \
+ DB_COLLATION=utf8mb4_unicode_ci \
+ render-database-bootstrap-sql.sh | create-database.sh
}
function wp_cli {
@@ -37,7 +36,13 @@ function should_skip_wordpress_setup {
function wait_for_database {
local attempts=0
- until mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" -e 'SELECT 1' >/dev/null 2>&1; do
+ until database_mariadb_with_password "${DB_PASSWORD}" \
+ --host="${DB_HOST}" \
+ --port="${DB_PORT}" \
+ --user="${DB_USER}" \
+ --database="${DB_NAME}" \
+ --execute='SELECT 1' \
+ >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 60 ]; then
echo "Database was not ready in time"
@@ -61,13 +66,15 @@ function install_wordpress {
return 0
fi
+ require_environment_variables WORDPRESS_ADMIN_PASSWORD
+
echo "WordPress not installed. Running wp-cli installation..."
- wp_cli core install \
+ printf '%s\n' "${WORDPRESS_ADMIN_PASSWORD}" | wp_cli core install \
--url="$(wordpress_home)" \
--title="${WORDPRESS_SITE_TITLE}" \
--admin_user="${WORDPRESS_ADMIN_USERNAME}" \
- --admin_password="${WORDPRESS_ADMIN_PASSWORD}" \
--admin_email="${WORDPRESS_ADMIN_EMAIL}" \
+ --prompt=admin_password \
--skip-email
wp_cli option update permalink_structure '/%postname%/'
@@ -96,9 +103,16 @@ function main {
return 0
fi
+ require_environment_variables \
+ DB_HOST DB_PORT DB_NAME DB_USER DB_PASSWORD \
+ WORDPRESS_AUTH_KEY WORDPRESS_SECURE_AUTH_KEY \
+ WORDPRESS_LOGGED_IN_KEY WORDPRESS_NONCE_KEY \
+ WORDPRESS_AUTH_SALT WORDPRESS_SECURE_AUTH_SALT \
+ WORDPRESS_LOGGED_IN_SALT WORDPRESS_NONCE_SALT
+
wait_for_wordpress_files
if [ "${DB_HOST}" = "mariadb" ]; then
- mysql_create_database
+ database_bootstrap_if_enabled mysql_create_database
fi
wait_for_database
install_wordpress
diff --git a/internal/buildkit/metadata.go b/internal/buildkit/metadata.go
index bf076cee..862153d4 100644
--- a/internal/buildkit/metadata.go
+++ b/internal/buildkit/metadata.go
@@ -232,7 +232,7 @@ func (m *Metadata) FirstTag(image, mode, fallback string) (string, error) {
}
func (m *Metadata) versionTags(image, fallback string) ([]string, error) {
- if tag, ok, err := m.lampAppVersionTag(image); ok || err != nil {
+ if tag, ok, err := m.composerScaffoldVersionTag(image); ok || err != nil {
if err != nil {
return nil, err
}
@@ -287,9 +287,9 @@ func (m *Metadata) versionTags(image, fallback string) ([]string, error) {
return uniqueStrings(result), nil
}
-func (m *Metadata) lampAppVersionTag(image string) (string, bool, error) {
+func (m *Metadata) composerScaffoldVersionTag(image string) (string, bool, error) {
suffix := phpFlavorSuffix(image)
- if suffix == "" || !isLAMPAppImage(image) {
+ if suffix == "" || !isComposerScaffoldImage(image) {
return "", false, nil
}
value, err := dockerfileArgDefault(filepath.Join(m.Root, "images", "nginx", "Dockerfile"), "NGINX_VERSION")
@@ -306,9 +306,9 @@ func (m *Metadata) lampAppVersionTag(image string) (string, bool, error) {
return "nginx-" + version + "-" + suffix, true, nil
}
-func isLAMPAppImage(image string) bool {
+func isComposerScaffoldImage(image string) bool {
switch imageContext(image) {
- case "drupal", "islandora", "ojs", "omeka-classic", "omeka-s", "wp":
+ case "drupal", "islandora", "wp":
return true
default:
return false
@@ -600,10 +600,14 @@ func splitLines(value string) []string {
func isGlobalPath(path string) bool {
return path == "docker-bake.hcl" ||
+ path == "go.mod" ||
+ path == "go.sum" ||
path == "Makefile" ||
strings.HasPrefix(path, "buildSrc/") ||
+ strings.HasPrefix(path, "cmd/") ||
strings.HasPrefix(path, "ci/") ||
- strings.HasPrefix(path, ".github/workflows/")
+ strings.HasPrefix(path, ".github/workflows/") ||
+ strings.HasPrefix(path, "internal/")
}
func (m *Metadata) expandDependents(affected []string) []string {
diff --git a/internal/buildkit/metadata_test.go b/internal/buildkit/metadata_test.go
index caada06a..e9b2980d 100644
--- a/internal/buildkit/metadata_test.go
+++ b/internal/buildkit/metadata_test.go
@@ -1,8 +1,11 @@
package buildkit
import (
+ "archive/zip"
"bytes"
+ "io"
"os"
+ "os/exec"
"path/filepath"
"strings"
"testing"
@@ -27,11 +30,18 @@ func TestMetadataTagsMatchForkedVersioning(t *testing.T) {
want []string
}{
{image: "activemq5", mode: "fallback", fallback: "local", want: []string{"local-5"}},
+ {image: "archivesspace", mode: "fallback", fallback: "local", want: []string{"local"}},
{image: "drupal-php83", mode: "fallback", fallback: "branch/name", want: []string{"branch-name-php83"}},
{image: "drupal-php83", mode: "version", fallback: "branch/name", want: []string{nginxTag + "-php83"}},
{image: "drupal-php84", mode: "version", fallback: "branch/name", want: []string{nginxTag + "-php84"}},
{image: "islandora-php84", mode: "version", fallback: "branch/name", want: []string{nginxTag + "-php84"}},
{image: "wp-php84", mode: "version", fallback: "branch/name", want: []string{nginxTag + "-php84"}},
+ {image: "ojs-php83", mode: "version", fallback: "branch/name", want: []string{"3.5.0-5-php83", "3.5-php83", "3-php83", "php83"}},
+ {image: "ojs-php84", mode: "version", fallback: "branch/name", want: []string{"3.5.0-5-php84", "3.5-php84", "3-php84", "php84", "latest-php84", "latest"}},
+ {image: "omeka-s-php83", mode: "version", fallback: "branch/name", want: []string{"4.2.1-php83", "4.2-php83", "4-php83", "php83"}},
+ {image: "omeka-s-php84", mode: "version", fallback: "branch/name", want: []string{"4.2.1-php84", "4.2-php84", "4-php84", "php84", "latest-php84", "latest"}},
+ {image: "omeka-classic-php83", mode: "version", fallback: "branch/name", want: []string{"3.2.1-php83", "3.2-php83", "3-php83", "php83"}},
+ {image: "omeka-classic-php84", mode: "version", fallback: "branch/name", want: []string{"3.2.1-php84", "3.2-php84", "3-php84", "php84", "latest-php84", "latest"}},
}
for _, tt := range cases {
@@ -50,6 +60,172 @@ func TestMetadataTagsMatchForkedVersioning(t *testing.T) {
}
}
+func TestBundledPHPAppsDownloadVerifiedReleases(t *testing.T) {
+ root := repoRoot(t)
+ cases := []struct {
+ image string
+ version string
+ checksum string
+ dest string
+ }{
+ {image: "ojs", version: "3.5.0-5", checksum: "fd59cb1add60ab4e56e40ed843ea43ff84af11f0b64c3940f2e03186ad11445e", dest: "/var/www/ojs"},
+ {image: "omeka-s", version: "4.2.1", checksum: "cc27d1c7aca0209523d19aa285f4a08e29e34950dcc446951a7c1311de348e82", dest: "/var/www/omeka-s"},
+ {image: "omeka-classic", version: "3.2.1", checksum: "2cb4d65511321cc5c009cb61516d9ed97378a800fc4a26eb46450c3c4ca230c2", dest: "/var/www/omeka-classic"},
+ }
+
+ for _, tt := range cases {
+ dockerfile := filepath.Join(root, "images", tt.image, "Dockerfile")
+ content, err := os.ReadFile(dockerfile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, want := range []string{
+ "SOFTWARE_VERSION=" + tt.version,
+ "SHA256=\"" + tt.checksum + "\"",
+ "download.sh",
+ "--sha256 \"${SHA256}\"",
+ "--strip",
+ "--dest " + tt.dest,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("%s missing %q", dockerfile, want)
+ }
+ }
+ }
+}
+
+func TestMarketedApplicationImagesOverrideInheritedLicense(t *testing.T) {
+ root := repoRoot(t)
+ cases := map[string]string{
+ "archivesspace": "ECL-2.0",
+ "archivesspace-solr": "Apache-2.0 AND ECL-2.0",
+ "drupal": "GPL-2.0-or-later",
+ "ojs": "GPL-3.0-only",
+ "omeka-classic": "GPL-3.0-or-later",
+ "omeka-s": "GPL-3.0-only",
+ "wp": "GPL-2.0-or-later",
+ }
+
+ for image, license := range cases {
+ dockerfile := filepath.Join(root, "images", image, "Dockerfile")
+ content, err := os.ReadFile(dockerfile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := `LABEL org.opencontainers.image.licenses="` + license + `"`
+ if !strings.Contains(string(content), want) {
+ t.Errorf("%s must override the inherited license with %q", dockerfile, license)
+ }
+ }
+}
+
+func TestBundledApplicationImagesPreserveLicenseTexts(t *testing.T) {
+ root := repoRoot(t)
+ cases := map[string]string{
+ "archivesspace": "cp /archivesspace/COPYING /usr/share/licenses/archivesspace/COPYING",
+ "archivesspace-solr": "cp /tmp/archivesspace-release/archivesspace/COPYING /usr/share/licenses/archivesspace-solr/COPYING",
+ "ojs": "cp docs/COPYING /usr/share/licenses/ojs/COPYING",
+ "omeka-classic": "cp /var/www/omeka-classic/license.txt /usr/share/licenses/omeka-classic/license.txt",
+ "omeka-s": "cp /var/www/omeka-s/LICENSE /usr/share/licenses/omeka-s/LICENSE",
+ }
+
+ for image, want := range cases {
+ dockerfile := filepath.Join(root, "images", image, "Dockerfile")
+ content, err := os.ReadFile(dockerfile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), want) {
+ t.Errorf("%s must preserve the bundled application's license text at a standard path", dockerfile)
+ }
+ }
+}
+
+func TestCurlHealthchecksFailOnHTTPError(t *testing.T) {
+ root := repoRoot(t)
+ entries, err := os.ReadDir(filepath.Join(root, "images"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, entry := range entries {
+ dockerfile := filepath.Join(root, "images", entry.Name(), "Dockerfile")
+ content, err := os.ReadFile(dockerfile)
+ if os.IsNotExist(err) {
+ continue
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, line := range strings.Split(string(content), "\n") {
+ if strings.Contains(line, "HEALTHCHECK") && strings.Contains(line, "curl ") && !strings.Contains(line, "curl -f") {
+ t.Errorf("%s healthcheck must make curl fail on HTTP errors: %s", dockerfile, line)
+ }
+ }
+ }
+}
+
+func TestActiveMQHealthchecksKeepCredentialsOutOfArguments(t *testing.T) {
+ root := repoRoot(t)
+ for _, image := range []string{"activemq5", "activemq6"} {
+ dockerfile := filepath.Join(root, "images", image, "Dockerfile")
+ content, err := os.ReadFile(dockerfile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "HEALTHCHECK CMD activemq-healthcheck.sh") {
+ t.Errorf("%s must delegate to the credential-safe healthcheck helper", dockerfile)
+ }
+ if strings.Contains(string(content), "HEALTHCHECK CMD curl") || strings.Contains(string(content), "cat /var/run/s6/container_environment/ACTIVEMQ_WEB_ADMIN_PASSWORD") {
+ t.Errorf("%s must not expand web credentials in the healthcheck command", dockerfile)
+ }
+
+ helper := filepath.Join(root, "images", image, "rootfs", "usr", "local", "bin", "activemq-healthcheck.sh")
+ helperContent, err := os.ReadFile(helper)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(helperContent)
+ for _, want := range []string{
+ "ACTIVEMQ_WEB_ADMIN_NAME ACTIVEMQ_WEB_ADMIN_PASSWORD",
+ "umask 077",
+ "chmod 0600",
+ "trap 'rm -f",
+ `curl --config "${curl_config}"`,
+ `'fail'`,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("%s missing %q", helper, want)
+ }
+ }
+ if strings.Contains(got, `curl -u`) || strings.Contains(got, `curl --user`) {
+ t.Errorf("%s passes credentials through curl arguments", helper)
+ }
+
+ for _, template := range []string{"credentials.properties.tmpl", "groups.properties.tmpl", "jetty-realm.properties.tmpl", "users.properties.tmpl"} {
+ templateFile := filepath.Join(root, "images", image, "rootfs", "etc", "confd", "templates", template)
+ templateContent, err := os.ReadFile(templateFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rendered := string(templateContent)
+ for _, want := range []string{`define "propertiesValueEscape"`, `replace $backslashes "\r" "\\r"`, `replace $carriageReturns "\n" "\\n"`, `replace $newlines "\t" "\\t"`, `replace $tabs " " "\\ "`} {
+ if !strings.Contains(rendered, want) {
+ t.Errorf("%s missing Java properties escaping contract %q", templateFile, want)
+ }
+ }
+ if template != "credentials.properties.tmpl" {
+ for _, want := range []string{`define "propertiesKeyEscape"`, `replace $spaces ":" "\\:"`, `replace $colons "=" "\\="`, `template "propertiesKeyEscape"`} {
+ if !strings.Contains(rendered, want) {
+ t.Errorf("%s missing Java properties key escaping contract %q", templateFile, want)
+ }
+ }
+ }
+ }
+ }
+}
+
func TestComposeEnvUsesCurrentVersionedImageForGenericAlias(t *testing.T) {
root := repoRoot(t)
metadata, err := LoadMetadata(root)
@@ -105,6 +281,74 @@ func TestPlanSupportsLevelFourImages(t *testing.T) {
}
}
+func TestPlannerImplementationPathsAreGlobal(t *testing.T) {
+ global := []string{
+ "cmd/buildkit/main.go",
+ "go.mod",
+ "go.sum",
+ "internal/buildkit/metadata.go",
+ }
+ for _, path := range global {
+ if !isGlobalPath(path) {
+ t.Errorf("isGlobalPath(%q) = false, want true", path)
+ }
+ }
+
+ if isGlobalPath("marketing/copy.md") {
+ t.Error("unrelated documentation must not trigger a global image build")
+ }
+}
+
+func TestMakefileNormalizesBranchWithoutShellReinterpolation(t *testing.T) {
+ root := repoRoot(t)
+ makefile := filepath.Join(root, "Makefile")
+ content, err := os.ReadFile(makefile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if !strings.Contains(got, "rev-parse --abbrev-ref HEAD | sed -E") {
+ t.Fatalf("%s must pipe the raw Git ref directly into normalization", makefile)
+ }
+ if strings.Contains(got, "BRANCH_RAW") {
+ t.Fatalf("%s must not interpolate an untrusted raw branch into another shell command", makefile)
+ }
+ if !strings.Contains(got, `BRANCH="$(BRANCH)"`) {
+ t.Fatalf("%s must quote the normalized branch passed to buildx bake", makefile)
+ }
+}
+
+func TestBakeCacheRefsNormalizeSlashBranch(t *testing.T) {
+ if _, err := exec.LookPath("docker"); err != nil {
+ t.Skip("docker CLI is not available")
+ }
+ root := repoRoot(t)
+ command := exec.Command("docker", "buildx", "bake", "--print", "activemq5-amd64")
+ command.Dir = root
+ command.Env = append(os.Environ(), "BRANCH=feature/foo")
+ output, err := command.CombinedOutput()
+ if err != nil {
+ t.Skipf("docker buildx bake is not available: %v\n%s", err, output)
+ }
+ got := string(output)
+ if !strings.Contains(got, "activemq5-feature-foo-amd64") {
+ t.Fatalf("slash branch was not normalized in cache refs:\n%s", got)
+ }
+ if strings.Contains(got, "feature/foo") {
+ t.Fatalf("slash branch leaked into a cache reference:\n%s", got)
+ }
+}
+
+func TestUpdateSHAReadmeIsFailClosed(t *testing.T) {
+ root := repoRoot(t)
+ testScript := filepath.Join(root, "ci", "tests", "update-sha-readme.sh")
+ command := exec.Command("bash", testScript)
+ output, err := command.CombinedOutput()
+ if err != nil {
+ t.Fatalf("%s failed: %v\n%s", testScript, err, output)
+ }
+}
+
func TestPlanOutputUsesEmptyArraysForEmptyLevels(t *testing.T) {
root := repoRoot(t)
metadata, err := LoadMetadata(root)
@@ -167,6 +411,166 @@ func TestOJSEnableBeaconTemplateUsesTruthyHelper(t *testing.T) {
}
}
+func TestOJSConfigEscapesDoubleQuotedSecrets(t *testing.T) {
+ root := repoRoot(t)
+ templateFile := filepath.Join(root, "images", "ojs", "rootfs", "etc", "confd", "templates", "config.inc.tmpl")
+ content, err := os.ReadFile(templateFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, variable := range []string{"OJS_SECRET_KEY", "OJS_SALT", "OJS_API_KEY_SECRET"} {
+ want := `template "doubleQuoteEscape" (getenv "` + variable + `")`
+ if !strings.Contains(got, want) {
+ t.Errorf("%s does not escape %s before rendering it in double quotes", templateFile, variable)
+ }
+ }
+}
+
+func TestOJSInstalledStateIsDurableAcrossConfdRendering(t *testing.T) {
+ root := repoRoot(t)
+ templateFile := filepath.Join(root, "images", "ojs", "rootfs", "etc", "confd", "templates", "config.inc.tmpl")
+ content, err := os.ReadFile(templateFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if strings.Contains(got, "installed = Off") {
+ t.Fatalf("%s hardcodes the pre-install lifecycle state", templateFile)
+ }
+ if !strings.Contains(got, `installed = {{ template "booleanOnOff" (getenv "OJS_INSTALLED") }}`) {
+ t.Fatalf("%s must render the reconciled OJS lifecycle state", templateFile)
+ }
+
+ for _, relative := range []string{
+ "images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd-oneshot/dependencies.d/ojs-install-state",
+ "images/ojs/rootfs/etc/s6-overlay/s6-rc.d/confd/dependencies.d/ojs-setup",
+ "images/ojs/tests/ContinuousConfdPreservesInstalledState/docker-compose.yml",
+ } {
+ if _, err := os.Stat(filepath.Join(root, relative)); err != nil {
+ t.Errorf("missing OJS lifecycle regression asset %s: %v", relative, err)
+ }
+ }
+
+ setupFile := filepath.Join(root, "images", "ojs", "rootfs", "etc", "s6-overlay", "scripts", "ojs-setup.sh")
+ setup, err := os.ReadFile(setupFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stateIndex := strings.Index(string(setup), "/etc/s6-overlay/scripts/ojs-install-state.sh")
+ renderIndex := strings.LastIndex(string(setup), "render_ojs_config")
+ if stateIndex < 0 || renderIndex < 0 || stateIndex > renderIndex {
+ t.Fatalf("%s must persist installed state before rerendering config", setupFile)
+ }
+}
+
+func TestCredentialBearingConfdTemplatesAreNotWorldReadable(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ "images/alpaca/rootfs/etc/confd/conf.d/alpaca.properties.toml",
+ "images/ojs/rootfs/etc/confd/conf.d/config.inc.toml",
+ "images/tomcat9/rootfs/etc/confd/conf.d/tomcat-users.toml",
+ "images/tomcat11/rootfs/etc/confd/conf.d/tomcat-users.toml",
+ }
+
+ for _, relative := range files {
+ content, err := os.ReadFile(filepath.Join(root, relative))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), `mode = "0640"`) {
+ t.Errorf("%s must render service-readable credentials without world access", relative)
+ }
+ }
+}
+
+func TestOmekaDatabaseTemplatesEscapeDoubleQuotedValues(t *testing.T) {
+ root := repoRoot(t)
+ cases := []struct {
+ file string
+ fields []string
+ }{
+ {
+ file: filepath.Join(root, "images", "omeka-s", "rootfs", "etc", "confd", "templates", "database.ini.tmpl"),
+ fields: []string{"DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD"},
+ },
+ {
+ file: filepath.Join(root, "images", "omeka-classic", "rootfs", "etc", "confd", "templates", "db.ini.tmpl"),
+ fields: []string{"DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASSWORD", "OMEKA_CLASSIC_TABLE_PREFIX"},
+ },
+ }
+
+ for _, tt := range cases {
+ content, err := os.ReadFile(tt.file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if !strings.Contains(got, `define "doubleQuoteEscape"`) {
+ t.Errorf("%s does not define doubleQuoteEscape", tt.file)
+ }
+ for _, field := range tt.fields {
+ want := `template "doubleQuoteEscape" (getenv "` + field + `")`
+ if !strings.Contains(got, want) {
+ t.Errorf("%s does not escape %s", tt.file, field)
+ }
+ }
+ }
+}
+
+func TestArchivesSpaceJDBCCredentialsAreURLAndRubyEscaped(t *testing.T) {
+ root := repoRoot(t)
+ templateFile := filepath.Join(root, "images", "archivesspace", "rootfs", "etc", "confd", "templates", "archivesspace.config.rb.tmpl")
+ content, err := os.ReadFile(templateFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, field := range []string{"$dbUser", "$dbPassword"} {
+ want := `URI.encode_www_form_component('{{ template "sqlEscape" ` + field + ` }}')`
+ if !strings.Contains(got, want) {
+ t.Errorf("%s must Ruby-escape and URL-encode %s", templateFile, field)
+ }
+ }
+ for _, unsafe := range []string{
+ `&user={{ template "sqlEscape" $dbUser }}`,
+ `&password={{ template "sqlEscape" $dbPassword }}`,
+ } {
+ if strings.Contains(got, unsafe) {
+ t.Errorf("%s inserts a raw JDBC credential: %s", templateFile, unsafe)
+ }
+ }
+
+ composeFile := filepath.Join(root, "images", "archivesspace", "tests", "ServiceHealthcheck", "docker-compose.yml")
+ compose, err := os.ReadFile(composeFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Count(string(compose), `DB_PASSWORD: "archive&=space%#password"`) != 2 {
+ t.Errorf("%s must exercise URL-sensitive database credentials for both services", composeFile)
+ }
+}
+
+func TestOmekaInstallersKeepAdminPasswordsOutOfCurlArguments(t *testing.T) {
+ root := repoRoot(t)
+ for _, image := range []string{"omeka-s", "omeka-classic"} {
+ file := filepath.Join(root, "images", image, "rootfs", "etc", "s6-overlay", "scripts", image+"-setup.sh")
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if strings.Contains(got, `--data-urlencode "password=${`) || strings.Contains(got, `-d "user[password`) {
+ t.Errorf("%s passes an admin password value in curl argv", file)
+ }
+ for _, want := range []string{"umask 077", `>"${form_dir}/admin-password"`, `@${form_dir}/admin-password`} {
+ if !strings.Contains(got, want) {
+ t.Errorf("%s missing secure form handling %q", file, want)
+ }
+ }
+ }
+}
+
func TestBaseVaultSecretsBootstrap(t *testing.T) {
root := repoRoot(t)
@@ -200,6 +604,11 @@ func TestBaseVaultSecretsBootstrap(t *testing.T) {
if !strings.Contains(string(containerEnvironmentContent), "GOOGLE_APPLICATION_CREDENTIALS") {
t.Fatalf("%s must preserve GOOGLE_APPLICATION_CREDENTIALS as a file path", containerEnvironment)
}
+ for _, want := range []string{"umask 077", "chmod 0700 /var/run/s6/container_environment", "chmod 0600"} {
+ if !strings.Contains(string(containerEnvironmentContent), want) {
+ t.Fatalf("%s must protect imported secret values with %q", containerEnvironment, want)
+ }
+ }
script := filepath.Join(root, "images", "base", "rootfs", "etc", "s6-overlay", "scripts", "vault-secrets.sh")
scriptContent, err := os.ReadFile(script)
@@ -215,6 +624,8 @@ func TestBaseVaultSecretsBootstrap(t *testing.T) {
"secret-site",
"chmod 0400",
"chown \"${app_uid}:0\"",
+ `if [[ "${name}" = "DB_ROOT_PASSWORD" ]]`,
+ "chown 0:0",
} {
if !strings.Contains(string(scriptContent), want) {
t.Fatalf("%s missing %q", script, want)
@@ -222,6 +633,553 @@ func TestBaseVaultSecretsBootstrap(t *testing.T) {
}
}
+func TestContainerEnvironmentEscapesExeclineValues(t *testing.T) {
+ root := repoRoot(t)
+ file := filepath.Join(root, "images", "base", "rootfs", "etc", "s6-overlay", "scripts", "container-environment.sh")
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if !strings.Contains(got, `define "execlineEscape"`) {
+ t.Fatalf("%s must define an execline value escape helper", file)
+ }
+ if !strings.Contains(got, `template \"execlineEscape\"`) {
+ t.Fatalf("%s must escape values before generating the execline import script", file)
+ }
+}
+
+func TestWithContEnvApplicationLongrunsDropDatabaseRootPassword(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ filepath.Join(root, "images", "activemq5", "rootfs", "etc", "s6-overlay", "s6-rc.d", "activemq", "run"),
+ filepath.Join(root, "images", "activemq6", "rootfs", "etc", "s6-overlay", "s6-rc.d", "activemq", "run"),
+ filepath.Join(root, "images", "alpaca", "rootfs", "etc", "s6-overlay", "s6-rc.d", "alpaca", "run"),
+ filepath.Join(root, "images", "archivesspace", "rootfs", "usr", "local", "bin", "archivesspace-startup.sh"),
+ filepath.Join(root, "images", "solr9", "rootfs", "etc", "s6-overlay", "s6-rc.d", "solr", "run"),
+ filepath.Join(root, "images", "solr10", "rootfs", "etc", "s6-overlay", "s6-rc.d", "solr", "run"),
+ filepath.Join(root, "images", "tomcat9", "rootfs", "etc", "s6-overlay", "s6-rc.d", "tomcat", "run"),
+ filepath.Join(root, "images", "tomcat11", "rootfs", "etc", "s6-overlay", "s6-rc.d", "tomcat", "run"),
+ }
+
+ for _, file := range files {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ unsetIndex := strings.Index(got, "unset DB_ROOT_PASSWORD")
+ execIndex := strings.LastIndex(got, "exec ")
+ if unsetIndex < 0 || execIndex < 0 || unsetIndex > execIndex {
+ t.Errorf("%s must unset DB_ROOT_PASSWORD before its final exec", file)
+ }
+ }
+}
+
+func TestApplicationDatabaseBootstrapIsExplicit(t *testing.T) {
+ root := repoRoot(t)
+ baseDockerfile := filepath.Join(root, "images", "base", "Dockerfile")
+ content, err := os.ReadFile(baseDockerfile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ base := string(content)
+ if !strings.Contains(base, "DB_BOOTSTRAP_ENABLED=false") {
+ t.Fatalf("%s must disable application-side database bootstrap by default", baseDockerfile)
+ }
+ if strings.Contains(base, "DB_ROOT_PASSWORD=password") {
+ t.Fatalf("%s must not provide a placeholder database root password", baseDockerfile)
+ }
+
+ files := []string{
+ filepath.Join(root, "images", "drupal", "rootfs", "etc", "s6-overlay", "scripts", "install.sh"),
+ filepath.Join(root, "images", "ojs", "rootfs", "etc", "s6-overlay", "scripts", "ojs-setup.sh"),
+ filepath.Join(root, "images", "omeka-s", "rootfs", "etc", "s6-overlay", "scripts", "omeka-s-setup.sh"),
+ filepath.Join(root, "images", "omeka-classic", "rootfs", "etc", "s6-overlay", "scripts", "omeka-classic-setup.sh"),
+ filepath.Join(root, "images", "wp", "rootfs", "etc", "s6-overlay", "scripts", "wordpress-setup.sh"),
+ filepath.Join(root, "images", "archivesspace", "rootfs", "usr", "local", "bin", "archivesspace-startup.sh"),
+ filepath.Join(root, "images", "fcrepo6", "rootfs", "etc", "s6-overlay", "scripts", "fcrepo-setup.sh"),
+ filepath.Join(root, "images", "fcrepo7", "rootfs", "etc", "s6-overlay", "scripts", "fcrepo-setup.sh"),
+ }
+ for _, file := range files {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "database_bootstrap_if_enabled") {
+ t.Errorf("%s must gate database creation behind the explicit bootstrap helper", file)
+ }
+ }
+}
+
+func TestMarketedApplicationImagesDoNotShipKnownCredentials(t *testing.T) {
+ root := repoRoot(t)
+ images := []string{"archivesspace", "drupal", "ojs", "omeka-s", "omeka-classic", "wp"}
+ banned := []string{
+ "DB_PASSWORD=changeme",
+ "DRUPAL_DEFAULT_ACCOUNT_PASSWORD=password",
+ "DRUPAL_DEFAULT_SALT=9PPaL0",
+ "OJS_SALT=changeme",
+ "OJS_API_KEY_SECRET=changeme",
+ "OJS_SECRET_KEY=changeme",
+ "OJS_ADMIN_PASSWORD=changeme",
+ "OMEKA_S_ADMIN_PASSWORD=changeme",
+ "OMEKA_CLASSIC_ADMIN_PASSWORD=changeme",
+ "WORDPRESS_ADMIN_PASSWORD=changeme",
+ "WORDPRESS_AUTH_KEY=changeme",
+ "WORDPRESS_NONCE_SALT=changeme",
+ }
+ for _, image := range images {
+ file := filepath.Join(root, "images", image, "Dockerfile")
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, value := range banned {
+ if strings.Contains(got, value) {
+ t.Errorf("%s ships known credential %q", file, value)
+ }
+ }
+ }
+}
+
+func TestNetworkServicesDoNotShipKnownCredentials(t *testing.T) {
+ root := repoRoot(t)
+ cases := []struct {
+ file string
+ banned []string
+ }{
+ {file: filepath.Join(root, "images", "activemq5", "Dockerfile"), banned: []string{"ACTIVEMQ_PASSWORD=password", "ACTIVEMQ_WEB_ADMIN_PASSWORD=password"}},
+ {file: filepath.Join(root, "images", "activemq6", "Dockerfile"), banned: []string{"ACTIVEMQ_PASSWORD=password", "ACTIVEMQ_WEB_ADMIN_PASSWORD=password"}},
+ {file: filepath.Join(root, "images", "alpaca", "Dockerfile"), banned: []string{"ALPACA_JMS_PASSWORD=password"}},
+ {file: filepath.Join(root, "images", "base", "Dockerfile"), banned: []string{"DB_PASSWORD=password", "JWT_ADMIN_TOKEN=islandora"}},
+ {file: filepath.Join(root, "images", "fcrepo6", "Dockerfile"), banned: []string{"DB_PASSWORD=password"}},
+ {file: filepath.Join(root, "images", "fcrepo7", "Dockerfile"), banned: []string{"DB_PASSWORD=password"}},
+ {file: filepath.Join(root, "images", "tomcat9", "Dockerfile"), banned: []string{"TOMCAT_ADMIN_PASSWORD=password", "TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^.*$", "TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^(127[.]|::1$)"}},
+ {file: filepath.Join(root, "images", "tomcat11", "Dockerfile"), banned: []string{"TOMCAT_ADMIN_PASSWORD=password", "TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^.*$", "TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^(127[.]|::1$)"}},
+ }
+ for _, tt := range cases {
+ content, err := os.ReadFile(tt.file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, value := range tt.banned {
+ if strings.Contains(string(content), value) {
+ t.Errorf("%s ships known credential %q", tt.file, value)
+ }
+ }
+ }
+}
+
+func TestOJSSetupInstallsAgainstExternalDatabaseHost(t *testing.T) {
+ root := repoRoot(t)
+ library := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "share", "libops", "database.sh")
+ environmentLibrary := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "share", "libops", "environment.sh")
+ setup := filepath.Join(root, "images", "ojs", "rootfs", "etc", "s6-overlay", "scripts", "ojs-setup.sh")
+ harness := `
+set -euo pipefail
+export OJS_SETUP_LIBRARY_ONLY=true
+export LIBOPS_DATABASE_LIBRARY="$1"
+export LIBOPS_ENVIRONMENT_LIBRARY="$2"
+source "$3"
+calls=$(mktemp)
+mysql_create_database() { printf 'bootstrap\n' >>"${calls}"; }
+wait_for_database() { printf 'wait\n' >>"${calls}"; }
+check_ojs_installed() { printf 'check\n' >>"${calls}"; return 1; }
+install_ojs() { printf 'install\n' >>"${calls}"; }
+export DB_HOST=external-database.example
+export DB_BOOTSTRAP_ENABLED=false
+export OJS_ADMIN_PASSWORD=test-admin-password
+setup_ojs_database
+test "$(cat "${calls}")" = $'wait\ncheck\ninstall'
+`
+ command := exec.Command("bash", "-c", harness, "bash", library, environmentLibrary, setup)
+ if output, err := command.CombinedOutput(); err != nil {
+ t.Fatalf("external database setup stub failed: %v\n%s", err, output)
+ }
+}
+
+func TestNonDatabaseLongRunsDropDatabaseRootPassword(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ filepath.Join(root, "images", "base", "rootfs", "etc", "s6-overlay", "s6-rc.d", "confd", "run"),
+ filepath.Join(root, "images", "nginx", "rootfs", "etc", "s6-overlay", "s6-rc.d", "nginx", "run"),
+ filepath.Join(root, "images", "php83", "rootfs", "etc", "s6-overlay", "s6-rc.d", "fpm", "run"),
+ filepath.Join(root, "images", "php84", "rootfs", "etc", "s6-overlay", "s6-rc.d", "fpm", "run"),
+ filepath.Join(root, "images", "scyllaridae", "rootfs", "etc", "s6-overlay", "s6-rc.d", "scyllaridae", "run"),
+ }
+ for _, file := range files {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "unset DB_ROOT_PASSWORD") && !strings.Contains(string(content), "unexport DB_ROOT_PASSWORD") {
+ t.Errorf("%s retains DB_ROOT_PASSWORD in a non-database longrun", file)
+ }
+ }
+}
+
+func TestPHPLongRunsDropSetupOnlyAdminPasswords(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ filepath.Join(root, "images", "nginx", "rootfs", "etc", "s6-overlay", "s6-rc.d", "nginx", "run"),
+ filepath.Join(root, "images", "php83", "rootfs", "etc", "s6-overlay", "s6-rc.d", "fpm", "run"),
+ filepath.Join(root, "images", "php84", "rootfs", "etc", "s6-overlay", "s6-rc.d", "fpm", "run"),
+ }
+ variables := []string{
+ "DRUPAL_DEFAULT_ACCOUNT_PASSWORD",
+ "OJS_ADMIN_PASSWORD",
+ "OMEKA_CLASSIC_ADMIN_PASSWORD",
+ "OMEKA_S_ADMIN_PASSWORD",
+ "WORDPRESS_ADMIN_PASSWORD",
+ }
+ for _, file := range files {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, variable := range variables {
+ if !strings.Contains(got, variable) {
+ t.Errorf("%s retains setup-only %s", file, variable)
+ }
+ }
+ }
+}
+
+func TestWordPressAdminPasswordUsesPromptInput(t *testing.T) {
+ root := repoRoot(t)
+ file := filepath.Join(root, "images", "wp", "rootfs", "etc", "s6-overlay", "scripts", "wordpress-setup.sh")
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if strings.Contains(got, `--admin_password="${WORDPRESS_ADMIN_PASSWORD}"`) {
+ t.Fatalf("%s exposes the administrator password in wp-cli argv", file)
+ }
+ if !strings.Contains(got, `--prompt=admin_password`) || !strings.Contains(got, `printf '%s\n' "${WORDPRESS_ADMIN_PASSWORD}"`) {
+ t.Fatalf("%s must provide the administrator password through wp-cli prompt input", file)
+ }
+}
+
+func TestTomcatManagerDefaultAllowsOnlyLoopback(t *testing.T) {
+ root := repoRoot(t)
+ for _, version := range []string{"tomcat9", "tomcat11"} {
+ file := filepath.Join(root, "images", version, "Dockerfile")
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE=^(127[.].*|::1)$") {
+ t.Errorf("%s does not restrict the manager valve to complete loopback addresses", file)
+ }
+
+ templateFile := filepath.Join(root, "images", version, "rootfs", "etc", "confd", "templates", "tomcat-users.xml.tmpl")
+ templateContent, err := os.ReadFile(templateFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(templateContent), `template "xmlAttributeEscape" (getenv "TOMCAT_ADMIN_PASSWORD")`) {
+ t.Errorf("%s does not XML-escape the administrator password", templateFile)
+ }
+ if strings.Contains(string(templateContent), `$escaped = replace`) {
+ t.Errorf("%s uses template reassignment unsupported by the bundled confd", templateFile)
+ }
+
+ readmeFile := filepath.Join(root, "images", version, "README.md")
+ readmeContent, err := os.ReadFile(readmeFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, want := range []string{"-p 127.0.0.1:8080:8080", `--env TOMCAT_MANAGER_REMOTE_ADDRESS_VALVE='.*'`} {
+ if !strings.Contains(string(readmeContent), want) {
+ t.Errorf("%s local manager example missing %q", readmeFile, want)
+ }
+ }
+ }
+}
+
+func TestDrupalInstalledQueryDoesNotInterpolateDatabaseName(t *testing.T) {
+ root := repoRoot(t)
+ library := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "share", "libops", "database.sh")
+ environmentLibrary := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "share", "libops", "environment.sh")
+ setup := filepath.Join(root, "images", "drupal", "rootfs", "etc", "s6-overlay", "scripts", "install.sh")
+ harness := `
+set -euo pipefail
+export DRUPAL_SETUP_LIBRARY_ONLY=true
+export LIBOPS_DATABASE_LIBRARY="$1"
+export LIBOPS_ENVIRONMENT_LIBRARY="$2"
+export DB_NAME="external-db-'quoted"
+source "$3"
+query=$(mysql_count_query)
+grep -Fq 'WHERE table_schema = DATABASE();' <<<"${query}"
+if grep -Fq "${DB_NAME}" <<<"${query}"; then exit 1; fi
+`
+ command := exec.Command("bash", "-c", harness, "bash", library, environmentLibrary, setup)
+ if output, err := command.CombinedOutput(); err != nil {
+ t.Fatalf("Drupal installed query interpolation test failed: %v\n%s", err, output)
+ }
+}
+
+func TestCommandHelpersDoNotEvalArguments(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "download.sh"),
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "create-service-user.sh"),
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "cleanup.sh"),
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "confd-import-environment.sh"),
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "wait-for-open-port.sh"),
+ filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "confd-render-templates.sh"),
+ filepath.Join(root, "images", "drupal", "rootfs", "usr", "local", "bin", "install-drupal-site.sh"),
+ }
+ for _, file := range files {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(content), "eval "+"set --") {
+ t.Errorf("%s evaluates reconstructed command-line arguments", file)
+ }
+
+ marker := filepath.Join(t.TempDir(), "argument-was-executed")
+ malicious := `space "$(touch ` + marker + `)" 'quoted'`
+ command := exec.Command("bash", file, "--help", malicious)
+ command.Env = append(os.Environ(), "DOWNLOAD_CACHE_DIRECTORY=/tmp")
+ _ = command.Run()
+ if _, err := os.Stat(marker); !os.IsNotExist(err) {
+ t.Errorf("%s executed a command embedded in an argument", file)
+ }
+ }
+}
+
+func TestDatabaseHelpersDoNotForwardPasswordsInArguments(t *testing.T) {
+ root := repoRoot(t)
+ cases := []struct {
+ file string
+ banned []string
+ }{
+ {
+ file: filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "create-database.sh"),
+ banned: []string{
+ `--password "${PASSWORD}"`,
+ },
+ },
+ {
+ file: filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "execute-sql-file.sh"),
+ banned: []string{
+ `--password "${PASSWORD}"`,
+ `--password="${PASSWORD}"`,
+ },
+ },
+ {
+ file: filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "wait-for-database.sh"),
+ banned: []string{
+ `--password="${PASSWORD}"`,
+ },
+ },
+ }
+ for _, tt := range cases {
+ content, err := os.ReadFile(tt.file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, banned := range tt.banned {
+ if strings.Contains(string(content), banned) {
+ t.Errorf("%s exposes a password through child argv: %s", tt.file, banned)
+ }
+ }
+ }
+}
+
+func TestDrupalInstallersKeepDatabasePasswordsOutOfArguments(t *testing.T) {
+ root := repoRoot(t)
+ primary := filepath.Join(root, "images", "drupal", "rootfs", "etc", "s6-overlay", "scripts", "install.sh")
+ helper := filepath.Join(root, "images", "drupal", "rootfs", "usr", "local", "bin", "install-drupal-site.sh")
+ islandora := filepath.Join(root, "images", "islandora", "rootfs", "etc", "islandora", "utilities.sh")
+
+ for _, file := range []string{primary, helper, islandora} {
+ content, err := os.ReadFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ for _, banned := range []string{"--db-url=", "--db-password", "--account-pass="} {
+ if strings.Contains(got, banned) {
+ t.Errorf("%s still forwards a credential using %q", file, banned)
+ }
+ }
+ }
+
+ helperContent, err := os.ReadFile(helper)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(helperContent)
+ for _, want := range []string{
+ `rawurlencode((string) getenv("LIBOPS_DRUPAL_URI_COMPONENT"))`,
+ `DRUSH_COMMAND_SITE_INSTALL_OPTIONS_DB_URL="${drush_database_url}"`,
+ `DB_PASSWORD=${LIBOPS_DRUPAL_INSTALL_DB_PASSWORD:-}`,
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("%s missing %q", helper, want)
+ }
+ }
+}
+
+func TestGenericDrupalInstallerEncodesDatabaseURLWithoutArgumentExposure(t *testing.T) {
+ root := repoRoot(t)
+ helper := filepath.Join(root, "images", "drupal", "rootfs", "usr", "local", "bin", "install-drupal-site.sh")
+ binDir := t.TempDir()
+ outputFile := filepath.Join(t.TempDir(), "drush-db-url")
+ rawPassword := `p"a\ss'word&+=:/?#%`
+
+ executeSQL := filepath.Join(binDir, "execute-sql-file.sh")
+ if err := os.WriteFile(executeSQL, []byte("#!/bin/sh\nprintf '0\\n'\n"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ drush := filepath.Join(binDir, "drush")
+ drushScript := `#!/bin/sh
+for argument do
+ case "${argument}" in
+ *"${EXPECTED_RAW_PASSWORD}"*) exit 42 ;;
+ esac
+done
+printf '%s\n' "${DRUSH_COMMAND_SITE_INSTALL_OPTIONS_DB_URL}" >"${DRUSH_OUTPUT}"
+`
+ if err := os.WriteFile(drush, []byte(drushScript), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ command := exec.Command("bash", helper,
+ "--host", "database",
+ "--port", "3306",
+ "--db-user", "user@name",
+ "--db-name", "drupal/name",
+ "standard", "--sites-subdir=default",
+ )
+ command.Env = append(os.Environ(),
+ "PATH="+binDir+":"+os.Getenv("PATH"),
+ "LIBOPS_DRUPAL_INSTALL_DB_PASSWORD="+rawPassword,
+ "EXPECTED_RAW_PASSWORD="+rawPassword,
+ "DRUSH_OUTPUT="+outputFile,
+ )
+ if output, err := command.CombinedOutput(); err != nil {
+ t.Fatalf("generic Drupal installer failed: %v\n%s", err, output)
+ }
+ url, err := os.ReadFile(outputFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := "mysql://user%40name:p%22a%5Css%27word%26%2B%3D%3A%2F%3F%23%25@database:3306/drupal%2Fname\n"
+ if string(url) != want {
+ t.Fatalf("encoded Drush database URL = %q, want %q", url, want)
+ }
+}
+
+func TestPHPRuntimeSecretConfigsAreGroupReadableOnly(t *testing.T) {
+ root := repoRoot(t)
+ files := []string{
+ "images/drupal/rootfs/etc/confd/conf.d/drupal.libops.settings.toml",
+ "images/wp/rootfs/etc/confd/conf.d/wordpress.application.toml",
+ }
+ for _, relative := range files {
+ content, err := os.ReadFile(filepath.Join(root, relative))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := string(content)
+ if !strings.Contains(got, "uid = 0\ngid = 101\nmode = \"0640\"") {
+ t.Errorf("%s must render root-owned, nginx-group-readable runtime secrets", relative)
+ }
+ }
+
+ developmentScript, err := os.ReadFile(filepath.Join(root, "images", "islandora", "rootfs", "etc", "s6-overlay", "scripts", "development-environment.sh"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(developmentScript), "groupmod") {
+ t.Error("Islandora development UID remapping must preserve the stable nginx GID used by runtime secret configs")
+ }
+}
+
+func TestVersionedImageReadmesMatchDockerfiles(t *testing.T) {
+ root := repoRoot(t)
+ images := []string{"alpaca", "mariadb11", "tomcat11"}
+ for _, image := range images {
+ version, err := dockerfileArgDefault(filepath.Join(root, "images", image, "Dockerfile"), "SOFTWARE_VERSION")
+ if err != nil {
+ t.Fatal(err)
+ }
+ version = normalizeVersion(image, version)
+ readme := filepath.Join(root, "images", image, "README.md")
+ content, err := os.ReadFile(readme)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(content), "version "+version+".") {
+ t.Errorf("%s does not document Dockerfile SOFTWARE_VERSION %s", readme, version)
+ }
+ }
+}
+
+func TestDownloadZipStripRequiresOneTopLevelDirectory(t *testing.T) {
+ root := repoRoot(t)
+ script := filepath.Join(root, "images", "base", "rootfs", "usr", "local", "bin", "download.sh")
+
+ invalidZip := filepath.Join(t.TempDir(), "invalid.zip")
+ writeTestZip(t, invalidZip, map[string]string{
+ "first/file.txt": "first",
+ "second/file.txt": "second",
+ })
+ invalidDest := t.TempDir()
+ command := exec.Command("bash", "-c", `set -euo pipefail; export DOWNLOAD_LIBRARY_ONLY=true; source "$1"; STRIP=true; REMOVE=(); unpack "$2" "$3"`, "bash", script, invalidZip, invalidDest)
+ output, err := command.CombinedOutput()
+ if err == nil || !strings.Contains(string(output), "exactly one top-level directory") {
+ t.Fatalf("multi-root ZIP was not rejected safely: err=%v output=%s", err, output)
+ }
+
+ validZip := filepath.Join(t.TempDir(), "valid.zip")
+ writeTestZip(t, validZip, map[string]string{"release/file.txt": "payload"})
+ validDest := t.TempDir()
+ command = exec.Command("bash", "-c", `set -euo pipefail; export DOWNLOAD_LIBRARY_ONLY=true; source "$1"; STRIP=true; REMOVE=(); unpack "$2" "$3"`, "bash", script, validZip, validDest)
+ if output, err := command.CombinedOutput(); err != nil {
+ t.Fatalf("single-root ZIP strip failed: %v\n%s", err, output)
+ }
+ content, err := os.ReadFile(filepath.Join(validDest, "file.txt"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(content) != "payload" {
+ t.Fatalf("stripped ZIP payload = %q", content)
+ }
+}
+
+func writeTestZip(t *testing.T, path string, entries map[string]string) {
+ t.Helper()
+ file, err := os.Create(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ archive := zip.NewWriter(file)
+ for name, content := range entries {
+ entry, err := archive.Create(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := io.WriteString(entry, content); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := archive.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := file.Close(); err != nil {
+ t.Fatal(err)
+ }
+}
+
func repoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
diff --git a/internal/buildkit/test_runner.go b/internal/buildkit/test_runner.go
index de454460..5fcce67a 100644
--- a/internal/buildkit/test_runner.go
+++ b/internal/buildkit/test_runner.go
@@ -454,7 +454,16 @@ func (r *Runner) specFor(test TestCase) testSpec {
}
case "drupal":
spec.Timeout = 10 * time.Minute
- case "ojs-php83", "ojs-php84", "omeka-s-php83", "omeka-s-php84", "omeka-classic-php83", "omeka-classic-php84", "wp-php83", "wp-php84", "drupal-php83", "drupal-php84", "islandora-php83", "islandora-php84":
+ case "mariadb11":
+ if test.Name == "RejectsEmptyRootPassword" {
+ spec.ExpectedExitCode["mariadb"] = []int{1}
+ }
+ case "ojs-php83", "ojs-php84", "omeka-s-php83", "omeka-s-php84", "omeka-classic-php83", "omeka-classic-php84":
+ spec.Timeout = 10 * time.Minute
+ if test.Name == "ServiceLogsClientIp" {
+ spec.CheckClientIPLog = true
+ }
+ case "wp-php83", "wp-php84", "drupal-php83", "drupal-php84", "islandora-php83", "islandora-php84":
if test.Name == "ServiceLogsClientIp" {
spec.CheckClientIPLog = true
}
@@ -559,8 +568,7 @@ func (r *Runner) waitForHealthy(ctx context.Context, test TestCase, env []string
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
targetServices := r.healthcheckServices(test, services)
- attempts := 20
- for attempt := 1; attempt <= attempts; attempt++ {
+ for {
healthy, err := r.anyHealthyContainer(ctx, test, env, targetServices)
if err == nil && healthy {
fmt.Fprintf(r.stdout, "healthy service found for %s/%s: %s\n", test.Image, test.Name, strings.Join(targetServices, ", "))
@@ -574,7 +582,6 @@ func (r *Runner) waitForHealthy(ctx context.Context, test TestCase, env []string
case <-ticker.C:
}
}
- return fmt.Errorf("no target service reported healthy after %d attempts", attempts)
}
func (r *Runner) healthcheckServices(test TestCase, services []string) []string {
diff --git a/renovate.json5 b/renovate.json5
index cd37c96c..f769aa28 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -63,6 +63,18 @@
],
allowedVersions: "/^7\\./",
},
+ {
+ matchDepNames: [
+ "islandora-syn-fcrepo6",
+ ],
+ allowedVersions: "/^1\\./",
+ },
+ {
+ matchDepNames: [
+ "islandora-syn-fcrepo7",
+ ],
+ allowedVersions: "/^2\\./",
+ },
{
matchDepNames: [
"apache-tomcat9",
@@ -85,7 +97,7 @@
matchDepNames: [
"omeka-classic",
],
- allowedVersions: "/^\\d+\\.\\d+$/",
+ allowedVersions: "/^\\d+\\.\\d+(?:\\.\\d+)?$/",
},
{
matchDatasources: [
@@ -174,6 +186,8 @@
"fits-servlet",
"golang",
"islandora-starter-site",
+ "islandora-syn-fcrepo6",
+ "islandora-syn-fcrepo7",
"mysql-connector-j",
"ojs",
"omeka-classic",
@@ -199,6 +213,17 @@
},
],
customManagers: [
+ {
+ customType: "regex",
+ description: "Update the digest-pinned BuildKit daemon image",
+ managerFilePatterns: [
+ "/^\\.github/workflows/build\\.yml$/",
+ ],
+ matchStrings: [
+ "# renovate: datasource=(?docker) depName=(?moby/buildkit)\\s+BUILDKIT_IMAGE: moby/buildkit:(?[^@\\s]+)@(?sha256:[a-f0-9]{64})",
+ ],
+ versioningTemplate: "docker",
+ },
{
customType: "regex",
description: "Update (COMMIT|_VERSION) variables in Dockerfiles",