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
172 changes: 162 additions & 10 deletions docs/developer/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,173 @@ 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](https://docs.podman.io/en/latest/markdown/podman-image.unit.5.html) 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
See the [podman-systemd.unit(5)](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) reference for the full quadlet unit format.

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

```ini
# /etc/containers/systemd/foreman.container (excerpt)
[Container]
Image=foreman.image
```

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
Comment thread
ekohl marked this conversation as resolved.

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 # local media layer

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

Precedence (last wins):

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

#### registries.conf vs .image.d drop-ins

Both `registries.conf` and `.image.d` drop-ins can redirect where an image is pulled from, but they behave differently and suit different use cases.
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.

Documenting both use cases is excellent.


`registries.conf` applies a transparent redirect at pull time — the image is fetched from the `location` registry but stored in local storage under the original `prefix` name. This means `podman images` shows the upstream name (e.g., `quay.io/foreman/foreman:nightly`), and the `.image` quadlet continues to reference that same name. This works well when the private registry mirrors upstream image names and tags exactly.
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.


`.image.d` drop-ins directly replace the `Image=` value in the quadlet unit. The image is pulled from and stored under the new reference. This is required when the image name or tag changes completely (e.g., `quay.io/foreman/foreman:nightly` → `registry.example.com/org/foreman-rhel9:stream`), since `registries.conf` cannot remap image names — only registry/namespace locations.
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.


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

When a product uses different image names or tags from the upstream references (e.g., `foreman-rhel9:stream` instead of `foreman:nightly`), the RPM ships `.image.d` drop-ins to override each image unit directly:

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

```ini
# /usr/share/containers/systemd/candlepin.image.d/10-product.conf (from RPM)
[Image]
Image=registry.example.com/org/candlepin-rhel9:stream
```

No user action required beyond installing the RPM and logging into the product registry:

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
podman login --authfile=/etc/foreman/registry-auth.json registry.example.com
```

##### Disconnected install from local media

In air-gapped environments, container images must be brought in without network access. `registries.conf` cannot express non-registry sources, but Podman can read images from local archive files. Images can be transported via USB or other local media using `podman save` / `podman load`, then referenced via drop-ins:

See [podman-export(1)](https://docs.podman.io/en/latest/markdown/podman-export.1.html) for producing archive files.
Comment on lines +144 to +146
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.

If you use the example below you don't need podman load because it's implicit. I also wonder what the difference between podman save and podman export is. Perhaps drop the last part?

Suggested change
In air-gapped environments, container images must be brought in without network access. `registries.conf` cannot express non-registry sources, but Podman can read images from local archive files. Images can be transported via USB or other local media using `podman save` / `podman load`, then referenced via drop-ins:
See [podman-export(1)](https://docs.podman.io/en/latest/markdown/podman-export.1.html) for producing archive files.
In air-gapped environments, container images must be brought in without network access. `registries.conf` cannot express non-registry sources, but Podman can read images from local archive files. These images can be transported via USB or other local media, then referenced via drop-ins.
See [podman-export(1)](https://docs.podman.io/en/latest/markdown/podman-export.1.html) for producing archive files.


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

##### User's own registry

When the private registry mirrors upstream image names and tags exactly, `registries.conf.d` handles namespace-level remapping. foremanctl images span two upstream namespaces, so two entries are needed at minimum. See [containers-registries.conf(5)](https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md) for the full format.

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

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

When the private registry uses different image names or tags, use `.image.d` drop-ins instead:

```ini
# /etc/containers/systemd/foreman.image.d/90-user.conf
[Image]
Image=katello.example.com/Default_Organization/foreman-rhel9:stream
```

##### Developer testing a container build

An `.image.d` drop-in overrides a single image without affecting others:

```ini
# /etc/containers/systemd/foreman.image.d/90-user.conf
# To test https://github.com/theforeman/foreman-oci-images/pull/12345
[Image]
Image=quay.io/foreman/stage/foreman:pr-12345
```

2. **Deploy as usual** - foremanctl will automatically detect and use the authentication file:
#### Authenticated Registry Handling

foremanctl uses `/etc/foreman/registry-auth.json` as the default credential store. When pulling images from an authenticated registry, log in using that file:

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

Credentials must be for the registry the image is **physically pulled from**. When using `registries.conf` redirects, that is the `location` registry. When using `.image.d` drop-ins, that is the registry in the `Image=` value.

foremanctl sets `REGISTRY_AUTH_FILE` in the `[Service]` section of each generated `.image` file. Quadlet propagates this setting to the generated `*-image.service`, so podman uses the auth file whenever the image service runs — including during `pull-images`:

```ini
# /etc/containers/systemd/foreman.image (generated by foremanctl, excerpt)
[Service]
Environment=REGISTRY_AUTH_FILE=/etc/foreman/registry-auth.json
```

This approach integrates seamlessly with both the happy path and advanced deployment paths described above. The authentication is handled transparently during image pulling operations.
If the auth file does not exist (unauthenticated registry), podman ignores it gracefully — no error is raised.

#### Image Pulling (pull-images)

The `foremanctl pull-images` command is an optional pre-deployment step that pulls all container images before running `foremanctl deploy`. This reduces deploy time and allows pre-staging images separately from deployment.

`pull-images` deploys the `.image` unit files (making them available for quadlet to merge with any existing drop-ins from installed RPMs), then starts each `*-image.service` to perform the actual pull. To ensure mutable tags (such as `nightly`, `latest`, or `stream`) are always refreshed, `pull-images` temporarily creates a `Policy=always` drop-in before starting each service and removes it afterward, restoring `Policy=missing` for normal operation. See the [`Policy` field in `podman-image.unit(5)`](https://docs.podman.io/en/latest/markdown/podman-image.unit.5.html) for the full list of pull policies.

```
/etc/containers/systemd/
foreman.image.d/
00-pull-always.conf # created by pull-images, removed after pull
90-user.conf # permanent user override (if present)
```

Because the pull goes through the image services, any `.image.d` drop-ins already in place (e.g., from a product RPM) are respected — the image is pulled from whatever source the merged configuration specifies.

## Deployer Stages

Expand All @@ -81,7 +231,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 +253,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
63 changes: 43 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,50 @@
roles:
- role: pre_install
post_tasks:
- name: Pull an image
containers.podman.podman_image:
- name: Deploy core image units
Comment thread
ehelms marked this conversation as resolved.
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: Pull images
ansible.builtin.include_role:
name: images
tasks_from: pull.yaml
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 }}"
Loading
Loading