diff --git a/defaults/main.yml b/defaults/main.yml index 888d7168b7..6189fbb5f5 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -4,16 +4,13 @@ # This file also serves as a documentation for such a variables. # Examples of role input variables: -postgresql_version: "{{ '16' if ansible_facts['os_family'] == 'RedHat' - and ansible_facts['distribution'] != 'Fedora' - and ansible_facts['distribution_major_version'] == '10' - else '13' }}" +postgresql_version: null postgresql_password: null postgresql_cert_name: null # the container build environment's RAM is independent from the deployment # (regardless if it's a bootc or system container one), so disable tuning there # by default -postgresql_server_tuning: "{{ false if (ansible_connection | d('')) == 'buildah' else true }}" +postgresql_server_tuning: null postgresql_ssl_enable: false # If you want to generate the certificas in the postgresql role diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml new file mode 100644 index 0000000000..3e4105a7a1 --- /dev/null +++ b/meta/argument_specs.yml @@ -0,0 +1,195 @@ +# SPDX-License-Identifier: MIT +--- +argument_specs: + main: + short_description: The postgresql role. + description: > + The postgresql role installs and configures a PostgreSQL database + server. It supports version selection, optional password + configuration, SSL/TLS certificates, server tuning, custom + configuration files, and SQL script execution. + options: + postgresql_version: + type: raw + default: null + description: > + The major version of PostgreSQL to install and configure. + Accepts a version string such as `13` or `16`, or an + integer major version. When unset, the default version + depends on the target platform; for example RHEL 10 + defaults to `16`. + postgresql_password: + type: raw + default: null + description: > + The password for the PostgreSQL superuser (`postgres`). When + set, password authentication is enabled in `pg_hba.conf`. + Accepts `null` or a string password value. + postgresql_cert_name: + type: raw + default: null + description: > + The base path or name of TLS certificate and key files to use + for the server, without the `.crt` or `.key` suffix. For + example, `/etc/certs/server` refers to `/etc/certs/server.crt` + and `/etc/certs/server.key`. Accepts `null` or a string. + postgresql_server_tuning: + type: raw + default: null + description: > + Whether to apply memory-based tuning settings such as + `shared_buffers` and `effective_cache_size` based on system + memory. Accepts `null`, `true`, or `false`. When unset, + tuning is enabled on booted systems and disabled in container + build environments. + postgresql_ssl_enable: + type: bool + default: false + description: > + Whether to enable SSL/TLS for PostgreSQL connections by + setting `ssl = on` in the server configuration. + postgresql_certificates: + type: list + elements: dict + default: [] + description: > + A list of certificate request specifications passed to the + certificate role for generating TLS certificates used by + PostgreSQL. + options: + name: + type: str + required: true + 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: > + The CA that will issue the certificate (for example + `self-sign` or `ipa`). + dns: + type: raw + description: > + A domain name or list of domain names to include in the + certificate Subject Alternative Name (SAN). + email: + type: raw + description: > + An email address or list of email addresses to include in + the certificate Subject Alternative Name (SAN). + ip: + type: raw + description: > + An IP address or list of IP addresses to include in the + certificate Subject Alternative Name (SAN). + auto_renew: + type: bool + default: true + description: > + Whether the certificate should be renewed automatically + before it expires. + owner: + type: str + description: > + The user name or user id for the certificate and key + files. + group: + type: str + description: > + The group name or group id for the certificate and key + files. + mode: + type: raw + description: > + The file system permissions for the certificate and key + files. Accepts a string (for example `0644`) or an + integer. + key_size: + type: int + description: > + The key size in bits. + common_name: + type: str + description: > + The Common Name requested for the certificate subject. + country: + type: str + description: > + The country code requested for the certificate subject. + state: + type: str + description: > + The state requested for the certificate subject. + locality: + type: str + description: > + The locality requested for the certificate subject. + organization: + type: str + description: > + The organization requested for the certificate subject. + organizational_unit: + type: str + description: > + The organizational unit requested for the certificate + subject. + contact_email: + type: str + description: > + The contact email requested for the certificate subject. + key_usage: + type: list + elements: str + choices: + - digitalSignature + - nonRepudiation + - keyEncipherment + - dataEncipherment + - keyAgreement + - keyCertSign + - cRLSign + - encipherOnly + - decipherOnly + default: + - digitalSignature + - keyEncipherment + description: > + The allowed Key Usage extensions for the certificate. + extended_key_usage: + type: list + elements: str + default: + - id-kp-serverAuth + - id-kp-clientAuth + description: > + The Extended Key Usage attributes for the certificate. + run_before: + type: str + description: > + A command to run before saving the certificate. + run_after: + type: str + description: > + A command to run after saving the certificate. + principal: + type: raw + description: > + A Kerberos principal or list of Kerberos principals. + provider: + type: str + default: certmonger + description: > + The underlying method used to request and manage the + certificate. + issuer: + type: str + description: > + The issuer certificate nickname or template name. + postgresql_secure_logging: + type: bool + default: true + description: > + Whether to suppress logging of sensitive commands such as + password changes. Set to `false` for troubleshooting. diff --git a/tasks/assert_role_vars.yml b/tasks/assert_role_vars.yml new file mode 100644 index 0000000000..9e8c4e4363 --- /dev/null +++ b/tasks/assert_role_vars.yml @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: MIT +--- +- name: Assert postgresql_version is a string or integer + ansible.builtin.assert: + that: + - >- + (postgresql_version is string and postgresql_version is match("^[1-9][0-9]*$")) + or (postgresql_version | type_debug) == 'int' + fail_msg: >- + postgresql_version must be a string or integer, + got {{ postgresql_version | type_debug }} + when: + - postgresql_version is defined + - postgresql_version is not none + +- name: Assert postgresql_password is null or a string + ansible.builtin.assert: + that: + - >- + (postgresql_password is none) + or (postgresql_password is string) + fail_msg: >- + postgresql_password must be null or a string, + got {{ postgresql_password | type_debug }} + +- name: Assert postgresql_cert_name is null or a string + ansible.builtin.assert: + that: + - >- + (postgresql_cert_name is none) + or (postgresql_cert_name is string) + fail_msg: >- + postgresql_cert_name must be null or a string, + got {{ postgresql_cert_name | type_debug }} + +- name: Assert postgresql_server_tuning is null or a boolean + ansible.builtin.assert: + that: + - >- + (postgresql_server_tuning is none) + or (postgresql_server_tuning is sameas true) + or (postgresql_server_tuning is sameas false) + fail_msg: >- + postgresql_server_tuning must be null or a boolean, + got {{ postgresql_server_tuning | type_debug }} + +- name: Assert dns in postgresql_certificates is a string or list of strings + ansible.builtin.assert: + that: + - >- + 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: >- + postgresql_certificates[{{ idx }}].dns must be a string or list of + strings, got {{ item.dns | type_debug }} + loop: "{{ postgresql_certificates }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.dns is defined + +- name: Assert email in postgresql_certificates is a string or list of strings + ansible.builtin.assert: + that: + - >- + 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: >- + postgresql_certificates[{{ idx }}].email must be a string or list of + strings, got {{ item.email | type_debug }} + loop: "{{ postgresql_certificates }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.email is defined + +- name: Assert ip in postgresql_certificates is a string or list of strings + ansible.builtin.assert: + that: + - >- + 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: >- + postgresql_certificates[{{ idx }}].ip must be a string or list of + strings, got {{ item.ip | type_debug }} + loop: "{{ postgresql_certificates }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.ip is defined + +- name: Assert principal in postgresql_certificates is a string or list of strings + ansible.builtin.assert: + that: + - >- + 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: >- + postgresql_certificates[{{ idx }}].principal must be a string or list of + strings, got {{ item.principal | type_debug }} + loop: "{{ postgresql_certificates }}" + loop_control: + index_var: idx + label: "{{ item.name | d('unnamed') }}" + when: item.principal is defined + +- name: Assert mode in postgresql_certificates is a string or integer + ansible.builtin.assert: + that: + - (item.mode | type_debug) in ['str', 'int', 'unicode'] + fail_msg: >- + postgresql_certificates[{{ idx }}].mode must be a string or integer, + got {{ item.mode | type_debug }} + loop: "{{ postgresql_certificates }}" + 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 4ccac26702..c3770fbd9c 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -5,6 +5,9 @@ - name: Set platform/version specific variables include_tasks: tasks/set_vars.yml +- name: Validate role parameters + ansible.builtin.include_tasks: assert_role_vars.yml + - name: Gather the package facts package_facts: no_log: "{{ ansible_verbosity < 3 }}" @@ -15,7 +18,7 @@ when: - ansible_facts["os_family"] == "RedHat" - ansible_facts["distribution_major_version"] == "8" - - postgresql_version | string not in __postgresql_versions_el8 + - __postgresql_version | string not in __postgresql_versions_el8 - name: Check if requested version is supported in system (RHEL9) fail: @@ -23,7 +26,7 @@ when: - ansible_facts["os_family"] == "RedHat" - ansible_facts["distribution_major_version"] == "9" - - postgresql_version | string not in __postgresql_versions_el9 + - __postgresql_version | string not in __postgresql_versions_el9 - name: Check if requested version is supported in system (RHEL10) fail: @@ -33,17 +36,17 @@ when: - ansible_facts["os_family"] == "RedHat" - ansible_facts["distribution_major_version"] == "10" - - postgresql_version | string not in __postgresql_versions_el10 + - __postgresql_version | string not in __postgresql_versions_el10 - name: Check requested and installed version of Postgresql fail: msg: >- - Setting version {{ postgresql_version }} while + Setting version {{ __postgresql_version }} while {{ ansible_facts.packages['postgresql'][0].version }} is installed when: - "'postgresql' in ansible_facts.packages" - ansible_facts.packages["postgresql"][0].version | - regex_search('([0-9]*).', '\\1') | first != postgresql_version | string + regex_search('([0-9]*).', '\\1') | first != __postgresql_version | string # rpm ostree pkg mgr cannot handle @groups/modules # so just assume the packages are present diff --git a/tasks/set_vars.yml b/tasks/set_vars.yml index 1212f39bd1..b9e69a0787 100644 --- a/tasks/set_vars.yml +++ b/tasks/set_vars.yml @@ -5,6 +5,22 @@ when: __postgresql_required_facts | difference(ansible_facts.keys() | list) | length > 0 +- name: Set postgresql effective value defaults + ansible.builtin.set_fact: + __postgresql_version_default: "{{ '16' if ansible_facts['os_family'] == 'RedHat' + and ansible_facts['distribution'] != 'Fedora' + and ansible_facts['distribution_major_version'] == '10' + else '13' }}" + __postgresql_server_tuning_default: "{{ false if (ansible_connection | d('')) == 'buildah' else true }}" + +- name: Set postgresql effective values from user input or defaults + ansible.builtin.set_fact: + __postgresql_version: "{{ postgresql_version if postgresql_version is not none + else __postgresql_version_default }}" + __postgresql_server_tuning: "{{ postgresql_server_tuning + if postgresql_server_tuning is not none + else __postgresql_server_tuning_default }}" + - name: Record role begin fingerprint sr_fingerprint: status: begin diff --git a/templates/postgresql-internal.conf.j2 b/templates/postgresql-internal.conf.j2 index ff4e629912..b5815df0d3 100644 --- a/templates/postgresql-internal.conf.j2 +++ b/templates/postgresql-internal.conf.j2 @@ -1,7 +1,7 @@ {{ ansible_managed | comment }} {{ "system_role:postgresql" | comment(prefix="", postfix="") }} -{% if postgresql_server_tuning %} +{% if __postgresql_server_tuning %} shared_buffers = {{ (ansible_facts.memory_mb.real.total / 4) | int | abs }}MB effective_cache_size = {{ (ansible_facts.memory_mb.real.total / 2) | int | abs }}MB {% endif %} diff --git a/tests/tasks/clean_instance.yml b/tests/tasks/clean_instance.yml index 8350b06f83..c8837eee02 100644 --- a/tests/tasks/clean_instance.yml +++ b/tests/tasks/clean_instance.yml @@ -13,9 +13,9 @@ - postgresql - postgresql-any - postgresql-private-libs - - postgresql{{ postgresql_version }} - - postgresql{{ postgresql_version }}-any - - postgresql{{ postgresql_version }}-private-libs + - postgresql{{ __postgresql_version }} + - postgresql{{ __postgresql_version }}-any + - postgresql{{ __postgresql_version }}-private-libs - name: Stop and disable postgresql service service: diff --git a/tests/tasks/install_and_check.yml b/tests/tasks/install_and_check.yml index 0110860e80..9c53c4a3a2 100644 --- a/tests/tasks/install_and_check.yml +++ b/tests/tasks/install_and_check.yml @@ -94,7 +94,7 @@ register: __version changed_when: false failed_when: not __version.stdout is - search(" " ~ postgresql_version ~ "[^0-9]") + search(" " ~ __postgresql_version ~ "[^0-9]") always: - name: Clean up diff --git a/tests/tests_invalid_input.yml b/tests/tests_invalid_input.yml new file mode 100644 index 0000000000..d9336a1314 --- /dev/null +++ b/tests/tests_invalid_input.yml @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: MIT +--- +- name: Verify invalid parameters are rejected + hosts: all + tasks: + - name: Run invalid input tests + block: + - name: Run role with valid defaults + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + + - name: Run argument specs validation tests + when: ansible_version.full is version('2.11', '>=') + block: + - name: Argument specs reject missing required ca in postgresql_certificates + block: + - name: Run role without required ca in postgresql_certificates + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_certificates: + - name: test_cert + rescue: + - 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 was rejected + ansible.builtin.assert: + that: + - __invalid_input_missing_ca_failed | default(false) + fail_msg: >- + argument_specs should reject postgresql_certificates + entries missing the required ca field + + - name: Argument specs reject non-boolean postgresql_ssl_enable + block: + - name: Run role with non-boolean postgresql_ssl_enable + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_ssl_enable: not_a_bool + rescue: + - name: Mark invalid postgresql_ssl_enable type rejected + ansible.builtin.set_fact: + __invalid_input_ssl_enable_type_failed: true + when: >- + 'postgresql_ssl_enable' in + (ansible_failed_result | default({}) | to_json) + and 'bool' in + (ansible_failed_result | default({}) | to_json | lower) + + - name: Assert invalid postgresql_ssl_enable type was rejected + ansible.builtin.assert: + that: + - __invalid_input_ssl_enable_type_failed | default(false) + fail_msg: >- + argument_specs should reject postgresql_ssl_enable + with a non-boolean value + + - name: Argument specs reject non-boolean postgresql_secure_logging + block: + - name: Run role with non-boolean postgresql_secure_logging + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_secure_logging: not_a_bool + rescue: + - name: Mark invalid postgresql_secure_logging type rejected + ansible.builtin.set_fact: + __invalid_input_secure_logging_type_failed: true + when: >- + 'postgresql_secure_logging' in + (ansible_failed_result | default({}) | to_json) + and 'bool' in + (ansible_failed_result | default({}) | to_json | lower) + + - name: Assert invalid postgresql_secure_logging type was rejected + ansible.builtin.assert: + that: + - __invalid_input_secure_logging_type_failed | default(false) + fail_msg: >- + argument_specs should reject postgresql_secure_logging + with a non-boolean value + + - name: Assert rejects postgresql_version as a dict + block: + - name: Run role with postgresql_version as a dict + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_version: + major: 13 + rescue: + - name: Mark postgresql_version dict rejected + ansible.builtin.set_fact: + __invalid_input_version_dict_failed: true + when: >- + 'postgresql_version must be' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert postgresql_version as dict was rejected + ansible.builtin.assert: + that: + - __invalid_input_version_dict_failed | default(false) + fail_msg: >- + assert_role_vars should reject postgresql_version when + given a dictionary value + + - name: Assert rejects postgresql_password as an integer + block: + - name: Run role with postgresql_password as an integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_password: 12345 + rescue: + - name: Mark postgresql_password integer rejected + ansible.builtin.set_fact: + __invalid_input_password_int_failed: true + when: >- + 'postgresql_password must be' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert postgresql_password as integer was rejected + ansible.builtin.assert: + that: + - __invalid_input_password_int_failed | default(false) + fail_msg: >- + assert_role_vars should reject postgresql_password when + given an integer value + + - name: Assert rejects postgresql_cert_name as an integer + block: + - name: Run role with postgresql_cert_name as an integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_cert_name: 42 + rescue: + - name: Mark postgresql_cert_name integer rejected + ansible.builtin.set_fact: + __invalid_input_cert_name_int_failed: true + when: >- + 'postgresql_cert_name must be' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert postgresql_cert_name as integer was rejected + ansible.builtin.assert: + that: + - __invalid_input_cert_name_int_failed | default(false) + fail_msg: >- + assert_role_vars should reject postgresql_cert_name when + given an integer value + + - name: Assert rejects postgresql_server_tuning as an integer + block: + - name: Run role with postgresql_server_tuning as an integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_server_tuning: 123 + rescue: + - name: Mark postgresql_server_tuning integer rejected + ansible.builtin.set_fact: + __invalid_input_server_tuning_int_failed: true + when: >- + 'postgresql_server_tuning must be' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert postgresql_server_tuning as integer was rejected + ansible.builtin.assert: + that: + - __invalid_input_server_tuning_int_failed | default(false) + fail_msg: >- + assert_role_vars should reject postgresql_server_tuning + when given an integer value + + - name: Assert rejects postgresql_certificates dns as an integer + block: + - name: Run role with postgresql_certificates dns as an integer + ansible.builtin.include_tasks: tasks/run_role_with_clear_facts.yml + vars: + postgresql_certificates: + - name: test_cert + ca: self-sign + dns: 123 + rescue: + - name: Mark postgresql_certificates dns integer rejected + ansible.builtin.set_fact: + __invalid_input_cert_dns_int_failed: true + when: >- + 'postgresql_certificates' in + (ansible_failed_result | default({}) | to_json) + and '.dns must be' in + (ansible_failed_result | default({}) | to_json) + + - name: Assert postgresql_certificates dns as integer was rejected + ansible.builtin.assert: + that: + - __invalid_input_cert_dns_int_failed | default(false) + fail_msg: >- + assert_role_vars should reject postgresql_certificates + entries where dns is an integer + + - name: Assert repeated role invocations resolve current postgresql_version + block: + - name: Run role set_vars with postgresql_version 13 + ansible.builtin.include_role: + name: linux-system-roles.postgresql + tasks_from: set_vars.yml + public: true + vars: + postgresql_version: "13" + + - name: Check effective version is 13 + ansible.builtin.assert: + that: + - __postgresql_version | string == '13' + fail_msg: >- + __postgresql_version should be 13 after explicit input + + - name: Run role set_vars with postgresql_version 16 + ansible.builtin.include_role: + name: linux-system-roles.postgresql + tasks_from: set_vars.yml + public: true + vars: + postgresql_version: "16" + + - name: Check effective version is 16 not a stale value + ansible.builtin.assert: + that: + - __postgresql_version | string == '16' + fail_msg: >- + __postgresql_version should be 16 on second invocation, + got {{ __postgresql_version | default('unset') }} + + always: + - name: Clear test facts + ansible.builtin.set_fact: + __invalid_input_missing_ca_failed: + __invalid_input_ssl_enable_type_failed: + __invalid_input_secure_logging_type_failed: + __invalid_input_version_dict_failed: + __invalid_input_password_int_failed: + __invalid_input_cert_name_int_failed: + __invalid_input_server_tuning_int_failed: + __invalid_input_cert_dns_int_failed: + tags: tests::cleanup diff --git a/tests/tests_versions.yml b/tests/tests_versions.yml index b4726f7200..18c9dd599e 100644 --- a/tests/tests_versions.yml +++ b/tests/tests_versions.yml @@ -19,7 +19,7 @@ - name: Save default postgresql_version set_fact: - __default_version: "{{ postgresql_version }}" + __default_version: "{{ __postgresql_version }}" - name: Install and cleanup the other supported versions include_tasks: tasks/install_and_check.yml diff --git a/vars/RedHat_10.yml b/vars/RedHat_10.yml index 0f8e7b5f9d..9e15bc7751 100644 --- a/vars/RedHat_10.yml +++ b/vars/RedHat_10.yml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MIT --- __postgresql_packages: >- - {{ ['postgresql' + postgresql_version + + {{ ['postgresql' + __postgresql_version + '-server'] }} diff --git a/vars/RedHat_8.yml b/vars/RedHat_8.yml index b8f9d35301..7587659140 100644 --- a/vars/RedHat_8.yml +++ b/vars/RedHat_8.yml @@ -2,4 +2,4 @@ --- # Put internal variables here with Red Hat Enterprise Linux 8 specific values. -__postgresql_packages: ["@postgresql:{{ postgresql_version }}/server"] +__postgresql_packages: ["@postgresql:{{ __postgresql_version }}/server"] diff --git a/vars/RedHat_9.yml b/vars/RedHat_9.yml index 982f3542ae..65f32da1a6 100644 --- a/vars/RedHat_9.yml +++ b/vars/RedHat_9.yml @@ -3,6 +3,6 @@ # Put internal variables here with Red Hat Enterprise Linux 9 specific values. __postgresql_packages: >- - {{ ['@postgresql:' + postgresql_version + - '/server'] if postgresql_version != '13' else + {{ ['@postgresql:' + __postgresql_version + + '/server'] if __postgresql_version != '13' else ['postgresql-server'] }}