Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .config/molecule/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
dependency:
name: galaxy
options:
requirements-file: ${MOLECULE_PROJECT_DIRECTORY}/../../requirements.yml

driver:
name: podman

platforms:
- name: quadlet
image: registry.access.redhat.com/ubi9/ubi
volumes:
- "${MOLECULE_PROJECT_DIRECTORY}/../../../:/vagrant:Z"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this mounting things at /vagrant? this should run in a container, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I was lazy when switched between drivers. I need the sources mounted into the container to do things like copying the custom certificates: https://github.com/theforeman/foremanctl/pull/548/changes#diff-8e0370b95ab38b1aa4d27ac77915e805f1832f8adba342ef74dda35f550660a1R6.
Since I was lazy to change the folder names, and if I use vagrant as the driver, I have synced the folder structure between the drivers.

tmpfs:
/tmp: exec
/run: rw,noexec,nosuid,nodev

provisioner:
name: ansible
env:
ANSIBLE_COLLECTIONS_PATH: ${MOLECULE_PROJECT_DIRECTORY}/../../../build/collections/foremanctl
ANSIBLE_COLLECTIONS_SCAN_SYS_PATH: "false"
inventory:
group_vars:
all:
certificates_ca_password: molecule-test-password
config_options:
defaults:
roles_path: ${MOLECULE_PROJECT_DIRECTORY}/..

verifier:
name: ansible

scenario:
test_sequence:
- cleanup
- destroy
- syntax
- create
- converge
- idempotence
- verify
- cleanup
- destroy
17 changes: 17 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ pytest tests/postgresql_test.py
pytest tests/foreman_test.py::test_foreman_service
```

Run Molecule role tests (requires `./setup-environment`):

```
# run all tests, including molecule
pytest --molecule
#run a specific molecule test
pytest --molecule tests/molecule_roles/test_roles.py::test_molecule_role[certificates[default]]
```
Running molecule tests directly is also possible with:

```
cd src/roles/<role>
molecule test --all
```

Shared Molecule settings (Podman driver, collections path, test sequence) are in `.config/molecule/config.yml`. Per-role converge and verify steps belong under `src/roles/<role>/molecule/common/`; each scenario only sets variables in `molecule.yml`.

Additonally, you can run [smoker](https://github.com/theforeman/smoker) based tests with:

```
Expand Down
1 change: 1 addition & 0 deletions development/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ jinja2
paramiko
passlib
ruff
molecule>=24.0.0
pytest-testinfra
pytest-durations
python-dateutil
Expand Down
13 changes: 13 additions & 0 deletions docs/developer/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ Integration tests run against Vagrant VMs defined in the [`Vagrantfile`](../../V

Testinfra fixtures in `tests/conftest.py` open Paramiko sessions through that SSH config, so `server`, `client`, and `database` are live hosts, not local stubs.

### Molecule role tests

Roles with scenarios under `src/roles/<role>/molecule/` can be tested from the repository root:

```bash
source .venv/bin/activate
pytest --molecule
```

This discovers every `molecule/<scenario>/molecule.yml` and runs `molecule test -s <scenario>` for that role. Without `--molecule`, those tests are skipped and the usual integration tests run instead.

Shared Molecule settings (Podman driver, collections path, test sequence) live in [`.config/molecule/config.yml`](../../.config/molecule/config.yml). Role-specific converge and verify steps are shared under `src/roles/<role>/molecule/common/`; each scenario only overrides variables in its `molecule.yml` and wires playbooks to the common task files.

### CI

GitHub Actions mirrors the same workflow: start VMs, deploy, run tests. The [`.github/workflows/test.yml`](../../.github/workflows/test.yml) matrix covers combinations of certificate source, database mode, security profile, and base box.
Expand Down
7 changes: 7 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[pytest]
testpaths = tests
# Ignore molecule scenario directories during normal test discovery
norecursedirs = molecule .molecule build .venv .git .ansible .vagrant
markers =
molecule: Molecule role tests (run with pytest --molecule)
iop: tests requiring IOP to be enabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: Set certificate hostnames from VM FQDN
ansible.builtin.set_fact:
certificates_hostnames:
- "{{ ansible_facts['fqdn'] }}"
- localhost
111 changes: 111 additions & 0 deletions src/roles/certificates/molecule/common/tasks/verify_custom_server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can tests with molecule be written in pytest or do they have to be written in Ansible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently molecule supports ansible and testinfra verifiers.
If that's not enough, we can use ansible to run any arbitrary script on the machine and test its result.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testinfra verifiers are pytest based right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I think so.

- name: Read internal CA certificate subject
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/ca.crt
-noout -subject
register: certificates_internal_ca_subject
changed_when: false

- name: Read custom server CA certificate subject
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/server-ca.crt
-noout -subject
register: certificates_server_ca_subject
changed_when: false

- name: Verify custom server CA differs from internal CA
ansible.builtin.assert:
that:
- certificates_internal_ca_subject.stdout != certificates_server_ca_subject.stdout
fail_msg: Custom server CA should have a different subject than the internal CA

- name: Read server certificate issuer
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}.crt
-noout -issuer
register: certificates_server_issuer
changed_when: false

- name: Verify server certificate is issued by custom server CA
ansible.builtin.assert:
that:
- certificates_server_issuer.stdout == certificates_server_ca_subject.stdout.replace('subject=', 'issuer=')
fail_msg: Server certificate should be issued by the custom server CA

- name: Read client certificate issuer
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}-client.crt
-noout -issuer
register: certificates_client_issuer
changed_when: false

- name: Verify client certificate is issued by internal CA
ansible.builtin.assert:
that:
- certificates_client_issuer.stdout == certificates_internal_ca_subject.stdout.replace('subject=', 'issuer=')
fail_msg: Client certificate should be issued by the internal CA

- name: Read localhost certificate issuer
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/localhost.crt
-noout -issuer
register: certificates_localhost_issuer
changed_when: false

- name: Verify localhost certificate is issued by internal CA
ansible.builtin.assert:
that:
- certificates_localhost_issuer.stdout == certificates_internal_ca_subject.stdout.replace('subject=', 'issuer=')
fail_msg: Localhost certificate should be issued by the internal CA

- name: Verify server certificate chains to CA bundle
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca-bundle.crt
{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}.crt
register: certificates_server_verify
changed_when: false
failed_when: "'OK' not in certificates_server_verify.stdout"

- name: Verify client certificate chains to internal CA
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca.crt
{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}-client.crt
register: certificates_client_verify
changed_when: false
failed_when: "'OK' not in certificates_client_verify.stdout"

- name: Verify localhost server certificate chains to internal CA
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca.crt
{{ certificates_ca_directory_certs }}/localhost.crt
register: certificates_localhost_verify
changed_when: false
failed_when: "'OK' not in certificates_localhost_verify.stdout"

- name: List CA bundle certificate subjects
ansible.builtin.shell: |
set -o pipefail
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' {{ certificates_ca_directory_certs }}/ca-bundle.crt \
| openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
| openssl pkcs7 -print_certs -noout -text \
| grep 'Subject:'
args:
executable: /bin/bash
register: certificates_bundle_subjects
changed_when: false

- name: Verify CA bundle contains internal and custom server CAs
ansible.builtin.assert:
that:
- certificates_internal_ca_subject.stdout.split('=', 1)[1] in certificates_bundle_subjects.stdout
- certificates_server_ca_subject.stdout.split('=', 1)[1] in certificates_bundle_subjects.stdout
- certificates_bundle_subjects.stdout | regex_findall('Subject:') | length == 2
fail_msg: CA bundle should contain exactly the internal CA and custom server CA
36 changes: 36 additions & 0 deletions src/roles/certificates/molecule/common/tasks/verify_default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
- name: Verify CA certificate subject
ansible.builtin.command:
cmd: >-
openssl x509 -in {{ certificates_ca_directory_certs }}/ca.crt
-noout -subject
register: certificates_ca_subject
changed_when: false
failed_when: "'Foreman Self-signed CA' not in certificates_ca_subject.stdout"

- name: Verify server certificate chains to CA
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca.crt
{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}.crt
register: certificates_server_verify
changed_when: false
failed_when: "'OK' not in certificates_server_verify.stdout"

- name: Verify client certificate chains to CA
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca.crt
{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}-client.crt
register: certificates_client_verify
changed_when: false
failed_when: "'OK' not in certificates_client_verify.stdout"

- name: Verify localhost server certificate chains to CA
ansible.builtin.command:
cmd: >-
openssl verify -CAfile {{ certificates_ca_directory_certs }}/ca.crt
{{ certificates_ca_directory_certs }}/localhost.crt
register: certificates_localhost_verify
changed_when: false
failed_when: "'OK' not in certificates_localhost_verify.stdout"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
- name: Check expected certificate files exist
ansible.builtin.stat:
path: "{{ item }}"
register: certificates_file_stats
failed_when: not certificates_file_stats.stat.exists
loop: "{{ certificates_expected_files }}"
17 changes: 17 additions & 0 deletions src/roles/certificates/molecule/common/vars/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
certificates_test_hostname: "{{ ansible_facts['fqdn'] }}"
certificates_ca_directory_certs: "{{ certificates_ca_directory }}/certs"
certificates_ca_directory_keys: "{{ certificates_ca_directory }}/private"
certificates_expected_files:
- "{{ certificates_ca_directory_certs }}/ca.crt"
- "{{ certificates_ca_directory_certs }}/server-ca.crt"
- "{{ certificates_ca_directory_certs }}/ca-bundle.crt"
- "{{ certificates_ca_directory_keys }}/ca.key"
- "{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}.crt"
- "{{ certificates_ca_directory_keys }}/{{ certificates_test_hostname }}.key"
- "{{ certificates_ca_directory_certs }}/{{ certificates_test_hostname }}-client.crt"
- "{{ certificates_ca_directory_keys }}/{{ certificates_test_hostname }}-client.key"
- "{{ certificates_ca_directory_certs }}/localhost.crt"
- "{{ certificates_ca_directory_keys }}/localhost.key"
- "{{ certificates_ca_directory_certs }}/localhost-client.crt"
- "{{ certificates_ca_directory_keys }}/localhost-client.key"
17 changes: 17 additions & 0 deletions src/roles/certificates/molecule/custom_server/converge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
- name: Converge
hosts: quadlet
become: true
vars:
molecule_custom_source_directory: /vagrant/tests/fixtures/foreman-certificate-check/certs/
pre_tasks:
- ansible.builtin.import_tasks: ../common/tasks/set_hostnames.yml

- name: Set custom server certificate paths
ansible.builtin.set_fact:
certificates_custom_server_certificate: "{{ molecule_custom_source_directory }}foreman.example.com.crt"
certificates_custom_server_key: "{{ molecule_custom_source_directory }}foreman.example.com.key"
certificates_custom_server_ca_certificate: "{{ molecule_custom_source_directory }}ca.crt"

roles:
- role: certificates
7 changes: 7 additions & 0 deletions src/roles/certificates/molecule/custom_server/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
provisioner:
inventory:
group_vars:
all:
certificates_source: custom_server
certificates_ca_directory: /var/lib/foremanctl/molecule-certs-custom
9 changes: 9 additions & 0 deletions src/roles/certificates/molecule/custom_server/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
- name: Verify
hosts: quadlet
gather_facts: true
vars_files:
- ../common/vars/verify.yml
tasks:
- ansible.builtin.import_tasks: ../common/tasks/verify_invariant.yml
- ansible.builtin.import_tasks: ../common/tasks/verify_custom_server.yml
8 changes: 8 additions & 0 deletions src/roles/certificates/molecule/default/converge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
- name: Converge
hosts: quadlet
become: true
pre_tasks:
- ansible.builtin.import_tasks: ../common/tasks/set_hostnames.yml
roles:
- role: certificates
6 changes: 6 additions & 0 deletions src/roles/certificates/molecule/default/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
provisioner:
inventory:
group_vars:
all:
certificates_ca_directory: /var/lib/foremanctl/molecule-certs
9 changes: 9 additions & 0 deletions src/roles/certificates/molecule/default/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
- name: Verify
hosts: quadlet
gather_facts: true
vars_files:
- ../common/vars/verify.yml
tasks:
- ansible.builtin.import_tasks: ../common/tasks/verify_invariant.yml
- ansible.builtin.import_tasks: ../common/tasks/verify_default.yml
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
def pytest_addoption(parser):
parser.addoption("--certificate-source", action="store", default="default", choices=('default', 'installer', 'custom_server'), help="Certificate source used during deployment")
parser.addoption("--database-mode", action="store", default="internal", choices=('internal', 'external'), help="Whether the database is internal or external")
parser.addoption(
"--molecule",
action="store_true",
default=False,
help="Run Molecule role tests under src/roles/ (requires ./setup-environment and ./forge vms start)",
)


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -238,6 +244,10 @@

def pytest_configure(config):
config.addinivalue_line("markers", "feature(name): mark a test as requiring a feature")
config.addinivalue_line(
"markers",
"molecule: Molecule role tests (selected with --molecule)",
)

config.user_parameters = UserParameters(config)

Expand All @@ -253,7 +263,20 @@
pytest.skip(f"test requires feature(s) {missing!r}")


def pytest_collection_modifyitems(config, items):
molecule_enabled = config.getoption("--molecule")

if not molecule_enabled:
skip_molecule = pytest.mark.skip(
reason="molecule role tests require --molecule",
)
for item in items:
if item.get_closest_marker("molecule"):
item.add_marker(skip_molecule)



class ResolveAdapter(HTTPAdapter):

Check failure on line 279 in tests/conftest.py

View workflow job for this annotation

GitHub Actions / Python Lint

ruff (E303)

tests/conftest.py:279:1: E303 Too many blank lines (3) help: Remove extraneous blank line(s)
def __init__(self, target_ip, *args, **kwargs):
self.target_ip = target_ip
super().__init__(*args, **kwargs)
Expand Down
Loading
Loading