From 5fe0f5703305f1114225f278913986a131220c5e Mon Sep 17 00:00:00 2001 From: Guillaume MARTINEZ Date: Wed, 2 Nov 2022 20:11:04 +0100 Subject: [PATCH] [Scaleway] Add module to manage function namespaces (#5415) * [Scaleway] Add module to manage function namespaces Signed-off-by: Lunik * rename short_descriptions Signed-off-by: Lunik * handle changed verification on hashed secret values Signed-off-by: Lunik * fix syntax for python 2.6 Signed-off-by: Lunik * fix missing argon2 in unittest Signed-off-by: Lunik * fix missing value on description field Signed-off-by: Lunik Signed-off-by: Lunik --- .github/BOTMETA.yml | 4 + meta/runtime.yml | 4 + plugins/module_utils/scaleway.py | 50 ++- .../scaleway/scaleway_function_namespace.py | 289 ++++++++++++++++++ .../scaleway_function_namespace_info.py | 142 +++++++++ .../scaleway_function_namespace/aliases | 6 + .../defaults/main.yml | 15 + .../tasks/main.yml | 260 ++++++++++++++++ .../scaleway_function_namespace_info/aliases | 6 + .../defaults/main.yml | 13 + .../tasks/main.yml | 43 +++ .../module_utils/cloud/test_scaleway.py | 128 ++++++++ tests/unit/requirements.txt | 3 + 13 files changed, 962 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/cloud/scaleway/scaleway_function_namespace.py create mode 100644 plugins/modules/cloud/scaleway/scaleway_function_namespace_info.py create mode 100644 tests/integration/targets/scaleway_function_namespace/aliases create mode 100644 tests/integration/targets/scaleway_function_namespace/defaults/main.yml create mode 100644 tests/integration/targets/scaleway_function_namespace/tasks/main.yml create mode 100644 tests/integration/targets/scaleway_function_namespace_info/aliases create mode 100644 tests/integration/targets/scaleway_function_namespace_info/defaults/main.yml create mode 100644 tests/integration/targets/scaleway_function_namespace_info/tasks/main.yml create mode 100644 tests/unit/plugins/module_utils/cloud/test_scaleway.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 4aaea6b773..5db3a6e6bd 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -468,6 +468,10 @@ files: maintainers: Lunik $modules/cloud/scaleway/scaleway_database_backup.py: maintainers: guillaume_ro_fr + $modules/cloud/scaleway/scaleway_function_namespace.py: + maintainers: Lunik + $modules/cloud/scaleway/scaleway_function_namespace_info.py: + maintainers: Lunik $modules/cloud/scaleway/scaleway_image_info.py: maintainers: Spredzy $modules/cloud/scaleway/scaleway_ip_info.py: diff --git a/meta/runtime.yml b/meta/runtime.yml index 350edaee11..9f9574afad 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1385,6 +1385,10 @@ plugin_routing: redirect: community.general.cloud.scaleway.scaleway_container_registry_info scaleway_database_backup: redirect: community.general.cloud.scaleway.scaleway_database_backup + scaleway_function_namespace: + redirect: community.general.cloud.scaleway.scaleway_function_namespace + scaleway_function_namespace_info: + redirect: community.general.cloud.scaleway.scaleway_function_namespace_info scaleway_image_facts: tombstone: removal_version: 3.0.0 diff --git a/plugins/module_utils/scaleway.py b/plugins/module_utils/scaleway.py index ee22d0f183..a44c52aa78 100644 --- a/plugins/module_utils/scaleway.py +++ b/plugins/module_utils/scaleway.py @@ -11,11 +11,21 @@ import re import sys import datetime import time +import traceback -from ansible.module_utils.basic import env_fallback +from ansible.module_utils.basic import env_fallback, missing_required_lib from ansible.module_utils.urls import fetch_url from ansible.module_utils.six.moves.urllib.parse import urlencode +SCALEWAY_SECRET_IMP_ERR = None +try: + from passlib.hash import argon2 + HAS_SCALEWAY_SECRET_PACKAGE = True +except Exception: + argon2 = None + SCALEWAY_SECRET_IMP_ERR = traceback.format_exc() + HAS_SCALEWAY_SECRET_PACKAGE = False + def scaleway_argument_spec(): return dict( @@ -80,6 +90,44 @@ def filter_sensitive_attributes(container, attributes): return container +class SecretVariables(object): + @staticmethod + def ensure_scaleway_secret_package(module): + if not HAS_SCALEWAY_SECRET_PACKAGE: + module.fail_json( + msg=missing_required_lib("passlib[argon2]", url='https://passlib.readthedocs.io/en/stable/'), + exception=SCALEWAY_SECRET_IMP_ERR + ) + + @staticmethod + def dict_to_list(source_dict): + return [ + dict(key=var[0], value=var[1]) + for var in source_dict.items() + ] + + @staticmethod + def list_to_dict(source_list, hashed=False): + key_value = 'hashed_value' if hashed else 'value' + return dict( + (var['key'], var[key_value]) + for var in source_list + ) + + @classmethod + def decode(cls, secrets_list, values_list): + secrets_dict = cls.list_to_dict(secrets_list, hashed=True) + values_dict = cls.list_to_dict(values_list, hashed=False) + for key in values_dict: + if key in secrets_dict: + if argon2.verify(values_dict[key], secrets_dict[key]): + secrets_dict[key] = values_dict[key] + else: + secrets_dict[key] = secrets_dict[key] + + return cls.dict_to_list(secrets_dict) + + def resource_attributes_should_be_changed(target, wished, verifiable_mutable_attributes, mutable_attributes): diff = dict() for attr in verifiable_mutable_attributes: diff --git a/plugins/modules/cloud/scaleway/scaleway_function_namespace.py b/plugins/modules/cloud/scaleway/scaleway_function_namespace.py new file mode 100644 index 0000000000..213666cdd3 --- /dev/null +++ b/plugins/modules/cloud/scaleway/scaleway_function_namespace.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Scaleway Serverless function namespace management module +# +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: scaleway_function_namespace +short_description: Scaleway Function namespace management +version_added: 6.0.0 +author: Guillaume MARTINEZ (@Lunik) +description: + - This module manages function namespaces on Scaleway account. +extends_documentation_fragment: + - community.general.scaleway + - community.general.scaleway_waitable_resource +requirements: + - passlib[argon2] >= 1.7.4 + +options: + state: + type: str + description: + - Indicate desired state of the function namespace. + default: present + choices: + - present + - absent + + project_id: + type: str + description: + - Project identifier. + required: true + + region: + type: str + description: + - Scaleway region to use (for example C(fr-par)). + required: true + choices: + - fr-par + - nl-ams + - pl-waw + + name: + type: str + description: + - Name of the function namespace. + required: true + + description: + description: + - Description of the function namespace. + type: str + default: '' + + environment_variables: + description: + - Environment variables of the function namespace. + - Injected in functions at runtime. + type: dict + + secret_environment_variables: + description: + - Secret environment variables of the function namespace. + - Updating thoses values will not output a C(changed) state in Ansible. + - Injected in functions at runtime. + type: dict +''' + +EXAMPLES = ''' +- name: Create a function namespace + community.general.scaleway_function_namespace: + project_id: '{{ scw_project }}' + state: present + region: fr-par + name: my-awesome-function-namespace + environment_variables: + MY_VAR: my_value + secret_environment_variables: + MY_SECRET_VAR: my_secret_value + register: function_namespace_creation_task + +- name: Make sure function namespace is deleted + community.general.scaleway_function_namespace: + project_id: '{{ scw_project }}' + state: absent + region: fr-par + name: my-awesome-function-namespace +''' + +RETURN = ''' +function_namespace: + description: The function namespace information. + returned: when I(state=present) + type: dict + sample: + description: "" + environment_variables: + MY_VAR: my_value + error_message: null + id: 531a1fd7-98d2-4a74-ad77-d398324304b8 + name: my-awesome-function-namespace + organization_id: e04e3bdc-015c-4514-afde-9389e9be24b0 + project_id: d44cea58-dcb7-4c95-bff1-1105acb60a98 + region: fr-par + registry_endpoint: "" + registry_namespace_id: "" + secret_environment_variables: + - key: MY_SECRET_VAR + value: $argon2id$v=19$m=65536,t=1,p=2$tb6UwSPWx/rH5Vyxt9Ujfw$5ZlvaIjWwNDPxD9Rdght3NarJz4IETKjpvAU3mMSmFg + status: pending +''' + +from copy import deepcopy + +from ansible_collections.community.general.plugins.module_utils.scaleway import ( + SCALEWAY_ENDPOINT, SCALEWAY_REGIONS, scaleway_argument_spec, Scaleway, + scaleway_waitable_resource_argument_spec, resource_attributes_should_be_changed, + SecretVariables +) +from ansible.module_utils.basic import AnsibleModule + + +STABLE_STATES = ( + "ready", + "absent" +) + +MUTABLE_ATTRIBUTES = ( + "description", + "environment_variables", + "secret_environment_variables", +) + + +def payload_from_wished_fn(wished_fn): + payload = { + "project_id": wished_fn["project_id"], + "name": wished_fn["name"], + "description": wished_fn["description"], + "environment_variables": wished_fn["environment_variables"], + "secret_environment_variables": SecretVariables.dict_to_list(wished_fn["secret_environment_variables"]) + } + + return payload + + +def absent_strategy(api, wished_fn): + changed = False + + fn_list = api.fetch_all_resources("namespaces") + fn_lookup = dict((fn["name"], fn) + for fn in fn_list) + + if wished_fn["name"] not in fn_lookup: + return changed, {} + + target_fn = fn_lookup[wished_fn["name"]] + changed = True + if api.module.check_mode: + return changed, {"status": "Function namespace would be destroyed"} + + api.wait_to_complete_state_transition(resource=target_fn, stable_states=STABLE_STATES, force_wait=True) + response = api.delete(path=api.api_path + "/%s" % target_fn["id"]) + if not response.ok: + api.module.fail_json(msg='Error deleting function namespace [{0}: {1}]'.format( + response.status_code, response.json)) + + api.wait_to_complete_state_transition(resource=target_fn, stable_states=STABLE_STATES) + return changed, response.json + + +def present_strategy(api, wished_fn): + changed = False + + fn_list = api.fetch_all_resources("namespaces") + fn_lookup = dict((fn["name"], fn) + for fn in fn_list) + + payload_fn = payload_from_wished_fn(wished_fn) + + if wished_fn["name"] not in fn_lookup: + changed = True + if api.module.check_mode: + return changed, {"status": "A function namespace would be created."} + + # Create function namespace + api.warn(payload_fn) + creation_response = api.post(path=api.api_path, + data=payload_fn) + + if not creation_response.ok: + msg = "Error during function namespace creation: %s: '%s' (%s)" % (creation_response.info['msg'], + creation_response.json['message'], + creation_response.json) + api.module.fail_json(msg=msg) + + api.wait_to_complete_state_transition(resource=creation_response.json, stable_states=STABLE_STATES) + response = api.get(path=api.api_path + "/%s" % creation_response.json["id"]) + return changed, response.json + + target_fn = fn_lookup[wished_fn["name"]] + decoded_target_fn = deepcopy(target_fn) + decoded_target_fn["secret_environment_variables"] = SecretVariables.decode(decoded_target_fn["secret_environment_variables"], + payload_fn["secret_environment_variables"]) + + patch_payload = resource_attributes_should_be_changed(target=decoded_target_fn, + wished=payload_fn, + verifiable_mutable_attributes=MUTABLE_ATTRIBUTES, + mutable_attributes=MUTABLE_ATTRIBUTES) + + if not patch_payload: + return changed, target_fn + + changed = True + if api.module.check_mode: + return changed, {"status": "Function namespace attributes would be changed."} + + fn_patch_response = api.patch(path=api.api_path + "/%s" % target_fn["id"], + data=patch_payload) + + if not fn_patch_response.ok: + api.module.fail_json(msg='Error during function namespace attributes update: [{0}: {1}]'.format( + fn_patch_response.status_code, fn_patch_response.json['message'])) + + api.wait_to_complete_state_transition(resource=target_fn, stable_states=STABLE_STATES) + response = api.get(path=api.api_path + "/%s" % target_fn["id"]) + return changed, response.json + + +state_strategy = { + "present": present_strategy, + "absent": absent_strategy +} + + +def core(module): + SecretVariables.ensure_scaleway_secret_package(module) + + region = module.params["region"] + wished_function_namespace = { + "state": module.params["state"], + "project_id": module.params["project_id"], + "name": module.params["name"], + "description": module.params['description'], + "environment_variables": module.params['environment_variables'], + "secret_environment_variables": module.params['secret_environment_variables'] + } + + api = Scaleway(module=module) + api.api_path = "functions/v1beta1/regions/%s/namespaces" % region + + changed, summary = state_strategy[wished_function_namespace["state"]](api=api, wished_fn=wished_function_namespace) + + module.exit_json(changed=changed, function_namespace=summary) + + +def main(): + argument_spec = scaleway_argument_spec() + argument_spec.update(scaleway_waitable_resource_argument_spec()) + argument_spec.update(dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + project_id=dict(type='str', required=True), + region=dict(type='str', required=True, choices=SCALEWAY_REGIONS), + name=dict(type='str', required=True), + description=dict(type='str', default=''), + environment_variables=dict(type='dict', default={}), + secret_environment_variables=dict(type='dict', default={}, no_log=True) + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/cloud/scaleway/scaleway_function_namespace_info.py b/plugins/modules/cloud/scaleway/scaleway_function_namespace_info.py new file mode 100644 index 0000000000..7e02e8e42d --- /dev/null +++ b/plugins/modules/cloud/scaleway/scaleway_function_namespace_info.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Scaleway Serverless function namespace info module +# +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: scaleway_function_namespace_info +short_description: Retrieve information on Scaleway Function namespace +version_added: 6.0.0 +author: Guillaume MARTINEZ (@Lunik) +description: + - This module return information about a function namespace on Scaleway account. +extends_documentation_fragment: + - community.general.scaleway + + +options: + project_id: + type: str + description: + - Project identifier. + required: true + + region: + type: str + description: + - Scaleway region to use (for example C(fr-par)). + required: true + choices: + - fr-par + - nl-ams + - pl-waw + + name: + type: str + description: + - Name of the function namespace. + required: true +''' + +EXAMPLES = ''' +- name: Get a function namespace info + community.general.scaleway_function_namespace_info: + project_id: '{{ scw_project }}' + region: fr-par + name: my-awesome-function-namespace + register: function_namespace_info_task +''' + +RETURN = ''' +function_namespace: + description: The function namespace information. + returned: always + type: dict + sample: + description: "" + environment_variables: + MY_VAR: my_value + error_message: null + id: 531a1fd7-98d2-4a74-ad77-d398324304b8 + name: my-awesome-function-namespace + organization_id: e04e3bdc-015c-4514-afde-9389e9be24b0 + project_id: d44cea58-dcb7-4c95-bff1-1105acb60a98 + region: fr-par + registry_endpoint: "" + registry_namespace_id: "" + secret_environment_variables: + - key: MY_SECRET_VAR + value: $argon2id$v=19$m=65536,t=1,p=2$tb6UwSPWx/rH5Vyxt9Ujfw$5ZlvaIjWwNDPxD9Rdght3NarJz4IETKjpvAU3mMSmFg + status: pending +''' + +from ansible_collections.community.general.plugins.module_utils.scaleway import ( + SCALEWAY_ENDPOINT, SCALEWAY_REGIONS, scaleway_argument_spec, Scaleway +) +from ansible.module_utils.basic import AnsibleModule + + +def info_strategy(api, wished_fn): + fn_list = api.fetch_all_resources("namespaces") + fn_lookup = dict((fn["name"], fn) + for fn in fn_list) + + if wished_fn["name"] not in fn_lookup: + msg = "Error during function namespace lookup: Unable to find function namespace named '%s' in project '%s'" % (wished_fn["name"], + wished_fn["project_id"]) + + api.module.fail_json(msg=msg) + + target_fn = fn_lookup[wished_fn["name"]] + + response = api.get(path=api.api_path + "/%s" % target_fn["id"]) + if not response.ok: + msg = "Error during function namespace lookup: %s: '%s' (%s)" % (response.info['msg'], + response.json['message'], + response.json) + api.module.fail_json(msg=msg) + + return response.json + + +def core(module): + region = module.params["region"] + wished_function_namespace = { + "project_id": module.params["project_id"], + "name": module.params["name"] + } + + api = Scaleway(module=module) + api.api_path = "functions/v1beta1/regions/%s/namespaces" % region + + summary = info_strategy(api=api, wished_fn=wished_function_namespace) + + module.exit_json(changed=False, function_namespace=summary) + + +def main(): + argument_spec = scaleway_argument_spec() + argument_spec.update(dict( + project_id=dict(type='str', required=True), + region=dict(type='str', required=True, choices=SCALEWAY_REGIONS), + name=dict(type='str', required=True) + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/scaleway_function_namespace/aliases b/tests/integration/targets/scaleway_function_namespace/aliases new file mode 100644 index 0000000000..a5ac5181f0 --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace/aliases @@ -0,0 +1,6 @@ +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +cloud/scaleway +unsupported diff --git a/tests/integration/targets/scaleway_function_namespace/defaults/main.yml b/tests/integration/targets/scaleway_function_namespace/defaults/main.yml new file mode 100644 index 0000000000..399d0ea1a3 --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace/defaults/main.yml @@ -0,0 +1,15 @@ +--- +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +scaleway_region: fr-par +name: fn-ansible-test +description: Function namespace used for testing scaleway_function_namespace ansible module +updated_description: Function namespace used for testing scaleway_function_namespace ansible module (Updated description) +environment_variables: + MY_VAR: my_value +secret_environment_variables: + MY_SECRET_VAR: my_secret_value +updated_secret_environment_variables: + MY_SECRET_VAR: my_other_secret_value diff --git a/tests/integration/targets/scaleway_function_namespace/tasks/main.yml b/tests/integration/targets/scaleway_function_namespace/tasks/main.yml new file mode 100644 index 0000000000..e760a4c7fa --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace/tasks/main.yml @@ -0,0 +1,260 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a function namespace (Check) + check_mode: yes + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_creation_check_task + +- ansible.builtin.debug: + var: fn_creation_check_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_creation_check_task is success + - fn_creation_check_task is changed + +- name: Create function_namespace + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_creation_task + +- ansible.builtin.debug: + var: fn_creation_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_creation_task is success + - fn_creation_task is changed + - fn_creation_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Create function namespace (Confirmation) + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_creation_confirmation_task + +- ansible.builtin.debug: + var: fn_creation_confirmation_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_creation_confirmation_task is success + - fn_creation_confirmation_task is not changed + - fn_creation_confirmation_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Update function namespace (Check) + check_mode: yes + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ updated_description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_update_check_task + +- ansible.builtin.debug: + var: fn_update_check_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_check_task is success + - fn_update_check_task is changed + +- name: Update function namespace + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ updated_description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_update_task + +- ansible.builtin.debug: + var: fn_update_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_task is success + - fn_update_task is changed + - fn_update_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Update function namespace (Confirmation) + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ updated_description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ secret_environment_variables }}' + register: fn_update_confirmation_task + +- ansible.builtin.debug: + var: fn_update_confirmation_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_confirmation_task is success + - fn_update_confirmation_task is not changed + - fn_update_confirmation_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Update function namespace secret variables (Check) + check_mode: yes + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ updated_secret_environment_variables }}' + register: fn_update_secret_check_task + +- ansible.builtin.debug: + var: fn_update_secret_check_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_secret_check_task is success + - fn_update_secret_check_task is changed + +- name: Update function namespace secret variables + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ updated_secret_environment_variables }}' + register: fn_update_secret_task + +- ansible.builtin.debug: + var: fn_update_secret_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_secret_task is success + - fn_update_secret_task is changed + - fn_update_secret_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Update function namespace secret variables (Confirmation) + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + environment_variables: '{{ environment_variables }}' + secret_environment_variables: '{{ updated_secret_environment_variables }}' + register: fn_update_secret_confirmation_task + +- ansible.builtin.debug: + var: fn_update_secret_confirmation_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_update_secret_confirmation_task is success + - fn_update_secret_confirmation_task is not changed + - fn_update_secret_confirmation_task.function_namespace.status == "ready" + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Delete function namespace (Check) + check_mode: yes + community.general.scaleway_function_namespace: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + project_id: '{{ scw_project }}' + register: fn_deletion_check_task + +- ansible.builtin.debug: + var: fn_deletion_check_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_deletion_check_task is success + - fn_deletion_check_task is changed + +- name: Delete function namespace + community.general.scaleway_function_namespace: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + project_id: '{{ scw_project }}' + register: fn_deletion_task + +- ansible.builtin.debug: + var: fn_deletion_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_deletion_task is success + - fn_deletion_task is changed + - "'hashed_value' in fn_creation_task.function_namespace.secret_environment_variables[0]" + +- name: Delete function namespace (Confirmation) + community.general.scaleway_function_namespace: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + project_id: '{{ scw_project }}' + register: fn_deletion_confirmation_task + +- ansible.builtin.debug: + var: fn_deletion_confirmation_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_deletion_confirmation_task is success + - fn_deletion_confirmation_task is not changed diff --git a/tests/integration/targets/scaleway_function_namespace_info/aliases b/tests/integration/targets/scaleway_function_namespace_info/aliases new file mode 100644 index 0000000000..a5ac5181f0 --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace_info/aliases @@ -0,0 +1,6 @@ +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +cloud/scaleway +unsupported diff --git a/tests/integration/targets/scaleway_function_namespace_info/defaults/main.yml b/tests/integration/targets/scaleway_function_namespace_info/defaults/main.yml new file mode 100644 index 0000000000..0b05eaac03 --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace_info/defaults/main.yml @@ -0,0 +1,13 @@ +--- +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +scaleway_region: fr-par +name: fn-ansible-test +description: Function namespace used for testing scaleway_function_namespace_info ansible module +updated_description: Function namespace used for testing scaleway_function_namespace_info ansible module (Updated description) +environment_variables: + MY_VAR: my_value +secret_environment_variables: + MY_SECRET_VAR: my_secret_value diff --git a/tests/integration/targets/scaleway_function_namespace_info/tasks/main.yml b/tests/integration/targets/scaleway_function_namespace_info/tasks/main.yml new file mode 100644 index 0000000000..793cd09239 --- /dev/null +++ b/tests/integration/targets/scaleway_function_namespace_info/tasks/main.yml @@ -0,0 +1,43 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright (c) 2022, Guillaume MARTINEZ +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create function_namespace + community.general.scaleway_function_namespace: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + description: '{{ description }}' + secret_environment_variables: '{{ secret_environment_variables }}' + +- name: Get function namespace info + community.general.scaleway_function_namespace_info: + name: '{{ name }}' + region: '{{ scaleway_region }}' + project_id: '{{ scw_project }}' + register: fn_info_task + +- ansible.builtin.debug: + var: fn_info_task + +- name: Check module call result + ansible.builtin.assert: + that: + - fn_info_task is success + - fn_info_task is not changed + - "'hashed_value' in fn_info_task.function_namespace.secret_environment_variables[0]" + +- name: Delete function namespace + community.general.scaleway_function_namespace: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + project_id: '{{ scw_project }}' diff --git a/tests/unit/plugins/module_utils/cloud/test_scaleway.py b/tests/unit/plugins/module_utils/cloud/test_scaleway.py new file mode 100644 index 0000000000..3dbd7fbbbc --- /dev/null +++ b/tests/unit/plugins/module_utils/cloud/test_scaleway.py @@ -0,0 +1,128 @@ +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import random + +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible_collections.community.general.plugins.module_utils.scaleway import SecretVariables, argon2 + + +class SecretVariablesTestCase(unittest.TestCase): + def test_dict_to_list(self): + source = dict( + attribute1="value1", + attribute2="value2" + ) + expect = [ + dict(key="attribute1", value="value1"), + dict(key="attribute2", value="value2") + ] + + result = SecretVariables.dict_to_list(source) + result = sorted(result, key=lambda el: el['key']) + self.assertEqual(result, expect) + + def test_list_to_dict(self): + source = [ + dict(key="secret1", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc"), + dict(key="secret2", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI") + ] + expect = dict( + secret1="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc", + secret2="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI" + ) + + self.assertEqual(SecretVariables.list_to_dict(source, hashed=True), expect) + + def test_list_to_dict(self): + source = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="value2") + ] + expect = dict( + secret1="value1", + secret2="value2" + ) + + self.assertEqual(SecretVariables.list_to_dict(source, hashed=False), expect) + + @unittest.skipIf(argon2 is None, "Missing required 'argon2' library") + def test_decode_full(self): + source_secret = [ + dict(key="secret1", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc"), + dict(key="secret2", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI"), + ] + source_value = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="value2"), + ] + + expect = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="value2"), + ] + + result = SecretVariables.decode(source_secret, source_value) + result = sorted(result, key=lambda el: el['key']) + self.assertEqual(result, expect) + + @unittest.skipIf(argon2 is None, "Missing required 'argon2' library") + def test_decode_dict_divergent_values(self): + source_secret = [ + dict(key="secret1", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc"), + dict(key="secret2", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI"), + ] + source_value = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="diverged_value2"), + ] + + expect = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI"), + ] + + result = SecretVariables.decode(source_secret, source_value) + result = sorted(result, key=lambda el: el['key']) + self.assertEqual(result, expect) + + @unittest.skipIf(argon2 is None, "Missing required 'argon2' library") + def test_decode_dict_missing_values_left(self): + source_secret = [ + dict(key="secret1", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc"), + ] + source_value = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="value2"), + ] + + expect = [ + dict(key="secret1", value="value1"), + ] + + result = SecretVariables.decode(source_secret, source_value) + result = sorted(result, key=lambda el: el['key']) + self.assertEqual(result, expect) + + @unittest.skipIf(argon2 is None, "Missing required 'argon2' library") + def test_decode_dict_missing_values_right(self): + source_secret = [ + dict(key="secret1", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$NuZk+6UATHNFV78nFRXFvA$3kivcXfzNHI1c/4ZBpP8BeBSGhhI82NfOh4Dd48JJgc"), + dict(key="secret2", hashed_value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI"), + ] + source_value = [ + dict(key="secret1", value="value1"), + ] + + expect = [ + dict(key="secret1", value="value1"), + dict(key="secret2", value="$argon2id$v=19$m=65536,t=1,p=2$etGO/Z8ImYDeKr6uFsyPAQ$FbL5+hG/duDEpa8UCYqXpEUQ5EacKg6i2iAs+Dq4dAI"), + ] + + result = SecretVariables.decode(source_secret, source_value) + result = sorted(result, key=lambda el: el['key']) + self.assertEqual(result, expect) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 788fa4cb6c..0aa7c1fc9f 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -41,3 +41,6 @@ dataclasses ; python_version == '3.6' # requirement for the elastic callback plugin elastic-apm ; python_version >= '3.6' + +# requirements for scaleway modules +passlib[argon2] \ No newline at end of file