Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ jobs:
vagrant ssh quadlet -- sudo systemctl restart fapolicyd
- name: Run image pull
run: |
./foremanctl pull-images
./foremanctl pull-images ${{ matrix.database == 'external' && '--database-mode=external' || '' }}
- name: Run deployment
run: |
./foremanctl deploy \
Expand Down
122 changes: 110 additions & 12 deletions docs/developer/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,119 @@ IOP (Insights Operating Platform) deploys on-premise Insights services for advis

See [IOP Architecture](iop.md) for details on the services deployed and configuration options.

### Authenticated Registry Handling
### Image Management

If you need to pull images from private or authenticated container registries, you can configure registry authentication using Podman's auth file.
foremanctl uses Podman quadlet `.image` units to separate image sourcing from container definitions. Each unique container image (foreman, candlepin, pulp, etc.) gets a corresponding `.image` file deployed to `/etc/containers/systemd/`. Container roles reference these by name rather than by full image URL:

#### Setting up Registry Authentication
```ini
# /etc/containers/systemd/foreman.image
[Image]
Image=quay.io/foreman/foreman:nightly
```

1. **Login to your registry** using Podman and save credentials to the default auth file location:
```bash
podman login <registry> --authfile=/etc/foreman/registry-auth.json
```ini
# /etc/containers/systemd/foreman.container (excerpt)
[Container]
Image=foreman.image
```

2. **Deploy as usual** - foremanctl will automatically detect and use the authentication file:
```bash
./foremanctl deploy
All containers that share a base image (e.g., foreman, dynflow-sidekiq, foreman-recurring) reference the same `.image` unit. systemd ensures the image is pulled before any dependent container starts.

#### Image Overrides via Drop-ins

foremanctl uses quadlet's native drop-in mechanism for image overrides. Each `.image` file has a corresponding `.image.d/` directory. Drop-in `.conf` files placed there are merged on top of the base in lexicographic order — last wins.

The quadlet generator reads from two directory tiers, with `/etc/` taking precedence over `/usr/share/`:

```
/usr/share/containers/systemd/
foreman.image.d/
10-product.conf # vendor/RPM layer
20-archive.conf # ISO/archive layer

/etc/containers/systemd/
foreman.image # base, always generated by foremanctl
foreman.image.d/
90-user.conf # user override layer
```

This approach integrates seamlessly with both the happy path and advanced deployment paths described above. The authentication is handled transparently during image pulling operations.
Precedence (last wins):

1. `foreman.image` — foremanctl default from `images.yml`
2. `10-product.conf` — vendor/RPM provided
3. `20-archive.conf` — ISO or archive extraction provided
4. `90-user.conf` — user provided (highest priority)

#### Use Cases

##### Upstream default (no user action)

foremanctl generates `.image` files from its built-in `images.yml`:

```ini
# /etc/containers/systemd/foreman.image (generated by foremanctl)
[Image]
Image=quay.io/foreman/foreman:nightly
```

##### RPM-provided images

A product RPM ships numbered drop-ins to `/usr/share/containers/systemd/` pointing at the product registry. No user action required beyond installing the RPM:

```ini
# /usr/share/containers/systemd/foreman.image.d/10-product.conf (from RPM)
[Image]
Image=registry.example.com/org/foreman:6.17
AuthFile=/etc/foreman/registry-auth.json
```

##### Disconnected install from ISO

The ISO extraction adds a higher-numbered drop-in alongside the RPM layer, redirecting pulls to local archives:

```ini
# /usr/share/containers/systemd/foreman.image.d/20-archive.conf (from ISO)
[Image]
Image=docker-archive:/opt/foreman/images/foreman-6.17.tar
```

##### User's own registry

For redirecting all images to a private registry that mirrors upstream image names, use a `registries.conf.d` entry — one file covers all images:

```ini
# /etc/containers/registries.conf.d/50-foremanctl-mirror.conf
[[registry]]
prefix = "quay.io/theforeman"
location = "katello.example.com/Default_Organization"
```

If image names or tags differ from upstream, use per-image drop-ins instead:

```ini
# /etc/containers/systemd/foreman.image.d/90-user.conf
[Image]
Image=katello.example.com/Default_Organization/foreman:6.17
AuthFile=/etc/foreman/registry-auth.json
```

##### Developer testing a container build

The developer creates a `90-user.conf` drop-in for the image under test. All other images are unaffected:

```ini
# /etc/containers/systemd/foreman.image.d/90-user.conf
[Image]
Image=quay.io/foreman/foreman:pr-12345
```

#### Authenticated Registry Handling

foremanctl configures all image units to use `/etc/foreman/registry-auth.json` as the credential file. When pulling images from an authenticated registry, log in once before deploying:

```bash
podman login <registry> --authfile=/etc/foreman/registry-auth.json
```

## Deployer Stages

Expand All @@ -81,7 +177,7 @@ Some of the stages will be made available to the user to run independently.
a. system requirements
b. tuning requirements
c. certificate requirements
4. Place `.container` files
4. Place `.image` and `.container` files
5. Create podman secrets
6. Reload systemd
7. (re)start services
Expand All @@ -103,7 +199,9 @@ When the user provides parameters to alter the deployment, the deployment utilit

## Container changes (Upgrades)

When the running containers change because the stream was changed in the configuration, the deployment utility will pull the new images and use the new images when starting services.
When the running containers change because the stream was changed in the configuration, the deployment utility regenerates `.image` units with the new image references and restarts services to pull and use the updated images.

User drop-in overrides in `.image.d/90-user.conf` take precedence over the base `.image` values — if a user-provided drop-in pins a specific tag, it will not be changed by an upgrade.

As there is currently no way for the deployment utility to verify which image version is used by a running service, the user is advised to stop all services before performing an upgrade.

Expand Down
62 changes: 42 additions & 20 deletions src/playbooks/pull-images/pull-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,49 @@
roles:
- role: pre_install
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?

ansible.builtin.include_role:
name: "{{ item }}"
environment:
REGISTRY_AUTH_FILE: "{{ registry_auth_file }}"
loop: "{{ images }}"
tasks_from: image.yaml
loop:
- foreman
- candlepin
- pulp
- redis

- name: Pull foreman_proxy images
containers.podman.podman_image:
name: "{{ item }}"
environment:
REGISTRY_AUTH_FILE: "{{ registry_auth_file }}"
loop: "{{ foreman_proxy_images }}"
when:
- "'foreman-proxy' in enabled_features"
- name: Deploy database image units
ansible.builtin.include_role:
name: postgresql
tasks_from: image.yaml
when: database_mode == 'internal'

- name: Deploy proxy image units
ansible.builtin.include_role:
name: foreman_proxy
tasks_from: image.yaml
when: "'foreman-proxy' in enabled_features"

- name: Pull database images
containers.podman.podman_image:
- name: Deploy IOP image units
ansible.builtin.include_role:
name: "{{ item }}"
environment:
REGISTRY_AUTH_FILE: "{{ registry_auth_file }}"
loop: "{{ database_images }}"
when:
- database_mode == 'internal'
tasks_from: image.yaml
loop:
- iop_kafka
- iop_ingress
- iop_puptoo
- iop_yuptoo
- iop_engine
- iop_gateway
- iop_inventory
- iop_advisor
- iop_remediation
- iop_vmaas
- iop_vulnerability
- iop_advisor_frontend
- iop_inventory_frontend
- iop_vulnerability_frontend
when: "'iop' in enabled_features"

- name: Run daemon reload
ansible.builtin.systemd:
daemon_reload: true
1 change: 0 additions & 1 deletion src/roles/candlepin/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ candlepin_ciphers:
- TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256
candlepin_container_image: quay.io/foreman/candlepin
candlepin_container_tag: "4.4.14"
candlepin_registry_auth_file: /etc/foreman/registry-auth.json

candlepin_database_host: localhost
candlepin_database_port: 5432
Expand Down
9 changes: 9 additions & 0 deletions src/roles/candlepin/tasks/image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
- name: Deploy candlepin image unit
ansible.builtin.include_role:
name: images
tasks_from: deploy_image.yaml
vars:
images_definition:
name: candlepin
image: "{{ candlepin_container_image }}:{{ candlepin_container_tag }}"
12 changes: 4 additions & 8 deletions src/roles/candlepin/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
- name: Deploy candlepin image
ansible.builtin.include_tasks: image.yaml

- name: Create log directories
ansible.builtin.file:
path: "{{ item }}"
Expand Down Expand Up @@ -55,17 +58,10 @@
notify:
- Restart candlepin

- name: Pull the Candlepin container image
containers.podman.podman_image:
name: "{{ candlepin_container_image }}:{{ candlepin_container_tag }}"
state: present
environment:
REGISTRY_AUTH_FILE: "{{ candlepin_registry_auth_file }}"

- name: Deploy Candlepin quadlet
containers.podman.podman_container:
name: "candlepin"
image: "{{ candlepin_container_image }}:{{ candlepin_container_tag }}"
image: candlepin.image
Comment thread
ehelms marked this conversation as resolved.
state: quadlet
network: host
hostname: "{{ ansible_facts['hostname'] }}.local"
Expand Down
1 change: 0 additions & 1 deletion src/roles/foreman/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
foreman_container_image: "quay.io/foreman/foreman"
foreman_container_tag: "nightly"
foreman_registry_auth_file: /etc/foreman/registry-auth.json

foreman_database_name: foreman
foreman_database_user: foreman
Expand Down
9 changes: 9 additions & 0 deletions src/roles/foreman/tasks/image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
- name: Deploy foreman image unit
ansible.builtin.include_role:
name: images
tasks_from: deploy_image.yaml
vars:
images_definition:
name: foreman
image: "{{ foreman_container_image }}:{{ foreman_container_tag }}"
14 changes: 5 additions & 9 deletions src/roles/foreman/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
---
- name: Pull the Foreman container image
containers.podman.podman_image:
name: "{{ foreman_container_image }}:{{ foreman_container_tag }}"
state: present
environment:
REGISTRY_AUTH_FILE: "{{ foreman_registry_auth_file }}"
- name: Deploy foreman image
ansible.builtin.include_tasks: image.yaml
Comment thread
ehelms marked this conversation as resolved.

- name: Create secret for DATABASE_URL
containers.podman.podman_secret:
Expand Down Expand Up @@ -98,7 +94,7 @@
- name: Deploy Foreman Container
containers.podman.podman_container:
name: "foreman"
image: "{{ foreman_container_image }}:{{ foreman_container_tag }}"
image: foreman.image
state: quadlet
sdnotify: true
network: host
Expand Down Expand Up @@ -136,7 +132,7 @@
containers.podman.podman_container:
name: "dynflow-sidekiq-%i"
quadlet_filename: "dynflow-sidekiq@"
image: "{{ foreman_container_image }}:{{ foreman_container_tag }}"
image: foreman.image
state: quadlet
sdnotify: true
network: host
Expand Down Expand Up @@ -191,7 +187,7 @@
name: "foreman-recurring-{{ item.instance }}"
quadlet_filename: "foreman-recurring@{{ item.instance }}"
state: quadlet
image: "{{ foreman_container_image }}:{{ foreman_container_tag }}"
image: foreman.image
sdnotify: false
network: host
hostname: "{{ ansible_facts['hostname'] }}.local"
Expand Down
1 change: 0 additions & 1 deletion src/roles/foreman_proxy/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
foreman_proxy_container_image: "quay.io/foreman/foreman-proxy"
foreman_proxy_container_tag: "nightly"
foreman_proxy_registry_auth_file: /etc/foreman/registry-auth.json

foreman_proxy_name: "{{ ansible_facts['fqdn'] }}"
foreman_proxy_https_port: 8443
Expand Down
9 changes: 9 additions & 0 deletions src/roles/foreman_proxy/tasks/image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
- name: Deploy foreman-proxy image unit
ansible.builtin.include_role:
name: images
tasks_from: deploy_image.yaml
vars:
images_definition:
name: foreman-proxy
image: "{{ foreman_proxy_container_image }}:{{ foreman_proxy_container_tag }}"
10 changes: 3 additions & 7 deletions src/roles/foreman_proxy/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
---
- name: Pull the Foreman Proxy container image
containers.podman.podman_image:
name: "{{ foreman_proxy_container_image }}:{{ foreman_proxy_container_tag }}"
state: present
environment:
REGISTRY_AUTH_FILE: "{{ foreman_proxy_registry_auth_file }}"
- name: Deploy foreman-proxy image
ansible.builtin.include_tasks: image.yaml

- name: Create config secrets
ansible.builtin.include_tasks: configs.yaml
Expand All @@ -15,7 +11,7 @@
- name: Deploy Foreman Proxy Container
containers.podman.podman_container:
name: "foreman-proxy"
image: "{{ foreman_proxy_container_image }}:{{ foreman_proxy_container_tag }}"
image: foreman-proxy.image
state: quadlet
sdnotify: true
network: host
Expand Down
3 changes: 3 additions & 0 deletions src/roles/images/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
images_quadlet_dir: /etc/containers/systemd
images_registry_auth_file: /etc/foreman/registry-auth.json
17 changes: 17 additions & 0 deletions src/roles/images/tasks/deploy_image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
- 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.

name: "{{ images_definition.image }}"
state: quadlet
quadlet_dir: "{{ images_quadlet_dir }}"
quadlet_filename: "{{ images_definition.name }}"
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.

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.


- 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.

ansible.builtin.file:
path: "{{ images_quadlet_dir }}/{{ images_definition.name }}.image.d"
state: directory
mode: "0755"
Loading
Loading