From 9e2056b2f32a0bec82d7f08b365c5f0223b5e1c2 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 27 Jul 2026 13:39:41 +0200 Subject: [PATCH 01/16] Implemented argument specs for certificate role --- meta/argument_specs.yml | 148 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 meta/argument_specs.yml diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 00000000..8e6dea6e --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,148 @@ +# SPDX-License-Identifier: MIT +--- +argument_specs: + main: + short_description: Manage TLS/SSL certificate issuance and renewal + options: + certificate_requests: + type: list + elements: dict + description: >- + List of certificate request specifications. + options: + name: + type: str + required: true + description: >- + Name of the certificate. A full path can be used to choose the + directory where files will be stored. + ca: + type: str + required: true + description: >- + CA that will issue the certificate (e.g. self-sign, ipa). + dns: + type: raw + description: >- + Domain or list of domains to include in the certificate SAN. + email: + type: raw + description: >- + Email or list of emails to include in the certificate SAN. + ip: + type: raw + description: >- + IP or list of IPs to include in the certificate SAN. + auto_renew: + type: bool + description: >- + Whether the certificate should be renewed automatically before + it expires. + owner: + type: str + description: >- + User name or user id for the certificate and key files. + group: + type: str + description: >- + Group name or group id for the certificate and key files. + mode: + type: raw + description: >- + File system permissions for the certificate and key files. + key_size: + type: int + description: >- + Key size in bits. + common_name: + type: str + description: >- + Common Name requested for the certificate subject. + country: + type: str + description: >- + Country code requested for the certificate subject. + state: + type: str + description: >- + State requested for the certificate subject. + locality: + type: str + description: >- + Locality requested for the certificate subject. + organization: + type: str + description: >- + Organization requested for the certificate subject. + organizational_unit: + type: str + description: >- + Organizational unit requested for the certificate subject. + contact_email: + type: str + description: >- + Contact email requested for the certificate subject. + key_usage: + type: list + elements: str + description: >- + Allowed Key Usage extensions for the certificate. + extended_key_usage: + type: list + elements: str + description: >- + Extended Key Usage attributes for the certificate. + run_before: + type: str + description: >- + Command to run before saving the certificate. + run_after: + type: str + description: >- + Command to run after saving the certificate. + principal: + type: raw + description: >- + Kerberos principal or list of principals. + provider: + type: str + description: >- + The underlying method used to request and manage the certificate. + certificate_wait: + type: bool + description: >- + Whether the task should wait for the certificate to be issued. + certificate_trust: + type: list + elements: dict + description: >- + List of certificates to install into the system trust store. + options: + name: + type: str + required: true + description: >- + Base file name of the trust anchor. + content: + type: str + description: >- + Inline PEM content of the certificate. + src: + type: str + description: >- + Path of a certificate file to copy to the trust store. + remote_src: + type: bool + description: >- + If true, src is a path on the managed host. + url: + type: str + description: >- + URL to download the certificate from. + state: + type: str + choices: + - present + - absent + description: >- + Whether the trust anchor should be present or absent. From de20a38e60e474402e97b70254ee460ed6e56bee Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Thu, 13 Aug 2026 11:40:24 +0200 Subject: [PATCH 02/16] Small README change --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6faabc96..f475dfa5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ ansible-galaxy collection install -vv -r meta/collection-requirements.yml ## Variables +Validation is enforced by `meta/argument_specs.yml`. + | Parameter | Description | Type | Required | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------|:----:|:--------:|-------------------| | certificate_wait | If the task should wait for the certificate to be issued. | bool | no | yes | From 19db6bd23e08d405a3fcbaf2f15fac6235f78544 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Thu, 13 Aug 2026 12:41:36 +0200 Subject: [PATCH 03/16] Added test for the new argument spec implementation --- tests/tests_invalid_input.yml | 180 ++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 tests/tests_invalid_input.yml diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml new file mode 100644 index 00000000..bd061233 --- /dev/null +++ b/tests/tests_invalid_input.yml @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: MIT +--- +- name: Verify argument specs reject invalid parameters + hosts: all + tasks: + # --- certificate_requests validation --- + # Role argument validation requires Ansible 2.10+; skipped on older versions. + + - name: Argument specs reject missing name in certificate_requests + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_requests missing name + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_requests: + - ca: self-sign + dns: www.example.com + rescue: + - name: Mark missing name rejected + ansible.builtin.set_fact: + __invalid_input_missing_name_failed: true + + - name: Assert missing name in certificate_requests failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required name field + + - name: Argument specs reject missing ca in certificate_requests + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_requests missing ca + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_requests: + - name: mycert + dns: www.example.com + rescue: + - name: Mark missing ca rejected + ansible.builtin.set_fact: + __invalid_input_missing_ca_failed: true + + - name: Assert missing ca in certificate_requests failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_missing_ca_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required ca field + + - name: Argument specs reject invalid key_size type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with key_size as string + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_requests: + - name: mycert + ca: self-sign + key_size: not_a_number + rescue: + - name: Mark invalid key_size rejected + ansible.builtin.set_fact: + __invalid_input_key_size_type_failed: true + + - name: Assert invalid key_size type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_key_size_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject key_size values that + are not integers + + - name: Argument specs reject invalid auto_renew type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with auto_renew as string + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_requests: + - name: mycert + ca: self-sign + auto_renew: not_a_bool + rescue: + - name: Mark invalid auto_renew rejected + ansible.builtin.set_fact: + __invalid_input_auto_renew_type_failed: true + + - name: Assert invalid auto_renew type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_auto_renew_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject auto_renew values that + are not booleans + + # --- certificate_wait validation --- + + - name: Argument specs reject invalid certificate_wait type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_wait as string + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_wait: not_a_bool + rescue: + - name: Mark invalid certificate_wait rejected + ansible.builtin.set_fact: + __invalid_input_wait_type_failed: true + + - name: Assert invalid certificate_wait type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_wait_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_wait values + that are not booleans + + # --- certificate_trust validation --- + + - name: Argument specs reject missing name in certificate_trust + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_trust missing name + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_trust: + - content: not-a-certificate + state: present + rescue: + - name: Mark missing trust name rejected + ansible.builtin.set_fact: + __invalid_input_trust_missing_name_failed: true + + - name: Assert missing name in certificate_trust failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_trust_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust entries + without the required name field + + - name: Argument specs reject invalid state in certificate_trust + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with invalid trust state + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_trust: + - name: mycert + content: not-a-certificate + state: invalid_state + rescue: + - name: Mark invalid trust state rejected + ansible.builtin.set_fact: + __invalid_input_trust_state_failed: true + + - name: Assert invalid state in certificate_trust failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_trust_state_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust state + values outside the allowed set (present, absent) From b5469ae69270b684092c4d4434b291ebcf22d38e Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Thu, 13 Aug 2026 12:58:06 +0200 Subject: [PATCH 04/16] Added one more test for the trust entry --- tests/tests_invalid_input.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index bd061233..914d0da2 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -178,3 +178,27 @@ fail_msg: >- meta/argument_specs should reject certificate_trust state values outside the allowed set (present, absent) + + # --- certificate_trust cross-field validation (assert in tasks, not argument_specs) --- + + - name: Assert rejects trust entry with no source when state is present + block: + - name: Run role with trust entry missing content, src, and url + ansible.builtin.include_role: + name: linux-system-roles.certificate + vars: + certificate_trust: + - name: mycert + state: present + rescue: + - name: Mark missing trust source rejected + ansible.builtin.set_fact: + __invalid_input_trust_no_source_failed: true + + - name: Assert trust entry with no source failed validation + ansible.builtin.assert: + that: + - __invalid_input_trust_no_source_failed | default(false) + fail_msg: >- + certificate_trust should reject entries with state present + that do not specify exactly one of content, src, or url From 7db3de70eb459824c9e7449fd94db08a8b63cd0b Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Thu, 13 Aug 2026 14:04:44 +0200 Subject: [PATCH 05/16] Added missed argument and reworked the test file --- README.md | 3 +- meta/argument_specs.yml | 4 + tests/tests_invalid_input.yml | 414 ++++++++++++++++++---------------- 3 files changed, 225 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index f475dfa5..e753448b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ ansible-galaxy collection install -vv -r meta/collection-requirements.yml ## Variables -Validation is enforced by `meta/argument_specs.yml`. +Type, required-field, and choice validation is enforced by +`meta/argument_specs.yml`. Role tasks enforce cross-field validation. | Parameter | Description | Type | Required | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------|:----:|:--------:|-------------------| diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml index 8e6dea6e..9508b82f 100644 --- a/meta/argument_specs.yml +++ b/meta/argument_specs.yml @@ -108,6 +108,10 @@ argument_specs: type: str description: >- The underlying method used to request and manage the certificate. + issuer: + type: str + description: >- + Issuer certificate nickname or template name. certificate_wait: type: bool description: >- diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index 914d0da2..7a0cd233 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -1,204 +1,228 @@ # SPDX-License-Identifier: MIT --- -- name: Verify argument specs reject invalid parameters +- name: Verify invalid parameters are rejected hosts: all tasks: - # --- certificate_requests validation --- - # Role argument validation requires Ansible 2.10+; skipped on older versions. - - - name: Argument specs reject missing name in certificate_requests - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with certificate_requests missing name - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_requests: - - ca: self-sign - dns: www.example.com - rescue: - - name: Mark missing name rejected - ansible.builtin.set_fact: - __invalid_input_missing_name_failed: true - - - name: Assert missing name in certificate_requests failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_missing_name_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_requests entries - without the required name field - - - name: Argument specs reject missing ca in certificate_requests - when: ansible_version.full is version('2.10', '>=') + - name: Run invalid input tests block: - - name: Run role with certificate_requests missing ca - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_requests: - - name: mycert - dns: www.example.com - rescue: - - name: Mark missing ca rejected - ansible.builtin.set_fact: - __invalid_input_missing_ca_failed: true - - - name: Assert missing ca in certificate_requests failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_missing_ca_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_requests entries - without the required ca field - - - name: Argument specs reject invalid key_size type - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with key_size as string - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_requests: - - name: mycert - ca: self-sign - key_size: not_a_number - rescue: - - name: Mark invalid key_size rejected - ansible.builtin.set_fact: - __invalid_input_key_size_type_failed: true - - - name: Assert invalid key_size type failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_key_size_type_failed | default(false) - fail_msg: >- - meta/argument_specs should reject key_size values that - are not integers - - - name: Argument specs reject invalid auto_renew type - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with auto_renew as string - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_requests: - - name: mycert - ca: self-sign - auto_renew: not_a_bool - rescue: - - name: Mark invalid auto_renew rejected - ansible.builtin.set_fact: - __invalid_input_auto_renew_type_failed: true - - - name: Assert invalid auto_renew type failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_auto_renew_type_failed | default(false) - fail_msg: >- - meta/argument_specs should reject auto_renew values that - are not booleans + # --- Valid invocation to confirm argument_specs accept defaults --- - # --- certificate_wait validation --- + - name: Run role with valid defaults + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml - - name: Argument specs reject invalid certificate_wait type - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with certificate_wait as string - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_wait: not_a_bool - rescue: - - name: Mark invalid certificate_wait rejected + - name: Mark valid defaults succeeded ansible.builtin.set_fact: - __invalid_input_wait_type_failed: true - - - name: Assert invalid certificate_wait type failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_wait_type_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_wait values - that are not booleans - - # --- certificate_trust validation --- - - - name: Argument specs reject missing name in certificate_trust - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with certificate_trust missing name - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_trust: - - content: not-a-certificate - state: present - rescue: - - name: Mark missing trust name rejected - ansible.builtin.set_fact: - __invalid_input_trust_missing_name_failed: true - - - name: Assert missing name in certificate_trust failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_trust_missing_name_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_trust entries - without the required name field - - - name: Argument specs reject invalid state in certificate_trust - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with invalid trust state - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_trust: - - name: mycert - content: not-a-certificate - state: invalid_state - rescue: - - name: Mark invalid trust state rejected - ansible.builtin.set_fact: - __invalid_input_trust_state_failed: true - - - name: Assert invalid state in certificate_trust failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_trust_state_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_trust state - values outside the allowed set (present, absent) - - # --- certificate_trust cross-field validation (assert in tasks, not argument_specs) --- - - - name: Assert rejects trust entry with no source when state is present - block: - - name: Run role with trust entry missing content, src, and url - ansible.builtin.include_role: - name: linux-system-roles.certificate - vars: - certificate_trust: - - name: mycert - state: present - rescue: - - name: Mark missing trust source rejected + __valid_defaults_succeeded: true + + - name: Assert role completed with valid defaults + ansible.builtin.assert: + that: + - __valid_defaults_succeeded | default(false) + fail_msg: >- + Role should succeed with default parameters + + # --- certificate_requests validation --- + # Role argument validation requires Ansible 2.10+; skipped on older versions. + + - name: Argument specs reject missing name in certificate_requests + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_requests missing name + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - ca: self-sign + dns: www.example.com + rescue: + - name: Mark missing name rejected + ansible.builtin.set_fact: + __invalid_input_missing_name_failed: true + + - name: Assert missing name in certificate_requests failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required name field + + - name: Argument specs reject missing ca in certificate_requests + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_requests missing ca + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + dns: www.example.com + rescue: + - name: Mark missing ca rejected + ansible.builtin.set_fact: + __invalid_input_missing_ca_failed: true + + - name: Assert missing ca in certificate_requests failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_missing_ca_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required ca field + + - name: Argument specs reject invalid key_size type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with key_size as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + key_size: not_a_number + rescue: + - name: Mark invalid key_size rejected + ansible.builtin.set_fact: + __invalid_input_key_size_type_failed: true + + - name: Assert invalid key_size type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_key_size_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject key_size values that + are not integers + + - name: Argument specs reject invalid auto_renew type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with auto_renew as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + auto_renew: not_a_bool + rescue: + - name: Mark invalid auto_renew rejected + ansible.builtin.set_fact: + __invalid_input_auto_renew_type_failed: true + + - name: Assert invalid auto_renew type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_auto_renew_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject auto_renew values that + are not booleans + + # --- certificate_wait validation --- + + - name: Argument specs reject invalid certificate_wait type + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_wait as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_wait: not_a_bool + rescue: + - name: Mark invalid certificate_wait rejected + ansible.builtin.set_fact: + __invalid_input_wait_type_failed: true + + - name: Assert invalid certificate_wait type failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_wait_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_wait values + that are not booleans + + # --- certificate_trust validation --- + + - name: Argument specs reject missing name in certificate_trust + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with certificate_trust missing name + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - content: not-a-certificate + state: present + rescue: + - name: Mark missing trust name rejected + ansible.builtin.set_fact: + __invalid_input_trust_missing_name_failed: true + + - name: Assert missing name in certificate_trust failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_trust_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust entries + without the required name field + + - name: Argument specs reject invalid state in certificate_trust + when: ansible_version.full is version('2.10', '>=') + block: + - name: Run role with invalid trust state + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - name: mycert + content: not-a-certificate + state: invalid_state + rescue: + - name: Mark invalid trust state rejected + ansible.builtin.set_fact: + __invalid_input_trust_state_failed: true + + - name: Assert invalid state in certificate_trust failed validation + when: ansible_version.full is version('2.10', '>=') + ansible.builtin.assert: + that: + - __invalid_input_trust_state_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust state + values outside the allowed set (present, absent) + + # --- certificate_trust cross-field validation (assert in tasks, not argument_specs) --- + + - name: Assert rejects trust entry with no source when state is present + block: + - name: Run role with trust entry missing content, src, and url + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - name: mycert + state: present + rescue: + - name: Mark missing trust source rejected + ansible.builtin.set_fact: + __invalid_input_trust_no_source_failed: true + + - name: Assert trust entry with no source failed validation + ansible.builtin.assert: + that: + - __invalid_input_trust_no_source_failed | default(false) + fail_msg: >- + certificate_trust should reject entries with state present + that do not specify exactly one of content, src, or url + + always: + - name: Clear test facts ansible.builtin.set_fact: - __invalid_input_trust_no_source_failed: true - - - name: Assert trust entry with no source failed validation - ansible.builtin.assert: - that: - - __invalid_input_trust_no_source_failed | default(false) - fail_msg: >- - certificate_trust should reject entries with state present - that do not specify exactly one of content, src, or url + __valid_defaults_succeeded: + __invalid_input_missing_name_failed: + __invalid_input_missing_ca_failed: + __invalid_input_key_size_type_failed: + __invalid_input_auto_renew_type_failed: + __invalid_input_wait_type_failed: + __invalid_input_trust_missing_name_failed: + __invalid_input_trust_state_failed: + __invalid_input_trust_no_source_failed: + tags: tests::cleanup From dfcfe5c8f245eb865b6d5088aa4c044521270a36 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 17 Aug 2026 11:00:34 +0200 Subject: [PATCH 06/16] Made recommended changes. Added assert_role_vars, refractored tests_invalid_input, and refractored argument_specs description to fit antsibull standards --- meta/argument_specs.yml | 164 ++++++++++------- tasks/assert_role_vars.yml | 70 ++++++++ tasks/main.yml | 4 + tests/tests_invalid_input.yml | 328 ++++++++++++++++++++-------------- 4 files changed, 363 insertions(+), 203 deletions(-) create mode 100644 tasks/assert_role_vars.yml diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml index 9508b82f..53b73f00 100644 --- a/meta/argument_specs.yml +++ b/meta/argument_specs.yml @@ -2,151 +2,181 @@ --- argument_specs: main: - short_description: Manage TLS/SSL certificate issuance and renewal + short_description: The certificate role. + description: > + The certificate role allows you to issue and manage TLS/SSL + certificates using various CA providers such as self-sign and IPA. + + This role will install necessary packages, configure the certificate + provider, and issue or renew certificates as specified in + `certificate_requests`. It can also manage system trust store + entries via `certificate_trust`. options: certificate_requests: type: list elements: dict - description: >- - List of certificate request specifications. + description: > + A list of certificate request specifications. Each item describes + a certificate to be issued or renewed. Defaults to an empty list. options: name: type: str required: true - description: >- - Name of the certificate. A full path can be used to choose the - directory where files will be stored. + description: > + The name of the certificate. A full path can be used to + choose the directory where files will be stored. ca: type: str required: true - description: >- - CA that will issue the certificate (e.g. self-sign, ipa). + description: > + The CA that will issue the certificate (e.g. self-sign, ipa). dns: type: raw - description: >- - Domain or list of domains to include in the certificate SAN. + description: > + A domain name or list of domain names to include in the + certificate Subject Alternative Name (SAN). email: type: raw - description: >- - Email or list of emails to include in the certificate SAN. + description: > + An email address or list of email addresses to include in the + certificate Subject Alternative Name (SAN). ip: type: raw - description: >- - IP or list of IPs to include in the certificate SAN. + description: > + An IP address or list of IP addresses to include in the + certificate Subject Alternative Name (SAN). auto_renew: type: bool - description: >- - Whether the certificate should be renewed automatically before - it expires. + description: > + Whether the certificate should be renewed automatically + before it expires. Defaults to `true`. owner: type: str - description: >- - User name or user id for the certificate and key files. + description: > + The user name or user id for the certificate and key files. group: type: str - description: >- - Group name or group id for the certificate and key files. + description: > + The group name or group id for the certificate and key files. mode: type: raw - description: >- - File system permissions for the certificate and key files. + description: > + The file system permissions for the certificate and key + files. Accepts a string (e.g. '0644') or an integer. key_size: type: int - description: >- - Key size in bits. + description: > + The key size in bits. common_name: type: str - description: >- - Common Name requested for the certificate subject. + description: > + The Common Name requested for the certificate subject. country: type: str - description: >- - Country code requested for the certificate subject. + description: > + The country code requested for the certificate subject. state: type: str - description: >- - State requested for the certificate subject. + description: > + The state requested for the certificate subject. locality: type: str - description: >- - Locality requested for the certificate subject. + description: > + The locality requested for the certificate subject. organization: type: str - description: >- - Organization requested for the certificate subject. + description: > + The organization requested for the certificate subject. organizational_unit: type: str - description: >- - Organizational unit requested for the certificate subject. + description: > + The organizational unit requested for the certificate subject. contact_email: type: str - description: >- - Contact email requested for the certificate subject. + description: > + The contact email requested for the certificate subject. key_usage: type: list elements: str - description: >- - Allowed Key Usage extensions for the certificate. + choices: + - digitalSignature + - nonRepudiation + - keyEncipherment + - dataEncipherment + - keyAgreement + - keyCertSign + - cRLSign + - encipherOnly + - decipherOnly + description: > + The allowed Key Usage extensions for the certificate. + Defaults to `digitalSignature` and `keyEncipherment`. extended_key_usage: type: list elements: str - description: >- - Extended Key Usage attributes for the certificate. + description: > + The Extended Key Usage attributes for the certificate. + Defaults to `id-kp-serverAuth` and `id-kp-clientAuth`. run_before: type: str - description: >- - Command to run before saving the certificate. + description: > + A command to run before saving the certificate. run_after: type: str - description: >- - Command to run after saving the certificate. + description: > + A command to run after saving the certificate. principal: type: raw - description: >- - Kerberos principal or list of principals. + description: > + A Kerberos principal or list of Kerberos principals. provider: type: str - description: >- - The underlying method used to request and manage the certificate. + description: > + The underlying method used to request and manage the + certificate. Defaults to `certmonger`. issuer: type: str - description: >- - Issuer certificate nickname or template name. + description: > + The issuer certificate nickname or template name. certificate_wait: type: bool - description: >- + description: > Whether the task should wait for the certificate to be issued. + Defaults to `true`. certificate_trust: type: list elements: dict - description: >- - List of certificates to install into the system trust store. + description: > + A list of certificates to install into or remove from the system + trust store. Defaults to an empty list. options: name: type: str required: true - description: >- - Base file name of the trust anchor. + description: > + The base file name of the trust anchor. content: type: str - description: >- - Inline PEM content of the certificate. + description: > + The inline PEM content of the certificate. src: type: str - description: >- - Path of a certificate file to copy to the trust store. + description: > + The path of a certificate file to copy to the trust store. remote_src: type: bool - description: >- - If true, src is a path on the managed host. + description: > + If true, `src` is a path on the managed host rather than + the controller. url: type: str - description: >- - URL to download the certificate from. + description: > + The URL to download the certificate from. state: type: str choices: - present - absent - description: >- + description: > Whether the trust anchor should be present or absent. + Defaults to `present`. diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml new file mode 100644 index 00000000..bbac800d --- /dev/null +++ b/tasks/assert_role_vars.yml @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: MIT +--- +- name: Assert dns is a string or list + ansible.builtin.assert: + that: + - item.dns is sequence and item.dns is not mapping + fail_msg: >- + certificate_requests[{{ idx }}].dns must be a string or list, + got {{ item.dns | type_debug }} + loop: "{{ certificate_requests }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.dns is defined + +- name: Assert email is a string or list + ansible.builtin.assert: + that: + - item.email is sequence and item.email is not mapping + fail_msg: >- + certificate_requests[{{ idx }}].email must be a string or list, + got {{ item.email | type_debug }} + loop: "{{ certificate_requests }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.email is defined + +- name: Assert ip is a string or list + ansible.builtin.assert: + that: + - item.ip is sequence and item.ip is not mapping + fail_msg: >- + certificate_requests[{{ idx }}].ip must be a string or list, + got {{ item.ip | type_debug }} + loop: "{{ certificate_requests }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.ip is defined + +- name: Assert principal is a string or list + ansible.builtin.assert: + that: + - item.principal is sequence and item.principal is not mapping + fail_msg: >- + certificate_requests[{{ idx }}].principal must be a string or list, + got {{ item.principal | type_debug }} + loop: "{{ certificate_requests }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.principal is defined + +- name: Assert mode is a string or integer + ansible.builtin.assert: + that: + - >- + item.mode is string + or (item.mode is integer + and item.mode is not sameas true + and item.mode is not sameas false) + fail_msg: >- + certificate_requests[{{ idx }}].mode must be a string or integer, + got {{ item.mode | type_debug }} + loop: "{{ certificate_requests }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.mode is defined diff --git a/tasks/main.yml b/tasks/main.yml index b1853818..9406f8be 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -2,6 +2,10 @@ - name: Set version specific variables include_tasks: tasks/set_vars.yml +- name: Validate certificate_requests fields + ansible.builtin.include_tasks: tasks/assert_role_vars.yml + when: certificate_requests | length > 0 + - name: Set/reset certificate_is_changed set_fact: certificate_is_changed: false diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index 7a0cd233..f5d51fee 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -21,197 +21,250 @@ fail_msg: >- Role should succeed with default parameters - # --- certificate_requests validation --- + # --- argument_specs validation --- # Role argument validation requires Ansible 2.10+; skipped on older versions. - - name: Argument specs reject missing name in certificate_requests + - name: Run argument specs validation tests when: ansible_version.full is version('2.10', '>=') block: - - name: Run role with certificate_requests missing name - ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml - vars: - certificate_requests: - - ca: self-sign - dns: www.example.com - rescue: - - name: Mark missing name rejected - ansible.builtin.set_fact: - __invalid_input_missing_name_failed: true + - name: Argument specs reject missing name in certificate_requests + block: + - name: Run role with certificate_requests missing name + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - ca: self-sign + dns: www.example.com + rescue: + - name: Mark missing name rejected + ansible.builtin.set_fact: + __invalid_input_missing_name_failed: true - - name: Assert missing name in certificate_requests failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_missing_name_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_requests entries - without the required name field + - name: Assert missing name in certificate_requests failed validation + ansible.builtin.assert: + that: + - __invalid_input_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required name field - - name: Argument specs reject missing ca in certificate_requests - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with certificate_requests missing ca - ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml - vars: - certificate_requests: - - name: mycert - dns: www.example.com - rescue: - - name: Mark missing ca rejected - ansible.builtin.set_fact: - __invalid_input_missing_ca_failed: true + - name: Argument specs reject missing ca in certificate_requests + block: + - name: Run role with certificate_requests missing ca + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + dns: www.example.com + rescue: + - name: Mark missing ca rejected + ansible.builtin.set_fact: + __invalid_input_missing_ca_failed: true - - name: Assert missing ca in certificate_requests failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_missing_ca_failed | default(false) - fail_msg: >- - meta/argument_specs should reject certificate_requests entries - without the required ca field + - name: Assert missing ca in certificate_requests failed validation + ansible.builtin.assert: + that: + - __invalid_input_missing_ca_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_requests entries + without the required ca field - - name: Argument specs reject invalid key_size type - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with key_size as string - ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml - vars: - certificate_requests: - - name: mycert - ca: self-sign - key_size: not_a_number - rescue: - - name: Mark invalid key_size rejected - ansible.builtin.set_fact: - __invalid_input_key_size_type_failed: true + - name: Argument specs reject invalid key_size type + block: + - name: Run role with key_size as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + key_size: not_a_number + rescue: + - name: Mark invalid key_size rejected + ansible.builtin.set_fact: + __invalid_input_key_size_type_failed: true - - name: Assert invalid key_size type failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_key_size_type_failed | default(false) - fail_msg: >- - meta/argument_specs should reject key_size values that - are not integers + - name: Assert invalid key_size type failed validation + ansible.builtin.assert: + that: + - __invalid_input_key_size_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject key_size values that + are not integers - - name: Argument specs reject invalid auto_renew type - when: ansible_version.full is version('2.10', '>=') - block: - - name: Run role with auto_renew as string - ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml - vars: - certificate_requests: - - name: mycert - ca: self-sign - auto_renew: not_a_bool - rescue: - - name: Mark invalid auto_renew rejected - ansible.builtin.set_fact: - __invalid_input_auto_renew_type_failed: true + - name: Argument specs reject invalid auto_renew type + block: + - name: Run role with auto_renew as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + auto_renew: not_a_bool + rescue: + - name: Mark invalid auto_renew rejected + ansible.builtin.set_fact: + __invalid_input_auto_renew_type_failed: true - - name: Assert invalid auto_renew type failed validation - when: ansible_version.full is version('2.10', '>=') - ansible.builtin.assert: - that: - - __invalid_input_auto_renew_type_failed | default(false) - fail_msg: >- - meta/argument_specs should reject auto_renew values that - are not booleans + - name: Assert invalid auto_renew type failed validation + ansible.builtin.assert: + that: + - __invalid_input_auto_renew_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject auto_renew values that + are not booleans - # --- certificate_wait validation --- + - name: Argument specs reject invalid certificate_wait type + block: + - name: Run role with certificate_wait as string + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_wait: not_a_bool + rescue: + - name: Mark invalid certificate_wait rejected + ansible.builtin.set_fact: + __invalid_input_wait_type_failed: true - - name: Argument specs reject invalid certificate_wait type - when: ansible_version.full is version('2.10', '>=') + - name: Assert invalid certificate_wait type failed validation + ansible.builtin.assert: + that: + - __invalid_input_wait_type_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_wait values + that are not booleans + + - name: Argument specs reject missing name in certificate_trust + block: + - name: Run role with certificate_trust missing name + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - content: not-a-certificate + state: present + rescue: + - name: Mark missing trust name rejected + ansible.builtin.set_fact: + __invalid_input_trust_missing_name_failed: true + + - name: Assert missing name in certificate_trust failed validation + ansible.builtin.assert: + that: + - __invalid_input_trust_missing_name_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust entries + without the required name field + + - name: Argument specs reject invalid state in certificate_trust + block: + - name: Run role with invalid trust state + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - name: mycert + content: not-a-certificate + state: invalid_state + rescue: + - name: Mark invalid trust state rejected + ansible.builtin.set_fact: + __invalid_input_trust_state_failed: true + + - name: Assert invalid state in certificate_trust failed validation + ansible.builtin.assert: + that: + - __invalid_input_trust_state_failed | default(false) + fail_msg: >- + meta/argument_specs should reject certificate_trust state + values outside the allowed set (present, absent) + + # --- certificate_trust cross-field validation (assert in tasks, not argument_specs) --- + + - name: Assert rejects trust entry with no source when state is present block: - - name: Run role with certificate_wait as string + - name: Run role with trust entry missing content, src, and url ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml vars: - certificate_wait: not_a_bool + certificate_trust: + - name: mycert + state: present rescue: - - name: Mark invalid certificate_wait rejected + - name: Mark missing trust source rejected ansible.builtin.set_fact: - __invalid_input_wait_type_failed: true + __invalid_input_trust_no_source_failed: true - - name: Assert invalid certificate_wait type failed validation - when: ansible_version.full is version('2.10', '>=') + - name: Assert trust entry with no source failed validation ansible.builtin.assert: that: - - __invalid_input_wait_type_failed | default(false) + - __invalid_input_trust_no_source_failed | default(false) fail_msg: >- - meta/argument_specs should reject certificate_wait values - that are not booleans + certificate_trust should reject entries with state present + that do not specify exactly one of content, src, or url - # --- certificate_trust validation --- + # --- assert_role_vars validation (raw type fields) --- - - name: Argument specs reject missing name in certificate_trust - when: ansible_version.full is version('2.10', '>=') + - name: Assert rejects dns as integer block: - - name: Run role with certificate_trust missing name + - name: Run role with dns as integer ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml vars: - certificate_trust: - - content: not-a-certificate - state: present + certificate_requests: + - name: mycert + ca: self-sign + dns: 12345 rescue: - - name: Mark missing trust name rejected + - name: Mark invalid dns type rejected ansible.builtin.set_fact: - __invalid_input_trust_missing_name_failed: true + __invalid_input_dns_type_failed: true - - name: Assert missing name in certificate_trust failed validation - when: ansible_version.full is version('2.10', '>=') + - name: Assert dns as integer failed validation ansible.builtin.assert: that: - - __invalid_input_trust_missing_name_failed | default(false) + - __invalid_input_dns_type_failed | default(false) fail_msg: >- - meta/argument_specs should reject certificate_trust entries - without the required name field + assert_role_vars should reject dns values that are not + a string or list - - name: Argument specs reject invalid state in certificate_trust - when: ansible_version.full is version('2.10', '>=') + - name: Assert rejects ip as boolean block: - - name: Run role with invalid trust state + - name: Run role with ip as boolean ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml vars: - certificate_trust: + certificate_requests: - name: mycert - content: not-a-certificate - state: invalid_state + ca: self-sign + ip: true rescue: - - name: Mark invalid trust state rejected + - name: Mark invalid ip type rejected ansible.builtin.set_fact: - __invalid_input_trust_state_failed: true + __invalid_input_ip_type_failed: true - - name: Assert invalid state in certificate_trust failed validation - when: ansible_version.full is version('2.10', '>=') + - name: Assert ip as boolean failed validation ansible.builtin.assert: that: - - __invalid_input_trust_state_failed | default(false) + - __invalid_input_ip_type_failed | default(false) fail_msg: >- - meta/argument_specs should reject certificate_trust state - values outside the allowed set (present, absent) + assert_role_vars should reject ip values that are not + a string or list - # --- certificate_trust cross-field validation (assert in tasks, not argument_specs) --- - - - name: Assert rejects trust entry with no source when state is present + - name: Assert rejects mode as boolean block: - - name: Run role with trust entry missing content, src, and url + - name: Run role with mode as boolean ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml vars: - certificate_trust: + certificate_requests: - name: mycert - state: present + ca: self-sign + mode: true rescue: - - name: Mark missing trust source rejected + - name: Mark invalid mode type rejected ansible.builtin.set_fact: - __invalid_input_trust_no_source_failed: true + __invalid_input_mode_type_failed: true - - name: Assert trust entry with no source failed validation + - name: Assert mode as boolean failed validation ansible.builtin.assert: that: - - __invalid_input_trust_no_source_failed | default(false) + - __invalid_input_mode_type_failed | default(false) fail_msg: >- - certificate_trust should reject entries with state present - that do not specify exactly one of content, src, or url + assert_role_vars should reject mode values that are not + a string or integer always: - name: Clear test facts @@ -225,4 +278,7 @@ __invalid_input_trust_missing_name_failed: __invalid_input_trust_state_failed: __invalid_input_trust_no_source_failed: + __invalid_input_dns_type_failed: + __invalid_input_ip_type_failed: + __invalid_input_mode_type_failed: tags: tests::cleanup From 5d396547f5f6cdaa7abfe57fdd9a64f89187d351 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 17 Aug 2026 11:13:19 +0200 Subject: [PATCH 07/16] Added test case to confirm that ansible iteras trough list arguments --- tests/tests_invalid_input.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index f5d51fee..58abd174 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -153,6 +153,30 @@ meta/argument_specs should reject certificate_trust entries without the required name field + - name: Argument specs reject invalid key_usage choice + block: + - name: Run role with invalid key_usage value + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + key_usage: + - digitalSignature + - invalidUsage + rescue: + - name: Mark invalid key_usage rejected + ansible.builtin.set_fact: + __invalid_input_key_usage_choice_failed: true + + - name: Assert invalid key_usage choice failed validation + ansible.builtin.assert: + that: + - __invalid_input_key_usage_choice_failed | default(false) + fail_msg: >- + meta/argument_specs should reject key_usage values + not in the allowed choices list + - name: Argument specs reject invalid state in certificate_trust block: - name: Run role with invalid trust state @@ -276,6 +300,7 @@ __invalid_input_auto_renew_type_failed: __invalid_input_wait_type_failed: __invalid_input_trust_missing_name_failed: + __invalid_input_key_usage_choice_failed: __invalid_input_trust_state_failed: __invalid_input_trust_no_source_failed: __invalid_input_dns_type_failed: From 4ddb0beb9efca27b7818328e3a93c64a559a66b3 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 17 Aug 2026 11:32:47 +0200 Subject: [PATCH 08/16] Good catch from coderabbit. Added stricter validation and a test for it --- tasks/assert_role_vars.yml | 44 ++++++++++++++++++++++------------- tests/tests_invalid_input.yml | 25 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml index bbac800d..30f81989 100644 --- a/tasks/assert_role_vars.yml +++ b/tasks/assert_role_vars.yml @@ -1,51 +1,63 @@ # SPDX-License-Identifier: MIT --- -- name: Assert dns is a string or list +- name: Assert dns is a string or list of strings ansible.builtin.assert: that: - - item.dns is sequence and item.dns is not mapping + - >- + item.dns is string + or (item.dns is sequence and item.dns is not mapping + and item.dns | reject('string') | list | length == 0) fail_msg: >- - certificate_requests[{{ idx }}].dns must be a string or list, - got {{ item.dns | type_debug }} + certificate_requests[{{ idx }}].dns must be a string or list of + strings, got {{ item.dns | type_debug }} loop: "{{ certificate_requests }}" loop_control: index_var: idx label: "{{ item.name | d('unnamed') }}" when: item.dns is defined -- name: Assert email is a string or list +- name: Assert email is a string or list of strings ansible.builtin.assert: that: - - item.email is sequence and item.email is not mapping + - >- + item.email is string + or (item.email is sequence and item.email is not mapping + and item.email | reject('string') | list | length == 0) fail_msg: >- - certificate_requests[{{ idx }}].email must be a string or list, - got {{ item.email | type_debug }} + certificate_requests[{{ idx }}].email must be a string or list of + strings, got {{ item.email | type_debug }} loop: "{{ certificate_requests }}" loop_control: index_var: idx label: "{{ item.name | d('unnamed') }}" when: item.email is defined -- name: Assert ip is a string or list +- name: Assert ip is a string or list of strings ansible.builtin.assert: that: - - item.ip is sequence and item.ip is not mapping + - >- + item.ip is string + or (item.ip is sequence and item.ip is not mapping + and item.ip | reject('string') | list | length == 0) fail_msg: >- - certificate_requests[{{ idx }}].ip must be a string or list, - got {{ item.ip | type_debug }} + certificate_requests[{{ idx }}].ip must be a string or list of + strings, got {{ item.ip | type_debug }} loop: "{{ certificate_requests }}" loop_control: index_var: idx label: "{{ item.name | d('unnamed') }}" when: item.ip is defined -- name: Assert principal is a string or list +- name: Assert principal is a string or list of strings ansible.builtin.assert: that: - - item.principal is sequence and item.principal is not mapping + - >- + item.principal is string + or (item.principal is sequence and item.principal is not mapping + and item.principal | reject('string') | list | length == 0) fail_msg: >- - certificate_requests[{{ idx }}].principal must be a string or list, - got {{ item.principal | type_debug }} + certificate_requests[{{ idx }}].principal must be a string or list of + strings, got {{ item.principal | type_debug }} loop: "{{ certificate_requests }}" loop_control: index_var: idx diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index 58abd174..82495058 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -246,6 +246,30 @@ assert_role_vars should reject dns values that are not a string or list + - name: Assert rejects dns list with non-string element + block: + - name: Run role with dns list containing integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_requests: + - name: mycert + ca: self-sign + dns: + - www.example.com + - 12345 + rescue: + - name: Mark dns list element rejected + ansible.builtin.set_fact: + __invalid_input_dns_element_failed: true + + - name: Assert dns list with non-string element failed validation + ansible.builtin.assert: + that: + - __invalid_input_dns_element_failed | default(false) + fail_msg: >- + assert_role_vars should reject dns lists containing + non-string elements + - name: Assert rejects ip as boolean block: - name: Run role with ip as boolean @@ -304,6 +328,7 @@ __invalid_input_trust_state_failed: __invalid_input_trust_no_source_failed: __invalid_input_dns_type_failed: + __invalid_input_dns_element_failed: __invalid_input_ip_type_failed: __invalid_input_mode_type_failed: tags: tests::cleanup From e121f9b2db4f900c3518543a4dca35c4897df6ea Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 17 Aug 2026 15:38:36 +0200 Subject: [PATCH 09/16] A test to see if the argument specs file is triggering the failing ci tests --- meta/{argument_specs.yml => argument_specs.yml.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename meta/{argument_specs.yml => argument_specs.yml.bak} (100%) diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml.bak similarity index 100% rename from meta/argument_specs.yml rename to meta/argument_specs.yml.bak From a09989ab41719db88b0fdf4393258e753be9ae5f Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Tue, 18 Aug 2026 10:57:00 +0200 Subject: [PATCH 10/16] Address CodeRabbit review: verify error messages in rescue blocks and add multiple-source trust test Co-Authored-By: Claude Opus 4.6 --- tests/tests_invalid_input.yml | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index 82495058..49741e70 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -39,6 +39,9 @@ - name: Mark missing name rejected ansible.builtin.set_fact: __invalid_input_missing_name_failed: true + when: >- + 'missing required arguments' in + (ansible_failed_result | default({}) | to_json) - name: Assert missing name in certificate_requests failed validation ansible.builtin.assert: @@ -60,6 +63,9 @@ - name: Mark missing ca rejected ansible.builtin.set_fact: __invalid_input_missing_ca_failed: true + when: >- + 'missing required arguments' in + (ansible_failed_result | default({}) | to_json) - name: Assert missing ca in certificate_requests failed validation ansible.builtin.assert: @@ -82,6 +88,9 @@ - name: Mark invalid key_size rejected ansible.builtin.set_fact: __invalid_input_key_size_type_failed: true + when: >- + 'key_size' in + (ansible_failed_result | default({}) | to_json) - name: Assert invalid key_size type failed validation ansible.builtin.assert: @@ -104,6 +113,9 @@ - name: Mark invalid auto_renew rejected ansible.builtin.set_fact: __invalid_input_auto_renew_type_failed: true + when: >- + 'auto_renew' in + (ansible_failed_result | default({}) | to_json) - name: Assert invalid auto_renew type failed validation ansible.builtin.assert: @@ -123,6 +135,9 @@ - name: Mark invalid certificate_wait rejected ansible.builtin.set_fact: __invalid_input_wait_type_failed: true + when: >- + 'certificate_wait' in + (ansible_failed_result | default({}) | to_json) - name: Assert invalid certificate_wait type failed validation ansible.builtin.assert: @@ -144,6 +159,9 @@ - name: Mark missing trust name rejected ansible.builtin.set_fact: __invalid_input_trust_missing_name_failed: true + when: >- + 'missing required arguments' in + (ansible_failed_result | default({}) | to_json) - name: Assert missing name in certificate_trust failed validation ansible.builtin.assert: @@ -168,6 +186,9 @@ - name: Mark invalid key_usage rejected ansible.builtin.set_fact: __invalid_input_key_usage_choice_failed: true + when: >- + 'key_usage' in + (ansible_failed_result | default({}) | to_json) - name: Assert invalid key_usage choice failed validation ansible.builtin.assert: @@ -190,6 +211,9 @@ - name: Mark invalid trust state rejected ansible.builtin.set_fact: __invalid_input_trust_state_failed: true + when: >- + 'invalid_state' in + (ansible_failed_result | default({}) | to_json) - name: Assert invalid state in certificate_trust failed validation ansible.builtin.assert: @@ -213,6 +237,9 @@ - name: Mark missing trust source rejected ansible.builtin.set_fact: __invalid_input_trust_no_source_failed: true + when: >- + 'exactly one' in + (ansible_failed_result | default({}) | to_json) - name: Assert trust entry with no source failed validation ansible.builtin.assert: @@ -222,6 +249,32 @@ certificate_trust should reject entries with state present that do not specify exactly one of content, src, or url + - name: Assert rejects trust entry with multiple sources + block: + - name: Run role with trust entry having both content and src + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + certificate_trust: + - name: mycert + content: not-a-certificate + src: /tmp/cert.pem + state: present + rescue: + - name: Mark multiple trust sources rejected + ansible.builtin.set_fact: + __invalid_input_trust_multiple_sources_failed: true + when: >- + 'exactly one' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert trust entry with multiple sources failed validation + ansible.builtin.assert: + that: + - __invalid_input_trust_multiple_sources_failed | default(false) + fail_msg: >- + certificate_trust should reject entries with state present + that specify more than one of content, src, or url + # --- assert_role_vars validation (raw type fields) --- - name: Assert rejects dns as integer @@ -237,6 +290,9 @@ - name: Mark invalid dns type rejected ansible.builtin.set_fact: __invalid_input_dns_type_failed: true + when: >- + 'dns must be a string' in + (ansible_failed_result | default({}) | to_json) - name: Assert dns as integer failed validation ansible.builtin.assert: @@ -261,6 +317,9 @@ - name: Mark dns list element rejected ansible.builtin.set_fact: __invalid_input_dns_element_failed: true + when: >- + 'dns must be a string' in + (ansible_failed_result | default({}) | to_json) - name: Assert dns list with non-string element failed validation ansible.builtin.assert: @@ -283,6 +342,9 @@ - name: Mark invalid ip type rejected ansible.builtin.set_fact: __invalid_input_ip_type_failed: true + when: >- + 'ip must be a string' in + (ansible_failed_result | default({}) | to_json) - name: Assert ip as boolean failed validation ansible.builtin.assert: @@ -305,6 +367,9 @@ - name: Mark invalid mode type rejected ansible.builtin.set_fact: __invalid_input_mode_type_failed: true + when: >- + 'mode must be a string' in + (ansible_failed_result | default({}) | to_json) - name: Assert mode as boolean failed validation ansible.builtin.assert: @@ -327,6 +392,7 @@ __invalid_input_key_usage_choice_failed: __invalid_input_trust_state_failed: __invalid_input_trust_no_source_failed: + __invalid_input_trust_multiple_sources_failed: __invalid_input_dns_type_failed: __invalid_input_dns_element_failed: __invalid_input_ip_type_failed: From 8512131059b001f154bf4de7160eef711aa61e35 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Tue, 18 Aug 2026 10:58:52 +0200 Subject: [PATCH 11/16] Restore argument_specs.yml from testing rename Co-Authored-By: Claude Opus 4.6 --- meta/{argument_specs.yml.bak => argument_specs.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename meta/{argument_specs.yml.bak => argument_specs.yml} (100%) diff --git a/meta/argument_specs.yml.bak b/meta/argument_specs.yml similarity index 100% rename from meta/argument_specs.yml.bak rename to meta/argument_specs.yml From fd1f6a6aed99e8baa7b3d02fa72b65b201309e7a Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Tue, 18 Aug 2026 15:27:27 +0200 Subject: [PATCH 12/16] Bug fix for CentOS 7 --- tasks/assert_role_vars.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml index 30f81989..d8835208 100644 --- a/tasks/assert_role_vars.yml +++ b/tasks/assert_role_vars.yml @@ -69,9 +69,7 @@ that: - >- item.mode is string - or (item.mode is integer - and item.mode is not sameas true - and item.mode is not sameas false) + or (item.mode | type_debug) == 'int' fail_msg: >- certificate_requests[{{ idx }}].mode must be a string or integer, got {{ item.mode | type_debug }} From 412af4fbcb146797bf982d2a15d1c7a81e414c44 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Wed, 19 Aug 2026 10:40:37 +0200 Subject: [PATCH 13/16] Fixed small inconsistency --- tasks/assert_role_vars.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml index d8835208..e623e903 100644 --- a/tasks/assert_role_vars.yml +++ b/tasks/assert_role_vars.yml @@ -67,9 +67,7 @@ - name: Assert mode is a string or integer ansible.builtin.assert: that: - - >- - item.mode is string - or (item.mode | type_debug) == 'int' + - (item.mode | type_debug) in ['str', 'int'] fail_msg: >- certificate_requests[{{ idx }}].mode must be a string or integer, got {{ item.mode | type_debug }} From 1138368cc95c33b31c0b132779ca32613ce4a098 Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Wed, 26 Aug 2026 15:57:53 +0200 Subject: [PATCH 14/16] Bugfix for CentOS 7 --- tasks/assert_role_vars.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml index e623e903..00b40426 100644 --- a/tasks/assert_role_vars.yml +++ b/tasks/assert_role_vars.yml @@ -67,7 +67,7 @@ - name: Assert mode is a string or integer ansible.builtin.assert: that: - - (item.mode | type_debug) in ['str', 'int'] + - (item.mode | type_debug) in ['str', 'int', 'unicode'] fail_msg: >- certificate_requests[{{ idx }}].mode must be a string or integer, got {{ item.mode | type_debug }} From 57d72a77e3cee0dce57ac423fbd93e976cf9b8ad Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Mon, 31 Aug 2026 16:02:56 +0200 Subject: [PATCH 15/16] Argument spec defaults moved from description to default: --- meta/argument_specs.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml index 53b73f00..adcbcf72 100644 --- a/meta/argument_specs.yml +++ b/meta/argument_specs.yml @@ -15,9 +15,10 @@ argument_specs: certificate_requests: type: list elements: dict + default: [] description: > A list of certificate request specifications. Each item describes - a certificate to be issued or renewed. Defaults to an empty list. + a certificate to be issued or renewed. options: name: type: str @@ -47,9 +48,10 @@ argument_specs: certificate Subject Alternative Name (SAN). auto_renew: type: bool + default: true description: > Whether the certificate should be renewed automatically - before it expires. Defaults to `true`. + before it expires. owner: type: str description: > @@ -108,15 +110,19 @@ argument_specs: - cRLSign - encipherOnly - decipherOnly + default: + - digitalSignature + - keyEncipherment description: > The allowed Key Usage extensions for the certificate. - Defaults to `digitalSignature` and `keyEncipherment`. extended_key_usage: type: list elements: str + default: + - id-kp-serverAuth + - id-kp-clientAuth description: > The Extended Key Usage attributes for the certificate. - Defaults to `id-kp-serverAuth` and `id-kp-clientAuth`. run_before: type: str description: > @@ -131,24 +137,26 @@ argument_specs: A Kerberos principal or list of Kerberos principals. provider: type: str + default: certmonger description: > The underlying method used to request and manage the - certificate. Defaults to `certmonger`. + certificate. issuer: type: str description: > The issuer certificate nickname or template name. certificate_wait: type: bool + default: true description: > Whether the task should wait for the certificate to be issued. - Defaults to `true`. certificate_trust: type: list elements: dict + default: [] description: > A list of certificates to install into or remove from the system - trust store. Defaults to an empty list. + trust store. options: name: type: str @@ -177,6 +185,6 @@ argument_specs: choices: - present - absent + default: present description: > Whether the trust anchor should be present or absent. - Defaults to `present`. From 709e85bc507ad4fba9da0e2e6f9b97b23e3ad71b Mon Sep 17 00:00:00 2001 From: Donat Szabo Date: Tue, 8 Sep 2026 11:19:40 +0200 Subject: [PATCH 16/16] Changed tests mninimum version number from 2.10 to 2.11 --- tests/tests_invalid_input.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml index 49741e70..1aa1d7a9 100644 --- a/tests/tests_invalid_input.yml +++ b/tests/tests_invalid_input.yml @@ -25,7 +25,7 @@ # Role argument validation requires Ansible 2.10+; skipped on older versions. - name: Run argument specs validation tests - when: ansible_version.full is version('2.10', '>=') + when: ansible_version.full is version('2.11', '>=') block: - name: Argument specs reject missing name in certificate_requests block: