Skip to content

Commit

Permalink
feat: add support for quadlet, secrets
Browse files Browse the repository at this point in the history
Feature: Add support for quadlets.  User can pass in quadlet units using
`podman_quadlet_units`.  Add support for secrets.  User can pass in
Ansible Vault encrypted secrets using `podman_secrets`.

Reason: quadlets are the new way to implement applications in podman that
use systemd services.  quadlets allow you to specify everything you need
to run your application - containers, services, volumes, networks, and
more - using simple, systemd style unit files.  Secrets such as passwords,
tokens, keys, etc. are an important part of application configuration, so
the role now allows those to be specified.

Result: Users can deploy entire, complex applications using the podman
system role using quadlet units.
  • Loading branch information
richm committed Jul 19, 2023
1 parent e061e47 commit 0ef00f3
Show file tree
Hide file tree
Showing 28 changed files with 1,302 additions and 120 deletions.
1 change: 1 addition & 0 deletions .ansible-lint
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ skip_list:
- var-naming[no-role-prefix]
exclude_paths:
- tests/roles/
- tests/files/
- .github/
- examples/roles/
mock_roles:
Expand Down
205 changes: 197 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ run `podman` containers.
## Requirements

The role requires podman version 4.2 or later.
The role requires podman version 4.4 or later for quadlet support and secret
support.
The role requires podman version 4.5 or later for support for using healthchecks
(only supported when using quadlet Container types).

### Collection requirements

Expand Down Expand Up @@ -61,6 +65,9 @@ except for the following:
specify this, then the global default `podman_systemd_unit_scope` will be
used. Otherwise, the scope will be `system` for root containers, and `user`
for user containers.
* `activate_systemd_unit` - Whether or not to activate the systemd unit when it
is created. If you do not specify this, then the global default
`podman_activate_systemd_unit` will be used, which is `true` by default.
* `kube_file_src` - This is the name of a file on the controller node which will
be copied to `kube_file` on the managed node. This is a file in Kubernetes
YAML format. Do not specify this if you specify `kube_file_content`.
Expand Down Expand Up @@ -96,17 +103,105 @@ For example, if you have
This will be copied to the file `/etc/containers/ansible-kubernetes.d/myappname.yml` on
the managed node.

### podman_quadlet_specs

List of [Quadlet specifications]
(https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
A quadlet spec is uniquely identified by a name and a type, where type is one of
the types of units like container, kube, network, volume, etc. You can either
pass in `name` and `type` explicitly, or the `name` and `type` will be derived
from the file name given in `file`, `file_src`, or `template_src`.

By default, the files will be copied to or created in
`/etc/containers/systemd/$name.$type` for root containers, and
`$HOME/.config/containers/systemd/$name.$type` for rootless containers, on the
managed node. You can provide a different location by using `file`, but then
you will likely need to change the systemd configuration to find the file, which
is not supported by this role.

When a quadlet spec depends on some other file e.g. a `quadlet.kube` that
depends on the `Yaml` file or a `ConfigMap`, then that file must be specified in
the `podman_quadlet_specs` list *before* the file that uses it. For example, if
you have a file `my-app.kube`:

```ini
[Kube]
ConfigMap=my-app-config.yml
Yaml=my-app.yml
...
```

Then you must specify `my-app-config.yml` and `my-app.yml` before `my-app.kube`:

```yaml
podman_quadlet_specs:
- file_src: my-app-config.yml
- file_src: my-app.yml
- file_src: my-app.kube
```

Most of the parameters for each quadlet spec are the same as for
`podman_kube_spec` above except that the `*kube*` parameters are not supported,
and the following are:

* `name` - The name of the unit. If not given, it will be derived from `file`,
`file_src`, or `template_src`. For example, if you specify
`file_src: /path/to/my-container.container` then the `name` will
be `my-container`.
* `type` - The type of unit (container, kube, volume, etc.). If not given, it
will be derived from `file`, `file_src`, or `template_src`. For example, if
you specify `file_src: /path/to/my-container.container` then the `type` will
be `container`. If the derived type is not recognized as a valid quadlet type,
for example, if you specify `file_src: my-kube.yml`, then it will just be copied
and not processed as a quadlet spec.
* `file_src` - The name of the file on the control node to copy to the managed
node to use as the source of the quadlet unit. If this file is in the quadlet
unit format and has a valid quadlet unit suffix, it will be used as a quadlet
unit, otherwise, it will just be copied.
* `file` - The name of the file on the managed node to use as the source of the
quadlet unit. If this file is in the quadlet unit format and has a valid
quadlet unit suffix, it will be used as a quadlet unit, otherwise, it will be
treated as a regular file.
* `file_content` - The contents of a file to copy to the managed node, in string
format. This is useful to pass in short files that can easily be specified
inline. You must also specify `name` and `type`.
* `template_src` - The name of the file on the control node which will be
processed as a Jinja `template` file then copied to the managed node to use as
the source of the quadlet unit. If this file is in the quadlet unit format
and has a valid quadlet unit suffix, it will be used as a quadlet unit,
otherwise, it will just be copied. If the file has a `.j2` suffix, that
suffix will be stripped to determine the quadlet file type.

For example, if you specify:

```yaml
podman_quadlet_specs:
- template_src: my-app.container.j2
```

Then the local file `templates/my-app.container.j2` will be processed as a Jinja
template file, then copied to `/etc/containers/systemd/my-app.container` as a
quadlet container unit spec on the managed node.

### podman_secrets

This is a list of secret specs in the same format as used by
[podman_secret](https://docs.ansible.com/ansible/latest/collections/containers/podman/podman_secret_module.html#ansible-collections-containers-podman-podman-secret-module)
You are *strongly* encouraged to use Ansible Vault to encrypt the value of the
`data` field.

### podman_create_host_directories

This is a boolean, default value is `false`. If `true`, the role will ensure
host directories specified in host mounts in `volumes.hostPath` specifications
in the Kubernetes YAML given in `podman_kube_specs`. NOTE: Directories must be
specified as absolute paths (for root containers), or paths relative to the home
directory (for non-root containers), in order for the role to manage them.
Anything else will be assumed to be some other sort of volume and will be
ignored. The role will apply its default ownership/permissions to the
directories. If you need to set ownership/permissions, see
`podman_host_directories`.
in the Kubernetes YAML given in `podman_kube_specs`, and from `Volume`
configuration in quadlet Container specification where a host path is specified.
NOTE: Directories must be specified as absolute paths (for root containers), or
paths relative to the home directory (for non-root containers), in order for the
role to manage them. Anything else will be assumed to be some other sort of
volume and will be ignored. The role will apply its default
ownership/permissions to the directories. If you need to set
ownership/permissions, see `podman_host_directories`.

### podman_host_directories

Expand Down Expand Up @@ -235,9 +330,47 @@ podman_policy_json:
type: insecureAcceptAnything
```

### podman_use_copr (EXPERIMENTAL)

Boolean - default is unset - if you want to enable the copr repo to use the
latest development version of podman, use `podman_use_copr: true`

### podman_fail_if_too_old (EXPERIMENTAL)

Boolean - default is unset - by default, the role will fail with an error if you
are using an older version of podman and try to use a feature only supported by
a newer version. For example, if you attempt to manage quadlet or secrets with
podman 4.3 or earlier, the role will fail with an error. If you want the role to
be skipped instead, use `podman_fail_if_too_old: false`.

## Variables Exported by the Role

None
### podman_version

This is the version string of the version used by podman. You can generally
use this in your templates. For example, if you want to specify a quadlet
`template_src` for a container, and have it use healthchecks and secrets if
using podman 4.5 or later:

```yaml
podman_quadlet_specs:
- template_src: my-app.container.j2
podman_secrets:
- name: my-app-pwd
data: .....
```

my-app.container.j2:
```ini
[Container]
{% if podman_version is version("4.5", ">=") %}
Secret=my-app-pwd,type=env,target=MYAPP_PASSWORD
HealthCmd=/usr/bin/test -f /path/to/my-app.file
HealthOnFailure=kill
{% else %}
PodmanArgs=--secret=my-app-pwd,type=env,target=MYAPP_PASSWORD
{% endif %}
```

## Example Playbooks

Expand Down Expand Up @@ -321,6 +454,62 @@ Create container running as root with Podman volume:
- linux-system-roles.podman
```

Create quadlet application with secrets. Defer starting the application until
all of the units have been created. Note the order of the files in
`podman_quadlet_specs` are in dependency order. Using
`podman_create_host_directories: true` will create any host mounted directories
specified by a `Volume=` directive in the container spec.

```yaml
podman_create_host_directories: true
podman_activate_systemd_unit: false
podman_quadlet_specs:
- name: quadlet-demo
type: network
file_content: |
[Network]
Subnet=192.168.30.0/24
Gateway=192.168.30.1
Label=app=wordpress
- file_src: quadlet-demo-mysql.volume
- template_src: quadlet-demo-mysql.container.j2
- file_src: envoy-proxy-configmap.yml
- file_src: quadlet-demo.yml
- file_src: quadlet-demo.kube
activate_systemd_unit: true
podman_firewall:
- port: 8000/tcp
state: enabled
- port: 9000/tcp
state: enabled
podman_secrets:
- name: mysql-root-password-container
state: present
skip_existing: true
data: "{{ root_password_from_vault }}"
- name: mysql-root-password-kube
state: present
skip_existing: true
data: |
apiVersion: v1
data:
password: "{{ root_password_from_vault | b64encode }}"
kind: Secret
metadata:
name: mysql-root-password-kube
- name: envoy-certificates
state: present
skip_existing: true
data: |
apiVersion: v1
data:
certificate.key: {{ key_from_vault | b64encode }}
certificate.pem: {{ cert_from_vault | b64encode }}
kind: Secret
metadata:
name: envoy-certificates
```

## License

MIT.
Expand Down
22 changes: 22 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,25 @@ podman_storage_conf: {}
# __podman_policy_json_system or __podman_policy_json_user
# paths, respectively, depending on execution mode selected
podman_policy_json: {}

# List of Quadlet specifications
# https://man.archlinux.org/man/extra/podman/quadlet.5.en
# A quadlet spec is uniquely identified by a name and a type,
# where type is one of the types of units like container,
# kube, network, volume, etc.
podman_quadlet_specs: []

# List of podman secret specs.
# Each item in the list is a dict in the same format
# as accepted by containers.podman.podman_secret
podman_secrets: []

# Activate all systemd units once created
# You can also do this on a per-unit basis by using
# activate_systemd_units in the spec for each unit
# For example, if you are deploying several specs,
# and you only want the last one in the list to activate
# which will trigger the others to activate via dependencies,
# then set `activate_systemd_unit: false` for each one
# except the last one uses `activate_systemd_unit: true`
podman_activate_systemd_unit: true
62 changes: 62 additions & 0 deletions tasks/cleanup_quadlet_spec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
# NOTE: Stopping, disabling, and removing units should also stop
# and remove any pods and containers as well.
- name: Stop and disable service
systemd:
name: "{{ __podman_service_name }}"
scope: "{{ __podman_systemd_scope }}"
state: stopped
enabled: false
become: "{{ __podman_rootless | ternary(true, omit) }}"
become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}"
environment:
XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}"
register: __podman_service_status
when: __podman_service_name | length > 0
failed_when:
- __podman_service_status is failed
- not __podman_service_status.stdout is search(__service_error)
vars:
__service_error: Could not find the requested service

- name: Remove quadlet file
file:
path: "{{ __podman_quadlet_file }}"
state: absent
register: __podman_file_removed

- name: Reload systemctl # noqa no-handler
systemd:
daemon_reload: true
scope: "{{ __podman_systemd_scope }}"
become: "{{ __podman_rootless | ternary(true, omit) }}"
become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}"
environment:
XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}"
when: __podman_file_removed is changed

- name: Cleanup container resources
when: __podman_file_removed is changed # noqa no-handler
block:
- name: Gather facts for all containers
containers.podman.podman_container_info:
environment:
XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}"
become: "{{ __podman_rootless | ternary(true, omit) }}"
become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}"
register: __podman_container_info

- name: Cancel linger if no more containers are running
command: loginctl disable-linger {{ __podman_user }}
when:
- __podman_rootless | bool
- __podman_container_info.containers | length == 0
changed_when: true

- name: Prune images no longer in use
command: podman image prune -f
environment:
XDG_RUNTIME_DIR: "{{ __podman_xdg_runtime_dir }}"
become: "{{ __podman_rootless | ternary(true, omit) }}"
become_user: "{{ __podman_rootless | ternary(__podman_user, omit) }}"
changed_when: true
19 changes: 6 additions & 13 deletions tasks/create_update_kube_spec.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
---
- name: Check if user is lingering
stat:
path: "/var/lib/systemd/linger/{{ __podman_user }}"
register: __podman_user_lingering
when: __podman_rootless | bool

- name: Enable lingering if needed
command: loginctl enable-linger {{ __podman_user }}
when:
- __podman_rootless | bool
- not __podman_user_lingering.stat.exists
changed_when: true
when: __podman_rootless | bool
args:
creates: /var/lib/systemd/linger/{{ __podman_user }}

- name: Get the host mount volumes
set_fact:
Expand Down Expand Up @@ -73,7 +66,7 @@
path: "{{ __podman_kube_file }}"
register: __podman_kube_stat
failed_when:
- __podman_kube is none
- not __podman_kube
- not __podman_kube_stat.stat.exists

- name: Ensure the kubernetes directory is present
Expand All @@ -83,7 +76,7 @@
owner: "{{ __podman_user }}"
group: "{{ __podman_group }}"
mode: "0755"
when: not __podman_kube is none
when: __podman_kube | length > 0

- name: Ensure kubernetes yaml files are present
copy:
Expand All @@ -93,7 +86,7 @@
group: "{{ __podman_group }}"
mode: "0644"
register: __podman_copy
when: not __podman_kube is none
when: __podman_kube | length > 0

- name: Update containers/pods
containers.podman.podman_play: "{{ __podman_kube_spec |
Expand Down
Loading

0 comments on commit 0ef00f3

Please sign in to comment.