diff --git a/.drone.yml b/.drone.yml index c0c72c2..8497d95 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,3 +16,10 @@ pipeline: commands: - pip install tox==3.14.6 - tox -e ${TOXENV} +--- +pipeline: + integration: + image: python:${IMAGE} + commands: + - pip install -e . + - cd integration-test-role && molecule test diff --git a/.envrc.sample b/.envrc.sample new file mode 100644 index 0000000..0f588d6 --- /dev/null +++ b/.envrc.sample @@ -0,0 +1,8 @@ +# The Hetzner Cloud API token for managing our instances +export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key) + +# So molecule will show credentials in the logs +export MOLECULE_NO_LOG=False + +# For unique integration testing VPS names +export INSTANCE_UUID=$RANDOM diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3cc3183 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +DIRECTORY := integration-test-role +DRIVER := hetznercloud + +default: test + +clean: + @rm -rf $(DIRECTORY) + +init: clean + @molecule init role -d $(DRIVER) $(DIRECTORY) + +test: + @cd $(DIRECTORY) && molecule test + +.PHONY: clean init test diff --git a/README.md b/README.md index 654940c..7c94a6b 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,24 @@ TODO. ## License The [LGPL](https://www.gnu.org/licenses/lgpl-3.0.en.html) license. + +## Testing + +Unit tests and such. + +```bash +$ pip install tox +$ tox -v +``` + +Integration tests. + +Only doable by [Autonomic Cooperative](https://autonomic.zone/) members. + +```bash +$ sudo apt install -y direnv +$ cp .envrc.sample .envrc +$ direnv allow +$ pip install -e . +$ cd integration-test-role && molecule test +``` diff --git a/integration-test-role/.travis.yml b/integration-test-role/.travis.yml new file mode 100644 index 0000000..36bbf62 --- /dev/null +++ b/integration-test-role/.travis.yml @@ -0,0 +1,29 @@ +--- +language: python +python: "2.7" + +# Use the new container infrastructure +sudo: false + +# Install ansible +addons: + apt: + packages: + - python-pip + +install: + # Install ansible + - pip install ansible + + # Check ansible version + - ansible --version + + # Create ansible.cfg with correct roles_path + - printf '[defaults]\nroles_path=../' >ansible.cfg + +script: + # Basic role syntax check + - ansible-playbook tests/test.yml -i tests/inventory --syntax-check + +notifications: + webhooks: https://galaxy.ansible.com/api/v1/notifications/ \ No newline at end of file diff --git a/integration-test-role/.yamllint b/integration-test-role/.yamllint new file mode 100644 index 0000000..8827676 --- /dev/null +++ b/integration-test-role/.yamllint @@ -0,0 +1,33 @@ +--- +# Based on ansible-lint config +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 3 + level: error + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/integration-test-role/README.md b/integration-test-role/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/integration-test-role/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/integration-test-role/defaults/main.yml b/integration-test-role/defaults/main.yml new file mode 100644 index 0000000..bed7e6f --- /dev/null +++ b/integration-test-role/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for integration-test-role \ No newline at end of file diff --git a/integration-test-role/handlers/main.yml b/integration-test-role/handlers/main.yml new file mode 100644 index 0000000..542be35 --- /dev/null +++ b/integration-test-role/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for integration-test-role \ No newline at end of file diff --git a/integration-test-role/meta/main.yml b/integration-test-role/meta/main.yml new file mode 100644 index 0000000..227ad9c --- /dev/null +++ b/integration-test-role/meta/main.yml @@ -0,0 +1,53 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # 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: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.9 + + # 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. + \ No newline at end of file diff --git a/integration-test-role/molecule/default/INSTALL.rst b/integration-test-role/molecule/default/INSTALL.rst new file mode 100644 index 0000000..74b5cbf --- /dev/null +++ b/integration-test-role/molecule/default/INSTALL.rst @@ -0,0 +1,16 @@ +*************************************** +Hetzner Cloud plugin installation guide +*************************************** + +Requirements +============ + +* Ansible >= 2.9 +* ``HCLOUD_TOKEN`` exposed in your environment + +Install +======= + +.. code-block:: bash + + $ pip install molecule-hetznercloud diff --git a/integration-test-role/molecule/default/converge.yml b/integration-test-role/molecule/default/converge.yml new file mode 100644 index 0000000..cfd5e20 --- /dev/null +++ b/integration-test-role/molecule/default/converge.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + tasks: + - name: "Include integration-test-role" + include_role: + name: "integration-test-role" diff --git a/integration-test-role/molecule/default/create.yml b/integration-test-role/molecule/default/create.yml new file mode 100644 index 0000000..7e541dd --- /dev/null +++ b/integration-test-role/molecule/default/create.yml @@ -0,0 +1,85 @@ +--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + vars: + ssh_port: 22 + ssh_user: root + ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key" + tasks: + - name: Create SSH key + openssh_keypair: + path: "{{ ssh_path }}" + force: true + register: generated_ssh_key + + - name: Register the SSH key name + set_fact: + ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}" + + - name: Register SSH key for test instance(s) + hcloud_ssh_key: + name: "{{ ssh_key_name }}" + public_key: "{{ generated_ssh_key.public_key }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + state: present + + - name: Create molecule instance(s) + hcloud_server: + name: "{{ item.name }}" + server_type: "{{ item.server_type }}" + ssh_keys: + - "{{ ssh_key_name }}" + volumes: "{{ item.volumes | default(omit) }}" + image: "{{ item.image }}" + location: "{{ item.location | default(omit) }}" + datacenter: "{{ item.datacenter | default(omit) }}" + user_data: "{{ item.user_data | default(omit) }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + state: present + register: server + with_items: "{{ molecule_yml.platforms }}" + async: 7200 + poll: 0 + + - name: Wait for instance(s) creation to complete + async_status: + jid: "{{ item.ansible_job_id }}" + register: hetzner_jobs + until: hetzner_jobs.finished + retries: 300 + with_items: "{{ server.results }}" + + - name: Populate instance config dict + set_fact: + instance_conf_dict: { + 'instance': "{{ item.hcloud_server.name }}", + 'ssh_key_name': "{{ ssh_key_name }}", + 'address': "{{ item.hcloud_server.ipv4_address }}", + 'user': "{{ ssh_user }}", + 'port': "{{ ssh_port }}", + 'identity_file': "{{ ssh_path }}", } + with_items: "{{ hetzner_jobs.results }}" + register: instance_config_dict + when: server.changed | bool + + - name: Convert instance config dict to a list + set_fact: + instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" + when: server.changed | bool + + - name: Dump instance config + copy: + content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}" + dest: "{{ molecule_instance_config }}" + when: server.changed | bool + + - name: Wait for SSH + wait_for: + port: "{{ ssh_port }}" + host: "{{ item.address }}" + search_regex: SSH + delay: 10 + with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}" diff --git a/integration-test-role/molecule/default/destroy.yml b/integration-test-role/molecule/default/destroy.yml new file mode 100644 index 0000000..aae4ed6 --- /dev/null +++ b/integration-test-role/molecule/default/destroy.yml @@ -0,0 +1,56 @@ +--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + - name: Populate the instance config + block: + - name: Populate instance config from file + set_fact: + instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}" + skip_instances: false + rescue: + - name: Populate instance config when file missing + set_fact: + instance_conf: {} + skip_instances: true + + - name: Destroy molecule instance(s) + hcloud_server: + name: "{{ item.instance }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + state: absent + register: server + with_items: "{{ instance_conf }}" + when: not skip_instances + async: 7200 + poll: 0 + + - name: Wait for instance(s) deletion to complete + async_status: + jid: "{{ item.ansible_job_id }}" + register: hetzner_jobs + until: hetzner_jobs.finished + retries: 300 + with_items: "{{ server.results }}" + + - name: Remove registered SSH key + hcloud_ssh_key: + name: "{{ instance_conf[0].ssh_key_name }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + state: absent + when: + - not skip_instances + - instance_conf | length # must contain at least one instance + + - name: Populate instance config + set_fact: + instance_conf: {} + + - name: Dump instance config + copy: + content: "{{ instance_conf | molecule_to_yaml | molecule_header }}" + dest: "{{ molecule_instance_config }}" + when: server.changed | bool diff --git a/integration-test-role/molecule/default/molecule.yml b/integration-test-role/molecule/default/molecule.yml new file mode 100644 index 0000000..4be258a --- /dev/null +++ b/integration-test-role/molecule/default/molecule.yml @@ -0,0 +1,13 @@ +--- +dependency: + name: galaxy +driver: + name: hetznercloud +platforms: + - name: "molecule-hetznercloud-integration-test-${INSTANCE_UUID}" + server_type: cx11 + image: debian-10 +provisioner: + name: ansible +verifier: + name: ansible diff --git a/integration-test-role/molecule/default/prepare.yml b/integration-test-role/molecule/default/prepare.yml new file mode 100644 index 0000000..ddb01fb --- /dev/null +++ b/integration-test-role/molecule/default/prepare.yml @@ -0,0 +1,9 @@ +--- +- name: Prepare + hosts: all + gather_facts: false + tasks: + - name: Install python for Ansible + raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-zipstream) + become: true + changed_when: false diff --git a/integration-test-role/molecule/default/verify.yml b/integration-test-role/molecule/default/verify.yml new file mode 100644 index 0000000..a82dd6f --- /dev/null +++ b/integration-test-role/molecule/default/verify.yml @@ -0,0 +1,9 @@ +--- +# This is an example playbook to execute Ansible tests. + +- name: Verify + hosts: all + tasks: + - name: Example assertion + assert: + that: true diff --git a/integration-test-role/tasks/main.yml b/integration-test-role/tasks/main.yml new file mode 100644 index 0000000..a5f9474 --- /dev/null +++ b/integration-test-role/tasks/main.yml @@ -0,0 +1,2 @@ +--- +# tasks file for integration-test-role \ No newline at end of file diff --git a/integration-test-role/tests/inventory b/integration-test-role/tests/inventory new file mode 100644 index 0000000..878877b --- /dev/null +++ b/integration-test-role/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/integration-test-role/tests/test.yml b/integration-test-role/tests/test.yml new file mode 100644 index 0000000..d6b95c3 --- /dev/null +++ b/integration-test-role/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - integration-test-role \ No newline at end of file diff --git a/integration-test-role/vars/main.yml b/integration-test-role/vars/main.yml new file mode 100644 index 0000000..136497b --- /dev/null +++ b/integration-test-role/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for integration-test-role \ No newline at end of file diff --git a/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/playbook.yml b/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml similarity index 100% rename from molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/playbook.yml rename to molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml diff --git a/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml b/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml index 028fe83..d964d5b 100644 --- a/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml +++ b/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/create.yml @@ -11,10 +11,8 @@ ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key" tasks: - name: Create SSH key - user: - name: "{{ lookup('env', 'USER') }}" - generate_ssh_key: true - ssh_key_file: "{{ ssh_path }}" + openssh_keypair: + path: "{{ ssh_path }}" force: true register: generated_ssh_key @@ -25,7 +23,8 @@ - name: Register SSH key for test instance(s) hcloud_ssh_key: name: "{{ ssh_key_name }}" - public_key: "{{ generated_ssh_key.ssh_public_key }}" + public_key: "{{ generated_ssh_key.public_key }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" state: present - name: Create molecule instance(s) diff --git a/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml b/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml index 211bc67..afd44a2 100644 --- a/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml +++ b/molecule_hetznercloud/cookiecutter/scenario/driver/hetznercloud/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/destroy.yml @@ -40,6 +40,7 @@ - name: Remove registered SSH key hcloud_ssh_key: name: "{{ instance_conf[0].ssh_key_name }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" state: absent when: - not skip_instances diff --git a/setup.cfg b/setup.cfg index af9be31..d1790f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,11 +42,11 @@ classifiers = Topic :: Utilities keywords = ansible - roles - testing + hetznercloud molecule plugin - hetznercloud + roles + testing verifier [options] @@ -59,6 +59,7 @@ setup_requires = setuptools_scm >= 3.5.0 setuptools_scm_git_archive >= 1.1 install_requires = + hcloud >= 1.6.3, < 2 molecule >= 3.0.3, <= 3.1 pyyaml >= 5.3.1, < 6