diff --git a/.github/workflows/legacy-build.yml b/.github/workflows/legacy-build.yml index 12f16c4..8ed6065 100644 --- a/.github/workflows/legacy-build.yml +++ b/.github/workflows/legacy-build.yml @@ -37,6 +37,9 @@ jobs: - 'legacy/demo/**' - 'legacy/Makefile' - '.github/workflows/legacy-build.yml' + v3.0: + - *shared + - 'legacy/3.0/**' v3.1: - *shared - 'legacy/3.1/**' @@ -64,7 +67,7 @@ jobs: BUILD_ALL: ${{ github.event_name != 'pull_request' }} run: | # Implemented versions; append when a new directory lands. - ALL='["3.1","3.2","3.3","4.0","4.1","4.2"]' + ALL='["3.0","3.1","3.2","3.3","4.0","4.1","4.2"]' if [ "${BUILD_ALL}" = "true" ]; then VERSIONS="${ALL}" else diff --git a/CHANGES.md b/CHANGES.md index f6d7910..d5c69a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,6 @@ # Changelog +- Add Plone 3.0 image to the legacy matrix. Refs #187 + [@ericof] - Add Plone 3.1 image to the legacy matrix. Refs #186 [@ericof] - Add Plone 3.2 image to the legacy matrix. Refs #185 diff --git a/README.md b/README.md index 38e4324..8ee1fbf 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ listed; earlier point releases remain in the repository and on Docker Hub. | 3.3.6 | 2.4.6 | Debian *(legacy)* | [`legacy/3.3/Dockerfile`](legacy/3.3/Dockerfile) | `3.3`, `3.3.6`, `3.3-demo`, `3.3.6-demo` | | 3.2.3 | 2.4.6 | Debian *(legacy)* | [`legacy/3.2/Dockerfile`](legacy/3.2/Dockerfile) | `3.2`, `3.2.3`, `3.2-demo`, `3.2.3-demo` | | 3.1.7 | 2.4.6 | Debian *(legacy)* | [`legacy/3.1/Dockerfile`](legacy/3.1/Dockerfile) | `3.1`, `3.1.7`, `3.1-demo`, `3.1.7-demo` | +| 3.0.5 | 2.4.6 | Debian *(legacy)* | [`legacy/3.0/Dockerfile`](legacy/3.0/Dockerfile) | `3.0`, `3.0.5`, `3.0-demo`, `3.0.5-demo` | ### Where the tags live diff --git a/legacy/3.0/Dockerfile b/legacy/3.0/Dockerfile new file mode 100644 index 0000000..39d9730 --- /dev/null +++ b/legacy/3.0/Dockerfile @@ -0,0 +1,232 @@ +# syntax=docker/dockerfile:1 +# +# Plone 3.0.x — the LAST tarball-era release (Zope 2.10.x / Python 2.4.x) +# +# Strategy: +# Stage 1 (fetch): modern userland downloads everything (old TLS never touches network) +# Stage 2 (build): Python 2.4 + PIL + ElementTree + Zope 2.10 on bookworm +# Stage 3 (runtime): slim image, non-root, /data volume, port 8080 +# +# Two things differ from 2.0/2.1/2.5, both verified rather than assumed: +# * the bundle is Plone-3.0.5/{Products,lib/python}, not one dir per product, +# so lib/python must ALSO be installed (see the install step below); +# * ElementTree is a new hard dependency (see shared/build-elementtree.sh). +# +# NOT FOR PRODUCTION. Zope 2.10 has known, unpatched security issues. +# Intended for content extraction, migration, and archaeology only. + +ARG PYTHON_VERSION=2.4.6 +ARG ZOPE_VERSION=2.10.13 +# [V 2026-08-20] 3.0.5 is the last FULL bundle, not 3.0.6. dist.plone.org has +# PloneBase-3.0.6.tar.gz but no Plone-3.0.6.tar.gz: the 3.0.6 roll shipped only +# as PloneBase, which is the stripped core (CMFPlone alone, 1.0 MB) WITHOUT the +# dependency products. The last full Plone-*.tar.gz for the 3.0 series is +# 3.0.5 (12.6 MB, 41 products), and the repo convention is to always use the +# full bundle. /archive/ holds only UnifiedInstaller tarballs for 3.0.x; the +# plain bundles live at the dist.plone.org root. +ARG PLONE_VERSION=3.0.5 +ARG PIL_VERSION=1.1.6 +ARG ELEMENTTREE_VERSION=1.2.7-20070827-preview + +# [V 2026-08-20] Verified: Zope-2.10.13-final.tgz (7.4 MB) exists at +# old.zope.dev, unpacks to Zope-2.10.13-final/ with a top-level ./configure, +# and its configure declares TARGET="2.4.6" — exactly the interpreter these +# images already build. Plone 3.0's INSTALL.txt requires Zope 2.10.4+. +ARG ZOPE_URL=https://old.zope.dev/Products/Zope/${ZOPE_VERSION}/Zope-${ZOPE_VERSION}-final.tgz +# [V] python.org keeps all historical releases +ARG PYTHON_URL=https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz +# [V 2026-08-20] Verified: Plone-3.0.5.tar.gz at the dist.plone.org root +# (NOT under /archive/, unlike the 2.x bundles). +ARG PLONE_URL=https://dist.plone.org/Plone-${PLONE_VERSION}.tar.gz +# [V 2026-08-20] effbot.org is dead (404) and PyPI lists PIL but serves no +# files; dist.plone.org/thirdparty is the surviving canonical source. +ARG PIL_URL=https://dist.plone.org/thirdparty/PIL-${PIL_VERSION}.tar.gz +# [V 2026-08-20] Plone's own docs point at effbot.org for this, which is dead. +# The same thirdparty mirror carries it — as a .zip, hence unzip below. +ARG ELEMENTTREE_URL=https://dist.plone.org/thirdparty/elementtree-${ELEMENTTREE_VERSION}.zip + +# --- simplejson ------------------------------------------------------------- +# Python has no stdlib `json` before 2.6, so every image below 4.0 ships +# simplejson instead. The release is NOT interchangeable between interpreters +# — see the header of shared/install-simplejson.sh, and note that the install +# gate will fail the build on a wrong pairing rather than let it through. +ARG SIMPLEJSON_VERSION=2.0.9 +ARG SIMPLEJSON_URL=https://files.pythonhosted.org/packages/source/s/simplejson/simplejson-${SIMPLEJSON_VERSION}.tar.gz + +# --------------------------------------------------------------------------- +# Stage 1: fetch — modern TLS, nothing else +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim AS fetch +ARG ZOPE_URL +ARG PYTHON_URL +ARG PLONE_URL +ARG PIL_URL +ARG ELEMENTTREE_URL +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /dist +RUN curl -fSL "${PYTHON_URL}" -o python.tgz \ + && curl -fSL "${ZOPE_URL}" -o zope.tgz \ + && curl -fSL "${PLONE_URL}" -o plone.tar.gz \ + && curl -fSL "${PIL_URL}" -o pil.tar.gz \ + && curl -fSL "${ELEMENTTREE_URL}" -o elementtree.zip + +# --------------------------------------------------------------------------- +# Stage 1b: fetch-simplejson — derived from fetch, deliberately not part of it +# --------------------------------------------------------------------------- +# pybuild does `COPY --from=fetch /dist /dist`, so folding this download into +# stage 1 would invalidate the interpreter build every time the simplejson pin +# moves and recompile CPython from source — hours, under emulation on an arm64 +# host. Deriving a stage leaves `fetch` byte-identical, so pybuild stays cached, +# and reuses the curl and ca-certificates already installed there. WORKDIR +# /dist is inherited, so the tarball lands beside the others. +FROM fetch AS fetch-simplejson +ARG SIMPLEJSON_URL +RUN curl -fSL --retry 5 --retry-delay 5 "${SIMPLEJSON_URL}" -o simplejson.tar.gz + +# --------------------------------------------------------------------------- +# Stage 2a: pybuild — Python 2.4 only, so `--target pybuild` is exactly gate G1 +# and a Zope failure cannot deny us a testable interpreter. +# --------------------------------------------------------------------------- +FROM debian:bookworm AS pybuild +ARG PYTHON_VERSION + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + gcc make libc6-dev zlib1g-dev libcrypt-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=fetch /dist /dist +COPY shared/build-python2.sh /usr/local/bin/build-python2.sh + +RUN build-python2.sh /dist/python.tgz "${PYTHON_VERSION}" /opt/python2.4 + +# --------------------------------------------------------------------------- +# Stage 2b: build — PIL, ElementTree, Zope 2.10, the instance, and Plone. +# --------------------------------------------------------------------------- +FROM pybuild AS build +ARG ZOPE_VERSION +ARG PLONE_VERSION +ARG PIL_VERSION +ARG ELEMENTTREE_VERSION + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libjpeg62-turbo-dev unzip \ + && rm -rf /var/lib/apt/lists/* + +# --- PIL -------------------------------------------------------------------- +COPY shared/build-pil.sh /usr/local/bin/build-pil.sh +RUN build-pil.sh /dist/pil.tar.gz "${PIL_VERSION}" /opt/python2.4/bin/python2.4 + +# --- ElementTree ------------------------------------------------------------ +COPY shared/build-elementtree.sh /usr/local/bin/build-elementtree.sh +RUN build-elementtree.sh /dist/elementtree.zip "${ELEMENTTREE_VERSION}" \ + /opt/python2.4/bin/python2.4 + +# --- Zope 2.10 -------------------------------------------------------------- +# Same configure + make as 2.7/2.8/2.9. -fcommon avoids gcc>=10 "multiple +# definition" errors from tentative definitions in the old C extensions. +RUN tar -xzf /dist/zope.tgz -C /tmp \ + && cd /tmp/Zope-${ZOPE_VERSION}-final \ + && ./configure --prefix=/opt/zope \ + --with-python=/opt/python2.4/bin/python2.4 \ + && CFLAGS="-fcommon -fno-strict-aliasing" make \ + && make install \ + && rm -rf /tmp/Zope-${ZOPE_VERSION}-final + +# --- Zope instance + Plone products ----------------------------------------- +# Placeholder inituser; the real one is written by the entrypoint at runtime. +RUN /opt/python2.4/bin/python2.4 /opt/zope/bin/mkzopeinstance.py \ + --dir /app/instance --user "admin:placeholder" + +# [V 2026-08-20] The 3.0 bundle layout differs from every 2.x bundle: +# +# Plone-3.0.5/ +# Products/<41 product dirs> +# lib/python/{archetypes,five,kss,plone,wicked} +# INSTALL.txt README.txt RELEASENOTES.txt +# +# so Products/ and lib/python/ must be installed to their counterparts in the +# instance, per INSTALL.txt: "The Zope Products are installed in the usual +# location: the Products directory of the Zope instance. Python package may be +# installed in the lib/python directory in the Zope instance". +# +# [V 2026-08-20] /lib/python IS on sys.path: Zope 2.10's +# Zope2/Startup/handlers.py:187 says "always insert instancehome/lib/python", +# and mkzopeinstance creates that directory in the skeleton. +# +# Both copies are asserted, so a layout change in a future bundle fails the +# build instead of yielding an instance that starts with half of Plone missing. +RUN mkdir -p /tmp/plone \ + && tar -xzf /dist/plone.tar.gz -C /tmp/plone --strip-components=1 \ + && test -d /tmp/plone/Products \ + && test -d /tmp/plone/lib/python \ + && cp -a /tmp/plone/Products/. /app/instance/Products/ \ + && cp -a /tmp/plone/lib/python/. /app/instance/lib/python/ \ + && test -f /app/instance/Products/CMFPlone/version.txt \ + && test -d /app/instance/lib/python/plone \ + && rm -rf /tmp/plone + +# Point the filestorage and logs at /data (populated at runtime). +RUN sed -i \ + -e 's|\$INSTANCE/var/Data.fs|/data/filestorage/Data.fs|' \ + -e 's|\$INSTANCE/log|/data/log|' \ + /app/instance/etc/zope.conf + +# --- simplejson ------------------------------------------------------------- +# Last in the stage, and with its ARG declared here rather than at the top, so +# that moving the pin does not invalidate the PIL and Plone layers above. +# site-packages lives under /opt/python2.4, which stage 3 copies wholesale, so +# nothing further is needed at runtime. +ARG SIMPLEJSON_VERSION +COPY --from=fetch-simplejson /dist/simplejson.tar.gz /dist/simplejson.tar.gz +COPY shared/install-simplejson.sh /usr/local/bin/install-simplejson.sh +RUN install-simplejson.sh /dist/simplejson.tar.gz "${SIMPLEJSON_VERSION}" \ + /opt/python2.4/bin/python2.4 + +# --------------------------------------------------------------------------- +# Stage 3: runtime +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim +ARG PLONE_VERSION +ARG ZOPE_VERSION +ARG PYTHON_VERSION + +LABEL org.opencontainers.image.title="Plone ${PLONE_VERSION} (legacy)" \ + org.opencontainers.image.description="Plone ${PLONE_VERSION} on Zope ${ZOPE_VERSION} / Python ${PYTHON_VERSION}. Migration and content-extraction use only — NOT for production." \ + org.opencontainers.image.vendor="Simples Consultoria" + +RUN apt-get update \ + && apt-get install -y --no-install-recommends zlib1g libcrypt1 libjpeg62-turbo \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --system -m -d /app -U -u 500 plone \ + && mkdir -p /data \ + && chown -R plone:plone /data + +COPY --from=build --chown=plone:plone /opt/python2.4 /opt/python2.4 +COPY --from=build --chown=plone:plone /opt/zope /opt/zope +COPY --from=build --chown=plone:plone /app /app +COPY --chown=plone:plone shared/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +COPY --chown=plone:plone shared/upgrade-plone.py /app/upgrade-plone.py +COPY --chown=plone:plone shared/configure-zeo.sh /app/configure-zeo.sh +RUN chmod +x /app/configure-zeo.sh + +ENV ZOPE_HOME=/opt/zope \ + INSTANCE_HOME=/app/instance \ + PYTHON=/opt/python2.4/bin/python2.4 \ + ZOPE_HTTP_PORT=8080 + +VOLUME /data +EXPOSE 8080 +WORKDIR /app +USER plone + +# Plone 3 does considerably more work at first start than 2.x (Five/ZCML +# registration across 41 products), so the warm-up window is longer. +HEALTHCHECK --interval=10s --timeout=5s --start-period=180s --retries=6 \ + CMD ["/opt/python2.4/bin/python2.4", "-c", "import urllib; urllib.urlopen('http://127.0.0.1:8080/')"] + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["start"] diff --git a/legacy/3.0/README.md b/legacy/3.0/README.md new file mode 100644 index 0000000..ee10707 --- /dev/null +++ b/legacy/3.0/README.md @@ -0,0 +1,115 @@ +# Plone 3.0 legacy image + +Plone 3.0.5 on Zope 2.10.13 / Python 2.4.6. The **last tarball-era release** — +3.2 onwards require buildout, which needs a different template. + +**Not for production.** Zope 2.10 has known, unpatched vulnerabilities. This +image exists for content extraction, migration rehearsal, and historical +inspection. + +## Usage + +```sh +docker build -f 3.0/Dockerfile -t plone-legacy:3.0.5 . +docker run -d -p 8080:8080 -e ADMIN_PASSWORD=secret \ + -v plone30-data:/data plone-legacy:3.0.5 +``` + +| | | +|---|---| +| Port | 8080 (`ZOPE_HTTP_PORT` to change) | +| Volume | `/data` (`filestorage/Data.fs`, `log/`) | +| User | `plone` (uid 500) | +| Env | `ADMIN_USER`, `ADMIN_PASSWORD` (first run only) | +| PIL | 1.1.6, with JPEG + PNG (no freetype/`_imagingft`) | +| ElementTree | 1.2.7 (required by Plone 3; see below) | + +## Two ways this differs from the 2.x images + +Both were verified against the actual bundle, not assumed from the 2.x pattern. + +### 1. The bundle has a different shape + +Where every 2.x bundle was one directory per product at the top level, 3.0 is: + +``` +Plone-3.0.5/ + Products/<41 product dirs> + lib/python/{archetypes,five,kss,plone,wicked} + INSTALL.txt README.txt RELEASENOTES.txt +``` + +`INSTALL.txt` is explicit: *"The Zope Products are installed in the usual +location: the Products directory of the Zope instance. Python package may be +installed in the lib/python directory in the Zope instance"*. Both copies are +made, and both are asserted in the Dockerfile — a `--strip-components=1` copy +of the top level alone would produce an instance that starts with half of +Plone missing and no error. + +`/lib/python` is genuinely on `sys.path`: Zope 2.10's +`Zope2/Startup/handlers.py:187` reads *"always insert instancehome/lib/python"*, +and `mkzopeinstance` creates that directory in its skeleton. + +### 2. ElementTree is a new hard dependency + +`INSTALL.txt` says *"The python ElementTree package is now required"*, and it +means it. `CMFPlone/setup/dependencies.py` only logs when it is missing, but +`Products/Marshall/handlers/atxml.py:51-54` does + +```python +try: + from celementtree import ElementTree +except ImportError: + from elementtree import ElementTree +``` + +with no outer guard, so with neither package installed the module raises +`ImportError` and Marshall fails to load. Python 2.5 absorbed this as +`xml.etree`, but these images run 2.4. + +Plone's docs point at `effbot.org` for the download, which is dead (404) — the +same dead end as PIL. `dist.plone.org/thirdparty/` carries +`elementtree-1.2.7-20070827-preview.zip`, and that is what is installed. + +## Validated facts / hypotheses + +- **[V 2026-08-20]** **3.0.5 is the last full bundle, not 3.0.6.** The matrix + listed `3.0.6*`. `PloneBase-3.0.6.tar.gz` exists (1.0 MB) but there is no + `Plone-3.0.6.tar.gz`: the 3.0.6 roll shipped only as PloneBase, the stripped + core without dependency products. The last full `Plone-*.tar.gz` is 3.0.5 + (12.6 MB, 41 products). Note also that unlike the 2.x bundles these live at + the **dist.plone.org root**, not under `/archive/`, which holds only + UnifiedInstaller tarballs for this series. +- **[V 2026-08-20]** `Zope-2.10.13-final.tgz` (7.4 MB) is downloadable from + `old.zope.dev`, unpacks to `Zope-2.10.13-final/` with a top-level + `./configure`, and that configure declares `TARGET="2.4.6"` — exactly the + interpreter these images already build. Plone 3.0 requires Zope 2.10.4+. +- **[V 2026-08-20]** Python 2.4.6, PIL 1.1.6 and Zope 2.10.13 all build through + the shared scripts with no 3.0-specific changes. `-fcommon` again sufficient. +- **[V 2026-08-20]** **A guard failed for the wrong reason: CRLF.** The + elementtree zip ships `setup.py` with `\r\n` endings, so every line really + ends `...setup\r`. GNU grep anchors `$` after the `\n` only, so the strict + pattern `'^from distutils.core import setup$'` matched **zero** times and the + build failed on its own assertion. Confirmed in-container: GNU grep 3.8 on + bookworm returns 0 for the strict pattern and 1 once `\r` is accounted for — + while the developer's host grep is lenient and matched, hiding it. + `build-elementtree.sh` now normalises line endings before asserting. + **Test text guards against the container's grep, not the host's.** +- **[V 2026-08-20]** `old.zope.dev` is intermittently slow and returned a + Cloudflare **522** on the first build attempt, then served the same URL fine + (16.6 s) on retry. Nothing in the image is wrong when this happens — just + build again. Worth knowing before debugging a phantom failure. +- **[V 2026-08-20]** The site factory matches 2.5, not 2.1: `/manage_main` + links `CMFPlone/addPloneSiteForm`, whose form posts to `addPloneSite`. The + smoke test's discovery handled 3.0 with no change. +- **[V 2026-08-20]** 42 entries land in `Products/` (41 product dirs plus a + loose file) and 6 in `lib/python`. Zero products fail to import or install, + and a Plone 3.0 site can be created and rendered (~20 kB carrying the Plone + `generator` meta tag). Passes twice consecutively. + +## Smoke test (CI gate) + +```sh +make test-3.0 # HTTP + auth + every product loads +SMOKE_CREATE_SITE=1 make test-3.0 # also creates a Plone site and checks its markup +``` diff --git a/legacy/Makefile b/legacy/Makefile index a6f7059..f97e8d9 100644 --- a/legacy/Makefile +++ b/legacy/Makefile @@ -14,7 +14,7 @@ PLATFORM ?= linux/amd64 # the matrix in README.md, and the ALL list in # ../.github/workflows/legacy-build.yml. One series lands per pull request; # append here as each one is ported. -SERIES := 3.1 3.2 3.3 4.0 4.1 4.2 +SERIES := 3.0 3.1 3.2 3.3 4.0 4.1 4.2 # Builder used for the -demo images, which MUST be a `docker`-driver one: # demo/Dockerfile's FROM reads the base image out of the local image store, and @@ -42,6 +42,10 @@ DEMO_TEST_PASSWORD ?= smoke-not-admin # these images are built from. # [V 2026-08-20] 4.1.6 is both the final 4.1.x release and the last # UnifiedInstaller published for the series, so this row needs no compromise. +# [V 2026-08-20] 3.0.5, not 3.0.6: the 3.0.6 roll shipped only as +# PloneBase-3.0.6.tar.gz (the stripped core, without dependency products). +# The last full Plone-*.tar.gz for the series is 3.0.5. +FULL_VERSION_3.0 := 3.0.5 FULL_VERSION_3.1 := 3.1.7 FULL_VERSION_3.2 := 3.2.3 FULL_VERSION_3.3 := 3.3.6 diff --git a/legacy/README.md b/legacy/README.md index 61ee643..e9b06a8 100644 --- a/legacy/README.md +++ b/legacy/README.md @@ -18,9 +18,10 @@ They exist for content extraction, migration rehearsal, and archaeology. | 3.3 | 3.3.6 | 2.10.13 | 2.4.6 | buildout | `simplejson` 2.0.9 (bundled) | **done** | | 3.2 | 3.2.3 | 2.10.7 | 2.4.6 | buildout | `simplejson` 2.0.9 (installed) | **done** | | 3.1 | 3.1.7 | 2.10.6 | 2.4.6 | buildout | `simplejson` 2.0.9 (installed) | **done** | +| 3.0 | 3.0.5 | 2.10.13 | 2.4.6 | tarball | `simplejson` 2.0.9 (installed) | **done** | The matrix extends down to Plone 1.0. One series lands per pull request; the -remaining five follow these. When adding a series, keep four places in sync: +remaining four follow these. When adding a series, keep four places in sync: the directory, the table above, `SERIES` and `FULL_VERSION_*` in the [`Makefile`](Makefile), and the paths-filter list plus `ALL` in [`../.github/workflows/legacy-build.yml`](../.github/workflows/legacy-build.yml). @@ -211,6 +212,8 @@ Shared build logic lives in `shared/`: | `build-pil.sh` | Builds PIL 1.1.6 with working JPEG + PNG | | `build-plone-buildout.sh` | Offline buildout from a UnifiedInstaller's bundled cache | | `docker-entrypoint-buildout.sh` | Buildout-era entrypoint (`bin/instance fg`, `parts/instance/` paths) | +| `docker-entrypoint.sh` | Tarball-era entrypoint (`bin/zopectl fg`, `etc/zope.conf` paths) | +| `build-elementtree.sh` | Builds ElementTree 1.2.7 for Plone 3.0's Marshall dependency | | `create-plone-site.sh` | Creates a site on a running instance, discovering the factory per era | | `seed-demo-site.sh` | Build-time driver behind the `-demo` images | | `reset-admin-password.py` | Applies `ADMIN_USER`/`ADMIN_PASSWORD` to a seeded database | @@ -220,9 +223,8 @@ Shared build logic lives in `shared/`: | `json-probe.py` | Asserts a JSON module imports and round-trips, used by the smoke test | | `smoke-test.sh` | The CI gate: HTTP, auth, product load, optional site creation | -Two further scripts arrive with the earlier series they serve: -`build-elementtree.sh` (Plone 3.0) and `docker-entrypoint.sh` (tarball era, -1.0 – 3.0). +Every shared script is now present: `build-elementtree.sh` and +`docker-entrypoint.sh` arrived with 3.0, the last series to need a new one. Per-version notes — including each version's validated/hypothesised ledger — live in that version directory's README. diff --git a/legacy/shared/build-elementtree.sh b/legacy/shared/build-elementtree.sh new file mode 100755 index 0000000..fabe8a0 --- /dev/null +++ b/legacy/shared/build-elementtree.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Install ElementTree for a historical CPython 2.x. +# +# Usage: build-elementtree.sh +# e.g. build-elementtree.sh /dist/elementtree.zip 1.2.7-20070827-preview \ +# /opt/python2.4/bin/python2.4 +# +# Why this is here: +# Plone 3.0's INSTALL.txt states "The python ElementTree package is now +# required". It is not merely advisory: Products/Marshall/handlers/atxml.py +# does +# try: from celementtree import ElementTree +# except ImportError: +# from elementtree import ElementTree +# with no outer guard, so with neither package present the module raises +# ImportError and Marshall fails to load. (CMFPlone/setup/dependencies.py +# also checks for it, but only logs.) +# +# Python 2.5 absorbed this as xml.etree, but the tarball-era images that need +# it run Python 2.4, so it has to be installed separately. +# +# Source: dist.plone.org/thirdparty, the same mirror used for PIL. The upstream +# effbot.org download page that Plone's own docs point at is dead (404). +# +# Unlike PIL, this package is pure Python and uses plain distutils — no +# setuptools bootstrap, no C extension, no multiarch problem. +set -eu + +SRC="${1:?usage: build-elementtree.sh }" +VERSION="${2:?missing version}" +PYTHON="${3:?missing python binary}" + +BUILD="/tmp/elementtree-${VERSION}" + +unzip -q "${SRC}" -d /tmp +test -d "${BUILD}" +cd "${BUILD}" + +# [V 2026-08-20] This zip ships setup.py with CRLF line endings, so every line +# really ends "...setup\r". GNU grep anchors $ after the \n only, which means a +# strict '^...setup$' pattern matches ZERO times and the guard below fails the +# build for the wrong reason. (Confirmed: GNU grep 3.8 on bookworm returns 0 for +# the strict pattern and 1 once \r is accounted for. BSD/ugrep on the developer's +# host is lenient here and hides it, so this must be tested in-container.) +# Normalising once up front keeps the assertions exact and readable. +sed -i 's/\r$//' setup.py + +# Assert the assumption that this is a distutils package, so a future release +# that switched to setuptools fails loudly here instead of trying to reach PyPI +# over TLS the interpreter does not have. +test "$(grep -c '^from distutils.core import setup$' setup.py)" = "1" +test "$(grep -c 'ez_setup\|setuptools' setup.py)" = "0" + +"${PYTHON}" setup.py install > /dev/null + +cd / +rm -rf "${BUILD}" + +"${PYTHON}" -c "\ +from elementtree import ElementTree; \ +e = ElementTree.fromstring('x'); \ +assert e.find('b').text == 'x'; \ +print 'elementtree', ElementTree.VERSION, 'ok'" diff --git a/legacy/shared/docker-entrypoint.sh b/legacy/shared/docker-entrypoint.sh new file mode 100755 index 0000000..a0bdcee --- /dev/null +++ b/legacy/shared/docker-entrypoint.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# Shared entrypoint for tarball-era Plone images (Zope 2.7-2.10). +# Expects: $ZOPE_HOME, $INSTANCE_HOME, $PYTHON, $ZOPE_HTTP_PORT (default 8080) +set -e + +ZOPE_HTTP_PORT="${ZOPE_HTTP_PORT:-8080}" +CONF="${INSTANCE_HOME}/etc/zope.conf" + +# /data is a volume: (re)create the expected layout on every start. +mkdir -p /data/filestorage /data/log + +# -demo images only: install the pre-built database on first start. +# +# The site is created at BUILD time and the resulting Data.fs is baked in at +# /app/seed/Data.fs — it cannot be baked at /data, because /data is a VOLUME +# and anything a later layer writes there is discarded. Seeding here, before +# the inituser block below, is what makes that block correctly skip itself: +# the seeded database already carries its admin user. +# +# Base images have no /app/seed, so this is a no-op for them. +# The ZEO_ADDRESS guard is not tidiness: under ZEO the storage is remote, so a +# seeded /data/filestorage/Data.fs is never opened, and the reset-admin-password +# step below would then be applied to the WRONG database. +if [ -n "${ZEO_ADDRESS:-}" ] && [ -f /app/seed/Data.fs ]; then + echo "ZEO_ADDRESS is set: not seeding the demo database (the storage is remote)" >&2 +fi +if [ -z "${ZEO_ADDRESS:-}" ] && [ ! -f /data/filestorage/Data.fs ] && [ -f /app/seed/Data.fs ]; then + echo "seeding /data/filestorage/Data.fs from the demo database" + cp /app/seed/Data.fs /data/filestorage/Data.fs + # [V 2026-08-20] The leftover inituser MUST go, and this is not tidiness. + # Zope applies inituser whenever the file exists — it does NOT check first + # whether the database already has users — and then deletes it. A -demo + # image inherits the base image's placeholder inituser, so without this the + # seeded admin was silently overwritten by mkzopeinstance's build-time + # placeholder: the site was there and correct, and no password worked. + rm -f "${INSTANCE_HOME}/inituser" + SEEDED=1 +fi + +# Zope <4 has no env interpolation in zope.conf; template the port at start. +# The mkzopeinstance default is "address 8080" inside . +# +# [V 2026-08-20] The substitution MUST be confined to the block. +# Zope 2.7's mkzopeinstance writes a second uncommented server block: +# +# +# address 8080 address 8021 +# +# +# so an unscoped `s|address N|address $PORT|` rewrote the FTP port to 8080 as +# well, and Zope died at startup binding the same port twice: +# ZConfig.ConfigurationError: There was a problem starting a server of type +# "FTPServer" ... (Address already in use) +# Zope 2.8 and 2.9 emit only , which is why this stayed latent +# until the 2.0 image was built. +sed -i "/^[[:space:]]*/,/^[[:space:]]*<\/http-server>/ \ +s|^\([[:space:]]*address[[:space:]]\+\)[0-9.:]*[0-9]\+|\1${ZOPE_HTTP_PORT}|" "${CONF}" + +# ZEO client, when ZEO_ADDRESS is set; a silent no-op otherwise. Runs AFTER the +# port templating above and before anything opens the database. +/app/configure-zeo.sh "${CONF}" "${INSTANCE_HOME}/var" + +# First run against an empty /data: write the emergency user so the admin can +# log in. Once a Data.fs exists, inituser is ignored by Zope anyway; we also +# skip rewriting it so an unset ADMIN_PASSWORD never locks out a restart. +# Whether to write inituser at all. +# +# Without ZEO the question is "is /data empty": once a Data.fs exists, Zope has +# its users already. Under ZEO there is no local Data.fs to ask, and answering +# it against a remote storage is not cheap. Writing inituser anyway is NOT +# harmless — [V 2026-08-20] Zope applies it whenever the file EXISTS, without +# checking whether the database already has users — so every client start would +# re-apply it to the shared database and clobber a changed password. Under ZEO +# it is therefore written only when the operator explicitly asks for it by +# supplying ADMIN_PASSWORD, and removed otherwise. +WRITE_INITUSER=0 +if [ -n "${ZEO_ADDRESS:-}" ]; then + if [ -n "${ADMIN_PASSWORD:-}" ]; then + echo "ZEO_ADDRESS is set and ADMIN_PASSWORD supplied: seeding the emergency user" + WRITE_INITUSER=1 + else + rm -f "${INSTANCE_HOME}/inituser" + fi +elif [ ! -f /data/filestorage/Data.fs ]; then + WRITE_INITUSER=1 +fi + +if [ "${WRITE_INITUSER}" = "1" ]; then + ADMIN_USER="${ADMIN_USER:-admin}" + if [ -z "${ADMIN_PASSWORD}" ]; then + echo "WARNING: ADMIN_PASSWORD not set; using 'admin'. Do not expose this container." >&2 + ADMIN_PASSWORD="admin" + fi + "${PYTHON}" "${ZOPE_HOME}/bin/zpasswd.py" \ + -u "${ADMIN_USER}" -p "${ADMIN_PASSWORD}" \ + "${INSTANCE_HOME}/inituser" +fi + +# -demo images only: apply ADMIN_USER/ADMIN_PASSWORD to the seeded database. +# +# inituser cannot do this. Zope consults it only while CREATING a database, and +# a demo image ships one already made — so without this step the credentials +# baked in at build time would be the only ones that ever worked, and a +# perfectly ordinary `-e ADMIN_PASSWORD=...` would be silently ineffective. +# +# Runs before the exec below, so nothing holds the ZODB lock yet. +if [ "${SEEDED:-0}" = "1" ] && [ -f /app/seed/reset-admin-password.py ]; then + reset_out=$("${INSTANCE_HOME}/bin/zopectl" run \ + /app/seed/reset-admin-password.py 2>&1) || true + # `zopectl run` does not reliably propagate the script's exit status, so + # trust the sentinel the script prints and nothing else. Treating a silent + # failure as success here would hand out an image nobody can log into. + if ! printf '%s' "${reset_out}" | grep -q 'PASSWORD-RESET-OK'; then + echo "FATAL: could not apply ADMIN_PASSWORD to the seeded database" >&2 + printf '%s\n' "${reset_out}" >&2 + exit 1 + fi + printf '%s\n' "${reset_out}" | grep -v 'PASSWORD-RESET-OK' || true +fi + +case "$1" in + start) + # runzope stays in the foreground: proper PID 1, signals reach ZServer. + exec "${INSTANCE_HOME}/bin/runzope" + ;; + debug) + exec "${INSTANCE_HOME}/bin/zopectl" debug + ;; + run) + # Forward EVERY argument. `$2` alone silently drops the script's own + # arguments, and passes an empty string when no script is named. + shift + exec "${INSTANCE_HOME}/bin/zopectl" run "$@" + ;; + upgrade) + # Deliberately NOT exec'd: the output has to be read. + # + # `zopectl run` does not reliably propagate the script's exit status — + # the same reason reset-admin-password.py prints a sentinel — so a + # failed migration exits 0 and looks exactly like a successful one. + # Verified [V 2026-08-21]: a 3.3 -> 4.0 hop that logged "Migration has + # failed" still exited 0. Trust the sentinel and nothing else. + upgrade_out=$("${INSTANCE_HOME}/bin/zopectl" run /app/upgrade-plone.py 2>&1) || true + printf '%s\n' "${upgrade_out}" | grep -v 'UPGRADE-OK' || true + if ! printf '%s' "${upgrade_out}" | grep -q 'UPGRADE-OK'; then + echo "FATAL: the upgrade did not complete; the database is unchanged" >&2 + exit 1 + fi + exit 0 + ;; + *) + exec "$@" + ;; +esac