-
Notifications
You must be signed in to change notification settings - Fork 39
ENT-13777, ENT-13784: Added container-based CFEngine package builder #2146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c995ac4
554c212
57bf999
fd1ebb4
9b9b7cc
038a316
25a5a88
1859e0c
3a5b5f4
fb44f60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| name: Build base images | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| build-and-push: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| packages: write | ||
| strategy: | ||
| matrix: | ||
| platform: | ||
| - ubuntu-20 | ||
| - ubuntu-22 | ||
| - ubuntu-24 | ||
| - debian-11 | ||
| - debian-12 | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Log in to ghcr.io | ||
| uses: docker/login-action@v4 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Build and push image | ||
| run: ./build-in-container.py --platform ${{ matrix.platform }} --push-image |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| #!/bin/bash | ||
| set -e | ||
|
|
||
| # Configuration via environment variables: | ||
| # PROJECT, BUILD_TYPE, EXPLICIT_ROLE, BUILD_NUMBER, EXPLICIT_VERSION | ||
|
|
||
| BASEDIR=/home/builder/build | ||
| export BASEDIR | ||
| export AUTOBUILD_PATH="$BASEDIR/buildscripts" | ||
|
|
||
| mkdir -p "$BASEDIR" | ||
|
|
||
| # Bind-mounted directories may be owned by the host user's UID. | ||
| # Fix ownership so builder can write to them. | ||
| sudo chown -R "$(id -u):$(id -g)" "$HOME/.cache" /output | ||
|
|
||
| # Prevent git "dubious ownership" errors | ||
| git config --global --add safe.directory '*' | ||
|
|
||
| # === Sync source repos === | ||
| repos="buildscripts core masterfiles" | ||
| if [ "$PROJECT" = "nova" ]; then | ||
| repos="$repos enterprise nova mission-portal" | ||
| fi | ||
|
|
||
| for repo in $repos; do | ||
| src="/srv/source/$repo" | ||
| # Use rsync -aL to follow symlinks during copy. | ||
| # The source dir may use symlinks (e.g., core -> cfengine/core/). | ||
| # -L resolves them at copy time, so the destination gets real files | ||
| # regardless of the host directory layout. | ||
| # Exclude acceptance test workdirs — they contain broken symlinks left | ||
| # over from previous test runs and are not needed for building. | ||
| if [ -d "$src" ] || [ -L "$src" ]; then | ||
| echo "Syncing $repo..." | ||
| sudo rsync -aL --exclude='config.cache' --exclude='workdir' --chown="$(id -u):$(id -g)" "$src/" "$BASEDIR/$repo/" | ||
| else | ||
| echo "ERROR: Required repository $repo not found" >&2 | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| install_mission_portal_deps() ( | ||
| set -e | ||
|
|
||
| if [ -f "$BASEDIR/mission-portal/public/scripts/package.json" ]; then | ||
| echo "Installing npm dependencies..." | ||
| npm ci --prefix "$BASEDIR/mission-portal/public/scripts/" | ||
| echo "Building react components..." | ||
| npm run build --prefix "$BASEDIR/mission-portal/public/scripts/" | ||
| rm -rf "$BASEDIR/mission-portal/public/scripts/node_modules" | ||
| fi | ||
|
|
||
| if [ -f "$BASEDIR/mission-portal/composer.json" ]; then | ||
| echo "Installing Mission Portal PHP dependencies..." | ||
| (cd "$BASEDIR/mission-portal" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) | ||
| fi | ||
|
|
||
| if [ -f "$BASEDIR/nova/api/http/composer.json" ]; then | ||
| echo "Installing Nova API PHP dependencies..." | ||
| (cd "$BASEDIR/nova/api/http" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) | ||
| fi | ||
|
|
||
| if [ -f "$BASEDIR/mission-portal/public/themes/default/bootstrap/cfengine_theme.less" ]; then | ||
| echo "Compiling Mission Portal styles..." | ||
| mkdir -p "$BASEDIR/mission-portal/public/themes/default/bootstrap/compiled/css" | ||
| (cd "$BASEDIR/mission-portal/public/themes/default/bootstrap" && | ||
| lessc --compress ./cfengine_theme.less ./compiled/css/cfengine.less.css) | ||
| fi | ||
|
|
||
| if [ -f "$BASEDIR/mission-portal/ldap/composer.json" ]; then | ||
| echo "Installing LDAP API PHP dependencies..." | ||
| (cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs) | ||
| fi | ||
| ) | ||
|
|
||
| # === Step runner with failure reporting === | ||
| # Disable set -e so we can capture exit codes and report which step failed. | ||
| set +e | ||
| run_step() { | ||
| local name="$1" | ||
| shift | ||
| echo "=== Running $name ===" | ||
| "$@" | ||
| local rc=$? | ||
| if [ $rc -ne 0 ]; then | ||
| echo "" | ||
| echo "=== FAILED: $name (exit code $rc) ===" | ||
| exit $rc | ||
| fi | ||
| } | ||
|
|
||
| # === Build steps === | ||
| run_step "01-autogen" "$BASEDIR/buildscripts/build-scripts/autogen" | ||
| run_step "02-install-dependencies" "$BASEDIR/buildscripts/build-scripts/install-dependencies" | ||
| if [ "$EXPLICIT_ROLE" = "hub" ]; then | ||
| run_step "03-mission-portal-deps" install_mission_portal_deps | ||
| fi | ||
| run_step "04-configure" "$BASEDIR/buildscripts/build-scripts/configure" | ||
| run_step "05-compile" "$BASEDIR/buildscripts/build-scripts/compile" | ||
| run_step "06-package" "$BASEDIR/buildscripts/build-scripts/package" | ||
|
|
||
| # === Copy output packages === | ||
| # Packages are created under $BASEDIR/<project>/ by dpkg-buildpackage / rpmbuild. | ||
| # Exclude deps-packaging to avoid copying dependency packages. | ||
| find "$BASEDIR" -maxdepth 4 \ | ||
| -path "$BASEDIR/buildscripts/deps-packaging" -prune -o \ | ||
| \( -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.gz' \) -print \ | ||
| -exec cp {} /output/ \; | ||
|
|
||
| echo "" | ||
| echo "=== Build complete ===" | ||
| ls -lh /output/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| # build-in-container | ||
|
|
||
| Build CFEngine packages inside Docker containers using build scripts. Requires | ||
| only Docker and Python 3 on the host. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```bash | ||
| # Build a community agent .deb for Ubuntu 22 | ||
| ./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG | ||
|
|
||
| # Build a nova hub release package for Debian 12 | ||
| ./build-in-container.py --platform debian-12 --project nova --role hub --build-type RELEASE | ||
| ``` | ||
|
|
||
| In the examples above, we run the script from inside `buildscripts/` (with | ||
| `buildscripts` as our current working directory). This is not required — if not | ||
| specified, defaults will: | ||
|
|
||
| - Look for sources relative to the script (parent directory of | ||
| `build-in-container.py`). | ||
| - Place cache files in the user's home directory | ||
| (`~/.cache/cfengine/buildscripts`). | ||
| - Use the current working directory for output packages (`./output/`). | ||
|
|
||
| ## Usage | ||
|
|
||
| ``` | ||
| ./build-in-container.py --platform PLATFORM --project PROJECT --role ROLE --build-type TYPE [OPTIONS] | ||
| ``` | ||
|
|
||
| ### Required arguments | ||
|
|
||
| | Option | Description | | ||
| | -------------- | ------------------------------------------------------- | | ||
| | `--platform` | Target platform (e.g. `ubuntu-22`, `debian-12`) | | ||
| | `--project` | `community` or `nova` (not required for `--push-image`) | | ||
| | `--role` | `agent` or `hub` (not required for `--push-image`) | | ||
| | `--build-type` | `DEBUG` or `RELEASE` (not required for `--push-image`) | | ||
|
|
||
| ### Optional arguments | ||
|
|
||
| | Option | Default | Description | | ||
| | ------------------ | -------------------------------- | ----------------------------------------------------------- | | ||
| | `--output-dir` | `./output` | Where to write output packages | | ||
| | `--cache-dir` | `~/.cache/cfengine/buildscripts` | Dependency cache directory | | ||
| | `--build-number` | `1` | Build number for package versioning | | ||
| | `--version` | auto | Override version string | | ||
| | `--rebuild-image` | | Force rebuild of Docker image (bypasses Docker layer cache) | | ||
| | `--push-image` | | Build image and push to registry, then exit | | ||
| | `--shell` | | Drop into a bash shell inside the container for debugging | | ||
| | `--list-platforms` | | List available platforms and exit | | ||
| | `--source-dir` | parent of `buildscripts/` | Root directory containing repos | | ||
|
|
||
| ## Supported platforms | ||
|
|
||
| | Name | Base image | | ||
| | ----------- | -------------- | | ||
| | `ubuntu-20` | `ubuntu:20.04` | | ||
| | `ubuntu-22` | `ubuntu:22.04` | | ||
| | `ubuntu-24` | `ubuntu:24.04` | | ||
| | `debian-11` | `debian:11` | | ||
| | `debian-12` | `debian:12` | | ||
|
|
||
| Adding a new Debian/Ubuntu platform requires only a new entry in the `PLATFORMS` | ||
| dict in `build-in-container.py`. Adding a non-debian based platform (e.g., | ||
| RHEL/CentOS) requires a new `container/Dockerfile.rhel` plus platform entries. | ||
|
|
||
| ## How it works | ||
|
|
||
| The system has three components: | ||
|
|
||
| 1. **`build-in-container.py`** (Python) -- the orchestrator that runs on the host. | ||
| Parses arguments, builds the Docker image, and launches the container with | ||
| the correct mounts and environment variables. | ||
|
|
||
| 2. **`build-in-container-inner.sh`** (Bash) -- runs inside the container. Copies | ||
| source repos from the read-only mount, then calls the existing build scripts | ||
| in order. | ||
|
|
||
| 3. **`container/Dockerfile.debian`** -- parameterized Dockerfile shared by all | ||
| Debian/Ubuntu platforms via a `BASE_IMAGE` build arg. | ||
|
|
||
| ### Container mounts | ||
|
|
||
| | Host path | Container path | Mode | Purpose | | ||
| | ---------------------------------------- | ----------------------------------------- | ---------- | ------------------------------------- | | ||
| | Source repos (parent of `buildscripts/`) | `/srv/source` | read-only | Protects host repos from modification | | ||
| | `~/.cache/cfengine/buildscripts/` | `/home/builder/.cache/buildscripts_cache` | read-write | Dependency cache shared across builds | | ||
| | `./output/` | `/output` | read-write | Output packages copied here | | ||
|
|
||
| ### Build steps | ||
|
|
||
| The inner script runs these steps in order: | ||
|
|
||
| 1. **autogen** -- runs `autogen.sh` in each repo | ||
| 2. **install-dependencies** -- builds and installs bundled dependencies | ||
| 3. **mission-portal-deps** -- (hub only) installs PHP/npm/LESS assets | ||
| 4. **configure** -- runs `./configure` with platform-appropriate flags | ||
| 5. **compile** -- compiles and installs to the dist tree | ||
| 6. **package** -- creates `.deb` or `.rpm` packages | ||
|
|
||
| ## Docker image management | ||
|
|
||
| By default, the script pulls a pre-built image from the container registry | ||
| (`ghcr.io/cfengine`). If the pull fails (e.g. no network, image not yet | ||
| published), it falls back to building the image locally. | ||
|
|
||
| Use `--rebuild-image` to skip the registry and force a local rebuild — useful | ||
| when iterating on the Dockerfile. The local build tracks the Dockerfile content | ||
| hash and skips rebuilding when nothing has changed. | ||
|
|
||
| ### Container registry | ||
|
|
||
| Images are hosted at `ghcr.io/cfengine` and versioned via `IMAGE_VERSION` in | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you might mention what credentials you need to push or to pull?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GITHUB_TOKEN to push (done through manually triggered workflow). To pull, no credentials needed as long as the images are made public (which we intend). You can push manually, but then you would need a PAT (classic) |
||
| `build-in-container.py`. To push a new image: | ||
|
|
||
| ```bash | ||
| # Build and push a single platform | ||
| ./build-in-container.py --platform ubuntu-22 --push-image | ||
| ``` | ||
|
|
||
| `--push-image` always builds with `--no-cache` to pick up the latest upstream | ||
| packages, then pushes to the registry. However, you must be logged in to | ||
| `ghcr.io` first. You can log in with a personal access token (classic) that has | ||
| the write:packages scope. Alternatively, trigger the GitHub Actions workflow | ||
| which handles authentication automatically. | ||
|
|
||
| #### GitHub Actions workflow | ||
|
|
||
| The `build-base-images.yml` workflow builds and pushes images for every | ||
| supported platform. It is triggered manually via `workflow_dispatch`. | ||
|
|
||
| The workflow authenticates to `ghcr.io` using the automatic `GITHUB_TOKEN` | ||
| provided by GitHub Actions. For this to work: | ||
|
|
||
| - The repository must grant `GITHUB_TOKEN` write access to packages. In the | ||
| GitHub repository settings, go to **Actions → General → Workflow permissions** | ||
| and select **Read and write permissions**. | ||
| - After the first push, each package defaults to private. To allow anonymous | ||
| pulls, go to the package on GitHub (**your org → Packages**), open **Package | ||
| settings**, and change the visibility to **Public**. This is a one-time step | ||
| per package — new tags (e.g. from bumping `IMAGE_VERSION`) inherit the | ||
| existing visibility. | ||
|
|
||
| ### Updating the toolchain | ||
|
|
||
| 1. Edit `container/Dockerfile.debian` as needed | ||
| 2. Test locally with `--rebuild-image` | ||
| 3. Bump `IMAGE_VERSION` in `build-in-container.py` | ||
| 4. Commit the Dockerfile change + version bump | ||
| 5. Push new images by triggering the GitHub Actions workflow | ||
|
|
||
| ## Debugging | ||
|
|
||
| ```bash | ||
| # Drop into a shell inside the container | ||
| ./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG --shell | ||
| ``` | ||
|
|
||
| The shell session has the same mounts and environment as a build run. The | ||
| container is ephemeral (`--rm`), so any changes are lost on exit. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we should rename
build-scriptsfolder to steps / build-steps 😅.