From ee360bc070c004e5cec7bfa54348f0cd9c745d05 Mon Sep 17 00:00:00 2001 From: Vincent Date: Fri, 12 Apr 2024 14:26:05 -0600 Subject: [PATCH] Platform manager role and ec2 test platform support (#4) Refactor `molecule.docker_platform` role to handle specifically docker testing containers and not molecule inventory files Molecule inventory files are now managed by the new `molecule.platform` role. References to `docker_platform` in molecule `create.yml` or `destroy.yml` files should be updated to use this role in order to work with this version Add `molecule.ec2_platform` role, which allows creation and use of ephemeral ec2 instances for test environments. Update `molecule.init` role to support deployment of both `docker` and `ec2` platforms. Note that only one platform type is supported per scenario! Also note that there are differences between the Molecule configuration for each platform, so this `init` role should be used to deploy the appropriate templates! --- .github/workflows/latest.yml | 12 +- .github/workflows/publish.yml | 20 -- README.md | 4 +- galaxy.yml | 4 +- molecule/default/create.yml | 15 +- molecule/default/destroy.yml | 15 +- roles/docker_platform/README.md | 98 +++++----- roles/docker_platform/tasks/absent.yml | 25 +-- roles/docker_platform/tasks/create.yml | 124 ------------ roles/docker_platform/tasks/present.yml | 147 +++++++------- roles/ec2_platform/README.md | 133 +++++++++++++ roles/ec2_platform/defaults/main.yml | 81 ++++++++ roles/ec2_platform/handlers/main.yml | 2 + roles/ec2_platform/meta/main.yml | 54 +++++ .../molecule/role-ec2_platform/cleanup.yml | 13 ++ .../role-ec2_platform/collections.yml | 10 + .../molecule/role-ec2_platform/converge.yml | 52 +++++ .../molecule/role-ec2_platform/create.yml | 58 ++++++ .../molecule/role-ec2_platform/destroy.yml | 18 ++ .../molecule/role-ec2_platform/init.yml | 10 + .../molecule/role-ec2_platform/molecule.yml | 71 +++++++ .../molecule/role-ec2_platform/prepare.yml | 83 ++++++++ .../role-ec2_platform/requirements.yml | 4 + .../role-ec2_platform/side_effect.yml | 10 + .../molecule/role-ec2_platform/verify.yml | 21 ++ roles/ec2_platform/tasks/absent.yml | 76 ++++++++ roles/ec2_platform/tasks/main.yml | 26 +++ roles/ec2_platform/tasks/present.yml | 169 ++++++++++++++++ roles/ec2_platform/vars/main.yml | 2 + roles/init/README.md | 20 +- roles/init/defaults/main.yml | 27 ++- roles/init/files/init.yml | 5 + roles/init/tasks/asserts.yml | 3 + roles/init/tasks/main.yml | 69 +++++++ roles/init/templates/collections.yml.j2 | 8 +- roles/init/templates/create.yml.j2 | 18 +- roles/init/templates/destroy.yml.j2 | 13 +- roles/init/templates/molecule.yml.j2 | 9 + roles/init/templates/prepare.yml.j2 | 1 + roles/platform/README.md | 87 +++++++++ roles/platform/defaults/main.yml | 15 ++ roles/platform/handlers/main.yml | 2 + roles/platform/meta/main.yml | 54 +++++ roles/platform/tasks/deprovision.yml | 25 +++ roles/platform/tasks/inventory.yml | 184 ++++++++++++++++++ roles/platform/tasks/main.yml | 14 ++ roles/platform/tasks/provision.yml | 37 ++++ roles/platform/tests/inventory | 2 + roles/platform/tests/test.yml | 5 + roles/platform/vars/main.yml | 12 ++ 50 files changed, 1641 insertions(+), 326 deletions(-) delete mode 100644 .github/workflows/publish.yml delete mode 100644 roles/docker_platform/tasks/create.yml create mode 100644 roles/ec2_platform/README.md create mode 100644 roles/ec2_platform/defaults/main.yml create mode 100644 roles/ec2_platform/handlers/main.yml create mode 100644 roles/ec2_platform/meta/main.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/cleanup.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/collections.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/converge.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/create.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/destroy.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/init.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/molecule.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/prepare.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/requirements.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/side_effect.yml create mode 100644 roles/ec2_platform/molecule/role-ec2_platform/verify.yml create mode 100644 roles/ec2_platform/tasks/absent.yml create mode 100644 roles/ec2_platform/tasks/main.yml create mode 100644 roles/ec2_platform/tasks/present.yml create mode 100644 roles/ec2_platform/vars/main.yml create mode 100644 roles/platform/README.md create mode 100644 roles/platform/defaults/main.yml create mode 100644 roles/platform/handlers/main.yml create mode 100644 roles/platform/meta/main.yml create mode 100644 roles/platform/tasks/deprovision.yml create mode 100644 roles/platform/tasks/inventory.yml create mode 100644 roles/platform/tasks/main.yml create mode 100644 roles/platform/tasks/provision.yml create mode 100644 roles/platform/tests/inventory create mode 100644 roles/platform/tests/test.yml create mode 100644 roles/platform/vars/main.yml diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index c5604e9..82397a0 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -1,14 +1,17 @@ --- name: Update `latest` tag -on: - release: - types: [published] +on: + push: + branches: + - main jobs: run: runs-on: ubuntu-latest - + permissions: + contents: write + packages: write steps: - name: Checkout repository uses: actions/checkout@v4 @@ -21,3 +24,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index cd396db..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- - -name: Deploy Collection - -on: {} -# release: -# types: -# - created - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build and Deploy Collection - uses: artis3n/ansible_galaxy_collection@v2 - with: - api_key: '${{ secrets.GALAXY_API_KEY }}' - diff --git a/README.md b/README.md index fa45d2c..ea0b649 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ More tips on using Molecule can be found [below](#using-molecule). The following roles are provided: * [init](roles/init) - Initialize the Molecule testing framework for a project -* [docker_platform](roles/docker_platform) - Create a docker-based test platform for Molecule +* [platform](roles/platform) - Deploy a Molecule platform for testing +* [docker_platform](roles/docker_platform) - Used by the `platform` role to create a Docker-based test platform +* [ec2_platform](roles/ec2_platform) - Used by the `platform` role to create an EC2-based test platform * [prepare_controller](roles/prepare_controller) - Prepare a molecule controller to run local code tests The recommended way to use this collection is to provision Molecule scenarios using the [init role](roles/init). The `init` role provides template configurations that will work in various project types. diff --git a/galaxy.yml b/galaxy.yml index 4de9585..f17736d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,7 +8,7 @@ namespace: influxdata name: molecule # The version of the collection. Must be compatible with semantic versioning -version: 1.3.1 +version: 1.4.0 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md @@ -56,7 +56,7 @@ repository: https://github.com/influxdata/ansible-collection-molecule #homepage: http://example.com # The URL to the collection issue tracker -#issues: https://github.com/influxdata/ansible-collection-molecule/issues +issues: https://github.com/influxdata/ansible-collection-molecule/issues # A list of file glob-like patterns used to filter any files or directories that should not be included in the build # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This diff --git a/molecule/default/create.yml b/molecule/default/create.yml index e84d78a..11fbf7a 100644 --- a/molecule/default/create.yml +++ b/molecule/default/create.yml @@ -5,17 +5,12 @@ tasks: - name: Create platform ansible.builtin.include_role: - name: influxdata.molecule.docker_platform + name: influxdata.molecule.platform vars: - docker_platform_name: "{{ item.name }}" - docker_platform_image: "{{ item.image }}" - docker_platform_systemd: "{{ item.systemd | default(false) }}" - docker_platform_modify_image: "{{ item.modify_image | default(false) }}" - docker_platform_modify_image_buildpath: "{{ item.modify_image_buildpath | default(molecule_ephemeral_directory + '/build') }}" - docker_platform_privileged: "{{ item.privileged | default (false) }}" - docker_platform_hostvars: "{{ item.hostvars | default({}) }}" - docker_platform_state: present - when: item.type == 'docker' + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" loop: "{{ molecule_yml.platforms }}" loop_control: label: item.name diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml index 1278d1c..d818ea5 100644 --- a/molecule/default/destroy.yml +++ b/molecule/default/destroy.yml @@ -1,13 +1,18 @@ --- - name: Perform cleanup - hosts: molecule + hosts: localhost gather_facts: false tasks: - - name: Remove platform + - name: Remove platform(s) ansible.builtin.include_role: - name: influxdata.molecule.docker_platform + name: influxdata.molecule.platform vars: - docker_platform_name: "{{ inventory_hostname }}" - docker_platform_state: absent + platform_name: "{{ item.name }}" + platform_state: absent + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name diff --git a/roles/docker_platform/README.md b/roles/docker_platform/README.md index 9468ec6..8375b19 100644 --- a/roles/docker_platform/README.md +++ b/roles/docker_platform/README.md @@ -3,59 +3,34 @@ molecule.docker_platform Create a docker-based test platform for Molecule. -Requirements ------------- - -1. Molecule should be installed and executable from a location in the users PATH -1. Ansible should be installed, with `ansible-playbook` executable via the users PATH -1. Docker should be installed -1. The current user should be a member of the `docker` group - -Role Variables --------------- - -```yaml -# Name of this Molecule platform -docker_platform_name: instance - -# Whether this platform should be deployed on the current system (present/absent) -docker_platform_state: present +This role is intended to be used via the `molecule.platform` role that is included with this collection, and should not be referenced directly in a playbook. -# Docker image that this platform runs -docker_platform_image: "geerlingguy/docker-rockylinux9-ansible:latest" +Configuration is done via the `platforms` section of the `molecule.yml` file in your Molecule scenario directory. -# Should the provided image be modified at runtime -docker_platform_modify_image: false +Required configuration options are: -# Path to docker build files that should be used to modify the image. Files are treated as templates -# and can contain jinja2 templating language ("{{ my_var }}" etc.) -docker_platform_modify_image_buildpath: "{{ molecule_ephemeral_directory }}/build" +- `name`: Name of the platform (string) +- `type`: `docker` +- `image`: Docker image to use for the platform (string) -# Command to be executed at runtime on the container -# Leave as "" to use container default -docker_platform_command: "" +Optional configuration options are: -# Is this a SystemD enabled container? -docker_platform_systemd: true +- `systemd`: Whether the container should be started with SystemD enabled (boolean) +- `modify_image`: Whether the provided image should be modified at runtime (boolean) +- `modify_image_buildpath`: Path to Docker build files that should be used to modify the image (string) -# A list of Docker volumes that should be attached to the container -docker_platform_volumes: [] +Requirements +------------ -# Run the container in Privileged mode (greater host access, less security!) -docker_platform_privileged: false +1. Docker should be installed +1. The current user should be a member of the `docker` group -# A list of tmpfs filesystem paths to be passed to the container -docker_platform_tmpfs: [] -``` +Role Variables +-------------- -Configuration that should not require modification: -```yaml -# Filesystem location of the Molecule ephemeral directory. Should not need to be updated by the user of this role! -docker_platform_molecule_ephemeral_directory: "{{ molecule_ephemeral_directory }}" -``` +This role should not be used directly in a playbook, and should instead be used via the `molecule.platform` role. -Molecule variables expected: -- `molecule_ephemeral_directory` +Detailed information on configuration variables for this role can be found in [defaults/main.yml](defaults/main.yml). Dependencies ------------ @@ -66,36 +41,53 @@ Dependencies Example Playbook ---------------- +This role is intended to be used via the `molecule.platform` role that is included with this collection, and should not be referenced directly in a playbook. + +Configuration is done via the `platforms` section of the `molecule.yml` file in your Molecule scenario directory. + +```yaml +platforms: + - name: docker-rockylinux9 + type: docker + image: geerlingguy/docker-rockylinux9-ansible:latest + systemd: True + modify_image: False + privileged: False + hostvars: {} +``` + +To utilize this role, use the `platform` role that is included with this collection in your `create.yml` playbook! + ```yaml - name: Create hosts: localhost gather_facts: false tasks: - - name: Create platform + - name: Create platform(s) ansible.builtin.include_role: - name: influxdata.molecule.docker_platform + name: influxdata.molecule.platform vars: - docker_platform_name: "{{ item.name }}" - docker_platform_image: "{{ item.image }}" - docker_platform_systemd: true + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" loop: "{{ molecule_yml.platforms }}" loop_control: label: item.name -# we want to avoid errors like "Failed to create temporary directory" -- name: Validate molecule inventory +# We want to avoid errors like "Failed to create temporary directory" +- name: Validate that inventory was refreshed hosts: molecule gather_facts: false tasks: - - name: Check kernel version + - name: Check uname ansible.builtin.raw: uname -a register: result changed_when: false - - name: Display kernel info + - name: Display uname info ansible.builtin.debug: msg: "{{ result.stdout }}" - ``` diff --git a/roles/docker_platform/tasks/absent.yml b/roles/docker_platform/tasks/absent.yml index d19690e..d54f927 100644 --- a/roles/docker_platform/tasks/absent.yml +++ b/roles/docker_platform/tasks/absent.yml @@ -11,17 +11,18 @@ state: absent auto_remove: true -- name: Remove dynamic molecule inventory - delegate_to: localhost - block: - - name: Remove dynamic inventory file - ansible.builtin.file: - path: "{{ docker_platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" - state: absent - - - name: Remove instance config file - ansible.builtin.file: - path: "{{ docker_platform_molecule_ephemeral_directory }}/instance_config.yml" - state: absent +# TODO: Remove just this host, not the whole inventory +#- name: Remove dynamic molecule inventory +# delegate_to: localhost +# block: +# - name: Remove dynamic inventory file +# ansible.builtin.file: +# path: "{{ docker_platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" +# state: absent +# +# - name: Remove instance config file +# ansible.builtin.file: +# path: "{{ docker_platform_molecule_ephemeral_directory }}/instance_config.yml" +# state: absent diff --git a/roles/docker_platform/tasks/create.yml b/roles/docker_platform/tasks/create.yml deleted file mode 100644 index 27ec4f1..0000000 --- a/roles/docker_platform/tasks/create.yml +++ /dev/null @@ -1,124 +0,0 @@ ---- -# Create a docker container for use by molecule -# -# Expected to be called in a loop with `platform` defined as the loop var -# (loop off of `platforms` list in molecule.yml) - - -- name: Docker image needs to be customized - block: - - name: Check build path - ansible.builtin.stat: - path: "{{ docker_platform_modify_image_buildpath }}" - register: __docker_platform_buildpath_stat - - - name: Build directory doesn't exist - block: - - name: Create build directory - ansible.builtin.file: - path: "{{ docker_platform_modify_image_buildpath }}" - state: directory - mode: 0755 - - - name: Copy templates - ansible.builtin.template: - src: templates/{{ __docker_platform_item }} - dest: "{{ docker_platform_modify_image_buildpath}}/{{ __docker_platform_item | regex_replace('\\.j2$', '') }}" - loop: - - bash.service.j2 - - entrypoint.sh.j2 - - Dockerfile.j2 - loop_control: - loop_var: __docker_platform_item - when: __docker_platform_buildpath_stat.stat.exists is false - - - name: Build local image name - ansible.builtin.set_fact: - __docker_platform_built_image_name: "molecule-local-build/{{ docker_platform_image | split(':') | first | split('/') | last }}-custom" - - - name: Docker image is built - community.docker.docker_image: - name: "{{ __docker_platform_built_image_name }}" - build: - path: "{{ docker_platform_modify_image_buildpath }}" - cache_from: "{{ docker_platform_image }}" - source: build - force_source: true # Always build a new image when this is run - tag: latest - register: image_build_output - - - name: Show image build details - ansible.builtin.debug: - var: image_build_output - verbosity: 1 - when: docker_platform_modify_image - -- name: Build docker volume list - ansible.builtin.set_fact: - __docker_platform_volume_list: "{{ docker_platform_volumes + ['/sys/fs/cgroup:/sys/fs/cgroup:rw'] - if docker_platform_systemd - else docker_platform_volumes }}" - -- name: "{{ docker_platform_name }} docker container is present and running" - community.docker.docker_container: - name: "{{ docker_platform_name }}" - image: "{{ __docker_platform_built_image_name | default(docker_platform_image) }}" - state: started - command: "{{ docker_platform_command }}" - log_driver: json-file - hostname: molecule-ci-{{ docker_platform_name }} - init: false - cgroupns_mode: "{{ 'host' if docker_platform_systemd is true else 'private' }}" - privileged: "{{ docker_platform_privileged }}" - tmpfs: "{{ docker_platform_tmpfs + ['/run', '/run/lock'] if docker_platform_systemd else docker_platform_tmpfs }}" - volumes: "{{ __docker_platform_volume_list }}" - register: __docker_platform_create_result - -- name: Print creation output - ansible.builtin.debug: - msg: "{{ __docker_platform_create_result }}" - verbosity: 1 - -- name: Fail if is not running - block: - - name: Retrieve {{ docker_platform_name }} container log - ansible.builtin.command: - cmd: docker logs {{ __docker_platform_create_result.container.Name }} - changed_when: false - register: __docker_platform_logfile_cmd - - - name: Container {{ docker_platform_name }} failed to start - ansible.builtin.fail: - msg: "{{ __docker_platform_logfile_cmd.stdout ~ __docker_platform_logfile_cmd.stderr }}" - when: > - __docker_platform_create_result.container.State.ExitCode != 0 or - not __docker_platform_create_result.container.State.Running - -- name: "{{ docker_platform_name }} Systemd status is healthy" - block: - - name: System service manager is Systemd - ansible.builtin.assert: - that: - - "ansible_service_mgr == 'systemd'" - fail_msg: Systemd is enabled, but container service manager isn't Systemd! Is this a Systemd-enabled container? - - - name: Systemd has completed initialization - ansible.builtin.command: - cmd: systemctl is-system-running - register: __docker_platform_systemctl_status - until: > - 'running' in __docker_platform_systemctl_status.stdout or - 'degraded' in __docker_platform_systemctl_status.stdout - retries: 30 - delay: 5 - changed_when: false - failed_when: __docker_platform_systemctl_status.rc > 1 - - - name: "{{ docker_platform_name }} Systemd is healthy" - ansible.builtin.assert: - that: - - "'running' in __docker_platform_systemctl_status.stdout" - success_msg: Systemd is healthy - fail_msg: Systemd is unhealthy - when: docker_platform_systemd - diff --git a/roles/docker_platform/tasks/present.yml b/roles/docker_platform/tasks/present.yml index dd1a81c..c3d6d16 100644 --- a/roles/docker_platform/tasks/present.yml +++ b/roles/docker_platform/tasks/present.yml @@ -1,84 +1,101 @@ --- +# Create a docker container for use by molecule +# +# Expected to be called in a loop with `platform` defined as the loop var +# (loop off of `platforms` list in molecule.yml) + - name: Initialize state ansible.builtin.set_fact: # Number of times this role has been included during this playbook run __docker_platform_run_count: "{{ __docker_platform_run_count | default(0) | int + 1 }}" -- name: Load system facts - ansible.builtin.setup: - filter: - - ansible_service_mgr +- name: Docker image needs to be customized + block: + - name: Check build path + ansible.builtin.stat: + path: "{{ docker_platform_modify_image_buildpath }}" + register: __docker_platform_buildpath_stat -- name: Create {{ docker_platform_name }} docker container - ansible.builtin.include_tasks: "{{ role_path }}/tasks/create.yml" + - name: Build directory doesn't exist + block: + - name: Create build directory + ansible.builtin.file: + path: "{{ docker_platform_modify_image_buildpath }}" + state: directory + mode: 0755 -- name: Load existing instance configuration - block: - - name: Load existing instance configuration file - ansible.builtin.slurp: - src: "{{ docker_platform_molecule_ephemeral_directory }}/instance_config.yml" - register: __docker_platform_current_instance_config_b64 - ignore_errors: true + - name: Copy templates + ansible.builtin.template: + src: templates/{{ __docker_platform_item }} + dest: "{{ docker_platform_modify_image_buildpath}}/{{ __docker_platform_item | regex_replace('\\.j2$', '') }}" + loop: + - bash.service.j2 + - entrypoint.sh.j2 + - Dockerfile.j2 + loop_control: + loop_var: __docker_platform_item + when: __docker_platform_buildpath_stat.stat.exists is false - - name: Decode instance configuration data + - name: Build local image name ansible.builtin.set_fact: - __docker_platform_current_instance_config: "{{ __docker_platform_current_instance_config_b64.content | default('') | b64decode | from_yaml }}" - when: __docker_platform_run_count | int > 1 - -- name: Write {{ docker_platform_name }} instance config file - ansible.builtin.copy: - # This is very basic - just needs an item there to show as managed with docker config - content: | - {% if __docker_platform_current_instance_config is defined %} - {{ __docker_platform_current_instance_config | to_yaml }} - {% endif %} - - instance: {{ docker_platform_name }} - connection: docker - dest: "{{ docker_platform_molecule_ephemeral_directory }}/instance_config.yml" - mode: "0600" + __docker_platform_built_image_name: "molecule-local-build/{{ docker_platform_image | split(':') | first | split('/') | last }}-custom" -- name: Load existing molecule inventory - block: - - name: Load existing molecule inventory file - ansible.builtin.slurp: - src: "{{ docker_platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" - register: __docker_platform_current_molecule_inventory_b64 - ignore_errors: true + - name: Docker image is built + community.docker.docker_image: + name: "{{ __docker_platform_built_image_name }}" + build: + path: "{{ docker_platform_modify_image_buildpath }}" + cache_from: "{{ docker_platform_image }}" + source: build + force_source: true # Always build a new image when this is run + tag: latest + register: image_build_output - - name: Decode instance configuration data - ansible.builtin.set_fact: - __docker_platform_current_molecule_inventory: "{{ __docker_platform_current_molecule_inventory_b64.content | default({}) | b64decode | from_yaml }}" - when: __docker_platform_run_count | int > 1 + - name: Show image build details + ansible.builtin.debug: + var: image_build_output + verbosity: 1 + when: docker_platform_modify_image -- name: Add {{ docker_platform_name }} to molecule_inventory - vars: - __docker_platform_inventory_partial_hostvars: "{{ { - 'ansible_connection': 'community.docker.docker' - } | combine(docker_platform_hostvars, recursive=true) }}" - __docker_platform_inventory_partial_yaml: | - all: - children: - molecule: - hosts: - "{{ docker_platform_name }}": {{ __docker_platform_inventory_partial_hostvars }} +- name: Build docker volume list ansible.builtin.set_fact: - __docker_platform_molecule_inventory: > - {{ __docker_platform_current_molecule_inventory | from_yaml | default({}) | combine(__docker_platform_inventory_partial_yaml | from_yaml, recursive=true) }} + __docker_platform_volume_list: "{{ docker_platform_volumes + ['/sys/fs/cgroup:/sys/fs/cgroup:rw'] + if docker_platform_systemd + else docker_platform_volumes }}" -- name: Write {{ docker_platform_name }} to molecule inventory file - ansible.builtin.copy: - content: | - {{ __docker_platform_molecule_inventory | to_yaml }} - dest: "{{ docker_platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" - mode: "0600" +- name: "{{ docker_platform_name }} docker container is present and running" + community.docker.docker_container: + name: "{{ docker_platform_name }}" + image: "{{ __docker_platform_built_image_name | default(docker_platform_image) }}" + state: started + command: "{{ docker_platform_command }}" + log_driver: json-file + hostname: molecule-ci-{{ docker_platform_name }} + init: false + cgroupns_mode: "{{ 'host' if docker_platform_systemd is true else 'private' }}" + privileged: "{{ docker_platform_privileged }}" + tmpfs: "{{ docker_platform_tmpfs + ['/run', '/run/lock'] if docker_platform_systemd else docker_platform_tmpfs }}" + volumes: "{{ __docker_platform_volume_list }}" + register: __docker_platform_create_result -- name: Force inventory refresh - ansible.builtin.meta: refresh_inventory +- name: Print creation output + ansible.builtin.debug: + msg: "{{ __docker_platform_create_result }}" + verbosity: 1 + +- name: Fail if is not running + block: + - name: Retrieve {{ docker_platform_name }} container log + ansible.builtin.command: + cmd: docker logs {{ __docker_platform_create_result.container.Name }} + changed_when: false + register: __docker_platform_logfile_cmd -- name: Fail if molecule group is missing - ansible.builtin.assert: - that: "'molecule' in groups" - fail_msg: | - molecule group was not found inside inventory groups: {{ groups }} + - name: Container {{ docker_platform_name }} failed to start + ansible.builtin.fail: + msg: "{{ __docker_platform_logfile_cmd.stdout ~ __docker_platform_logfile_cmd.stderr }}" + when: > + __docker_platform_create_result.container.State.ExitCode != 0 or + not __docker_platform_create_result.container.State.Running diff --git a/roles/ec2_platform/README.md b/roles/ec2_platform/README.md new file mode 100644 index 0000000..706abf9 --- /dev/null +++ b/roles/ec2_platform/README.md @@ -0,0 +1,133 @@ +molecule.ec2_platform +========= + +Create an Amazon EC2-based test platform for Molecule. + +This role is intended to be used via the `molecule.platform` role that is included with this collection, and should not be referenced directly in a playbook. + +Configuration is done via the `platforms` section of the `molecule.yml` file in your Molecule scenario directory. + +Required configuration options are: + +- `name`: The name of the platform (string) +- `type`: `ec2` +- `image`: The AMI ID to use for the instance (string) +- `region`: The AWS region to deploy the instance in (string) +- `vpc_id`: The VPC ID to deploy the instance in (string) +- `vpc_subnet_id`: The VPC subnet ID to deploy the instance in (string) + +Optional configuration options are: + +- `assign_public_ip`: Whether or not to assign a public IP to the instance (boolean) +- `aws_profile`: The AWS profile to use for authentication (string) +- `boot_wait_seconds`: The number of seconds to wait for the instance to boot (integer) +- `instance_type`: The instance type to use for the instance (string) +- `key_inject_method`: The method to use for injecting the SSH key into the instance ("cloud-init"/"ec2") +- `key_name`: The name of the SSH key pair to use for the instance (string) +- `private_key_path`: The path to the private key file for the SSH key pair (string) +- `public_key_path`: The path to the public key file for the SSH key pair (string) +- `security_group_name`: The name of the security group to use for the instance (string) +- `security_group_description`: The description of the security group to use for the instance (string) +- `security_group_rules`: A list of security group rules to apply to the instance (list of dicts) +- `security_group_rules_egress`: A list of security group egress rules to apply to the instance (list of dicts) +- `ssh_user`: The SSH user to use for connecting to the instance (string) +- `ssh_port`: The SSH port to use for connecting to the instance (integer) +- `cloud_config`: The cloud-config data to use for the instance (dictionary) +- `image_name`: The name of the image to use for the instance (string) +- `image_owner`: The owner of the image to use for the instance (string) +- `security_groups`: A list of security group names to apply to the instance (list of strings) +- `tags`: A dictionary of tags to apply to the instance (dictionary) +- `volumes`: A list of volumes to attach to the instance (list of dicts) + +Requirements +------------ + +**Python Modules** +- `boto3` + +Role Variables +-------------- + +In order to connect to AWS, you will need the following environment variables to be set: + +```bash + + export AWS_ACCESS_KEY_ID="blahblahblahblah" + export AWS_SECRET_ACCESS_KEY="hurpderpherpderpdpeypedpderpyderp" + export AWS_SESSION_TOKEN="hurpderpherpderpdpeypedpderpyderpahahwhizbanglotsofstuffblablablabla" +``` + +The `AWS_SESSION_TOKEN` variable is only required if you are using temporary credentials. + +Full role configuration options are available in the [defaults/main.yml](defaults/main.yml) file. + +Dependencies +------------ + +**Collections** +- `amazon.aws` + +Example Playbook +---------------- + +This role is intended to be used via the `molecule.platform` role that is included with this collection, and should not be referenced directly in a playbook. + +Configuration is done via the `platforms` section of the `molecule.yml` file in your Molecule scenario directory. + +```yaml +platforms: + - name: ec2-rockylinux9 + type: ec2 + image: "ami-067daee80a6d36ac0" + instance_type: "t3.micro" + region: "us-east-2" + vpc_id: "vpc-12345678" + vpc_subnet_id: "subnet-12345678" +``` + +To utilize this role, use the `platform` role that is included with this collection in your `create.yml` playbook! + +```yaml +- name: Create + hosts: localhost + gather_facts: false + tasks: + - name: Create platform(s) + ansible.builtin.include_role: + name: syndr.molecule.platform + vars: + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name + +# We want to avoid errors like "Failed to create temporary directory" +- name: Validate that inventory was refreshed + hosts: molecule + gather_facts: false + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Display uname info + ansible.builtin.debug: + msg: "{{ result.stdout }}" + +``` + + +License +------- + +MIT + +Author Information +------------------ + +- [@syndr](https://github.com/syndr/) + diff --git a/roles/ec2_platform/defaults/main.yml b/roles/ec2_platform/defaults/main.yml new file mode 100644 index 0000000..6d6f39d --- /dev/null +++ b/roles/ec2_platform/defaults/main.yml @@ -0,0 +1,81 @@ +--- +# defaults file for ec2_platform + +# Name of this Molecule platform +ec2_platform_name: instance + +# Run config handling +ec2_platform_default_run_id: "{{ lookup('password', '/dev/null chars=ascii_lowercase length=5') }}" +ec2_platform_default_run_config: + run_id: "{{ ec2_platform_default_run_id }}" + +ec2_platform_run_config_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ec2-platform-run-config.yml" +ec2_platform_run_config_from_file: "{{ (lookup('file', ec2_platform_run_config_path, errors='ignore') or '{}') | from_yaml }}" +ec2_platform_run_config: '{{ ec2_platform_default_run_config | combine(ec2_platform_run_config_from_file) }}' + +# Platform settings handling +ec2_platform_default_assign_public_ip: true +ec2_platform_default_aws_profile: "{{ lookup('env', 'AWS_PROFILE') }}" +ec2_platform_default_boot_wait_seconds: 120 +ec2_platform_default_instance_type: t3a.medium +ec2_platform_default_key_inject_method: cloud-init # valid values: [cloud-init, ec2] +ec2_platform_default_key_name: "molecule-{{ ec2_platform_run_config.run_id }}" +ec2_platform_default_private_key_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/id_rsa" +ec2_platform_default_public_key_path: "{{ ec2_platform_default_private_key_path }}.pub" +ec2_platform_default_ssh_user: ansible +ec2_platform_default_ssh_port: 22 +ec2_platform_default_user_data: '' + +ec2_platform_default_security_group_name: "molecule-{{ ec2_platform_run_config.run_id }}" +ec2_platform_default_security_group_description: Ephemeral security group for Molecule instances +ec2_platform_default_security_group_rules: + - proto: tcp + from_port: "{{ ec2_platform_default_ssh_port }}" + to_port: "{{ ec2_platform_default_ssh_port }}" + cidr_ip: "0.0.0.0/0" + - proto: icmp + from_port: 8 + to_port: -1 + cidr_ip: "0.0.0.0/0" +ec2_platform_default_security_group_rules_egress: + - proto: -1 + from_port: 0 + to_port: 0 + cidr_ip: "0.0.0.0/0" + +ec2_platform_defaults: + assign_public_ip: "{{ ec2_platform_default_assign_public_ip }}" + aws_profile: "{{ ec2_platform_default_aws_profile }}" + boot_wait_seconds: "{{ ec2_platform_default_boot_wait_seconds }}" + instance_type: "{{ ec2_platform_default_instance_type }}" + key_inject_method: "{{ ec2_platform_default_key_inject_method }}" + key_name: "{{ ec2_platform_default_key_name }}" + private_key_path: "{{ ec2_platform_default_private_key_path }}" + public_key_path: "{{ ec2_platform_default_public_key_path }}" + security_group_name: "{{ ec2_platform_default_security_group_name }}" + security_group_description: "{{ ec2_platform_default_security_group_description }}" + security_group_rules: "{{ ec2_platform_default_security_group_rules }}" + security_group_rules_egress: "{{ ec2_platform_default_security_group_rules_egress }}" + ssh_user: "{{ ec2_platform_default_ssh_user }}" + ssh_port: "{{ ec2_platform_default_ssh_port }}" + cloud_config: {} + image: "" + image_name: "" + image_owner: [self] + name: "" + region: "" + security_groups: [] + tags: {} + volumes: [] + vpc_id: "" + vpc_subnet_id: "" + +# Merging defaults into a list of dicts is, it turns out, not straightforward +#ec2_platforms: >- +# {{ [ec2_platform_defaults | dict2items] +# | product(molecule_yml.platforms | map('dict2items') | list) +# | map('flatten', levels=1) +# | list +# | map('items2dict') +# | list }} + diff --git a/roles/ec2_platform/handlers/main.yml b/roles/ec2_platform/handlers/main.yml new file mode 100644 index 0000000..67429cd --- /dev/null +++ b/roles/ec2_platform/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for ec2_platform diff --git a/roles/ec2_platform/meta/main.yml b/roles/ec2_platform/meta/main.yml new file mode 100644 index 0000000..de14aaf --- /dev/null +++ b/roles/ec2_platform/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + role_name: ec2_platform + namespace: syndr + author: syndr + description: Provision and deprovision an EC2-based Molecule platform using Ansible. + company: UltronCORE + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: MIT + + min_ansible_version: 2.16 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/ec2_platform/molecule/role-ec2_platform/cleanup.yml b/roles/ec2_platform/molecule/role-ec2_platform/cleanup.yml new file mode 100644 index 0000000..26e7fa5 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/cleanup.yml @@ -0,0 +1,13 @@ +--- +# The cleanup.yml playbook should be used to remove any test infrastructure that was created by this test process +# and is not present within the instance itself (IE: the docker container created by Molecule). For example, it +# could be used to remove AWS infrastructure created as part of this test and that should not persist. + +- name: Remove external test infrastructure + hosts: molecule + tasks: + - name: Cleanup tasks not configured + delegate_to: localhost + ansible.builtin.debug: + msg: Add your cleanup tasks here as required! + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/collections.yml b/roles/ec2_platform/molecule/role-ec2_platform/collections.yml new file mode 100644 index 0000000..4fdc1bc --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/collections.yml @@ -0,0 +1,10 @@ +--- + +collections: + - name: community.docker +# - name: git+https://github.com/syndr/ansible-collection-molecule.git +# type: git +# version: latest + - name: syndr.molecule + version: 1.4.0-dev + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/converge.yml b/roles/ec2_platform/molecule/role-ec2_platform/converge.yml new file mode 100644 index 0000000..f904f40 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/converge.yml @@ -0,0 +1,52 @@ +--- +# Verify that the target code runs successfullly. +# Note that this playbook (converge.yml) must be idempotent! + +# Check that the molecule inventory is correctly configured +- name: Fail if molecule group is missing + hosts: localhost + tasks: + - name: Print host inventory groups + ansible.builtin.debug: + msg: "{{ groups }}" + + - name: Assert group existence + ansible.builtin.assert: + that: "'molecule' in groups" + fail_msg: | + molecule group was not found inside inventory groups: {{ groups }} + +- name: Converge + hosts: molecule + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Verify kernel type + ansible.builtin.assert: + that: result.stdout | regex_search("^Linux") + + - name: Do preparation + block: + - name: Load local host facts + ansible.builtin.setup: + gather_subset: + - '!all' + - '!min' + - local + + - name: Show local Ansible facts + ansible.builtin.debug: + var: ansible_facts.ansible_local + verbosity: 1 + + - name: Load preparation facts + ansible.builtin.set_fact: + test_prepare_fact: "{{ ansible_local.molecule.test_prepare_fact }}" + + - name: Add your project test configuration here + ansible.builtin.debug: + msg: Typically this will be via the ansible.builtin.include_role module or via import_playbook + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/create.yml b/roles/ec2_platform/molecule/role-ec2_platform/create.yml new file mode 100644 index 0000000..2815558 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/create.yml @@ -0,0 +1,58 @@ +--- +- name: Create + hosts: localhost + gather_facts: false + tasks: + - name: Create platform(s) + ansible.builtin.include_role: + name: syndr.molecule.platform + vars: + + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name + +# We want to avoid errors like "Failed to create temporary directory" +- name: Validate that inventory was refreshed + hosts: molecule + gather_facts: false + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Display uname info + ansible.builtin.debug: + msg: "{{ result.stdout }}" + + - name: Load system facts + ansible.builtin.setup: + filter: + - ansible_service_mgr + + - name: Check on Systemd + block: + - name: Wait for systemd to complete initialization. + ansible.builtin.command: systemctl is-system-running + register: systemctl_status + until: > + 'running' in systemctl_status.stdout or + 'degraded' in systemctl_status.stdout + retries: 30 + delay: 5 + changed_when: false + failed_when: systemctl_status.rc > 1 + + - name: Check systemd status + ansible.builtin.assert: + that: + - systemctl_status.stdout == 'running' + fail_msg: Systemd-enabled container does not have a healthy Systemd! + success_msg: Systemd is running + when: ansible_service_mgr == 'systemd' + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/destroy.yml b/roles/ec2_platform/molecule/role-ec2_platform/destroy.yml new file mode 100644 index 0000000..6569c07 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/destroy.yml @@ -0,0 +1,18 @@ +--- + +- name: Perform cleanup + hosts: localhost + gather_facts: false + tasks: + - name: Remove platform(s) + ansible.builtin.include_role: + name: syndr.molecule.platform + vars: + platform_name: "{{ item.name }}" + platform_state: absent + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/init.yml b/roles/ec2_platform/molecule/role-ec2_platform/init.yml new file mode 100644 index 0000000..1da5128 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/init.yml @@ -0,0 +1,10 @@ +--- +# Initialize a Molecule scenario for use within a role + +- name: Provision file structure + hosts: localhost + tasks: + - name: Launch provisioner + ansible.builtin.include_role: + name: syndr.molecule.init + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/molecule.yml b/roles/ec2_platform/molecule/role-ec2_platform/molecule.yml new file mode 100644 index 0000000..fd0f925 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/molecule.yml @@ -0,0 +1,71 @@ +--- +role_name_check: 0 +dependency: + name: galaxy +driver: + name: default + options: + managed: true +platforms: + - name: ec2-rockylinux9 + type: ec2 + image: ami-067daee80a6d36ac0 + region: us-east-2 + vpc_id: vpc-0eb9fd1391f4207ec + vpc_subnet_id: subnet-0aa189c0d6fc53923 + instance_type: t3.micro + hostvars: {} +provisioner: + name: ansible + log: True + playbooks: + prepare: prepare.yml + converge: converge.yml + side_effect: side_effect.yml + verify: verify.yml + cleanup: cleanup.yml + config_options: + defaults: + gathering: explicit + playbook_vars_root: top + verbosity: ${ANSIBLE_VERBOSITY:-0} +scenario: + create_sequence: + - dependency + - create + - prepare + check_sequence: + - dependency + - cleanup + - destroy + - create + - prepare + - converge + - check + - destroy + converge_sequence: + - dependency + - create + - prepare + - converge + destroy_sequence: + - dependency + - cleanup + - destroy + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +verifier: + name: ansible + enabled: true + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/prepare.yml b/roles/ec2_platform/molecule/role-ec2_platform/prepare.yml new file mode 100644 index 0000000..6d66d37 --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/prepare.yml @@ -0,0 +1,83 @@ +--- + +- name: Prepare controller for execution + hosts: localhost + tags: always + tasks: + - name: Configure for standalone role testing + ansible.builtin.include_role: + name: syndr.molecule.prepare_controller + vars: + prepare_controller_project_type: role + +- name: Prepare target host for execution + hosts: molecule + tags: always + become: true + tasks: + ## + # Creating an admin service account for Molecule/Ansible to use for testing + # + # - If you run Ansible as a service account (you should) on your hosts and + # not as root, it is wise to also test as a non-root user! + # + # - To use this account, add the following to any plays targeting test + # infrastructure (such as in converge.yml): + # + # vars: + # ansible_user: molecule_runner + ## + + - name: Create ansible service account + vars: + molecule_user: molecule_runner + block: + - name: Create ansible group + ansible.builtin.group: + name: "{{ molecule_user }}" + + - name: Create ansible user + ansible.builtin.user: + name: "{{ molecule_user }}" + group: "{{ molecule_user }}" + + - name: Sudoers.d directory exists + ansible.builtin.file: + path: /etc/sudoers.d + state: directory + owner: root + group: root + mode: 0751 + + - name: Ansible user has sudo + ansible.builtin.copy: + content: | + {{ molecule_user }} ALL=(ALL) NOPASSWD: ALL + dest: /etc/sudoers.d/ansible + owner: root + group: root + mode: 0600 + + - name: "Save vars to host (IE: generated test credentials, etc.)" + become: true + block: + - name: Ansible facts directory exists + ansible.builtin.file: + path: /etc/ansible/facts.d + state: directory + owner: root + group: root + mode: 0755 + + - name: Persistent data saved to local Ansible facts + ansible.builtin.copy: + dest: /etc/ansible/facts.d/molecule.fact + content: "{{ {'test_prepare_fact': 'this is an example!'} | to_json }}" + owner: root + group: root + mode: 0644 + + - name: Add your host preparation tasks here! + ansible.builtin.debug: + msg: "IE: adding system users, installing required packages, etc." + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/requirements.yml b/roles/ec2_platform/molecule/role-ec2_platform/requirements.yml new file mode 100644 index 0000000..39b222d --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/requirements.yml @@ -0,0 +1,4 @@ +--- + +roles: [] + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/side_effect.yml b/roles/ec2_platform/molecule/role-ec2_platform/side_effect.yml new file mode 100644 index 0000000..4d1d7af --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/side_effect.yml @@ -0,0 +1,10 @@ +--- +# The side effect playbook executes actions which produce side effects to the instances(s). Intended to test HA failover scenarios or the like. + +- name: Test side effects + hosts: molecule + tasks: + - name: No side effect tests configured + ansible.builtin.debug: + msg: Add side-effect tests here! + diff --git a/roles/ec2_platform/molecule/role-ec2_platform/verify.yml b/roles/ec2_platform/molecule/role-ec2_platform/verify.yml new file mode 100644 index 0000000..3237d2c --- /dev/null +++ b/roles/ec2_platform/molecule/role-ec2_platform/verify.yml @@ -0,0 +1,21 @@ +--- +# Verify that the role being tested has done what it's supposed to + +- name: Verify + hosts: molecule + tasks: + - name: Load local host facts + ansible.builtin.setup: + gather_subset: + - '!all' + - '!min' + - local + + - name: Load test data (example) + ansible.builtin.set_fact: + test_prepare_fact: "{{ ansible_local.molecule.test_prepare_fact }}" + + - name: Add your verification tasks here + ansible.builtin.debug: + msg: "IE: For a 'users' role, check that the test user exists" + diff --git a/roles/ec2_platform/tasks/absent.yml b/roles/ec2_platform/tasks/absent.yml new file mode 100644 index 0000000..d27d841 --- /dev/null +++ b/roles/ec2_platform/tasks/absent.yml @@ -0,0 +1,76 @@ +--- +# Remove ec2 test resources + +- name: Load Molecule instance config + ansible.builtin.set_fact: + __ec2_instance_molecule_instance_config: "{{ (lookup('file', molecule_instance_config, errors='ignore') or '{}') | from_yaml }}" + +- name: Validate platform configurations + ansible.builtin.assert: + that: + - ec2_platform is mapping + - ec2_platform.name is string and ec2_platform.name | length > 0 + - ec2_platform.aws_profile is string + - ec2_platform.key_inject_method is in ["cloud-init", "ec2"] + - ec2_platform.key_name is string and ec2_platform.key_name | length > 0 + - ec2_platform.region is string + - ec2_platform.security_group_name is string and ec2_platform.security_group_name | length > 0 + - ec2_platform.security_groups is sequence + - ec2_platform.vpc_id is string + - ec2_platform.vpc_subnet_id is string and ec2_platform.vpc_subnet_id | length > 0 + quiet: true + +- name: Look up subnets to determine VPCs (if needed) + amazon.aws.ec2_vpc_subnet_info: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + subnet_ids: "{{ ec2_platform.vpc_subnet_id }}" + when: not ec2_platform.vpc_id + register: __ec2_platform_subnet_info + +- name: Validate discovered information + ansible.builtin.assert: + that: ec2_platform.vpc_id or (__ec2_platform_subnet_info.subnets | length > 0) + quiet: true + fail_msg: "No VPCs found for subnet: {{ ec2_platform.vpc_subnet_id }}" + +- name: Look up EC2 instance by tag + amazon.aws.ec2_instance_info: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + filters: + "tag:molecule-run-id": "{{ ec2_platform_run_config.run_id }}" + register: __ec2_instance_info + +- name: Destroy ephemeral EC2 instances + when: __ec2_instance_info.instances | length > 0 + amazon.aws.ec2_instance: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + instance_ids: "{{ __ec2_instance_info.instances | map(attribute='instance_id') | list }}" + vpc_subnet_id: "{{ ec2_platform.vpc_subnet_id }}" + state: absent + register: __ec2_instance_destroy + +- name: Destroy ephemeral security groups (if needed) + amazon.aws.ec2_security_group: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + vpc_id: "{{ ec2_platform.vpc_id or __ec2_platform_subnet_info.subnets[0] }}" + name: "{{ ec2_platform.security_group_name }}" + state: absent + when: ec2_platform.security_groups | length == 0 + +- name: Destroy ephemeral keys (if needed) + amazon.aws.ec2_key: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + name: "{{ ec2_platform.key_name }}" + state: absent + when: ec2_platform.key_inject_method == "ec2" + +- name: Remove ec2 instance config file + ansible.builtin.file: + path: "{{ ec2_platform_run_config_path }}" + state: absent + diff --git a/roles/ec2_platform/tasks/main.yml b/roles/ec2_platform/tasks/main.yml new file mode 100644 index 0000000..02eaffe --- /dev/null +++ b/roles/ec2_platform/tasks/main.yml @@ -0,0 +1,26 @@ +--- +# tasks file for ec2_platform + +- name: 🐞 Show ec2_platform_definition + ansible.builtin.debug: + var: ec2_platform_definition + verbosity: 1 + +# Merge the defaults with any options provided to this role +- name: Generate runtime configuration + ansible.builtin.set_fact: + ec2_platform: "{{ ec2_platform_defaults | combine(ec2_platform_definition | default({})) }}" + +- name: 🦋 Show ec2_platform + ansible.builtin.debug: + var: ec2_platform + verbosity: 1 + +- name: Platform is deployed + ansible.builtin.include_tasks: "{{ role_path }}/tasks/present.yml" + when: ec2_platform_state == 'present' + +- name: Platform is not deployed + ansible.builtin.include_tasks: "{{ role_path }}/tasks/absent.yml" + when: ec2_platform_state == 'absent' + diff --git a/roles/ec2_platform/tasks/present.yml b/roles/ec2_platform/tasks/present.yml new file mode 100644 index 0000000..02a0212 --- /dev/null +++ b/roles/ec2_platform/tasks/present.yml @@ -0,0 +1,169 @@ +--- +# The ec2 platform has been created + +- name: Validate platform configuration - {{ ec2_platform.name | default('invalid') }} + ansible.builtin.assert: + that: + - ec2_platform is mapping + - ec2_platform.name is string and ec2_platform.name | length > 0 + - ec2_platform.assign_public_ip is boolean + - ec2_platform.aws_profile is string + - ec2_platform.boot_wait_seconds is integer and ec2_platform.boot_wait_seconds >= 0 + - ec2_platform.cloud_config is mapping + - ec2_platform.image is string + - ec2_platform.image_name is string + - ec2_platform.image_owner is sequence or (ec2_platform.image_owner is string and ec2_platform.image_owner | length > 0) + - ec2_platform.instance_type is string and ec2_platform.instance_type | length > 0 + - ec2_platform.key_inject_method is in ["cloud-init", "ec2"] + - ec2_platform.key_name is string and ec2_platform.key_name | length > 0 + - ec2_platform.private_key_path is string and ec2_platform.private_key_path | length > 0 + - ec2_platform.public_key_path is string and ec2_platform.public_key_path | length > 0 + - ec2_platform.region is string + - ec2_platform.security_group_name is string and ec2_platform.security_group_name | length > 0 + - ec2_platform.security_group_description is string and ec2_platform.security_group_description | length > 0 + - ec2_platform.security_group_rules is sequence + - ec2_platform.security_group_rules_egress is sequence + - ec2_platform.security_groups is sequence + - ec2_platform.ssh_user is string and ec2_platform.ssh_user | length > 0 + - ec2_platform.ssh_port is integer and ec2_platform.ssh_port in range(1, 65536) + - ec2_platform.tags is mapping + - ec2_platform.volumes is sequence + - ec2_platform.vpc_id is string + - ec2_platform.vpc_subnet_id is string and ec2_platform.vpc_subnet_id | length > 0 + quiet: true + +# TODO: Merge, not overwrite -- already does? +- name: Write run config to file + ansible.builtin.copy: + dest: "{{ ec2_platform_run_config_path }}" + content: "{{ ec2_platform_run_config | to_yaml }}" + mode: "0600" + +- name: Generate local key pairs + community.crypto.openssh_keypair: + path: "{{ ec2_platform.private_key_path }}" + type: rsa + size: 2048 + regenerate: never + register: __ec2_platform_local_keypair + +- name: Look up EC2 AMI(s) by owner and name (if image not set) + amazon.aws.ec2_ami_info: + owners: "{{ ec2_platform.image_owner }}" + filters: "{{ ec2_platform.image_filters | default({}) | combine(__ec2_platform_image_name_map) }}" + vars: + __ec2_platform_image_name_map: >- + "{% if ec2_platform.image_name is defined and ec2_platform.image_name | length > 0 %} + {{ {'name': ec2_platform.image_name} }} + {% else %}{}{% endif %}" + when: not ec2_platform.image + register: __ec2_platform_ami_info + +- name: Look up subnets to determine VPCs (if needed) + amazon.aws.ec2_vpc_subnet_info: + subnet_ids: "{{ ec2_platform.vpc_subnet_id }}" + when: not ec2_platform.vpc_id + register: __ec2_platform_subnet_info + +- name: Validate discovered information + ansible.builtin.assert: + that: + - ec2_platform.image or (__ec2_platform_ami_info.results[0].images | length > 0) + - ec2_platform.vpc_id or (__ec2_platform_subnet_info.results[0].subnets | length > 0) + quiet: true + +- name: Create ephemeral EC2 keys (if needed) + amazon.aws.ec2_key: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + name: "{{ ec2_platform.key_name }}" + key_material: "{{ __ec2_platform_local_keypair.public_key }}" + when: ec2_platform.key_inject_method == "ec2" + register: __ec2_platform_ec2_keys + +- name: Create ephemeral security groups (if needed) + amazon.aws.ec2_security_group: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + iam_instance_profile: "{{ ec2_platform.iam_instance_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + vpc_id: "{{ ec2_platform.vpc_id or vpc_subnet.vpc_id }}" + name: "{{ ec2_platform.security_group_name }}" + description: "{{ ec2_platform.security_group_description }}" + rules: "{{ ec2_platform.security_group_rules }}" + rules_egress: "{{ ec2_platform.security_group_rules_egress }}" + vars: + vpc_subnet: "{{ __ec2_platform_subnet_info.results[0].subnets[0] }}" + when: ec2_platform.security_groups | length == 0 + +- name: Create ephemeral EC2 instance + amazon.aws.ec2_instance: + profile: "{{ ec2_platform.aws_profile | default(omit) }}" + region: "{{ ec2_platform.region | default(omit) }}" + filters: "{{ __ec2_platform_filters }}" + instance_type: "{{ ec2_platform.instance_type }}" + image_id: "{{ __ec2_platform_image_id }}" + vpc_subnet_id: "{{ ec2_platform.vpc_subnet_id }}" + security_groups: "{{ __ec2_platform_security_groups }}" + network: + assign_public_ip: "{{ ec2_platform.assign_public_ip }}" + volumes: "{{ ec2_platform.volumes }}" + key_name: "{{ (ec2_platform.key_inject_method == 'ec2') | ternary(ec2_platform.key_name, omit) }}" + tags: "{{ __ec2_platform_tags }}" + user_data: "{{ __ec2_platform_user_data }}" + state: "running" + wait: true + vars: + __ec2_platform_security_groups: "{{ ec2_platform.security_groups or [ec2_platform.security_group_name] }}" + __ec2_platform_generated_image_id: "{{ (ami_info.results[0].images | sort(attribute='creation_date', reverse=True))[0].image_id }}" + __ec2_platform_image_id: "{{ ec2_platform.image or __ec2_platform_generated_image_id }}" + + __ec2_platform_generated_cloud_config: + users: + - name: "{{ ec2_platform.ssh_user }}" + ssh_authorized_keys: + - "{{ __ec2_platform_local_keypair.public_key }}" + sudo: "ALL=(ALL) NOPASSWD:ALL" + __ec2_platform_cloud_config: >- + {{ (ec2_platform.key_inject_method == 'cloud-init') + | ternary((ec2_platform.cloud_config | combine(__ec2_platform_generated_cloud_config)), ec2_platform.cloud_config) }} + __ec2_platform_user_data: |- + #cloud-config + {{ __ec2_platform_cloud_config | to_yaml }} + + __ec2_platform_generated_tags: + instance: "{{ ec2_platform.name }}" + "molecule-run-id": "{{ ec2_platform_run_config.run_id }}" + Name: molecule-{{ ec2_platform.name }} + __ec2_platform_tags: "{{ (ec2_platform.tags or {}) | combine(__ec2_platform_generated_tags) }}" + __ec2_platform_filter_keys: "{{ __ec2_platform_generated_tags.keys() | map('regex_replace', '^(.+)$', 'tag:\\1') }}" + __ec2_platform_filters: "{{ dict(__ec2_platform_filter_keys | zip(__ec2_platform_generated_tags.values())) }}" + register: __ec2_instance_creation + + +# NOTE: Var is used by the `platform` role to write Molecule instance configuration +- name: Collect instance configs + vars: + __ec2_platform_instance: "{{ __ec2_instance_creation.instances[0] }}" + ansible.builtin.set_fact: + ec2_platform_instance_config: + instance: "{{ ec2_platform.name }}" + address: "{{ ec2_platform.assign_public_ip | ternary(__ec2_platform_instance.public_ip_address, __ec2_platform_instance.private_ip_address) }}" + user: "{{ ec2_platform.ssh_user }}" + port: "{{ ec2_platform.ssh_port }}" + identity_file: "{{ ec2_platform.private_key_path }}" + instance_ids: + - "{{ __ec2_platform_instance.instance_id }}" + +- name: Wait for SSH connectivity + ansible.builtin.wait_for: + host: "{{ ec2_platform_instance_config.address }}" + port: "{{ ec2_platform_instance_config.port }}" + search_regex: SSH + delay: 10 + timeout: 320 + +# TODO: Add an actual check here instead of only waiting +- name: Wait for boot process to finish + ansible.builtin.pause: + seconds: "{{ ec2_platform.boot_wait_seconds }}" + diff --git a/roles/ec2_platform/vars/main.yml b/roles/ec2_platform/vars/main.yml new file mode 100644 index 0000000..11e401d --- /dev/null +++ b/roles/ec2_platform/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for ec2_platform diff --git a/roles/init/README.md b/roles/init/README.md index 60e923f..2dcc72d 100644 --- a/roles/init/README.md +++ b/roles/init/README.md @@ -124,6 +124,17 @@ Role Variables # The type of project that this Molecule configuration will be integrated into init_project_type: auto +# The type of platform that this Molecule configuration will be testing on (docker, ec2) +# WARN: mixing platform types is not supported! +init_platform_type: docker + +# Version of this collection that should be used by the Molecule test +# - Set to "" to attempt to use the running version +init_collection_version: "" + +# Source of the collection that this role is part of (galaxy, git) +init_collection_source: git + # Filesystem location of the molecule scenario being initialized init_scenario_dir: "{{ molecule_scenario_directory | default(playbook_dir) }}" @@ -144,12 +155,9 @@ init_project_dir: "{{ init_scenario_dir.split('/')[:-2] | join('/') }}" # modify_image: (true/false) # modify_image_buildpath: (string) # path to directory containing Dockerfile # privileged: (true/false) -init_platforms: - - name: docker-rocklinux9 - type: docker - config: - image: "geerlingguy/docker-rockylinux9-ansible:latest" - systemd: true +# +# If not specified, the role will attempt to use the default platform configuration +init_platforms: [] # Create backups of any files that would be clobbered by running this role init_file_backup: true diff --git a/roles/init/defaults/main.yml b/roles/init/defaults/main.yml index 26f6ea9..be8a275 100644 --- a/roles/init/defaults/main.yml +++ b/roles/init/defaults/main.yml @@ -4,6 +4,17 @@ # The type of project that this Molecule configuration will be integrated into init_project_type: auto +# The type of platform that this Molecule configuration will be testing on (docker, ec2) +# WARN: mixing platform types is not supported! +init_platform_type: docker + +# Version of this collection that should be used by the Molecule test +# - Set to "" to attempt to use the running version +init_collection_version: "" + +# Source of the collection that this role is part of (galaxy, git) +init_collection_source: git + # Filesystem location of the molecule scenario being initialized init_scenario_dir: "{{ molecule_scenario_directory | default(playbook_dir) }}" @@ -24,12 +35,9 @@ init_project_dir: "{{ init_scenario_dir.split('/')[:-2] | join('/') }}" # modify_image: (true/false) # modify_image_buildpath: (string) # path to directory containing Dockerfile # privileged: (true/false) -init_platforms: - - name: docker-rockylinux9 - type: docker - config: - image: "geerlingguy/docker-rockylinux9-ansible:latest" - systemd: true +# +# If not specified, the role will attempt to use the default platform configuration +init_platforms: [] # Create backups of any files that would be clobbered by running this role init_file_backup: true @@ -39,3 +47,10 @@ init_file_backup: true # - Set to "" to disable init_ansible_secret_path: "" +# Configuration defaults to be used if the collection manifest is not accessible +init_collection_defaults: + repository: https://github.com/syndr/ansible-collection-molecule + name: molecule + namespace: syndr + version: latest + diff --git a/roles/init/files/init.yml b/roles/init/files/init.yml index 3985bd1..323480b 100644 --- a/roles/init/files/init.yml +++ b/roles/init/files/init.yml @@ -7,4 +7,9 @@ - name: Launch provisioner ansible.builtin.include_role: name: influxdata.molecule.init + vars: + # Supported platform types are: docker, ec2 + init_platform_type: docker + # Supported collection sources are: git, galaxy + init_collection_source: git diff --git a/roles/init/tasks/asserts.yml b/roles/init/tasks/asserts.yml index 307793e..30dd9aa 100644 --- a/roles/init/tasks/asserts.yml +++ b/roles/init/tasks/asserts.yml @@ -8,6 +8,9 @@ - init_scenario_dir is string - init_project_dir is string - init_file_backup in [true, false] + - init_platform_type in ['docker', 'ec2'] + - init_collection_version is string + - init_collection_source in ['galaxy', 'git'] fail_msg: Global configuration option for init role is not sane success_msg: Sanity check passed diff --git a/roles/init/tasks/main.yml b/roles/init/tasks/main.yml index 778a914..85300ce 100644 --- a/roles/init/tasks/main.yml +++ b/roles/init/tasks/main.yml @@ -8,6 +8,75 @@ ansible.builtin.include_tasks: "{{ role_path }}/tasks/auto.yml" when: init_project_type == 'auto' +- name: Build base platform definition + when: init_platforms is not truthy + # TODO: Define these values in the role defaults + block: + - name: Build base docker platform definition + when: init_platform_type == 'docker' + ansible.builtin.set_fact: + init_platforms: + - name: docker-rockylinux9 + type: docker + config: + image: "geerlingguy/docker-rockylinux9-ansible:latest" + systemd: true + + - name: Build base ec2 platform definition + when: init_platform_type == 'ec2' + ansible.builtin.set_fact: + init_platforms: + - name: ec2-rockylinux9 + type: ec2 + config: + image: "ami-067daee80a6d36ac0" + instance_type: "t3.micro" + region: "us-east-2" + vpc_id: "vpc-12345678" + vpc_subnet_id: "subnet-12345678" + + - name: Platform definition is valid + ansible.builtin.assert: + that: + - init_platforms is defined + - init_platforms | length > 0 + - init_platforms[0].name is string + - init_platforms[0].type is string + - init_platforms[0].config is mapping + fail_msg: "Platform definition failed! Check the platform configuration." + success_msg: "Platform definition is valid" + +- name: Load collection meta information + block: + - name: Load collection meta data + ansible.builtin.slurp: + src: "{{ role_path }}/../../MANIFEST.json" + register: __init_collection_meta + ignore_errors: true + + - name: 🐜 Show collection meta data + ansible.builtin.debug: + var: __init_collection_meta.content | b64decode | from_json + verbosity: 1 + ignore_errors: true + + - name: Extract collection meta info + when: + - __init_collection_meta is not failed + - __init_collection_meta.content is defined + - (__init_collection_meta.content | b64decode | from_json).collection_info.version is defined + ansible.builtin.set_fact: + __init_collection_meta: "{{ (__init_collection_meta.content | b64decode | from_json) }}" + +- name: Set collection information + ansible.builtin.set_fact: + init_collection_version: >- + "{{ init_collection_version if init_collection_version is truthy + else __init_collection_meta.collection_info.version | default(init_collection_defaults.version) }}" + __init_collection_name: "{{ __init_collection_meta.collection_info.name | default(init_collection_defaults.name) }}" + __init_collection_namespace: "{{ __init_collection_meta.collection_info.namespace | default(init_collection_defaults.namespace) }}" + __init_collection_repository: "{{ __init_collection_meta.collection_info.repository | default(init_collection_defaults.repository) }}" + - name: Deploy molecule configuration ansible.builtin.template: src: "{{ role_path }}/templates/molecule.yml.j2" diff --git a/roles/init/templates/collections.yml.j2 b/roles/init/templates/collections.yml.j2 index d576911..6dde64e 100644 --- a/roles/init/templates/collections.yml.j2 +++ b/roles/init/templates/collections.yml.j2 @@ -2,7 +2,11 @@ collections: - name: community.docker - - name: git+https://github.com/influxdata/ansible-collection-molecule.git +{% if init_collection_source == 'git' %} + - name: git+{{ __init_collection_repository }}.git type: git - version: latest +{% elif init_collection_source == 'galaxy' %} + - name: {{ __init_collection_namespace }}.{{ __init_collection_name }} +{% endif %} + version: {{ init_collection_version }} diff --git a/roles/init/templates/create.yml.j2 b/roles/init/templates/create.yml.j2 index f33754b..fe9ce25 100644 --- a/roles/init/templates/create.yml.j2 +++ b/roles/init/templates/create.yml.j2 @@ -5,18 +5,12 @@ tasks: - name: Create docker platform(s) ansible.builtin.include_role: - name: {{ ansible_collection_name }}.docker_platform - vars: -{% raw %} - docker_platform_name: "{{ item.name }}" - docker_platform_image: "{{ item.image }}" - docker_platform_systemd: "{{ item.systemd | default(false) }}" - docker_platform_modify_image: "{{ item.modify_image | default(false) }}" - docker_platform_modify_image_buildpath: "{{ item.modify_image_buildpath | default(molecule_ephemeral_directory + '/build') }}" - docker_platform_privileged: "{{ item.privileged | default (false) }}" - docker_platform_hostvars: "{{ item.hostvars | default({}) }}" - docker_platform_state: present - when: item.type == 'docker' + name: {{ ansible_collection_name }}.platform + vars:{% raw %} + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" loop: "{{ molecule_yml.platforms }}" loop_control: label: item.name diff --git a/roles/init/templates/destroy.yml.j2 b/roles/init/templates/destroy.yml.j2 index 4254478..2d49265 100644 --- a/roles/init/templates/destroy.yml.j2 +++ b/roles/init/templates/destroy.yml.j2 @@ -1,15 +1,20 @@ --- - name: Perform cleanup - hosts: molecule + hosts: localhost gather_facts: false tasks: - name: Remove platform ansible.builtin.include_role: - name: {{ ansible_collection_name }}.docker_platform + name: {{ ansible_collection_name }}.platform vars: {% raw %} - docker_platform_name: "{{ inventory_hostname }}" - docker_platform_state: absent + platform_name: "{{ item.name }}" + platform_state: absent + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name {% endraw %} diff --git a/roles/init/templates/molecule.yml.j2 b/roles/init/templates/molecule.yml.j2 index 9aca6f8..b2343e7 100644 --- a/roles/init/templates/molecule.yml.j2 +++ b/roles/init/templates/molecule.yml.j2 @@ -6,7 +6,9 @@ driver: name: default options: managed: true +{% if 'docker' in init_platforms | map(attribute='type') %} login_cmd_template: 'docker exec -ti {instance} bash' +{% endif %} platforms: {% for init_platform in init_platforms %} - name: {{ init_platform.name }} @@ -21,6 +23,13 @@ platforms: privileged: {{ init_platform.config.privileged | default(false) }} hostvars: {} {% endif %} +{% if init_platform.type == 'ec2' %} + image: {{ init_platform.config.image }} + region: {{ init_platform.config.region }} + vpc_id: {{ init_platform.config.vpc_id }} + vpc_subnet_id: {{ init_platform.config.vpc_subnet_id }} +{% endif %} + hostvars: {} {% endfor %} provisioner: name: ansible diff --git a/roles/init/templates/prepare.yml.j2 b/roles/init/templates/prepare.yml.j2 index bbcf8b5..683aee4 100644 --- a/roles/init/templates/prepare.yml.j2 +++ b/roles/init/templates/prepare.yml.j2 @@ -13,6 +13,7 @@ - name: Prepare target host for execution hosts: molecule tags: always + become: true tasks: ## # Creating an admin service account for Molecule/Ansible to use for testing diff --git a/roles/platform/README.md b/roles/platform/README.md new file mode 100644 index 0000000..1a0269d --- /dev/null +++ b/roles/platform/README.md @@ -0,0 +1,87 @@ +molecule.platform +========= + +Deploy a Molecule platform for testing. + +This role handles both the creation and destruction of a Molecule platform, as well as configuration of internal Molecule inventory files necessary to utilize them. It is the recommended way to utilize this collection's platform roles. + +Supported platforms are: +- `docker` +- `ec2` + +Requirements +------------ + +1. Molecule should be installed and executable from a location in the users PATH +1. Ansible should be installed, with `ansible-playbook` executable via the users PATH + +Role Variables +-------------- + +```yaml +# Name of this Molecule platform +platform_name: instance + +# Whether this platform should be deployed on the current system (present/absent) +platform_state: present + +# What type of platform should be deployed +platform_type: docker + +# Molecule platform configuration +platform_molecule_cfg: {} +``` + +Dependencies +------------ + +**Roles included with this collection:** +- `molecule.docker_platform` +- `molecule.ec2_platform` + +Example Playbook +---------------- + +```yaml +- name: Create + hosts: localhost + gather_facts: false + tasks: + - name: Create platform(s) + ansible.builtin.include_role: + name: syndr.molecule.platform + vars: + platform_name: "{{ item.name }}" + platform_state: present + platform_type: "{{ item.type }}" + platform_molecule_cfg: "{{ item }}" + loop: "{{ molecule_yml.platforms }}" + loop_control: + label: item.name + +# We want to avoid errors like "Failed to create temporary directory" +- name: Validate that inventory was refreshed + hosts: molecule + gather_facts: false + tasks: + - name: Check uname + ansible.builtin.raw: uname -a + register: result + changed_when: false + + - name: Display uname info + ansible.builtin.debug: + msg: "{{ result.stdout }}" + +``` + +License +------- + +MIT + +Author Information +------------------ + +- [@syndr](https://github.com/syndr/) + diff --git a/roles/platform/defaults/main.yml b/roles/platform/defaults/main.yml new file mode 100644 index 0000000..70657a0 --- /dev/null +++ b/roles/platform/defaults/main.yml @@ -0,0 +1,15 @@ +--- +# defaults file for platform + +# Name of this Molecule platform +platform_name: instance + +# Whether this platform should be deployed on the current system (present/absent) +platform_state: present + +# What type of platform should be deployed +platform_type: docker + +# Molecule platform configuration +platform_molecule_cfg: {} + diff --git a/roles/platform/handlers/main.yml b/roles/platform/handlers/main.yml new file mode 100644 index 0000000..a68801b --- /dev/null +++ b/roles/platform/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for platform diff --git a/roles/platform/meta/main.yml b/roles/platform/meta/main.yml new file mode 100644 index 0000000..481d17f --- /dev/null +++ b/roles/platform/meta/main.yml @@ -0,0 +1,54 @@ +galaxy_info: + role_name: platform + namespace: syndr + author: syndr + description: Create and destroy a platform for Molecule testing + company: UltronCORE + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: MIT + + min_ansible_version: 2.16 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/platform/tasks/deprovision.yml b/roles/platform/tasks/deprovision.yml new file mode 100644 index 0000000..e4dc627 --- /dev/null +++ b/roles/platform/tasks/deprovision.yml @@ -0,0 +1,25 @@ +--- +# Remove deployed resources + +- name: Initilze state + ansible.builtin.set_fact: + # Number of times this role has been included during this playbook run + __platform_run_count: "{{ __platform_run_count | default(0) | int + 1 }}" + +- name: Remove docker-type platform + when: platform_type == 'docker' + ansible.builtin.include_role: + name: "{{ ansible_collection_name }}.docker_platform" + vars: + docker_platform_name: "{{ platform_name }}" + docker_platform_state: absent + +- name: Remove ec2-type platform + when: platform_type == 'ec2' + ansible.builtin.include_role: + name: "{{ ansible_collection_name }}.ec2_platform" + vars: + ec2_platform_name: "{{ platform_name }}" + ec2_platform_state: absent + ec2_platform_definition: "{{ platform_molecule_cfg }}" + diff --git a/roles/platform/tasks/inventory.yml b/roles/platform/tasks/inventory.yml new file mode 100644 index 0000000..3e5fcde --- /dev/null +++ b/roles/platform/tasks/inventory.yml @@ -0,0 +1,184 @@ +--- +# Add a host to the Molecule inventory + +- name: Load existing instance configuration + block: + - name: Load existing instance configuration file + ansible.builtin.slurp: + src: "{{ platform_molecule_ephemeral_directory }}/instance_config.yml" + register: __platform_current_instance_config_b64 + ignore_errors: true + + - name: Decode instance configuration data + ansible.builtin.set_fact: + __platform_current_instance_config: "{{ __platform_current_instance_config_b64.content | default('') | b64decode | from_yaml }}" + +- name: Generate new instance configuration + when: platform_state == 'present' + block: + - name: Generate {{ platform_name }} instance configuration (Docker) + when: platform_type == 'docker' + ansible.builtin.set_fact: + __platform_new_instance_config: "{{ { + 'instance': platform_name, + 'connection': 'docker' + } }}" + __platform_ansible_hostvars: + ansible_connection: "community.docker.docker" + + - name: Generate {{ platform_name }} instance configuration (EC2) + when: platform_type == 'ec2' + ansible.builtin.set_fact: + # NOTE: This depends on the ec2_platform_instance_config being set by the ec2_platform role + __platform_new_instance_config: "{{ { + 'instance': platform_name, + 'address': ec2_platform_instance_config.address, + 'user': ec2_platform_instance_config.user, + 'port': ec2_platform_instance_config.port, + 'identity_file': ec2_platform_instance_config.identity_file, + 'instance_ids': ec2_platform_instance_config.instance_ids + } }}" + __platform_ansible_connection: "ssh" + __platform_ansible_hostvars: + ansible_connection: "ssh" + ansible_host: "{{ ec2_platform_instance_config.address }}" + ansible_port: "{{ ec2_platform_instance_config.port }}" + ansible_user: "{{ ec2_platform_instance_config.user }}" + ansible_ssh_private_key_file: "{{ ec2_platform_instance_config.identity_file }}" + + - name: Instance configuration {{ platform_name }} is valid + ansible.builtin.assert: + that: + - __platform_new_instance_config is defined + - __platform_new_instance_config.instance is string + fail_msg: "Instance configuration for {{ platform_name }} failed! Check the platform configuration." + success_msg: "Instance configuration for {{ platform_name }} is defined" + + - name: 🪲 Current instance config + ansible.builtin.debug: + var: __platform_current_instance_config + verbosity: 1 + + - name: Instance name matching this already exists in configuration + when: + - __platform_current_instance_config is truthy + - platform_name in __platform_current_instance_config | map(attribute='instance') | list + block: + - name: Mark config update as unneeded + ansible.builtin.set_fact: + __platform_instance_config_update_needed: false + + - name: Existing configuration does not match desired + when: __platform_new_instance_config != (__platform_current_instance_config | selectattr('instance', 'equalto', platform_name) | list | first) + block: + - name: Remove existing {{ platform_name }} configuration (does not match) + ansible.builtin.set_fact: + __platform_current_instance_config: "{{ + __platform_current_instance_config | rejectattr('instance', 'equalto', platform_name) | list }}" + __platform_instance_config_update_needed: true + +- name: Remove instance configuration + when: platform_state == 'absent' + block: + - name: Remove existing {{ platform_name }} configuration + when: + - __platform_current_instance_config is truthy + - platform_name in __platform_current_instance_config | map(attribute='instance') | list + ansible.builtin.set_fact: + __platform_current_instance_config: "{{ + __platform_current_instance_config | rejectattr('instance', 'equalto', platform_name) | list }}" + __platform_instance_config_update_needed: true + +- name: dump new instance config + ansible.builtin.debug: + var: __platform_new_instance_config + ignore_errors: true + +- name: dump current instance config + ansible.builtin.debug: + var: __platform_current_instance_config + ignore_errors: true + +- name: Write {{ platform_name }} instance config file + when: + - __platform_instance_config_update_needed + - __platform_current_instance_config | default(false, true)is truthy or __platform_new_instance_config | default(false, true) is truthy + vars: + __platform_instance_config: >- + {{ __platform_current_instance_config | default([], true) + [__platform_new_instance_config] + if __platform_new_instance_config | default(false, true) is truthy + else __platform_current_instance_config }} + ansible.builtin.copy: + content: "{{ __platform_instance_config | to_nice_yaml(indent=2) }}" + dest: "{{ platform_molecule_ephemeral_directory }}/instance_config.yml" + mode: "0600" + +# If the file would be empty, remove it +- name: Remove molecule instance config file + when: + - platform_state == 'absent' + - __platform_current_instance_config | default(false) is not truthy + - __platform_new_instance_config | default(false) is not truthy + ansible.builtin.file: + path: "{{ platform_molecule_ephemeral_directory }}/instance_config.yml" + state: absent + +- name: Load existing molecule inventory + when: __platform_run_count | int > 1 or platform_state == 'absent' + block: + - name: Load existing molecule inventory file + ansible.builtin.slurp: + src: "{{ platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" + register: __platform_current_molecule_inventory_b64 + ignore_errors: true + + - name: Decode instance configuration data + ansible.builtin.set_fact: + __platform_current_molecule_inventory: "{{ __platform_current_molecule_inventory_b64.content | default({}) | b64decode | from_yaml }}" + +- name: Add {{ platform_name }} to molecule_inventory + when: platform_state == 'present' + vars: + __platform_inventory_partial: "{{ { + 'all': { + 'children': { + 'molecule': { + 'hosts': { + platform_name: __platform_ansible_hostvars + }}}}} }}" + ansible.builtin.set_fact: + __platform_molecule_inventory: > + {{ __platform_current_molecule_inventory | from_yaml | default({}) | combine(__platform_inventory_partial, recursive=true) }} + +- name: Remove {{ platform_name }} from molecule_inventory + when: + - platform_state == 'absent' + - __platform_current_molecule_inventory is truthy + ansible.builtin.set_fact: + __platform_molecule_inventory: "{{ + __platform_current_molecule_inventory | combine({ + 'all': { + 'children': { + 'molecule': { + 'hosts': (__platform_current_molecule_inventory.all.children.molecule.hosts | default({}) | + dict2items | rejectattr('key', 'equalto', platform_name) | items2dict) + }}}}, recursive=true) }}" + +- name: Write {{ platform_name }} to molecule inventory file + ansible.builtin.copy: + content: | + {{ __platform_molecule_inventory | default({}) | to_nice_yaml(indent=2) }} + dest: "{{ platform_molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" + mode: "0600" + +- name: Force inventory refresh + ansible.builtin.meta: refresh_inventory + +- name: Fail if molecule group is missing + when: __platform_molecule_inventory is defined + ansible.builtin.assert: + that: "'molecule' in groups" + fail_msg: | + molecule group was not found inside inventory groups: {{ groups }} + + diff --git a/roles/platform/tasks/main.yml b/roles/platform/tasks/main.yml new file mode 100644 index 0000000..7b9ddec --- /dev/null +++ b/roles/platform/tasks/main.yml @@ -0,0 +1,14 @@ +--- +# tasks file for platform + +- name: Platform is provisioned + ansible.builtin.include_tasks: "{{ role_path }}/tasks/provision.yml" + when: platform_state == 'present' + +- name: Platform is destroyed + ansible.builtin.include_tasks: "{{ role_path }}/tasks/deprovision.yml" + when: platform_state == 'absent' + +- name: Configure Molecule inventory + ansible.builtin.include_tasks: "{{ role_path }}/tasks/inventory.yml" + diff --git a/roles/platform/tasks/provision.yml b/roles/platform/tasks/provision.yml new file mode 100644 index 0000000..895fabd --- /dev/null +++ b/roles/platform/tasks/provision.yml @@ -0,0 +1,37 @@ +--- +# Create a host and requisite configuration for use by molecule +# + +- name: Initialize state + ansible.builtin.set_fact: + # Number of times this role has been included during this playbook run + __platform_run_count: "{{ __platform_run_count | default(0) | int + 1 }}" + +- name: Load system facts + ansible.builtin.setup: + filter: + - ansible_service_mgr + +- name: Configure platform for docker type + when: platform_type == 'docker' + ansible.builtin.include_role: + name: "{{ ansible_collection_name }}.docker_platform" + vars: + docker_platform_name: "{{ platform_name }}" + docker_platform_state: present + docker_platform_image: "{{ platform_molecule_cfg.image }}" + docker_platform_systemd: "{{ platform_molecule_cfg.systemd | default(false) }}" + docker_platform_modify_image: "{{ platform_molecule_cfg.modify_image | default(false) }}" + docker_platform_modify_image_buildpath: "{{ platform_molecule_cfg.modify_image_buildpath | default(molecule_ephemeral_directory + '/build') }}" + docker_platform_privileged: "{{ platform_molecule_cfg.privileged | default (false) }}" + docker_platform_hostvars: "{{ platform_molecule_cfg.hostvars | default({}) }}" + +- name: Configure platform for ec2 type + when: platform_type == 'ec2' + ansible.builtin.include_role: + name: "{{ ansible_collection_name }}.ec2_platform" + vars: + ec2_platform_name: "{{ platform_name }}" + ec2_platform_state: present + ec2_platform_definition: "{{ platform_molecule_cfg }}" + diff --git a/roles/platform/tests/inventory b/roles/platform/tests/inventory new file mode 100644 index 0000000..878877b --- /dev/null +++ b/roles/platform/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/roles/platform/tests/test.yml b/roles/platform/tests/test.yml new file mode 100644 index 0000000..360779d --- /dev/null +++ b/roles/platform/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - platform diff --git a/roles/platform/vars/main.yml b/roles/platform/vars/main.yml new file mode 100644 index 0000000..aa4a248 --- /dev/null +++ b/roles/platform/vars/main.yml @@ -0,0 +1,12 @@ +--- +# vars file for platform + +# Filesystem location of the Molecule ephemeral directory. Should not need to be updated by the user of this role! +platform_molecule_ephemeral_directory: "{{ molecule_ephemeral_directory }}" + +# Does the molecule instance configuration file need to be updated? (assume yes) +__platform_instance_config_update_needed: true + + # Default connection method for hosts -- update as needed in tasks/inventory.yml +__platform_ansible_connection: "ssh" +