From 5ceafddad3e9a31016316f219cd57959e8645100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 28 Aug 2026 13:26:18 -0300 Subject: [PATCH] Add the Plone 3.0 legacy image Seventh series of the legacy matrix, the first tarball-era image, and the last port that needs new shared scripts. Refs #187. build-elementtree.sh and docker-entrypoint.sh both arrive here, which completes the shared set: legacy/shared/ now matches the source repository's file list exactly, and the closures for 2.5, 2.1, 2.0 and 1.0 are all already satisfied. The remaining four ports are copy-and-wire. Three things make 3.0 structurally different from every series ported so far. Zope is fetched and built here rather than by buildout. The tarball comes from old.zope.dev -- a host no other series uses -- and goes through ./configure && make && make install into /opt/zope, with CFLAGS="-fcommon" because gcc >= 10 rejects the tentative definitions in Zope 2.10's C extensions. build-plone- buildout.sh drops out of the closure entirely. The bundle layout is unique to this release. 3.0 ships Products/ plus lib/python/ rather than one directory per product as the 2.x bundles do, and per its INSTALL.txt both trees install into their counterparts inside the Zope instance. The Dockerfile asserts both copies afterwards, so a layout change in a future bundle fails the build rather than producing an instance that starts with half of Plone missing. Verified on the built image: lib/python holds archetypes, five, kss, plone and wicked. ElementTree is load-bearing rather than defensive. Products/Marshall/handlers/atxml.py does, at module scope: try: from celementtree import ElementTree except ImportError: from elementtree import ElementTree The except catches only the C variant; the pure-Python fallback is unguarded, so with no elementtree installed the module raises and Marshall fails to load. The smoke gate's product-load check is what covers this. 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, so 3.0.5 is the last full Plone-*.tar.gz for the series. The source URL is dist.plone.org/ directly rather than dist.plone.org/archive/ as 1.0 - 2.5 use. legacy/README.md's shared-scripts table gains both new scripts, and the sentence promising further scripts with later series is replaced -- there are none left. Verified locally: make lint passes with 8 Dockerfiles and 11 shell scripts; the base image passes the smoke gate with SMOKE_CREATE_SITE=1 (6/6), and the -demo variant builds and passes make test-demo-3.0 (6/6). The -demo run is the first exercise of docker-entrypoint.sh in this repository: seed-demo-site.sh drives it through site creation, clean shutdown and the ADMIN_PASSWORD reset on the seeded database, and the build-time password no longer authenticates afterwards. The versions were read out of the built image rather than assumed: Products/CMFPlone/version.txt reads 3.0.5, getZopeVersion() returns (2, 10, 13, '', -1) from /opt/zope, and elementtree and simplejson 2.0.9 both resolve to site-packages. The local docker build reused cached layers from the source repository, so a cold build is exercised for the first time in CI -- and for this series that cold build includes compiling Zope 2.10.13 from source. --- .github/workflows/legacy-build.yml | 5 +- CHANGES.md | 2 + README.md | 1 + legacy/3.0/Dockerfile | 232 +++++++++++++++++++++++++++++ legacy/3.0/README.md | 115 ++++++++++++++ legacy/Makefile | 6 +- legacy/README.md | 10 +- legacy/shared/build-elementtree.sh | 63 ++++++++ legacy/shared/docker-entrypoint.sh | 154 +++++++++++++++++++ 9 files changed, 582 insertions(+), 6 deletions(-) create mode 100644 legacy/3.0/Dockerfile create mode 100644 legacy/3.0/README.md create mode 100755 legacy/shared/build-elementtree.sh create mode 100755 legacy/shared/docker-entrypoint.sh 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