Skip to content

Add Podman quadlet .image file support#501

Open
ehelms wants to merge 1 commit into
theforeman:masterfrom
ehelms:image-quadlet-support
Open

Add Podman quadlet .image file support#501
ehelms wants to merge 1 commit into
theforeman:masterfrom
ehelms:image-quadlet-support

Conversation

@ehelms
Copy link
Copy Markdown
Member

@ehelms ehelms commented May 8, 2026

Why are you introducing these changes? (Problem description, related links)

Container image references were embedded directly in each service role as variable defaults, making it difficult to override images for product builds, disconnected installs, or developer testing without modifying foremanctl-managed files.

Resolves: #277

What are the changes introduced in this pull request?

  • Introduce a shared images role with deploy_image.yaml that generates a .image quadlet unit file and .image.d/ drop-in directory for each container image using containers.podman.podman_image (state: quadlet)
  • Each service role gains an image.yaml task that calls the images role with its image name and tag, removing per-role image variable defaults
  • Container roles now reference Image=<name>.image instead of full registry URLs, binding image pull to the quadlet unit lifecycle managed by systemd
  • pull-images playbook updated to deploy image units and run daemon_reload rather than pulling images directly; actual pulls happen on first service start
  • Drop-in hierarchy (/usr/share/containers/systemd/<name>.image.d/10-product.conf, 20-archive.conf, /etc/containers/systemd/<name>.image.d/90-user.conf) allows RPM vendors, ISO extraction, and users to override images without modifying foremanctl-generated files
  • images_registry_auth_file defaults to /etc/foreman/registry-auth.json; users with authenticated registries run podman login <registry> --authfile=/etc/foreman/registry-auth.json before deploying
  • Add tests/images_test.py for core image files, drop-in directories, and systemd service existence; IOP image tests in tests/iop/images_test.py

How to test this pull request

Steps to reproduce:

  1. Deploy with ./foremanctl deploy
  2. Verify .image files exist under /etc/containers/systemd/ for each service
  3. Verify <name>-image systemd services exist
  4. Run ./forge test and confirm images_test.py passes
  5. Place /etc/containers/systemd/foreman.image.d/90-user.conf with an alternate image tag, run systemctl daemon-reload, and confirm the override takes precedence

Checklist

  • Tests added/updated (if applicable)
  • Documentation updated (if applicable)

@ekohl
Copy link
Copy Markdown
Member

ekohl commented May 8, 2026

Do you intend to resolve #277 with this?

@ekohl
Copy link
Copy Markdown
Member

ekohl commented May 8, 2026

Add a Podman quadlet .image unit system with three-tier precedence: admin overrides (/etc/foremanctl/images.d/) > vendor/RPM overrides (/usr/share/foremanctl/images.d/) > generated defaults from Ansible variables

I'm wondering if we can leverage the built in /usr/share/containers/systemd/ somehow. It's a major change, but I've been wondering if we should ship the container definitions in an RPM and deploy them to /usr/share/containers/systemd, letting foremanctl only add overrides in /etc/containers/systemd.

Also not sure if you can use /etc/containers/systemd/foreman.image.d/override.conf to replace parts of it. If so, perhaps an RPM can ship /usr/share/containers/systemd/foreman.image.d/disconnected.conf to provide disconnected installation instructions.

@ehelms
Copy link
Copy Markdown
Member Author

ehelms commented May 8, 2026

I'm wondering if we can leverage the built in /usr/share/containers/systemd/ somehow. It's a major change, but I've been wondering if we should ship the container definitions in an RPM and deploy them to /usr/share/containers/systemd, letting foremanctl only add overrides in /etc/containers/systemd.

As we stated before, tieing ourselves to an RPM laying down some base container will slow us down as any time it requires a change we have to wait for the RPM to cycle and it would split the base from any overrides. I think we would end up just deploying everything via overrides. Just feel like friction right now.

I did think about this and here is an alternative using this concept:

Updated Proposal

Place .image files alongside .container files in /etc/containers/systemd/, using quadlet's native drop-in mechanism for overrides.

Layout:

/etc/containers/systemd/
foreman.image # base, templated by foremanctl
foreman.image.d/
50-disconnected.conf # optional: disconnected registry override
90-user.conf # optional: user override

Precedence (last wins):

  1. foreman.image -- foremanctl default
  2. 50-disconnected.conf -- disconnected layer
  3. 90-user.conf -- user layer

Numbered prefixes enforce ordering with gaps for future layers.

What this eliminates:

  • Custom /etc/foremanctl/images.d/ and /usr/share/foremanctl/images.d/ directories
  • Manual stat/copy/conditional precedence logic in deploy_image.yaml
  • The entire images role

What stays the same:

  • Container roles reference Image=foreman.image (no change)
  • foremanctl templates the base .image file (just changes the destination)
  • daemon-reload after deployment (already happening)

@ekohl
Copy link
Copy Markdown
Member

ekohl commented May 12, 2026

Updated Proposal

This is what I had in mind.

Custom /etc/foremanctl/images.d/ and /usr/share/foremanctl/images.d/ directories

This is IMHO the big benefit.

@evgeni
Copy link
Copy Markdown
Member

evgeni commented May 13, 2026

I like the idea!

There is an alternative approach from @bochi in #503, which is much simpler in the diff size, but requires custom code vs using native systemd/podman override options. Mostly mentioning it here as I wonder if this PR would also solve their usecase.

@bochi
Copy link
Copy Markdown

bochi commented May 13, 2026

i like this approach too and will change my PR to only have the feature addition but no changes to image locations. looking forward to this getting merged 🙂

@ianballou
Copy link
Copy Markdown

Being able to override the container images easily would be really beneficial for development. If we eventually have containers built from PRs much like packit does with RPMs today, you could just drop those in. Or, when we need to test new versions of Pulp, we could simply drop that new Pulp container in (theforeman/pulp-oci-images#13).

Comment thread docs/developer/deployment.md Outdated
Comment thread src/roles/candlepin/tasks/main.yml
@@ -1,6 +1,5 @@
---
iop_advisor_frontend_container_image: "quay.io/iop/advisor-frontend"
iop_advisor_frontend_container_tag: "foreman-3.18"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but shouldn't this follow nightly?

Comment thread tests/images_test.py Outdated
@ehelms ehelms force-pushed the image-quadlet-support branch 2 times, most recently from 9b0d041 to 2f9968f Compare May 15, 2026 20:24
Comment thread docs/developer/deployment.md Outdated
Image=quay.io/foreman/foreman:nightly
```

**User's own registry:** The user creates a `90-user.conf` drop-in to point at their own registry. This overrides the base layer:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular use case may be easier with containers-registries.conf. From reading the docs I think adding the following to /etc/containers/registries.conf should work:

[[registry]]
prefix = "quay.io/theforeman"
location = "katello.example.com/Default_Organization"

Comment thread docs/developer/deployment.md Outdated
post_tasks:
- name: Pull an image
containers.podman.podman_image:
- name: Deploy core image units
Copy link
Copy Markdown
Contributor

@arvind4501 arvind4501 May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what i understand is this only writes .image files to /etc/containers/systemd/ and does a daemon_reload but does that actually pulls a image? i mean does it download the images that can be used without re-pulling in deploy?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.podman.io/en/latest/markdown/podman-image.unit.5.html#usage-summary says enerating a systemd .service that runs podman image pull. is confusing, does it pull by default or on start of .service

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it generates a .service (like foreman.image.service) file but it requires someone/something to start it. Whether that's a user running systemctl or another service depending on it (I'd expect foreman.service to).

In this case I'd expect foremanctl pull-images to ensure the service is started, but some questions that pop up:

  • What if the service (like foreman.image.service) was already started? Is it a noop or does it run again?
  • What happens if an updated image is pulled? Does it restart dependent services?

Looking at the docs Policy influences this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quadlet generator makes each .image unit a Type=oneshot service. When a container starts, systemd activates its image service dependency, which runs podman image pull (or no-ops if the image already exists), then exits. The service transitions active → inactive when the oneshot finishes.

This strategy has lots of benefits and one small downside, on systemctl start foreman.target:

  • Each container Requires its image service
  • Since the image services are inactive (they completed as oneshots), systemd re-runs them
  • Each one checks whether the image exists, which is fast if it's cached — but with 20 image services being re-checked concurrently on a busy system, it adds latency

Because of this I had to increase the foreman.target retries when we test start/stop/restart of it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs mention Policy=always and that implies it always connects over the network. Perhaps we should use Policy=missing so it's only a local operation after the first installation?

Then we do need an explicit process to update images, but we already have an update and upgrade guide. Perhaps that's good anyway? Otherwise you can unexpectedly pull in z-streams after a reboot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a container starts, systemd activates its image service dependency, which runs podman image pull

so technically it happens when containers start not when we run foremanctl pull-images? thats seems odd with what we expect from pull-images, as a user i can pull-images and then deploy which reduces the deployment time as i already have latest images.
or i am interpreting wrong?

@ehelms ehelms force-pushed the image-quadlet-support branch from 2f9968f to e392b79 Compare May 19, 2026 14:11
Comment thread src/roles/foreman/tasks/main.yaml
Comment thread src/roles/images/tasks/deploy_image.yaml Outdated
dest: "{{ images_quadlet_dir }}/{{ images_definition.name }}.image"
mode: "0644"

- name: Create drop-in directory for {{ images_definition.name }}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't deploy anything here, right? it's just "so it's there so someone else can do it"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. So, arguably, I can drop this here and just let whatever needs to do this create them (e.g. downstreams with different images or upstream release RPMs). Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought packages would not touch it here anyway but use /usr/lib?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/usr/share but yes you are correct. This override directory would be used for these two scenarios:

One argument is these are specialized and thus the directory should be created at the time of use. This adds a step the user or developer has to remember. Whereas, having the directory allows quick and easy drop in with less overhead remembering two steps.

What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One argument is these are specialized and thus the directory should be created at the time of use. This adds a step the user or developer has to remember. Whereas, having the directory allows quick and easy drop in with less overhead remembering two steps.

IMHO you can't count on things existing just because Ansible created it so I'd prefer the tools to be robust by having a mkdir -p in there. So my suggestion is to remove the drop-in directory from Ansible.

Comment thread src/vars/images.yml Outdated
Comment thread docs/developer/deployment.md Outdated
@ehelms ehelms force-pushed the image-quadlet-support branch from e392b79 to f0e2546 Compare May 19, 2026 16:01
Comment thread tests/iop/images_test.py Outdated
Comment thread src/roles/images/tasks/deploy_image.yaml Outdated
@@ -0,0 +1,15 @@
---
- name: Generate image file for {{ images_definition.name }}
containers.podman.podman_image:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on #501 (comment): I think we should set Policy=missing so it doesn't automatically update on every reboot/restart.

Then we can use podman auto-update to force updates.

Introduce .image quadlet units to decouple image sourcing from container
definitions. Container roles now reference Image=<name>.image instead of
full registry URLs, and a new images role handles deployment with a
three-tier precedence model: admin overrides (/etc/foremanctl/images.d/)
> vendor RPMs (/usr/share/foremanctl/images.d/) > generated defaults.

Resolves: theforeman#277

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
quadlet_file_mode: "0644"
quadlet_options:
- "Policy=missing"
- "Environment=REGISTRY_AUTH_FILE={{ images_registry_auth_file }}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per containers/ansible-podman-collections#1034 (comment) you can use authfile:

Suggested change
- "Environment=REGISTRY_AUTH_FILE={{ images_registry_auth_file }}"
authfile: "{{ images_registry_auth_file }}"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. This will set AuthFile and this will enforce the existence of this file. What we want is to set the environment variable like we do in puppet-iop.

@ehelms
Copy link
Copy Markdown
Member Author

ehelms commented May 20, 2026

I'm not yet sure what change in the design is not leading to the failure. I will need to follow up with deeper investigation.

quadlet_file_mode: "0644"
quadlet_options:
- "Policy=missing"
- "Environment=REGISTRY_AUTH_FILE={{ images_registry_auth_file }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- "Environment=REGISTRY_AUTH_FILE={{ images_registry_auth_file }}"
- "AUTH_FILE={{ images_registry_auth_file }}"

we can't use Environment or REGISTRY_AUTH_FILE directly in [Image] section as thats not one of the valid options(https://docs.podman.io/en/latest/markdown/podman-image.unit.5.html#options)
we could use REGISTRY_AUTH_FILE but not directly in .image file https://docs.podman.io/en/latest/markdown/podman-image.unit.5.html#authfile-path

Copy link
Copy Markdown
Contributor

@arvind4501 arvind4501 May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and mostly CI is also failing because of same reason

systemd[1]: Reloading.
May 20 09:47:23 quadlet.example.com quadlet-generator[34868]: converting "postgresql.image": unsupported key 'Environment' in group 'Image' in /etc/containers/systemd/postgresql.image
May 20 09:47:23 quadlet.example.com quadlet-generator[34868]: processing encountered some errors
May 20 09:47:23 quadlet.example.com systemd-rc-local-generator[34879]: /etc/rc.d/rc.local is not marked executable, skipping.
May 20 09:47:23 quadlet.example.com systemd[34865]: /usr/lib/systemd/system-generators/podman-system-generator failed with exit status 1.

bochi added a commit to bochi/foremanctl that referenced this pull request May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define .image files for a single definition of images

6 participants