commit ebc499acbf9f4c95dc67e6620e8943c5cef5d39b Author: Nicolas Massé Date: Mon Apr 23 18:23:23 2018 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8b42eb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.retry diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3153fe0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Nicolas MASSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..c1475af --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,8 @@ +--- +threescale_cicd_openapi_file_format: YAML +threescale_cicd_delay: 10 +threescale_cicd_retries: 50 +threescale_cicd_default_application_name: 'Ansible smoke-tests default application' +threescale_cicd_default_application_description: 'This app is used to run smoke tests during the deployment phase. It will be automatically recreated if you delete it.' +threescale_cicd_staging_environment_name: sandbox +threescale_cicd_production_environment_name: production diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..cb9a817 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,14 @@ +galaxy_info: + author: Nicolas Massé + description: Enables CI/CD with 3scale API Management Platform + company: Red Hat + license: MIT + min_ansible_version: 2.4 + galaxy_tags: + - 3scale + - threescale + - 3scale-amp + - api-management + - continuous-deployment + +dependencies: [] diff --git a/tasks/create_application_plans.yml b/tasks/create_application_plans.yml new file mode 100644 index 0000000..347b51b --- /dev/null +++ b/tasks/create_application_plans.yml @@ -0,0 +1,34 @@ +--- +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_tmp_plan }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- name: Update the application plan + uri: + url: 'https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/application_plans/{{ (threescale_cicd_existing_application_plans_details|selectattr("system_name", "equalto", threescale_cicd_tmp_plan.system_name)|first).id }}.json' + validate_certs: no + method: PUT + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 200 + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_tmp_plan.system_name in threescale_cicd_existing_application_plans' + +- name: Create the application plan + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/application_plans.json + validate_certs: no + method: POST + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 201 + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_tmp_plan.system_name not in threescale_cicd_existing_application_plans' + +- set_fact: + threescale_cicd_existing_application_plans: '{{ threescale_cicd_existing_application_plans|union([ threescale_cicd_tmp_plan.system_name ]) }}' + threescale_cicd_existing_application_plans_details: '{{ threescale_cicd_existing_application_plans_details|union([{ "system_name": threescale_cicd_tmp_plan.system_name, "id": threescale_cicd_tmpresponse.json.application_plan.id }]) }}' + when: 'threescale_cicd_tmp_plan.system_name not in threescale_cicd_existing_application_plans' diff --git a/tasks/create_default_application.yml b/tasks/create_default_application.yml new file mode 100644 index 0000000..9593ac1 --- /dev/null +++ b/tasks/create_default_application.yml @@ -0,0 +1,80 @@ +--- + +- name: Get the default (first) account + uri: + url: https://{{ inventory_hostname }}/admin/api/accounts.json?access_token={{ threescale_cicd_access_token|urlencode }}&state=approved&page=1&per_page=1 + validate_certs: no + register: threescale_cicd_tmp_allaccounts + when: 'threescale_cicd_default_account_id is not defined' + +- set_fact: + threescale_cicd_default_account_id: '{{ threescale_cicd_tmp_allaccounts.json.accounts[0].account.id }}' + when: 'threescale_cicd_default_account_id is not defined' + +- name: Pick the first given application plan if no default application plan is given + set_fact: + threescale_cicd_default_application_plan: '{{ (threescale_cicd_application_plans|first).system_name }}' + when: 'threescale_cicd_default_application_plan is not defined and threescale_cicd_application_plans is defined and threescale_cicd_application_plans|length > 0' + +- name: Find the application plan id + set_fact: + threescale_cicd_default_application_plan_id: '{{ (threescale_cicd_existing_application_plans_details|selectattr("system_name", "equalto", threescale_cicd_default_application_plan)|first).id }}' + when: 'threescale_cicd_default_application_plan is defined' + +- name: Compute the appid for the default application + set_fact: + # The default appid is a SHA1 hash of the application name, api name and salted with the 3scale access token so that it cannot be guessed + threescale_cicd_default_application_appid: '{{ (threescale_cicd_default_application_name ~ threescale_cicd_api_system_name ~ threescale_cicd_access_token)|hash(''sha1'') }}' + when: 'threescale_cicd_default_application_appid is not defined' + +- set_fact: + threescale_cicd_tmp_search_criteria: 'app_id' + when: 'threescale_cicd_api_security_scheme.type == ''oauth2''' + +- set_fact: + threescale_cicd_tmp_search_criteria: 'user_key' + when: 'threescale_cicd_api_security_scheme.type == ''apiKey''' + +- name: Check if the default application exists + uri: + url: 'https://{{ inventory_hostname }}/admin/api/applications/find.json?access_token={{ threescale_cicd_access_token|urlencode }}&{{ threescale_cicd_tmp_search_criteria }}={{ threescale_cicd_default_application_appid|urlencode }}' + validate_certs: no + method: GET + status_code: 200,404 + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_default_application_id is not defined and threescale_cicd_default_application_appid is defined' + +- set_fact: + threescale_cicd_default_application_id: '{{ threescale_cicd_tmpresponse.json.application.id }}' + when: 'threescale_cicd_default_application_id is not defined and threescale_cicd_default_application_appid is defined and threescale_cicd_tmpresponse.status == 200' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ "access_token=" ~ (threescale_cicd_access_token|urlencode) ~ "&plan_id=" ~ threescale_cicd_default_application_plan_id ~ "&name=" ~ (threescale_cicd_default_application_name|urlencode) ~ "&description=" ~ (threescale_cicd_default_application_description|urlencode) ~ "&" ~ threescale_cicd_tmp_search_criteria ~ "=" ~ (threescale_cicd_default_application_appid|urlencode) }}' + +- name: Create the application + uri: + url: https://{{ inventory_hostname }}/admin/api/accounts/{{ threescale_cicd_default_account_id }}/applications.json + validate_certs: no + method: POST + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 201 + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_default_application_id is not defined' + +- set_fact: + threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}' + when: 'threescale_cicd_default_application_id is not defined' + +- name: Update the application + uri: + url: https://{{ inventory_hostname }}/admin/api/accounts/{{ threescale_cicd_default_account_id }}/applications/{{ threescale_cicd_default_application_id }}.json + validate_certs: no + method: PUT + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 200 + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_default_application_id is defined' + +- set_fact: + threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}' + when: 'threescale_cicd_default_application_id is defined' diff --git a/tasks/create_mapping_rule.yml b/tasks/create_mapping_rule.yml new file mode 100644 index 0000000..9234cc7 --- /dev/null +++ b/tasks/create_mapping_rule.yml @@ -0,0 +1,24 @@ +--- + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_tmp_wanted_mapping_rules[threescale_cicd_tmp_mapping_rule_to_create] }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- set_fact: + # Add the metric_id to the payload + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ "metric_id=" ~ ((threescale_cicd_existing_metrics_details|selectattr("system_name", "equalto", threescale_cicd_tmp_mapping_rule_to_create)|first).id|urlencode) }}' + +- name: Create the mapping rule + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules.json + validate_certs: no + method: POST + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 201 + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 201' diff --git a/tasks/create_service.yml b/tasks/create_service.yml new file mode 100644 index 0000000..031a867 --- /dev/null +++ b/tasks/create_service.yml @@ -0,0 +1,51 @@ +--- + +- set_fact: + threescale_cicd_tmp_body_create_svc: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_create_svc: '{{ threescale_cicd_tmp_body_create_svc ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_api_service_definition }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- name: Create the service + uri: + url: https://{{ inventory_hostname }}/admin/api/services.json + validate_certs: no + method: POST + body: '{{ threescale_cicd_tmp_body_create_svc }}' + status_code: 201,422 + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 201' + +- set_fact: + threescale_cicd_api_service_id: '{{ threescale_cicd_tmpresponse.json.service.id }}' + when: 'threescale_cicd_tmpresponse.status == 201' + +- name: Retrieve existing Services from the 3scale Admin Portal + uri: + url: "https://{{ inventory_hostname }}/admin/api/services.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + register: threescale_cicd_tmp_allservices + when: 'threescale_cicd_tmpresponse.status == 422' + +- set_fact: + threescale_cicd_existing_services: '{{ threescale_cicd_tmp_allservices.json|json_query(''services[*].service.system_name'') }}' + threescale_cicd_existing_services_details: '{{ threescale_cicd_tmp_allservices.json|json_query(''services[].{"system_name": service.system_name, "id": service.id}'') }}' + when: 'threescale_cicd_tmpresponse.status == 422' + +- set_fact: + threescale_cicd_api_service_id: '{{ (threescale_cicd_existing_services_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first)[''id''] }}' + when: 'threescale_cicd_tmpresponse.status == 422' + +- name: Update the service + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}.json + validate_certs: no + method: PUT + body: '{{ threescale_cicd_tmp_body_create_svc }}' + status_code: 200 + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200' + when: 'threescale_cicd_tmpresponse.status == 422' diff --git a/tasks/delete_unused_metrics.yml b/tasks/delete_unused_metrics.yml new file mode 100644 index 0000000..2bb815e --- /dev/null +++ b/tasks/delete_unused_metrics.yml @@ -0,0 +1,21 @@ +--- +- set_fact: + threescale_cicd_tmp_metrics_to_delete: [] + +- set_fact: + threescale_cicd_tmp_metrics_to_delete: '{{ threescale_cicd_tmp_metrics_to_delete|union([threescale_cicd_tmp_metric.id]) }}' + with_items: '{{ threescale_cicd_existing_metrics_details }}' + loop_control: + loop_var: threescale_cicd_tmp_metric + when: 'threescale_cicd_tmp_metric.system_name != "hits" and threescale_cicd_tmp_metric.system_name not in threescale_cicd_api_operations' + +- name: Delete the method + uri: + url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics/{{ threescale_cicd_metric_id }}/methods/{{ threescale_cicd_tmp_metric }}.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + method: DELETE + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200' + with_items: '{{ threescale_cicd_tmp_metrics_to_delete }}' + loop_control: + loop_var: threescale_cicd_tmp_metric diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..cd1e6d1 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,126 @@ +--- + +- name: Ensure pre-requisites are met + assert: + that: + - "threescale_cicd_access_token is defined" + - "threescale_cicd_openapi_file is defined" + msg: |- + This module requires at least two variables: + - threescale_cicd_access_token that contains an Access Token with Read/Write privileges on the 3scale Account Management API. This variable is usually set in your inventory file. + - threescale_cicd_openapi_file that is the path to the OpenAPI file you want to deploy in 3scale. This variable is usually passed as an extra variable (-e threescale_cicd_openapi_file=...) + +- name: Set the threescale_cicd_sso_issuer_endpoint variable from the inventory + set_fact: + threescale_cicd_sso_issuer_endpoint: '{{ (hostvars[groups[''sso''][0]].scheme|default(''https'')) ~ ''://'' ~ hostvars[groups[''sso''][0]].client_id ~ '':'' ~ hostvars[groups[''sso''][0]].client_secret ~ ''@'' ~ groups[''sso''][0] ~ ''/auth/realms/'' ~ hostvars[groups[''sso''][0]].realm }}' + when: 'threescale_cicd_sso_issuer_endpoint is not defined and ''sso'' in groups and groups[''sso''] > 0' + +- name: Set the threescale_cicd_apicast_sandbox_endpoint variable from the inventory + set_fact: + threescale_cicd_apicast_sandbox_endpoint: '{{ (hostvars[groups[''apicast-sandbox''][0]].scheme|default(''https'')) ~ ''://'' ~ groups[''apicast-sandbox''][0] }}' + when: 'threescale_cicd_apicast_sandbox_endpoint is not defined and ''apicast-sandbox'' in groups and groups[''apicast-sandbox''] > 0' + +- name: Set the threescale_cicd_apicast_production_endpoint variable from the inventory + set_fact: + threescale_cicd_apicast_production_endpoint: '{{ (hostvars[groups[''apicast-production''][0]].scheme|default(''https'')) ~ ''://'' ~ groups[''apicast-production''][0] }}' + when: 'threescale_cicd_apicast_production_endpoint is not defined and ''apicast-production'' in groups and groups[''apicast-production''] > 0' + +# Load the API definition from the provided OpenAPI file +- import_tasks: read_openapi_file.yml + +# Retrieve existing services from the 3scale Admin Portal +- import_tasks: retrieve_existing_services.yml + +- name: Compute the service system_name + set_fact: + threescale_cicd_api_system_name: '{{ threescale_cicd_api_environment_name ~ "_" ~ threescale_cicd_api_system_name }}' + when: 'threescale_cicd_api_environment_name is defined' + +- debug: + msg: "Will work on service with system_name = {{ threescale_cicd_api_system_name }}" + +- set_fact: + threescale_cicd_api_deployment_type: 'self_managed' + when: 'threescale_cicd_api_deployment_type is not defined and (threescale_cicd_apicast_sandbox_endpoint is defined or threescale_cicd_apicast_production_endpoint is defined)' + +- set_fact: + threescale_cicd_api_deployment_type: 'hosted' + when: 'threescale_cicd_api_deployment_type is not defined' + +- set_fact: + threescale_cicd_api_service_definition: + name: '{{ threescale_cicd_api_name }}' + deployment_option: '{{ threescale_cicd_api_deployment_type }}' + system_name: '{{ threescale_cicd_api_system_name }}' + backend_version: '{{ threescale_cicd_api_backend_version }}' + +# Create the service definition +- import_tasks: create_service.yml + +- set_fact: + threescale_cicd_api_credentials_location: '{{ ''headers'' if threescale_cicd_api_security_scheme.in == ''header'' else threescale_cicd_api_security_scheme.in }}' + when: 'threescale_cicd_api_security_scheme.type == ''apiKey''' + +- set_fact: + threescale_cicd_api_credentials_location: 'headers' + when: 'threescale_cicd_api_security_scheme.type == ''oauth2''' + +- set_fact: + threescale_cicd_api_proxy_definition: + credentials_location: '{{ threescale_cicd_api_credentials_location }}' + api_backend: '{{ threescale_cicd_api_backend_scheme ~ ''://'' ~ threescale_cicd_api_backend_hostname }}' + +- set_fact: + threescale_cicd_api_proxy_definition: '{{ threescale_cicd_api_proxy_definition|combine({ ''auth_user_key'': threescale_cicd_api_security_scheme.name }) }}' + when: 'threescale_cicd_api_security_scheme.type == ''apiKey''' + +- set_fact: + threescale_cicd_api_proxy_definition: '{{ threescale_cicd_api_proxy_definition|combine({ ''sandbox_endpoint'': threescale_cicd_apicast_sandbox_endpoint }) }}' + when: 'threescale_cicd_apicast_sandbox_endpoint is defined' + +- set_fact: + threescale_cicd_api_proxy_definition: '{{ threescale_cicd_api_proxy_definition|combine({ ''endpoint'': threescale_cicd_apicast_production_endpoint }) }}' + when: 'threescale_cicd_apicast_production_endpoint is defined' + +# Update the proxy +- import_tasks: update_proxy.yml + +# Update the metrics +- import_tasks: update_metrics.yml + +# Update the mapping rules +- import_tasks: update_mapping_rules.yml + +- name: Get the list of existing application plans + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/application_plans.json?access_token={{ threescale_cicd_access_token|urlencode }} + validate_certs: no + register: threescale_cicd_tmpresponse + +- set_fact: + threescale_cicd_existing_application_plans: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[*].application_plan.system_name'') }}' + threescale_cicd_existing_application_plans_details: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[].{"system_name": application_plan.system_name, "id": application_plan.id}'') }}' + +# Create application plans if needed +- include_tasks: create_application_plans.yml + with_items: '{{ threescale_cicd_application_plans|default([]) }}' + loop_control: + loop_var: threescale_cicd_tmp_plan + +# Run smoke tests on the staging gateway +- include_tasks: smoke_tests.yml + vars: + threescale_cicd_env: staging + when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined' + +# Promote to production +- import_tasks: promote.yml + +# Run smoke tests on the production gateway +- include_tasks: smoke_tests.yml + vars: + threescale_cicd_env: production + when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined' + +# Delete the metrics that are not needed anymore +- import_tasks: delete_unused_metrics.yml diff --git a/tasks/promote.yml b/tasks/promote.yml new file mode 100644 index 0000000..105b18d --- /dev/null +++ b/tasks/promote.yml @@ -0,0 +1,32 @@ +--- + +- name: Get the version of the staging proxy definition + uri: + url: 'https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/configs/{{ threescale_cicd_staging_environment_name }}/latest.json?access_token={{ threescale_cicd_access_token|urlencode }}' + validate_certs: no + register: threescale_cicd_tmpresponse + +- set_fact: + threescale_cicd_tmp_staging_proxy_version: '{{ threescale_cicd_tmpresponse.json.proxy_config.version }}' + +- name: Get the version of the production proxy definition + uri: + url: 'https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/configs/{{ threescale_cicd_production_environment_name }}/latest.json?access_token={{ threescale_cicd_access_token|urlencode }}' + validate_certs: no + register: threescale_cicd_tmpresponse + +- set_fact: + threescale_cicd_tmp_production_proxy_version: '{{ threescale_cicd_tmpresponse.json.proxy_config.version }}' + +- set_fact: + threescale_cicd_tmp_body_create_svc: 'access_token={{ threescale_cicd_access_token|urlencode }}&to={{ threescale_cicd_production_environment_name|urlencode }}' + +- name: Promote to production + uri: + url: 'https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/configs/{{ threescale_cicd_staging_environment_name }}/{{ threescale_cicd_tmp_staging_proxy_version }}/promote.json' + body: '{{ threescale_cicd_tmp_body_create_svc }}' + status_code: 201 + validate_certs: no + method: POST + register: threescale_cicd_tmpresponse + when: 'threescale_cicd_tmp_staging_proxy_version != threescale_cicd_tmp_production_proxy_version' diff --git a/tasks/read_openapi_file.yml b/tasks/read_openapi_file.yml new file mode 100644 index 0000000..f55f4ac --- /dev/null +++ b/tasks/read_openapi_file.yml @@ -0,0 +1,143 @@ +--- +- name: Parse the OpenAPI file (YAML format) + set_fact: + threescale_cicd_openapi_file_content: '{{ lookup(''file'', threescale_cicd_openapi_file) |from_yaml }}' + when: "threescale_cicd_openapi_file_format|upper == 'YAML'" + +- name: Parse the OpenAPI file (JSON format) + set_fact: + threescale_cicd_openapi_file_content: '{{ lookup(''file'', threescale_cicd_openapi_file) |from_json }}' + when: "threescale_cicd_openapi_file_format|upper == 'JSON'" + +- name: Extract the OpenAPI format version + set_fact: + threescale_cicd_openapi_file_version: '{{ threescale_cicd_openapi_file_content|json_query(''swagger'') }}' + +- name: Check the OpenAPI format version + assert: + that: + - "threescale_cicd_openapi_file_version == '2.0'" + msg: "Currently only the OpenAPI/Swagger 2.0 is handled. If needed, fill an issue or submit a pull request!" + +# TODO rewrite this in a more "Ansible compatible" way +- name: Extract API Methods + set_fact: + threescale_cicd_api_name: '{{ threescale_cicd_openapi_file_content.info.title|default("API") }}' + threescale_cicd_api_basepath: '{{ threescale_cicd_openapi_file_content.basePath|default("") }}' + threescale_cicd_api_operations: >- + {% set operations = {} -%} + {% if 'paths' in threescale_cicd_openapi_file_content -%} + {% for path, verbs in threescale_cicd_openapi_file_content['paths'].items() -%} + {% if path.startswith('/') -%} + {% for verb, method_description in verbs.items() -%} + {% if verb != '$ref' and verb != 'parameters' -%} + {% if 'operationId' in method_description -%} + {% set operation_id = method_description['operationId'] -%} + {% else -%} + {% set operation_id = verb.upper() + path -%} + {% endif -%} + {% set operation_id = operation_id|regex_replace('[^0-9a-zA-Z_]+', '_') -%} + {% set operation = { operation_id: { 'path': path, 'verb': verb } } -%} + {% if 'summary' in method_description -%} + {% if operation[operation_id].update({ 'friendly_name': method_description.summary }) -%}{% endif -%} + {% endif -%} + {% if 'description' in method_description -%} + {% if operation[operation_id].update({ 'description': method_description.description }) -%}{% endif -%} + {% endif -%} + {% if operations.update(operation) -%}{% endif -%} + {% endif -%} + {% endfor -%} + {% endif -%} + {% endfor -%} + {% endif -%} + {{ operations }} + +- name: Extract the wanted system_name from OpenAPI + set_fact: + threescale_cicd_api_system_name: '{{ threescale_cicd_openapi_file_content.info[''x-threescale-system-name''] }}' + when: 'threescale_cicd_api_system_name is not defined and ''x-threescale-system-name'' in threescale_cicd_openapi_file_content.info' + +- name: Generate a system_name from the API title + set_fact: + threescale_cicd_api_system_name: '{{ threescale_cicd_openapi_file_content.info[''title'']|default(''api'')|regex_replace(''[^a-zA-Z0-9_]+'', ''_'') }}' + when: 'threescale_cicd_api_system_name is not defined' + +- name: Extract the security definitions and requirements from OpenAPI + set_fact: + threescale_cicd_api_security_requirements: '{{ threescale_cicd_openapi_file_content.security|default({}) }}' + threescale_cicd_api_security_definitions: '{{ threescale_cicd_openapi_file_content.securityDefinitions|default({}) }}' + +- name: Make sure there is one and exactly one security requirement + assert: + that: + - 'threescale_cicd_api_security_requirements|length == 1' + msg: 'You have {{ threescale_cicd_api_security_requirements|length }} global security requirements. There must be one and only one security requirement.' + +- name: Find the security requirement to use + set_fact: + threescale_cicd_api_security_scheme: '{{ threescale_cicd_api_security_requirements.keys()[0] }}' + +- name: Make sure the requested security definition exists + assert: + that: + - 'threescale_cicd_api_security_scheme in threescale_cicd_api_security_definitions' + +- name: Find the security definition to use + set_fact: + threescale_cicd_api_security_scheme: '{{ threescale_cicd_api_security_definitions[threescale_cicd_api_security_scheme] }}' + +- name: Make sure the security scheme is consistent with 3scale + assert: + that: + - 'threescale_cicd_api_security_scheme.type == ''apiKey'' or (threescale_cicd_api_security_scheme.type == ''oauth2'' and threescale_cicd_sso_issuer_endpoint is defined)' + +- name: Find the correct backend_version to use + set_fact: + threescale_cicd_api_backend_version: '1' + when: 'threescale_cicd_api_security_scheme.type == ''apiKey''' + +- name: Find the correct backend_version to use + set_fact: + threescale_cicd_api_backend_version: 'oauth' + when: 'threescale_cicd_api_security_scheme.type == ''oauth2''' + +- name: Extract the backend hostname from OpenAPI + set_fact: + threescale_cicd_api_backend_hostname: '{{ threescale_cicd_openapi_file_content.host }}' + when: 'threescale_cicd_api_backend_hostname is not defined and ''host'' in threescale_cicd_openapi_file_content' + +- name: Extract the backend scheme from OpenAPI + set_fact: + threescale_cicd_api_backend_scheme: '{{ threescale_cicd_openapi_file_content.schemes|default(["http"])|first }}' + when: 'threescale_cicd_api_backend_scheme is not defined' + +- assert: + that: + - 'threescale_cicd_api_backend_scheme is defined' + - 'threescale_cicd_api_backend_hostname is defined' + msg: 'The backend hostname and scheme must either be in the swagger or declared as extra variables (threescale_cicd_api_backend_scheme / threescale_cicd_api_backend_hostname)' + + +- name: Find the smoke-test flagged operation + set_fact: + threescale_cicd_openapi_smoketest_operation: '{{ threescale_cicd_openapi_file_content|json_query(''paths.*.get[? "x-threescale-smoketests-operation" ].operationId|[0]'') }}' + when: 'threescale_cicd_openapi_smoketest_operation is not defined' + +- assert: + that: + # Operation must exists + - 'threescale_cicd_openapi_smoketest_operation in threescale_cicd_api_operations' + # Must be a GET + - 'threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].verb == ''get''' + # Must NOT have a placeholder in the path + - 'threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].path.find("{") == -1' + msg: "The smoketest operation {{ threescale_cicd_openapi_smoketest_operation }} must be a GET and cannot have a placeholder in its path." + when: 'threescale_cicd_openapi_smoketest_operation is defined and threescale_cicd_openapi_smoketest_operation|length > 0' + +- set_fact: + threescale_cicd_openapi_smoketest_operation: '{{ threescale_cicd_openapi_smoketest_operation|regex_replace(''[^0-9a-zA-Z_]+'', ''_'') }}' + when: 'threescale_cicd_openapi_smoketest_operation is defined and threescale_cicd_openapi_smoketest_operation|length > 0' + +- set_fact: + threescale_cicd_openapi_smoketest_path: '{{ threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].path }}' + when: 'threescale_cicd_openapi_smoketest_operation is defined and threescale_cicd_openapi_smoketest_operation|length > 0' diff --git a/tasks/retrieve_existing_services.yml b/tasks/retrieve_existing_services.yml new file mode 100644 index 0000000..1da9686 --- /dev/null +++ b/tasks/retrieve_existing_services.yml @@ -0,0 +1,11 @@ +--- + +- name: Retrieve existing ActiveDocs from the 3scale Admin Portal + uri: + url: "https://{{ inventory_hostname }}/admin/api/active_docs.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + register: threescale_cicd_tmp_allactivedocs + +- set_fact: + threescale_cicd_existing_activedocs: '{{ threescale_cicd_tmp_allactivedocs.json|json_query(''api_docs[*].api_doc.system_name'') }}' + threescale_cicd_existing_activedocs_details: '{{ threescale_cicd_tmp_allactivedocs.json|json_query(''api_docs[].{"system_name": api_doc.system_name, "id": api_doc.id}'') }}' diff --git a/tasks/smoke_tests.yml b/tasks/smoke_tests.yml new file mode 100644 index 0000000..b42b001 --- /dev/null +++ b/tasks/smoke_tests.yml @@ -0,0 +1,57 @@ +--- + +- import_tasks: create_default_application.yml + +- set_fact: + threescale_cicd_tmp_gateway_endpoint: "" + +- name: Try to get the staging gateway url from extra var / inventory + set_fact: + threescale_cicd_tmp_gateway_endpoint: '{{ threescale_cicd_apicast_sandbox_endpoint }}' + when: "threescale_cicd_apicast_sandbox_endpoint is defined and threescale_cicd_env == 'staging'" + +- name: Try to get the production gateway url from extra var / inventory + set_fact: + threescale_cicd_tmp_gateway_endpoint: '{{ threescale_cicd_apicast_production_endpoint }}' + when: "threescale_cicd_apicast_production_endpoint is defined and threescale_cicd_env == 'production'" + +- name: Get the gateway endpoint from the proxy definition + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy.json?access_token={{ threescale_cicd_access_token|urlencode }} + validate_certs: no + method: GET + register: threescale_cicd_tmpresponse + when: "threescale_cicd_tmp_gateway_endpoint|length == 0" + +- name: Extract the staging gateway endpoint from the proxy definition + set_fact: + threescale_cicd_tmp_gateway_endpoint: '{{ threescale_cicd_tmpresponse.json|json_query(''proxy.sandbox_endpoint'') }}' + when: "threescale_cicd_tmp_gateway_endpoint|length == 0 and threescale_cicd_env == 'staging'" + +- name: Extract the production gateway endpoint from the proxy definition + set_fact: + threescale_cicd_tmp_gateway_endpoint: '{{ threescale_cicd_tmpresponse.json|json_query(''proxy.sandbox_endpoint'') }}' + when: "threescale_cicd_tmp_gateway_endpoint|length == 0 and threescale_cicd_env == 'production'" + +- set_fact: + threescale_cicd_openapi_smoketest_querystring: "" + threescale_cicd_openapi_smoketest_headers: {} + +- include_tasks: smoke_tests_apikey.yml + when: 'threescale_cicd_api_security_scheme.type == ''apiKey''' + +- include_tasks: smoke_tests_oauth.yml + when: 'threescale_cicd_api_security_scheme.type == ''oauth2''' + +- debug: + msg: "Starting a smoke test on '{{ threescale_cicd_tmp_gateway_endpoint }}{{ threescale_cicd_openapi_smoketest_path }}'..." + +- name: Running smoke tests ! + uri: + url: '{{ threescale_cicd_tmp_gateway_endpoint }}{{ threescale_cicd_openapi_smoketest_path }}{{ threescale_cicd_openapi_smoketest_querystring }}' + headers: '{{ threescale_cicd_openapi_smoketest_headers }}' + validate_certs: no + method: GET + register: threescale_cicd_tmpresponse + retries: '{{ threescale_cicd_retries }}' + delay: '{{ threescale_cicd_delay }}' diff --git a/tasks/smoke_tests_apikey.yml b/tasks/smoke_tests_apikey.yml new file mode 100644 index 0000000..b6c1ed8 --- /dev/null +++ b/tasks/smoke_tests_apikey.yml @@ -0,0 +1,9 @@ +--- + +- set_fact: + threescale_cicd_openapi_smoketest_querystring: "?{{ threescale_cicd_api_security_scheme.name|urlencode }}={{ threescale_cicd_default_application_details.user_key }}" + when: 'threescale_cicd_api_credentials_location == "query"' + +- set_fact: + threescale_cicd_openapi_smoketest_headers: "{{ threescale_cicd_openapi_smoketest_headers|combine({ threescale_cicd_api_security_scheme.name|urlencode: threescale_cicd_default_application_details.user_key}) }}" + when: 'threescale_cicd_api_credentials_location == "headers"' diff --git a/tasks/smoke_tests_oauth.yml b/tasks/smoke_tests_oauth.yml new file mode 100644 index 0000000..9004006 --- /dev/null +++ b/tasks/smoke_tests_oauth.yml @@ -0,0 +1,4 @@ +--- + +- name: TODO + fail: diff --git a/tasks/update_mapping_rule.yml b/tasks/update_mapping_rule.yml new file mode 100644 index 0000000..dd74ea9 --- /dev/null +++ b/tasks/update_mapping_rule.yml @@ -0,0 +1,26 @@ +--- + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_tmp_wanted_mapping_rules[threescale_cicd_tmp_mapping_rule_to_update] }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- set_fact: + # Add the metric_id to the payload + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ "metric_id=" ~ ((threescale_cicd_existing_metrics_details|selectattr("system_name", "equalto", threescale_cicd_tmp_mapping_rule_to_update)|first).id|urlencode) }}' + # The ID of the mapping rule to update + threescale_cicd_tmp_mapping_rule_id: '{{ threescale_cicd_tmp_existing_mapping_rules[threescale_cicd_tmp_mapping_rule_to_update] }}' + +- name: Update the mapping rule + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules/{{ threescale_cicd_tmp_mapping_rule_id }}.json + validate_certs: no + method: PUT + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 200 + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200' diff --git a/tasks/update_mapping_rules.yml b/tasks/update_mapping_rules.yml new file mode 100644 index 0000000..e459489 --- /dev/null +++ b/tasks/update_mapping_rules.yml @@ -0,0 +1,55 @@ +--- + +- name: Retrieve existing mapping rules from the 3scale Admin Portal + uri: + url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + register: threescale_cicd_tmp_allmappingrules + +- set_fact: + threescale_cicd_existing_mappingrules_details: '{{ threescale_cicd_tmp_allmappingrules.json|json_query(''mapping_rules[].{"metric_id": mapping_rule.metric_id, "id": mapping_rule.id}'') }}' + threescale_cicd_tmp_wanted_mapping_rules: {} + threescale_cicd_tmp_existing_mapping_rules: {} + +- name: Build a list of our expected/wanted mapping rules + set_fact: + threescale_cicd_tmp_wanted_mapping_rules: '{{ threescale_cicd_tmp_wanted_mapping_rules|combine({ threescale_cicd_tmp_operation.key: { "http_method": threescale_cicd_tmp_operation.value.verb.upper(), "pattern": threescale_cicd_tmp_operation.value.path ~ "$", "delta": 1 } }) }}' + with_dict: '{{ threescale_cicd_api_operations }}' + loop_control: + loop_var: threescale_cicd_tmp_operation + +- name: Map metric id to system_name + set_fact: + threescale_cicd_tmp_existing_mapping_rules: '{{ threescale_cicd_tmp_existing_mapping_rules|combine({ (threescale_cicd_existing_metrics_details|selectattr("id", "equalto", threescale_cicd_tmp_metric.metric_id)|first).system_name: threescale_cicd_tmp_metric.id}) }}' + with_items: '{{ threescale_cicd_existing_mappingrules_details }}' + loop_control: + loop_var: threescale_cicd_tmp_metric + +- set_fact: + # create the items that we want but don't have yet + threescale_cicd_tmp_mapping_rules_to_create: '{{ threescale_cicd_tmp_wanted_mapping_rules.keys()|difference(threescale_cicd_tmp_existing_mapping_rules.keys()) }}' + # delete the items that we don't want but we have + threescale_cicd_tmp_mapping_rules_to_delete: '{{ threescale_cicd_tmp_existing_mapping_rules.keys()|difference(threescale_cicd_tmp_wanted_mapping_rules.keys()) }}' + # update the items that we want and we have + threescale_cicd_tmp_mapping_rules_to_update: '{{ threescale_cicd_tmp_existing_mapping_rules.keys()|intersect(threescale_cicd_tmp_wanted_mapping_rules.keys()) }}' + +- include_tasks: "create_mapping_rule.yml" + with_items: '{{ threescale_cicd_tmp_mapping_rules_to_create }}' + loop_control: + loop_var: threescale_cicd_tmp_mapping_rule_to_create + +- include_tasks: "update_mapping_rule.yml" + with_items: '{{ threescale_cicd_tmp_mapping_rules_to_update }}' + loop_control: + loop_var: threescale_cicd_tmp_mapping_rule_to_update + +- name: Delete the unused mapping rules + uri: + url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules/{{ threescale_cicd_tmp_existing_mapping_rules[threescale_cicd_tmp_mapping_rule_to_delete] }}.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + method: DELETE + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200' + with_items: '{{ threescale_cicd_tmp_mapping_rules_to_delete }}' + loop_control: + loop_var: threescale_cicd_tmp_mapping_rule_to_delete diff --git a/tasks/update_method.yml b/tasks/update_method.yml new file mode 100644 index 0000000..eb088c0 --- /dev/null +++ b/tasks/update_method.yml @@ -0,0 +1,43 @@ +--- + +- set_fact: + threescale_cicd_api_method_definition: + system_name: '{{ threescale_cicd_tmp_operation.key }}' + friendly_name: '{{ threescale_cicd_tmp_operation.value.friendly_name|default(threescale_cicd_tmp_operation.key) }}' + description: '{{ threescale_cicd_tmp_operation.value.description|default('''') }}' + unit: 'hits' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_update_method: '{{ threescale_cicd_tmp_body_update_method ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_api_method_definition }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- name: Update the method + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics/{{ threescale_cicd_metric_id }}/methods/{{ (threescale_cicd_existing_metrics_details|selectattr('system_name', 'equalto', threescale_cicd_tmp_operation.key)|first).id }}.json + validate_certs: no + method: PATCH + body: '{{ threescale_cicd_tmp_body_update_method }}' + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200' + when: 'threescale_cicd_tmp_operation.key in threescale_cicd_existing_metrics' + +- name: Create the method + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics/{{ threescale_cicd_metric_id }}/methods.json + validate_certs: no + method: POST + body: '{{ threescale_cicd_tmp_body_update_method }}' + status_code: 201 + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 201' + when: 'threescale_cicd_tmp_operation.key not in threescale_cicd_existing_metrics' + +- set_fact: + threescale_cicd_existing_metrics: '{{ threescale_cicd_existing_metrics|union([ threescale_cicd_tmp_operation.key ]) }}' + threescale_cicd_existing_metrics_details: '{{ threescale_cicd_existing_metrics_details|union([ { "system_name": threescale_cicd_tmp_operation.key, "id": threescale_cicd_tmpresponse.json|json_query("method.id") } ]) }}' + when: 'threescale_cicd_tmp_operation.key not in threescale_cicd_existing_metrics' diff --git a/tasks/update_metrics.yml b/tasks/update_metrics.yml new file mode 100644 index 0000000..7c2ff6c --- /dev/null +++ b/tasks/update_metrics.yml @@ -0,0 +1,20 @@ +--- + +- name: Retrieve existing metrics from the 3scale Admin Portal + uri: + url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics.json?access_token={{ threescale_cicd_access_token|urlencode }}" + validate_certs: no + register: threescale_cicd_tmp_allmetrics + +- set_fact: + threescale_cicd_existing_metrics: '{{ threescale_cicd_tmp_allmetrics.json|json_query(''metrics[*].metric.system_name'') }}' + threescale_cicd_existing_metrics_details: '{{ threescale_cicd_tmp_allmetrics.json|json_query(''metrics[].{"system_name": metric.system_name, "id": metric.id}'') }}' + +- name: Find the "hits" metric id + set_fact: + threescale_cicd_metric_id: '{{ (threescale_cicd_existing_metrics_details|selectattr(''system_name'', ''equalto'', ''hits'')|first).id }}' + +- include_tasks: "update_method.yml" + with_dict: '{{ threescale_cicd_api_operations }}' + loop_control: + loop_var: threescale_cicd_tmp_operation diff --git a/tasks/update_proxy.yml b/tasks/update_proxy.yml new file mode 100644 index 0000000..608d1bd --- /dev/null +++ b/tasks/update_proxy.yml @@ -0,0 +1,19 @@ +--- + +- set_fact: + threescale_cicd_tmp_body_update_proxy: '{{ "access_token=" ~ threescale_cicd_access_token|urlencode }}' + +- set_fact: + threescale_cicd_tmp_body_update_proxy: '{{ threescale_cicd_tmp_body_update_proxy ~ "&" ~ (threescale_cicd_tmp_param.key|urlencode) ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}' + with_dict: '{{ threescale_cicd_api_proxy_definition }}' + loop_control: + loop_var: threescale_cicd_tmp_param + +- name: Update the proxy + uri: + url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy.json + validate_certs: no + method: PATCH + body: '{{ threescale_cicd_tmp_body_update_proxy }}' + register: threescale_cicd_tmpresponse + changed_when: 'threescale_cicd_tmpresponse.status == 200'