diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 311967a1..8b4d723d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,7 +22,7 @@ Applicable spec: ### Checklist -- [ ] The [charm style guide](https://juju.is/docs/sdk/styleguide) was applied +- [ ] The [charm style guide](https://documentation.ubuntu.com/juju/3.6/reference/charm) was applied - [ ] The [contributing guide](https://github.com/canonical/is-charms-contributing-guide) was applied - [ ] The changes are compliant with [ISD054 - Managing Charm Complexity](https://discourse.charmhub.io/t/specification-isd014-managing-charm-complexity/11619) - [ ] The documentation for charmhub is updated. diff --git a/.github/workflows/integration_test_app.yaml b/.github/workflows/integration_test_app.yaml index c81933f3..4182b5a9 100644 --- a/.github/workflows/integration_test_app.yaml +++ b/.github/workflows/integration_test_app.yaml @@ -38,7 +38,7 @@ jobs: - uses: canonical/setup-lxd@v0.1.3 - uses: actions/setup-python@v6 with: - python-version: '3.14' + python-version: "3.14" - name: Install tox run: | sudo apt-get update diff --git a/app/pyproject.toml b/app/pyproject.toml index 01406142..9499d32f 100644 --- a/app/pyproject.toml +++ b/app/pyproject.toml @@ -3,7 +3,7 @@ [project] name = "github-runner-image-builder" -version = "0.14.0" +version = "0.14.1" authors = [ { name = "Canonical IS DevOps", email = "is-devops-team@canonical.com" }, ] diff --git a/app/src/github_runner_image_builder/openstack_builder.py b/app/src/github_runner_image_builder/openstack_builder.py index 36cc7f63..093abe32 100644 --- a/app/src/github_runner_image_builder/openstack_builder.py +++ b/app/src/github_runner_image_builder/openstack_builder.py @@ -344,6 +344,12 @@ def run( script_secrets=image_config.script_config.script_secrets, ssh_conn=ssh_conn, ) + # Cleaning is needed to be compatible with GARM + logger.info("Cleaning cloud-init state before snapshot.") + ssh_conn.run( + "sudo cloud-init clean --logs --machine-id --seed --configs all", + timeout=EXTERNAL_SCRIPT_GENERAL_TIMEOUT, + ) _shutoff_server(conn=conn, server=builder) image = store.create_snapshot( cloud_name=cloud_config.cloud_name, diff --git a/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 b/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 index 90647de0..e5991eb0 100644 --- a/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 +++ b/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 @@ -166,6 +166,34 @@ function configure_system_users() { /usr/sbin/groupadd -f microk8s /usr/sbin/groupadd -f docker /usr/sbin/usermod --append --groups docker,microk8s,lxd,sudo ubuntu + + # Create runner user as alias to ubuntu for GARM compatibility. + # GARM expects a runner user with /home/runner/actions-runner path. + echo "Configuring runner user alias for GARM compatibility" + UBUNTU_UID=$(/usr/bin/id -u ubuntu) + UBUNTU_GID=$(/usr/bin/id -g ubuntu) + # --non-unique: allow reusing ubuntu's UID (duplicate UIDs are rejected by default). + # --uid/--gid: share ubuntu's UID/GID so both users have identical file permissions. + # --no-create-home: skip creating /home/runner; runner's home is set to /home/ubuntu instead. + if /usr/bin/id -u runner >/dev/null 2>&1; then + echo "runner user already exists; ensuring UID/GID/home match ubuntu" + /usr/sbin/usermod --non-unique --uid "$UBUNTU_UID" --gid "$UBUNTU_GID" --home /home/ubuntu runner + else + /usr/sbin/useradd --non-unique --uid "$UBUNTU_UID" --gid "$UBUNTU_GID" --no-create-home --home-dir /home/ubuntu runner + fi + /usr/sbin/usermod --append --groups docker,microk8s,lxd,sudo runner + # Symlink /home/runner -> /home/ubuntu so GARM's hardcoded /home/runner/actions-runner path resolves correctly. + # If /home/runner exists as a plain directory (not a symlink), remove it first; otherwise ln -sfnT cannot + # replace it. A non-empty directory is an unexpected state, so fail with a clear error instead of letting + # ln fail later with a less specific message. + # -T: treat destination as a file path, never as a directory target. + if [ -d /home/runner ] && ! [ -L /home/runner ]; then + if ! rmdir /home/runner 2>/dev/null; then + echo "ERROR: /home/runner exists and is not empty; cannot replace it with a symlink to /home/ubuntu" >&2 + return 1 + fi + fi + /usr/bin/ln -sfnT /home/ubuntu /home/runner } diff --git a/app/tests/unit/test_openstack_builder.py b/app/tests/unit/test_openstack_builder.py index 26205fa3..90beda21 100644 --- a/app/tests/unit/test_openstack_builder.py +++ b/app/tests/unit/test_openstack_builder.py @@ -878,6 +878,34 @@ def test__generate_cloud_init_script( /usr/sbin/groupadd -f microk8s /usr/sbin/groupadd -f docker /usr/sbin/usermod --append --groups docker,microk8s,lxd,sudo ubuntu + + # Create runner user as alias to ubuntu for GARM compatibility. + # GARM expects a runner user with /home/runner/actions-runner path. + echo "Configuring runner user alias for GARM compatibility" + UBUNTU_UID=$(/usr/bin/id -u ubuntu) + UBUNTU_GID=$(/usr/bin/id -g ubuntu) + # --non-unique: allow reusing ubuntu's UID (duplicate UIDs are rejected by default). + # --uid/--gid: share ubuntu's UID/GID so both users have identical file permissions. + # --no-create-home: skip creating /home/runner; runner's home is set to /home/ubuntu instead. + if /usr/bin/id -u runner >/dev/null 2>&1; then + echo "runner user already exists; ensuring UID/GID/home match ubuntu" + /usr/sbin/usermod --non-unique --uid "$UBUNTU_UID" --gid "$UBUNTU_GID" --home /home/ubuntu runner + else + /usr/sbin/useradd --non-unique --uid "$UBUNTU_UID" --gid "$UBUNTU_GID" --no-create-home --home-dir /home/ubuntu runner + fi + /usr/sbin/usermod --append --groups docker,microk8s,lxd,sudo runner + # Symlink /home/runner -> /home/ubuntu so GARM's hardcoded /home/runner/actions-runner path resolves correctly. + # If /home/runner exists as a plain directory (not a symlink), remove it first; otherwise ln -sfnT cannot + # replace it. A non-empty directory is an unexpected state, so fail with a clear error instead of letting + # ln fail later with a less specific message. + # -T: treat destination as a file path, never as a directory target. + if [ -d /home/runner ] && ! [ -L /home/runner ]; then + if ! rmdir /home/runner 2>/dev/null; then + echo "ERROR: /home/runner exists and is not empty; cannot replace it with a symlink to /home/ubuntu" >&2 + return 1 + fi + fi + /usr/bin/ln -sfnT /home/ubuntu /home/runner }} diff --git a/docs/changelog.md b/docs/changelog.md index a77c20cf..02573a62 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ +## [#223 Fix GARM image incompatibility](https://github.com/canonical/github-runner-image-builder-operator/pull/223) (2026-05-27) + +- Add `runner` user as an alias to the `ubuntu` user (same UID/GID, same home directory) so GARM can boot runners from images produced by this charm. + ## [#213 Fix proxy setup] - Fix proxy setup for image-relation joined hook. @@ -11,6 +15,7 @@ ## [#221 Add resource recommendation for charm deployment](https://github.com/canonical/github-runner-image-builder-operator/pull/221) + - Add 2 vCPUs, 8 GiB RAM, and 20 GiB disk OpenStack flavor recommendation. diff --git a/docs/reference/charm-architecture.md b/docs/reference/charm-architecture.md index abe1dff1..e86ccd4b 100644 --- a/docs/reference/charm-architecture.md +++ b/docs/reference/charm-architecture.md @@ -108,15 +108,15 @@ storing OpenStack credentials on disk and initializing the image-builder applica 2. [config-changed](https://documentation.ubuntu.com/juju/3.6/reference/hook/#config-changed): The configuration of the charm has changed. The charm applies the configuration (e.g. changes to proxy or OpenStack credentials). 3. `run`: This is a custom event that is periodically triggered by a cron job. It is used to call the image-builder application to build the image. 4. `run-action`: This is an action event fired by the user to manually trigger the image-builder to build the image. -5. `image-relation-changed`: This is a [relation event](https://juju.is/docs/sdk/relation-events) that fires when relation data changes. It also triggers the image-builder to build the image. +5. `image-relation-changed`: This is a [relation event](https://documentation.ubuntu.com/juju/3.6/reference/hook/#relation-hooks) that fires when relation data changes. It also triggers the image-builder to build the image. Once the build is complete, the image-builder will upload the image taking into account the newly changed relation data (e.g. if the OpenStack project has changed). -> See more about events in the Juju docs: [Event](https://juju.is/docs/sdk/event) +> See more about events in the ops docs: [Event](https://documentation.ubuntu.com/juju/3.6/reference/hook/) ## Charm code overview The `src/charm.py` is the default entry point for a charm and has the GithubRunnerImageBuilderCharm Python class which inherits from CharmBase. CharmBase is the base class -from which all charms are formed, defined by [Ops](https://juju.is/docs/sdk/ops) (Python framework for developing charms). +from which all charms are formed, defined by [Ops](https://documentation.ubuntu.com/ops/latest/) (Python framework for developing charms). > See more in the Juju docs: [Charm](https://documentation.ubuntu.com/juju/3.6/reference/charm/)