diff --git a/tasks/common/custom-facts.yml b/tasks/common/custom-facts.yml index dd081bc..720260b 100644 --- a/tasks/common/custom-facts.yml +++ b/tasks/common/custom-facts.yml @@ -62,7 +62,10 @@ 'MemoryHigh': memory, 'Restart': restart_policy } - | combine((systemd.enable_accounting | default(true)) | ternary({'CPUAccounting': 'yes', 'MemoryAccounting': 'yes', 'IOAccounting': 'yes', 'TasksAccounting': 'yes', 'IPAccounting': 'yes'}, {}), recursive=True) + | combine( + (systemd.enable_accounting | default(true)) | + ternary({'CPUAccounting': 'yes', 'MemoryAccounting': 'yes', 'IOAccounting': 'yes', + 'TasksAccounting': 'yes', 'IPAccounting': 'yes'}, {}), recursive=True) }} _default_unit_install: WantedBy: multi-user.target diff --git a/tasks/common/storage-setup.yml b/tasks/common/storage-setup.yml index eb652d1..d30433c 100644 --- a/tasks/common/storage-setup.yml +++ b/tasks/common/storage-setup.yml @@ -5,28 +5,31 @@ ansible.builtin.file: path: "{{ host_data_dir }}" state: directory - owner: "{{ user | default(ansible_user) }}" - group: "{{ group | default(user) | default(ansible_user) }}" + owner: "{{ user | default(ansible_user_id) }}" + group: "{{ group | default(user) | default(ansible_user_id) }}" mode: "0755" + recurse: false - name: Ensure existence of config directories {{ _config_file_directories }} become: true ansible.builtin.file: path: "{{ host_data_dir + item.value.destinationPath | dirname }}" state: directory - owner: "{{ user | default(ansible_user) }}" - group: "{{ group | default(user) | default(ansible_user) }}" + owner: "{{ user | default(ansible_user_id) }}" + group: "{{ group | default(user) | default(ansible_user_id) }}" mode: "0755" + recurse: false with_dict: "{{ config }}" - name: Generate local rendering of service config files {{ _config_file_names }} + become: true ansible.builtin.copy: content: "{{ item.value.data | default(omit) }}" dest: "{{ host_data_dir + item.value.destinationPath }}" - mode: "{{ item.value.mode | default(644) }}" + mode: "{{ item.value.mode | default('0644') }}" src: "{{ item.value.sourcePath | default(omit) }}" - owner: "{{ user | default(ansible_user) }}" - group: "{{ group | default(user) | default(ansible_user) }}" + owner: "{{ user | default(ansible_user_id) }}" + group: "{{ group | default(user) | default(ansible_user_id) }}" with_dict: "{{ config }}" - name: Ensure existence of host data directories {{ _data_dirs_hostpaths }} @@ -35,6 +38,6 @@ path: "{{ item.value.hostPath }}" state: directory mode: "0755" - owner: "{{ user | default(ansible_user) }}" - group: "{{ group | default(user) | default(ansible_user) }}" + owner: "{{ user | default(ansible_user_id) }}" + group: "{{ group | default(user) | default(ansible_user_id) }}" with_dict: "{{ data_dirs }}" diff --git a/tasks/common/user-setup.yml b/tasks/common/user-setup.yml new file mode 100644 index 0000000..67e5242 --- /dev/null +++ b/tasks/common/user-setup.yml @@ -0,0 +1,20 @@ +--- +- name: Ensure service group [{{ group | default(user) }}] exists + when: user is defined and user != 'root' and user != ansible_user_id + become: true + ansible.builtin.group: + name: "{{ group | default(user) }}" + state: present + system: true + +- name: Ensure service user [{{ user }}] exists + when: user is defined and user != 'root' and user != ansible_user_id + become: true + ansible.builtin.user: + name: "{{ user }}" + group: "{{ group | default(user) }}" + state: present + system: true + create_home: false + shell: /usr/sbin/nologin + comment: "Service user for {{ name }}" diff --git a/tasks/container/setup.yml b/tasks/container/setup.yml index 1e5afbb..8fdc0a1 100644 --- a/tasks/container/setup.yml +++ b/tasks/container/setup.yml @@ -3,7 +3,7 @@ community.docker.docker_container: name: "{{ name }}" image: "{{ image }}" - user: "{{ user | default('root') }}" + user: root command: "{{ command | default(omit) }}" env: "{{ config_env }}" published_ports: "{{ _port_list }}" diff --git a/tasks/main.yml b/tasks/main.yml index 1ae8558..bea0227 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -27,6 +27,10 @@ - name: Setup service components when: uninstall is not defined or not uninstall | bool block: + - name: Ensure service user exists + ansible.builtin.include_tasks: common/user-setup.yml + when: user is defined + - name: Manage storage/data setup ansible.builtin.include_tasks: common/storage-setup.yml diff --git a/tests/molecule/container-basic/molecule.yml b/tests/molecule/container-basic/molecule.yml index dc3cd69..fc626d6 100644 --- a/tests/molecule/container-basic/molecule.yml +++ b/tests/molecule/container-basic/molecule.yml @@ -22,3 +22,14 @@ verifier: directory: ./tests scenario: name: container-basic + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - verify + - cleanup + - destroy diff --git a/tests/molecule/container-basic/tests/test_container_basic.py b/tests/molecule/container-basic/tests/test_container_basic.py index ebad2d2..1b8a3f3 100644 --- a/tests/molecule/container-basic/tests/test_container_basic.py +++ b/tests/molecule/container-basic/tests/test_container_basic.py @@ -2,6 +2,12 @@ def test_container_running(host): container = host.docker("nginx-container") assert container.is_running +def test_container_user_is_root(host): + """Verify that the container always runs as root.""" + container = host.docker("nginx-container") + user = container.inspect()["Config"]["User"] + assert user == "root", f"Container user is not 'root', it is '{user}'." + def test_container_exposes_ports(host): socket = host.socket("tcp://0.0.0.0:8080") assert socket.is_listening diff --git a/tests/molecule/container-full/molecule.yml b/tests/molecule/container-full/molecule.yml index 3b612e1..200b545 100644 --- a/tests/molecule/container-full/molecule.yml +++ b/tests/molecule/container-full/molecule.yml @@ -23,3 +23,14 @@ verifier: directory: ./tests scenario: name: container-full + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - verify + - cleanup + - destroy diff --git a/tests/molecule/container-full/tests/test_container_full.py b/tests/molecule/container-full/tests/test_container_full.py index fe68adf..7056317 100644 --- a/tests/molecule/container-full/tests/test_container_full.py +++ b/tests/molecule/container-full/tests/test_container_full.py @@ -9,7 +9,6 @@ def test_container_exposes_ports(host): def test_container_user_is_root(host): container = host.docker("full-container") user = container.inspect()["Config"]["User"] - assert user == "root", f"Container user is not 'root', it is '{user}'." def test_container_creates_hostdirectory(host): diff --git a/tests/molecule/systemd-basic/molecule.yml b/tests/molecule/systemd-basic/molecule.yml index 9334c5f..cf99bc0 100644 --- a/tests/molecule/systemd-basic/molecule.yml +++ b/tests/molecule/systemd-basic/molecule.yml @@ -27,3 +27,14 @@ verifier: directory: ./tests scenario: name: systemd-basic + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - verify + - cleanup + - destroy diff --git a/tests/molecule/systemd-full/converge.yml b/tests/molecule/systemd-full/converge.yml index 77ce46d..1519f4b 100644 --- a/tests/molecule/systemd-full/converge.yml +++ b/tests/molecule/systemd-full/converge.yml @@ -7,12 +7,12 @@ vars: setup_mode: systemd name: test-service - user: root + user: prometheus binary_url: https://github.com/prometheus/prometheus/releases/download/v2.47.0/prometheus-2.47.0.linux-amd64.tar.gz binary_strip_components: 1 binary_file_name_override: prometheus # set to trigger existence of extracted binary check destination_directory: /usr/bin - command: /usr/bin/prometheus --config.file=/test/mnt/etc/prometheus/prometheus.yml + command: /usr/bin/prometheus --config.file=/test/mnt/etc/prometheus/prometheus.yml --storage.tsdb.path=/var/tmp/prometheus cpus: 50 memory: 512M config: diff --git a/tests/molecule/systemd-full/molecule.yml b/tests/molecule/systemd-full/molecule.yml index 00563c3..ec4e5eb 100644 --- a/tests/molecule/systemd-full/molecule.yml +++ b/tests/molecule/systemd-full/molecule.yml @@ -27,3 +27,14 @@ verifier: directory: ./tests scenario: name: systemd-full + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - verify + - cleanup + - destroy diff --git a/tests/molecule/systemd-full/tests/test_systemd_full.py b/tests/molecule/systemd-full/tests/test_systemd_full.py index 5177c9a..1d97f5d 100644 --- a/tests/molecule/systemd-full/tests/test_systemd_full.py +++ b/tests/molecule/systemd-full/tests/test_systemd_full.py @@ -11,7 +11,7 @@ def test_systemd_service_properties(host): # Verify key systemd properties assert "ExecStart=/usr/bin/prometheus --config.file=/test/mnt/etc/prometheus/prometheus.yml" in service_content, "ExecStart is not correctly set." - assert "User=root" in service_content, "The service user is not correctly set to root." + assert "User=prometheus" in service_content, "The service user is not correctly set to prometheus." assert "CPUQuota=50%" in service_content, "CPUQuota is not correctly set." assert "MemoryHigh=512M" in service_content, "MemoryHigh is not correctly set." assert "Restart=on-failure" in service_content, "Restart policy is not correctly set." @@ -59,8 +59,8 @@ def test_data_directory_exists(host): data_dir = host.file("/var/tmp/prometheus") assert data_dir.exists, "The data directory does not exist." assert data_dir.is_directory, "The data directory is not a directory." - assert data_dir.user == "root", "The data directory is not owned by root." - assert data_dir.group == "root", "The data directory group is not root." + assert data_dir.user == "prometheus", "The data directory is not owned by prometheus." + assert data_dir.group == "prometheus", "The data directory group is not prometheus." def test_service_logs_no_errors(host): @@ -88,3 +88,26 @@ def test_systemd_ip_accounting(host): accounting_output = host.run("systemctl show test-service --property=IPIngressBytes,IPEgressBytes") assert accounting_output.rc == 0, "Failed to get IP accounting data from systemd." assert "IPIngressBytes=" in accounting_output.stdout and "IPEgressBytes=" in accounting_output.stdout, "IP accounting data is not available." + +def test_service_group_exists(host): + """Verify that the prometheus service group was created.""" + group = host.group("prometheus") + assert group.exists, "The prometheus group does not exist." + # System groups typically have GID < 1000 + assert group.gid < 1000, f"The prometheus group is not a system group (GID: {group.gid})." + +def test_service_user_exists(host): + """Verify that the prometheus service user was created.""" + user = host.user("prometheus") + assert user.exists, "The prometheus user does not exist." + assert user.shell == "/usr/sbin/nologin", "The prometheus user shell is not set to /usr/sbin/nologin." + assert user.group == "prometheus", "The prometheus user is not in the prometheus group." + # System users typically have UID < 1000 + assert user.uid < 1000, f"The prometheus user is not a system user (UID: {user.uid})." + +def test_config_file_ownership(host): + """Verify that config files are owned by the prometheus user.""" + config_file = host.file("/test/mnt/etc/prometheus/prometheus.yml") + assert config_file.exists, "The Prometheus configuration file does not exist." + assert config_file.user == "prometheus", "The config file is not owned by prometheus." + assert config_file.group == "prometheus", "The config file group is not prometheus."