From 591f37bfa5942c8590920141f8cb457735ff3c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chip=20Wolf=20=E2=80=AE?= Date: Thu, 16 Jul 2026 23:29:05 +0100 Subject: [PATCH] generic-packs: support oci:// references via install-oci-pack (#4164) --- docs/mods-and-plugins/index.md | 20 +++++++++++ scripts/start-setupModpack | 33 ++++++++++++++++++- scripts/start-utils | 6 ++++ .../generic-packs-oci/docker-compose.yml | 18 ++++++++++ .../setuponlytests/generic-packs-oci/fake.jar | 0 .../generic-packs-oci/require.sh | 1 + .../generic-packs-oci/verify.sh | 7 ++++ 7 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/setuponlytests/generic-packs-oci/docker-compose.yml create mode 100644 tests/setuponlytests/generic-packs-oci/fake.jar create mode 100644 tests/setuponlytests/generic-packs-oci/require.sh create mode 100644 tests/setuponlytests/generic-packs-oci/verify.sh diff --git a/docs/mods-and-plugins/index.md b/docs/mods-and-plugins/index.md index 0cc978cd..f021295f 100644 --- a/docs/mods-and-plugins/index.md +++ b/docs/mods-and-plugins/index.md @@ -151,6 +151,26 @@ GENERIC_PACKS_SUFFIX=.zip would expand to `https://cdn.example.org/configs-v9.0.1.zip,https://cdn.example.org/mods-v4.3.6.zip`. +### Generic packs from an OCI registry + +An entry can also reference a pack stored as an [OCI](https://opencontainers.org/) artifact in a container registry by prefixing it with `oci://`. The reference may use a tag or an immutable `@sha256:` digest: + +``` +GENERIC_PACKS=oci://ghcr.io/itzg/oci-modpack-template/tech:latest,oci://ghcr.io/itzg/oci-modpack-template/magic:latest +``` + +OCI, URL, and local-path entries can be mixed in the same list and are applied in the order given. Each artifact's layers are pulled into a content-addressed cache under `/data/packs/oci`, so a digest already on disk (for example a base layer shared between packs) is not downloaded again. `GENERIC_PACKS_PREFIX`/`GENERIC_PACKS_SUFFIX`, `GENERIC_PACKS_DISABLE_MODS`, and the update/checksum behaviour below all apply to OCI entries exactly as they do to URLs. + +The artifact must be a Minecraft modpack artifact (artifact type `application/vnd.itzg.minecraft.modpack.v1+json` with `…modpack.layer.v1.tar+gzip` layers); any other reference is rejected before its contents touch `/data`. Validation and registry authentication are handled by `mc-image-helper`. + +For a private registry, point `GENERIC_PACKS_OCI_AUTH_FILE` at a registry login file (the `auth.json`/`config.json` produced by `docker login`, `podman login`, etc.): + +``` +GENERIC_PACKS_OCI_AUTH_FILE=/run/secrets/registry-auth.json +``` + +When it is unset, a credentials file at `~/.config/containers/auth.json` or `~/.docker/config.json` is used if present, otherwise the pull is anonymous. + If applying large generic packs, the update can be time-consuming. To skip the update set `SKIP_GENERIC_PACK_UPDATE_CHECK` to "true". Conversely, the generic pack(s) can be forced to be applied by setting `FORCE_GENERIC_PACK_UPDATE` to "true". The most time-consuming portion of the generic pack update is generating and comparing the SHA1 checksum. To skip the checksum generation, set `SKIP_GENERIC_PACK_CHECKSUM` to "true". diff --git a/scripts/start-setupModpack b/scripts/start-setupModpack index 1390c578..11a08f54 100755 --- a/scripts/start-setupModpack +++ b/scripts/start-setupModpack @@ -181,7 +181,38 @@ function handleGenericPacks() { packFiles=() for packEntry in "${packs[@]}"; do pack="${GENERIC_PACKS_PREFIX}${packEntry}${GENERIC_PACKS_SUFFIX}" - if isURL "${pack}"; then + if isOCIRef "${pack}"; then + # All OCI refs share one staging dir so layers common to multiple + # sibling artifacts (the point of layered distribution) are pulled + # exactly once across all of GENERIC_PACKS. + mkdir -p /data/packs/oci /data/.tmp + log "Pulling OCI generic pack from $pack" + # A list file is used rather than stdout because mc-image-helper INFO + # logs share that stream; it holds one absolute layer path per line in + # apply order. + layerListFile=$(mktemp -p /data/.tmp) + ociArgs=(install-oci-pack + --ref "$pack" + --output-directory /data/packs/oci + --filename-strategy digest + --layer-list-file "$layerListFile") + if [[ "${GENERIC_PACKS_OCI_AUTH_FILE:-}" ]]; then + ociArgs+=(--auth-file "${GENERIC_PACKS_OCI_AUTH_FILE}") + fi + if ! mc-image-helper "${ociArgs[@]}"; then + logError "Failed to pull OCI generic pack $pack" + logError "(OCI generic packs require mc-image-helper with install-oci-pack support)" + rm -f "$layerListFile" + exit 2 + fi + mapfile -t ociLayers < "$layerListFile" + rm -f "$layerListFile" + if [ ${#ociLayers[@]} -eq 0 ]; then + logError "OCI generic pack $pack resolved to zero layers" + exit 2 + fi + packFiles+=("${ociLayers[@]}") + elif isURL "${pack}"; then mkdir -p /data/packs log "Downloading generic pack from $pack" if ! outfile=$(get -o /data/packs --output-filename --skip-up-to-date "$pack"); then diff --git a/scripts/start-utils b/scripts/start-utils index c1e8ebbf..0aa8ada8 100755 --- a/scripts/start-utils +++ b/scripts/start-utils @@ -173,6 +173,12 @@ function isURL() { [[ $value =~ ^(https?|ftp):// ]] } +function isOCIRef() { + local value=$1 + + [[ $value =~ ^oci:// ]] +} + function isValidFileURL() { suffix=${1:?Missing required suffix arg} url=${2:?Missing required url arg} diff --git a/tests/setuponlytests/generic-packs-oci/docker-compose.yml b/tests/setuponlytests/generic-packs-oci/docker-compose.yml new file mode 100644 index 00000000..e1896b27 --- /dev/null +++ b/tests/setuponlytests/generic-packs-oci/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mc: + image: ${IMAGE_TO_TEST:-itzg/minecraft-server} + environment: + EULA: "true" + SETUP_ONLY: "true" + # two OCI packs that share a common base layer; the shared blob is + # pulled into /data/packs/oci once and reused for the second pack + GENERIC_PACKS: oci://ghcr.io/itzg/oci-modpack-template/tech:latest,oci://ghcr.io/itzg/oci-modpack-template/magic:latest + LOG_TIMESTAMP: "true" + # the following are only used to speed up test execution + TYPE: CUSTOM + CUSTOM_SERVER: /servers/fake.jar + VERSION: 1.18.1 + DEBUG: "true" + volumes: + - ./data:/data + - ./fake.jar:/servers/fake.jar diff --git a/tests/setuponlytests/generic-packs-oci/fake.jar b/tests/setuponlytests/generic-packs-oci/fake.jar new file mode 100644 index 00000000..e69de29b diff --git a/tests/setuponlytests/generic-packs-oci/require.sh b/tests/setuponlytests/generic-packs-oci/require.sh new file mode 100644 index 00000000..9c74749c --- /dev/null +++ b/tests/setuponlytests/generic-packs-oci/require.sh @@ -0,0 +1 @@ +[[ $EXTENDED_TESTS ]] || exit 1 diff --git a/tests/setuponlytests/generic-packs-oci/verify.sh b/tests/setuponlytests/generic-packs-oci/verify.sh new file mode 100644 index 00000000..d9d63de6 --- /dev/null +++ b/tests/setuponlytests/generic-packs-oci/verify.sh @@ -0,0 +1,7 @@ +set -e +# OCI layers were cached +[ -d /data/packs/oci ] +compgen -G "/data/packs/oci/sha256:*" > /dev/null +# pack content was extracted and applied to /data +[ -d /data/mods ] +compgen -G "/data/mods/*" > /dev/null