diff --git a/README.md b/README.md index 253e6c4d..383577d4 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,62 @@ must also set `podman_registry_username`. You can override this on a per-spec basis with `registry_password`. The use of `container_image_password` was unsupported and is deprecated. +### podman_credential_files + +This is a `list`. Each element of the list is a `dict` describing a podman +credential file used to authenticate to registries. See `man +containers-auth.json` and `man containers-registries.conf`:`credential-helpers` +for more information about the format of these files, and the default directory +search path. +NOTE: These files contain authentication credentials. Please be careful with +them. You are strongly encouraged to use Ansible Vault to encrypt them. +The keys of each `dict` are as follows: + +* `state` - default is `present`. Use `absent` to remove files. +* `file_src` - This is the name of a file on the controller node which will be + copied to `file` on the managed node. Do not specify this if you specify + `file_content` or `template_src`, which will take precedence over `file_src`. +* `template_src` - This is the name of a file on the controller node which will + be templated using the `template` module and copied to `file` on the managed + node. Do not specify this if you specify `file_content` or `file_src`. +* `file_content` - This is a string in `containers-auth.json` format. It will be + used as the contents of `file` on the managed node. Do not specify this if + you specify `file_src` or `template_src`. +* `file` - This is the name of a file on the managed node that will contain the + `auth.json` contents. The default value will be + `$HOME/.config/containers/auth.json`. If you specify a relative path, it will + be relative to `$HOME/.config/containers`. If you specify something other + than the defaults mentioned in `man containers-auth.json`, you will also need + to configure `credential-helpers` in `containers-registries.conf` using + `podman_registries_conf`. Any missing parent directories will be created. +* `run_as_user` - Use this to specify a per-credential file owner. If you do + not specify this, then the global default `podman_run_as_user` value will be + used. Otherwise, `root` will be used. NOTE: The user must already exist - the + role will not create one. The user must be present in `/etc/subuid`. NOTE: + This is used as the user for the `$HOME` directory if `file` is not specified, + and as the owner of the file. If you want the owner of the file to be + different than the user used for `$HOME`, specify `file` as an absolute path. +* `run_as_group` - Use this to specify a per-credential file group. If you do + not specify this, then the global default `podman_run_as_group` value will be + used. Otherwise, `root` will be used. NOTE: The group must already exist - + the role will not create one. The group must be present in `/etc/subgid`. +* `mode` - The mode of the file - default is `"0600"`. + +For example, if you have + +```yaml + podman_credential_files: + - file_src: auth.json + run_as_user: my_user +``` + +The local file `auth.json` will be looked up in the usual Ansible `file` search +paths and will be copied to the file +`/home/my_user/.config/containers/auth.json` on the managed node. The file +owner will be `my_user` and the mode will be `"0600"`. The directories +`/home/my_user/.config` and `/home/my_user/.config/containers` will be created +if they do not exist. + ## Variables Exported by the Role ### podman_version diff --git a/defaults/main.yml b/defaults/main.yml index f5b7e025..ded93c21 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -117,3 +117,10 @@ podman_registry_username: "{{ container_image_user | d('') }}" # password to use to authenticate to the registry # override by specifying registry_password on a per-spec basis podman_registry_password: "{{ container_image_password | d('') }}" + +# List of files containing registry credentials +# If not using the default containers-auth.json, you may need to +# also provide an entry for "credential-helpers" in the registries.conf +# For more information, see "man containers-auth.json" and +# "man containers-registries.conf" +podman_credential_files: [] diff --git a/tasks/handle_credential_files.yml b/tasks/handle_credential_files.yml new file mode 100644 index 00000000..6b4e1a0c --- /dev/null +++ b/tasks/handle_credential_files.yml @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: MIT +--- +- name: Set user and group + set_fact: + __podman_credential_user: "{{ __podman_credential_item['run_as_user'] + if 'run_as_user' in __podman_credential_item else podman_run_as_user }}" + __podman_credential_group: "{{ __podman_credential_item['run_as_group'] + if 'run_as_group' in __podman_credential_item else podman_run_as_group }}" + +# NOTE: Sets __podman_group that we use below +- name: Check user and group information + include_tasks: handle_user_group.yml + vars: + __podman_user: "{{ __podman_credential_user }}" + __podman_spec_item: "{{ __podman_credential_item }}" + +- name: Set credential variables + set_fact: + __podman_credential_str: "{{ __podman_credential_item['file_content'] + if 'file_content' in __podman_credential_item + else lookup('template', __podman_credential_item['template_src']) + if 'template_src' in __podman_credential_item + else none }}" + __podman_credential_file_src: "{{ __podman_credential_item['file_src'] + if 'file_src' in __podman_credential_item + else none }}" + __podman_credential_file: "{{ __authdir ~ 'auth.json' + if not 'file' in __podman_credential_item + else __authdir ~ __podman_credential_item['file'] + if not __podman_credential_item['file'] is abs + else __podman_credential_item['file'] }}" + __podman_credential_mode: "{{ __podman_credential_item['mode'] + if 'mode' in __podman_credential_item else '0600' }}" + __podman_credential_state: "{{ __podman_credential_item['state'] + if 'state' in __podman_credential_item else 'present' }}" + vars: + __authdir: "{{ + ansible_facts['getent_passwd'][__podman_credential_user][4] ~ + '/.config/containers/' }}" + no_log: true + +- name: Handle state present + when: __podman_credential_state == "present" + block: + - name: Ensure the credentials directory is present + file: + path: "{{ __podman_credential_file | dirname }}" + state: directory + owner: "{{ __podman_credential_user }}" + group: "{{ __podman_group }}" + mode: "0700" + + - name: Ensure credential file is copied + copy: + src: "{{ __podman_credential_file_src }}" + dest: "{{ __podman_credential_file }}" + owner: "{{ __podman_credential_user }}" + group: "{{ __podman_group }}" + mode: "{{ __podman_credential_mode }}" + when: __podman_credential_file_src | length > 0 + no_log: true + + - name: Ensure credential file content is present + copy: + content: "{{ __podman_credential_str }}" + dest: "{{ __podman_credential_file }}" + owner: "{{ __podman_credential_user }}" + group: "{{ __podman_group }}" + mode: "{{ __podman_credential_mode }}" + when: + - __podman_credential_str | length > 0 + - not __podman_credential_file_src + no_log: true + +- name: Handle state absent + when: __podman_credential_state == "absent" + block: + - name: Ensure credential file is absent + file: + path: "{{ __podman_credential_file }}" + state: absent + no_log: true + + - name: Find files in credentials directory + find: + path: "{{ __podman_credential_file | dirname }}" + file_type: any + hidden: true + register: __credential_dir_files + no_log: true + + - name: Ensure the credentials directory is absent if empty + file: + path: "{{ __podman_credential_file | dirname }}" + state: absent + when: __credential_dir_files.matched == 0 diff --git a/tasks/handle_user_group.yml b/tasks/handle_user_group.yml index 0b98d994..339b14ff 100644 --- a/tasks/handle_user_group.yml +++ b/tasks/handle_user_group.yml @@ -17,8 +17,9 @@ - name: Set group for podman user set_fact: __podman_group: |- - {%- if "run_as_group" in __podman_spec_item -%} - {{ __podman_spec_item["run_as_group"] }} + {% set item_group = __podman_spec_item.get("run_as_group") %} + {%- if item_group and item_group | length > 0 -%} + {{ item_group }} {%- elif podman_run_as_group is not none -%} {{ podman_run_as_group }} {%- else -%} diff --git a/tasks/main.yml b/tasks/main.yml index 1077b5cf..4ab96b69 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -118,6 +118,13 @@ loop_var: __podman_secret_item no_log: true +- name: Handle credential files + include_tasks: handle_credential_files.yml + loop: "{{ podman_credential_files }}" + loop_control: + loop_var: __podman_credential_item + no_log: true + - name: Handle Kubernetes specifications include_tasks: handle_kube_spec.yml loop: "{{ podman_kube_specs }}" diff --git a/tests/tests_auth_and_security.yml b/tests/tests_auth_and_security.yml index f7306af4..42c563ac 100644 --- a/tests/tests_auth_and_security.yml +++ b/tests/tests_auth_and_security.yml @@ -18,8 +18,6 @@ ContainerName: auth_test_1_quadlet podman_kube_spec_base: state: started - registry_username: "{{ __podman_test_username }}" - registry_password: "{{ __podman_test_password }}" kube_file_content: apiVersion: v1 kind: Pod @@ -68,11 +66,23 @@ rescue: - name: Check error fail: - msg: Role did not fail as expected - when: "'UNREACHABLE' in ansible_failed_result.msg" + msg: >- + Role did not fail with the expected message {{ expected_msg }} but + instead failed with {{ ansible_failed_result | to_nice_json }} + when: not expected_msg in ansible_failed_result.results[0].msg | d("") + vars: + expected_msg: Failed to pull image - name: Run remaining tasks in block with cleanup block: + - name: Create a local tmpdir + tempfile: + prefix: lsr_ + suffix: _podman + state: directory + register: __local_tmpdir + delegate_to: localhost + - name: Run the role with credentials in spec include_role: name: linux-system-roles.podman @@ -95,6 +105,77 @@ podman_kube_specs: - "{{ podman_kube_spec_base }}" + - name: Get authfile locally + fetch: + src: "{{ __podman_test_authfile }}" + dest: "{{ __local_tmpdir.path ~ '/' }}" + flat: true + + - name: Provide a credentials file - root + include_role: + name: linux-system-roles.podman + vars: + podman_credential_files: + - file_src: "{{ __local_tmpdir.path ~ '/auth.json' }}" + file: /root/.config/containers/auth.json + podman_quadlet_specs: + - "{{ podman_quadlet_spec_base }}" + podman_kube_specs: + - "{{ podman_kube_spec_base }}" + + - name: Create a user for rootless + user: + name: auth_test_user1 + uid: 2001 + + - name: Provide a credentials file - rootless + include_role: + name: linux-system-roles.podman + vars: + podman_credential_files: + - template_src: "{{ __local_tmpdir.path ~ '/auth.json' }}" + run_as_user: auth_test_user1 + __run_as_user: + run_as_user: auth_test_user1 + podman_quadlet_specs: + - "{{ podman_quadlet_spec_base | combine(__run_as_user) }}" + podman_kube_specs: + - "{{ podman_kube_spec_base | combine(__run_as_user) }}" + always: + - name: Remove all container resources - root + include_role: + name: linux-system-roles.podman + vars: + podman_kube_specs: + - "{{ podman_kube_spec_base | combine({'state': 'absent'}) }}" + podman_quadlet_specs: + - "{{ podman_quadlet_spec_base | combine({'state': 'absent'}) }}" + podman_credential_files: + - state: absent + + - name: Remove pods and units - rootless + include_role: + name: linux-system-roles.podman + vars: + podman_run_as_user: auth_test_user1 + podman_kube_specs: + - "{{ podman_kube_spec_base | combine({'state': 'absent'}) }}" + podman_quadlet_specs: + - "{{ podman_quadlet_spec_base | combine({'state': 'absent'}) }}" + podman_credential_files: + - state: absent + + - name: Remove user + user: + name: auth_test_user1 + state: absent + + - name: Remove local tmpdir + file: + path: "{{ __local_tmpdir.path }}" + state: absent + delegate_to: localhost + - name: Clean up registry include_tasks: tasks/cleanup_registry.yml