diff --git a/.gitea/workflows/build-rpm.yml b/.gitea/workflows/build-rpm.yml new file mode 100644 index 0000000..67b4fec --- /dev/null +++ b/.gitea/workflows/build-rpm.yml @@ -0,0 +1,36 @@ +name: Build development RPM + +on: + push: + branches: ['main'] + pull_request: + workflow_dispatch: + +jobs: + rpm: + runs-on: ubuntu-latest + container: + image: registry.fedoraproject.org/fedora:43 + steps: + - name: Install build tools + run: | + dnf -y install rpm-build python3 resource-agents git tar gzip diffutils findutils coreutils bash nodejs + - name: Check out source + uses: https://github.com/actions/checkout@v4 + with: + persist-credentials: false + - name: Build and check RPMs + run: bash scripts/build-rpm.sh + - name: Test installation in the disposable job container + run: | + dnf -y install ./dist/*.noarch.rpm + python3 tests/verify_installed_rpm.py + # v3 uses the artifact protocol supported by older act_runner instances. + - name: Save development packages + uses: https://github.com/actions/upload-artifact@v3 + with: + name: ocf-managed-container-fedora43 + path: dist/ + if-no-files-found: error + retention-days: 14 + # No registry uploads, release creation, or deployment while licensing is pending. diff --git a/.gitignore b/.gitignore index 2f6680d..a4ac404 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ __pycache__/ secrets/ *.pem *.key + +# RPM build trees and development artifacts +.build/ +dist/ diff --git a/README.md b/README.md index 9698342..55265d2 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,11 @@ the frozen reference or automatically run destructive HA tests. ```bash git clone ssh://git@gitea.loxthelion.com:2222/david/ocf-managed-container.git ``` + +## RPM builds + +Gitea Actions builds development RPMs on `main` pushes and pull requests using +an `ubuntu-latest` runner with a Fedora 43 job container. See +[packaging instructions](packaging/README.md) for local builds and artifacts. +[License provenance remains pending](packaging/PROVENANCE.md); release publishing +and deployment are not configured. diff --git a/packaging/Containerfile b/packaging/Containerfile new file mode 100644 index 0000000..eea3a1a --- /dev/null +++ b/packaging/Containerfile @@ -0,0 +1,3 @@ +FROM registry.fedoraproject.org/fedora:43 +RUN dnf -y install rpm-build python3 resource-agents git tar gzip diffutils findutils \ + coreutils bash nodejs && dnf clean all diff --git a/packaging/PROVENANCE.md b/packaging/PROVENANCE.md new file mode 100644 index 0000000..168a0e4 --- /dev/null +++ b/packaging/PROVENANCE.md @@ -0,0 +1,40 @@ +# Licensing and provenance status + +License selection is pending. No MIT, Apache, or GPL license has been granted +for this project by this document. `LicenseRef-Provenance-Pending` in the draft +RPM spec is a local status marker, not an assertion of an approved license. +Builds are development artifacts; this workflow does not publish releases or +upload to an RPM registry. + +The original development conversation is: +https://chatgpt.com/share/6aa4e495-3e6c-83e9-a13c-fc65a972146b + +The relevant discussion references the OCF developer guide and ClusterLabs' +`heartbeat/podman`, `heartbeat/docker`, `heartbeat/anything`, and +`heartbeat/pgsql` agents. One early proposal describes Podman as a model/base; +other passages discuss combining concepts from the container and PostgreSQL +agents. These establish influence, not proof of copied implementation. + +On 2026-09-12 a preliminary comparison of the frozen reference against the +current upstream main versions of those four agents found only short exact +runs after trimming whitespace and omitting blank lines (maximum 3–4 lines). +The inspected matches were ordinary XML and shell control-flow fragments. +This is not a historical-version audit and does not exclude adapted code. + +Source references: + +- https://github.com/ClusterLabs/resource-agents/blob/main/doc/dev-guides/ra-dev-guide.asc +- https://github.com/ClusterLabs/resource-agents/blob/main/heartbeat/podman +- https://github.com/ClusterLabs/resource-agents/blob/main/heartbeat/docker +- https://github.com/ClusterLabs/resource-agents/blob/main/heartbeat/anything +- https://github.com/ClusterLabs/resource-agents/blob/main/heartbeat/pgsql + +The inspected podman, docker and anything headers specify GPL version 2. +The inspected pgsql header says GPL without a version. The developer guide +explains that LGPL-licensed ocf-shellfuncs can be used by non-GPL agents. +The RPM depends on the system OCF library and does not bundle that library. + +Before a licensed release, resolve the provenance of any adapted material, +retain required notices, select an appropriate license for project-owned +contributions, add the actual license text, and replace the draft RPM marker. +The private development conversation itself is not bundled in source archives. diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..d36b290 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,74 @@ +# Development RPM builds + +The Gitea workflow selects the `ubuntu-latest` runner and runs the build inside +Fedora 43, matching the saved lab package context. The workstation currently +reports Fedora 44; this first workflow does not claim a Fedora 44 build test. +It runs on main pushes, pull requests and manual dispatch. + +The build installs only the OCF executable and nine modules, plus documentation. +It includes no Quadlet, secrets, databases or cluster configuration. There are +no RPM lifecycle scriptlets. Installation does not start, stop or restart a +service or change maintenance mode. Host PostgreSQL packages are not required. + +The draft LicenseRef-Provenance-Pending field is a status marker, not a license +grant. See PROVENANCE.md. Packages are unsigned development artifacts. The +workflow does not publish a release or upload to the Gitea RPM registry. + +## Local build + +In a disposable Fedora 43 build environment, install the packages listed in +Containerfile, then run from a Git checkout: + +```bash +bash scripts/build-rpm.sh +``` + +Artifacts are written to dist/: a noarch binary RPM, a source RPM and SHA256SUMS. +Build trees remain under .build/ for inspection. The script packages the current +working tree; use a clean checkout for a build identified by its Git revision. +Only explicitly listed project directories enter the source archive. Lab +snapshots, local agent configuration, .git and secrets are excluded. + +Version comes from packaging/VERSION. Development Release values are +`0..git<12-character commit>`, plus the Fedora dist suffix. +A future release with Release 1 will sort after these development builds of the +same version. SOURCE_REVISION and SOURCE_DATE_EPOCH can supply build identity +when testing an exported source tree; RPM_OUTPUT_DIR selects the output path. + +%check runs the offline differential suite and checks the staged executable's +metadata from both the build directory and /tmp. No live HA actions run. +Actual target-node validate-all and HA transitions remain separate lab tests. + +## Gitea requirements + +Enable repository Actions. The runner must support container jobs and reach +Gitea, GitHub (for actions), the Fedora image registry, and Fedora repositories. +Checkout uses the job token over HTTPS; no SSH private key is required. + +The workflow uses upload-artifact v3 for compatibility with older act_runner +installations. Newer Gitea runners may support v4; check the installed server +and runner versions before changing the artifact action. Artifact upload and +runner integration must be verified by the first actual Gitea run. + +Before release publishing, resolve licensing, add its text to the package, +choose signing and version-tag policy, and configure registry authentication. + +## Validation performed + +On 2026-09-12, the working-tree candidate was built with Fedora 43 / RPM 6.0.2 +in a disposable rootless Podman container. Binary and source RPM builds passed, +including all 22 offline differential scenarios and staged metadata checks. +DNF successfully resolved dependencies and installed the binary RPM inside that +container. tests/verify_installed_rpm.py verified the installed architecture, +RPM integrity, exact agent bytes, root ownership, 0755/0644 modes, allowed file +paths, absence of lifecycle scriptlets, and metadata from an unrelated cwd. +The source RPM rebuilt with the same version/release without Git metadata. + +Fedora's automatic shebang rewriting is disabled for this shell-only package +so the entrypoint remains byte-identical to the tested source. The Corosync +runtime dependency is the corosync package (its cmapctl binary is in /usr/bin +on Fedora 43). No package was installed on either lab host. + +The Gitea server reports 1.27.3. Its Actions API returned HTTP 401 without +authentication, so a successful local build does not establish runner checkout +or artifact upload success. Check the first run in the repository's Actions tab. diff --git a/packaging/VERSION b/packaging/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/packaging/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/packaging/ocf-managed-container.spec b/packaging/ocf-managed-container.spec new file mode 100644 index 0000000..5f5b0d2 --- /dev/null +++ b/packaging/ocf-managed-container.spec @@ -0,0 +1,69 @@ +%global ocf_provider_dir /usr/lib/ocf/resource.d/loxthelion +# Preserve the proven entrypoint byte-for-byte, including its /bin/bash shebang. +%global __brp_mangle_shebangs %{nil} +%{!?package_version:%global package_version 0.1.0} +%{!?package_release:%global package_release 0.dev} + +Name: ocf-managed-container +Version: %{package_version} +Release: %{package_release}%{?dist} +Summary: PostgreSQL HA agent for a persistent rootless container +# Development status only; see PROVENANCE.md. No release publishing yet. +License: LicenseRef-Provenance-Pending +URL: https://gitea.loxthelion.com/david/ocf-managed-container +Source0: %{name}-%{version}.tar.gz +BuildArch: noarch + +BuildRequires: bash +BuildRequires: python3 +BuildRequires: resource-agents +BuildRequires: hostname +BuildRequires: diffutils +BuildRequires: coreutils +Requires: bash +Requires: /usr/lib/ocf/lib/heartbeat/ocf-shellfuncs +Requires: podman +Requires: systemd +Requires: /usr/sbin/runuser +Requires: /usr/bin/getent +Requires: corosync +Requires: coreutils +Requires: gawk +Requires: sed +Requires: hostname + +%description +OCF agent managing PostgreSQL through a persistent rootless systemd/Quadlet +container, with role-aware monitoring, promotion, demotion and rejoin. +This development package has unresolved license provenance. It does not +configure the cluster or container and does not start or stop services. + +%prep +%autosetup +cp packaging/README.md RPM-BUILD.md + +%build +# Shell modules require no compilation. + +%install +install -d %{buildroot}%{ocf_provider_dir}/managed-container.d +install -m 0755 refactored/managed-container %{buildroot}%{ocf_provider_dir}/managed-container +install -m 0644 refactored/managed-container.d/*.sh %{buildroot}%{ocf_provider_dir}/managed-container.d/ + +%check +python3 tests/verify_refactor.py +# Check the actual staged layout, including resolution from an unrelated cwd. +agent="%{buildroot}%{ocf_provider_dir}/managed-container" +"$agent" meta-data > staged-metadata.xml +reference/managed-container meta-data > reference-metadata.xml +cmp reference-metadata.xml staged-metadata.xml +(cd /tmp && "$agent" meta-data) > other-cwd-metadata.xml +cmp reference-metadata.xml other-cwd-metadata.xml + +%files +%defattr(-,root,root,-) +%doc README.md packaging/PROVENANCE.md RPM-BUILD.md +%dir %{ocf_provider_dir} +%attr(0755,root,root) %{ocf_provider_dir}/managed-container +%dir %{ocf_provider_dir}/managed-container.d +%attr(0644,root,root) %{ocf_provider_dir}/managed-container.d/*.sh diff --git a/scripts/build-rpm.sh b/scripts/build-rpm.sh new file mode 100755 index 0000000..588e004 --- /dev/null +++ b/scripts/build-rpm.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$project_dir" +package_version="$(cat packaging/VERSION)" +[[ "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Invalid packaging/VERSION' >&2; exit 1; } +source_revision="${SOURCE_REVISION:-$(git rev-parse HEAD)}" +[[ "$source_revision" =~ ^[0-9a-f]{40,64}$ ]] || { echo 'Invalid SOURCE_REVISION' >&2; exit 1; } +export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}" +[[ "$SOURCE_DATE_EPOCH" =~ ^[0-9]+$ ]] || { echo 'Invalid SOURCE_DATE_EPOCH' >&2; exit 1; } +package_release="0.$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%d%H%M%S).git${source_revision:0:12}" +output_dir="${RPM_OUTPUT_DIR:-$project_dir/dist}" +mkdir -p "$output_dir" "$project_dir/.build" +output_dir="$(CDPATH= cd -- "$output_dir" && pwd)" +build_dir="$(mktemp -d "$project_dir/.build/rpm.XXXXXXXX")" +mkdir -p "$build_dir"/{SOURCES,SPECS,BUILD,BUILDROOT,RPMS,SRPMS} + +# Explicit source list excludes lab snapshots, local configuration and secrets. +# Include the frozen reference and test harness so the SRPM runs the same checks. +tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner \ + --exclude='__pycache__' --exclude='*.pyc' \ + --transform="s,^,ocf-managed-container-$package_version/," \ + -czf "$build_dir/SOURCES/ocf-managed-container-$package_version.tar.gz" \ + README.md HANDOFF.md refactored reference tests packaging scripts +# Freeze identity into the SRPM spec so rpmbuild --rebuild needs no Git metadata +# or external macro definitions and retains the same version and release. +sed -e "s/^Version:.*/Version: $package_version/" \ + -e "s/^Release:.*/Release: $package_release%{?dist}/" \ + packaging/ocf-managed-container.spec > "$build_dir/SPECS/ocf-managed-container.spec" +rpmbuild -ba --define "_topdir $build_dir" \ + "$build_dir/SPECS/ocf-managed-container.spec" + +for package in "$build_dir"/RPMS/noarch/*.rpm "$build_dir"/SRPMS/*.rpm; do + cp "$package" "$output_dir/" +done +rpm -qp --qf '%{NAME} %{VERSION}-%{RELEASE} %{ARCH}\nLicense: %{LICENSE}\n' \ + "$build_dir"/RPMS/noarch/*.rpm +rpm -qpl "$build_dir"/RPMS/noarch/*.rpm +# The package must never run service or cluster lifecycle scriptlets. +[[ -z "$(rpm -qp --scripts "$build_dir"/RPMS/noarch/*.rpm)" ]] +(cd "$output_dir" && sha256sum ./*.rpm > SHA256SUMS) +printf 'Development RPMs: %s\nBuild tree: %s\n' "$output_dir" "$build_dir" diff --git a/tests/verify_installed_rpm.py b/tests/verify_installed_rpm.py new file mode 100644 index 0000000..1acd1eb --- /dev/null +++ b/tests/verify_installed_rpm.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Read-only checks after installing the RPM in a disposable build container.""" +from pathlib import Path +import stat +import subprocess + +ROOT = Path(__file__).resolve().parents[1] +PROVIDER = Path('/usr/lib/ocf/resource.d/loxthelion') +AGENT = PROVIDER / 'managed-container' +PACKAGE = 'ocf-managed-container' + + +def output(*args): + return subprocess.check_output(args, text=True) + + +assert output('rpm', '-q', '--qf', '%{ARCH}', PACKAGE) == 'noarch' +assert output('rpm', '-q', '--scripts', PACKAGE) == '' +subprocess.run(['rpm', '-V', PACKAGE], check=True) +expected = {AGENT: ROOT / 'refactored/managed-container'} +for path in (ROOT / 'refactored/managed-container.d').glob('*.sh'): + expected[PROVIDER / 'managed-container.d' / path.name] = path +assert len(expected) == 10 +for path, source in expected.items(): + assert path.read_bytes() == source.read_bytes(), path + info = path.stat() + assert (info.st_uid, info.st_gid) == (0, 0), path + assert stat.S_IMODE(info.st_mode) == (0o755 if path == AGENT else 0o644), path + +owned = set(output('rpm', '-ql', PACKAGE).splitlines()) +allowed = {str(path) for path in expected} | {str(PROVIDER), str(PROVIDER / 'managed-container.d')} +unexpected = {path for path in owned - allowed if not path.startswith('/usr/share/doc/' + PACKAGE)} +assert not unexpected, unexpected + +reference = output(str(ROOT / 'reference/managed-container'), 'meta-data') +installed = subprocess.check_output([str(AGENT), 'meta-data'], cwd='/tmp', text=True) +assert installed == reference +print('PASS installed RPM: noarch, file integrity, root ownership, modes, payload, no scriptlets, metadata/module loading')