Browse Source

Merge pull request #24 from nmasse-itix/dev-issue-22

Transform facts to variables wherever possible
pull/25/head
Nicolas Massé 7 years ago
committed by GitHub
parent
commit
8aabac6973
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .travis.yml
  2. 84
      README.md
  3. 2
      ansible.cfg
  4. 33
      defaults/main.yml
  5. 23
      tasks/api-calls/create_activedoc.yml
  6. 22
      tasks/api-calls/create_application.yml
  7. 23
      tasks/api-calls/create_application_plan.yml
  8. 19
      tasks/api-calls/create_mapping_rule.yml
  9. 23
      tasks/api-calls/create_method.yml
  10. 24
      tasks/api-calls/create_service.yml
  11. 14
      tasks/api-calls/delete_mapping_rule.yml
  12. 17
      tasks/api-calls/delete_metric.yml
  13. 13
      tasks/api-calls/find_application.yml
  14. 10
      tasks/api-calls/find_first_account.yml
  15. 20
      tasks/api-calls/get_proxy_version.yml
  16. 22
      tasks/api-calls/keycloak/authenticate.yml
  17. 23
      tasks/api-calls/keycloak/patch_client.yml
  18. 18
      tasks/api-calls/keycloak/wait_for_client.yml
  19. 19
      tasks/api-calls/promote_proxy.yml
  20. 20
      tasks/api-calls/smoke_test.yml
  21. 19
      tasks/api-calls/update_activedoc.yml
  22. 21
      tasks/api-calls/update_application.yml
  23. 19
      tasks/api-calls/update_application_plan.yml
  24. 21
      tasks/api-calls/update_mapping_rule.yml
  25. 18
      tasks/api-calls/update_method.yml
  26. 23
      tasks/api-calls/update_proxy.yml
  27. 19
      tasks/api-calls/update_service.yml
  28. 32
      tasks/check_requirements.yml
  29. 1
      tasks/cleanup.yaml
  30. 113
      tasks/create_activedocs.yml
  31. 38
      tasks/create_application_plans.yml
  32. 89
      tasks/create_default_application.yml
  33. 28
      tasks/create_mapping_rule.yml
  34. 58
      tasks/create_service.yml
  35. 25
      tasks/delete_unused_metrics.yml
  36. 142
      tasks/main.yml
  37. 65
      tasks/patch_default_application_for_oauth.yml
  38. 42
      tasks/promote.yml
  39. 188
      tasks/read_openapi_file.yml
  40. 59
      tasks/smoke_tests.yml
  41. 9
      tasks/smoke_tests_apikey.yml
  42. 36
      tasks/smoke_tests_oauth.yml
  43. 10
      tasks/steps/activedoc.yml
  44. 8
      tasks/steps/application_plan.yml
  45. 6
      tasks/steps/application_plans.yml
  46. 6
      tasks/steps/cleanup_metrics.yml
  47. 32
      tasks/steps/default_application.yml
  48. 59
      tasks/steps/discover.yml
  49. 25
      tasks/steps/mapping_rules.yml
  50. 7
      tasks/steps/method.yml
  51. 16
      tasks/steps/methods.yml
  52. 6
      tasks/steps/promote.yml
  53. 3
      tasks/steps/proxy.yml
  54. 42
      tasks/steps/read_openapi.yml
  55. 64
      tasks/steps/requirements.yml
  56. 7
      tasks/steps/service.yml
  57. 14
      tasks/steps/smoke_test.yml
  58. 28
      tasks/steps/variables_from_inventory.yml
  59. 30
      tasks/update_mapping_rule.yml
  60. 59
      tasks/update_mapping_rules.yml
  61. 47
      tasks/update_method.yml
  62. 20
      tasks/update_metrics.yml
  63. 23
      tasks/update_proxy.yml
  64. 11
      templates/api-calls/create_activedoc.j2
  65. 16
      templates/api-calls/create_application.j2
  66. 9
      templates/api-calls/create_application_plan.j2
  67. 10
      templates/api-calls/create_mapping_rule.j2
  68. 10
      templates/api-calls/create_method.j2
  69. 15
      templates/api-calls/create_service.j2
  70. 7
      templates/api-calls/find_application.j2
  71. 5
      templates/api-calls/keycloak/authenticate.j2
  72. 1
      templates/api-calls/keycloak/patch_client.j2
  73. 1
      templates/api-calls/promote_proxy.j2
  74. 8
      templates/api-calls/smoke-test/headers.j2
  75. 10
      templates/api-calls/smoke-test/url.j2
  76. 1
      templates/api-calls/update_activedoc.j2
  77. 1
      templates/api-calls/update_application.j2
  78. 1
      templates/api-calls/update_application_plan.j2
  79. 1
      templates/api-calls/update_mapping_rule.j2
  80. 1
      templates/api-calls/update_method.j2
  81. 20
      templates/api-calls/update_proxy.j2
  82. 1
      templates/api-calls/update_service.j2
  83. 5
      templates/existing_mapping_rules.j2
  84. 7
      templates/metrics_to_delete.j2
  85. 5
      templates/openapi/apicast_production_endpoint.j2
  86. 5
      templates/openapi/apicast_sandbox_endpoint.j2
  87. 6
      templates/openapi/generate_base_system_name.j2
  88. 4
      templates/openapi/generate_final_system_name.j2
  89. 26
      templates/openapi/openapi_operations.j2
  90. 11
      templates/openapi/private_base_url.j2
  91. 5
      templates/openapi/service_name.j2
  92. 3
      templates/openapi/sso_issuer_endpoint.j2
  93. 22
      templates/rewritten_openapi.j2
  94. 5
      templates/wanted_mapping_rules.j2
  95. 16
      tests/3scale-saas-with-hosted-apicast-apikey.yml
  96. 45
      tests/3scale-saas-with-hosted-apicast-multi-environment.yml
  97. 16
      tests/3scale-saas-with-hosted-apicast-oidc.yml
  98. 16
      tests/3scale-saas-with-hosted-apicast-with-basePath.yml
  99. 10
      tests/inventory.j2
  100. 92
      vars/main.yml

9
.travis.yml

@ -6,13 +6,14 @@ install:
- pip install jmespath - pip install jmespath
script: script:
- ansible-playbook tests/write-inventory-files.yml - ansible-playbook tests/write-inventory-files.yml
- ansible-playbook -i tests/inventory tests/3scale-saas-with-hosted-apicast-apikey.yml - ansible-playbook -v -i tests/inventory tests/3scale-saas-with-hosted-apicast-apikey.yml
- ansible-playbook -i tests/inventory tests/3scale-saas-with-hosted-apicast-oidc.yml - ansible-playbook -v -i tests/inventory tests/3scale-saas-with-hosted-apicast-oidc.yml
- ansible-playbook -i tests/inventory tests/3scale-saas-with-hosted-apicast-with-basePath.yml - ansible-playbook -v -i tests/inventory tests/3scale-saas-with-hosted-apicast-with-basePath.yml
- ansible-playbook -v -i tests/inventory tests/3scale-saas-with-hosted-apicast-multi-environment.yml
env: env:
global: global:
# travis encrypt "THREESCALE_INVENTORY=$(yaml2json tests/3scale-inventory.yaml|base64)" # travis encrypt "THREESCALE_INVENTORY=$(yaml2json tests/3scale-inventory.yaml|base64)"
secure: "YKSBZJonKq/RwBtrg1wrlbw9GUaQhY0LmbUpShIPMeD1DqLQtOz9OIijAP2Uvtnn5F8josyzyYeZoRz4HOWT6DNdbZoclIT86FdT9yp9pIBuKYqaGOMmdwhTl+BXudeTCAvuj4k6eNux24WY+AqWZoXgr4E0rRJQJyD/G7gn4CMDQmn+an+RK43nhkXgMYNm6SHbR3c3wBypdWJivfgvdBfJb2VY8Q3XXQNh+ivUNW5us4Sf+G+UsjbQKeTF6G1rz8FUxGc2tElWcc+6fXWDTWI1WkZVfNm7f6cGii7X22OBDgOkoUjUKeVj/vmpgp7uOSk3XiWIC+gNsT/cS9/6XzZCnfhGmPal3QP5hXnsP5gBfKYzy8zZEp9H8NLyGA8K7M0cZGuFDdxg0HnIyXNnn7Denjyt3TopFR9ENsNOEbBar8XEb4oZicRWrXC9O/Sse4rCm5vGffXt+lcoAuypmxhASDZKNKsQRXhG+JRMZ2ONB8QOdH22mQ+JEOhOFNtiY2O9eEQOdtU92B9vWYWOztSGZ8/+AQ729bCzOw7AfxtxUyYadFnEdibAeWsl+xnlAdwmo0sNoCTGBKFMXlyP/gmcE7NdCLqWS6MSM2u4ARuQt7IS29wwlRseaMUli69vp0SqvjT6lelx4bonLFi74treKIQVLoxqSQaiU7KVTr0=" secure: "SKrCC5Nd1lXFU9mCrmGUSbqmEFGzT6/3KTXGQ/bASgSx4r0AuDHt48cI/XPQ6XGCIGaxAt2oRWzJZJ00+Y+5A1TCYAXI4X75mTVl+mgZ3ul5hSK1/KfPPoLciZIcv678FLmmpryNRapK+zxG+OKR1puNFQm9himhF9x0JICigFZSVLMLGnpvHDo2GguFv+4aO1tkdZMT5IzBlPD11Kn98QVCruF/dHiBXtSxuo5ja0/uDsGotMcUQRNa637WIQ5D7YgDREpeLrHzmbpW2zr7HI30oA68k+BxZFqlQ/cyI7f5ogNE73ID+FBSSxVXqcn7TD2nmYL3NQpMztTCzM6YlODIuAvdWUxggeBJflpIVoza0HLP7CB76GAmRSkvwGbnGAHWvCOtvczmJ/hXGgAEdRL5q3eJiGebRvhb6SAVMZ3LOH9LlLU9fKDVGqzolFi4+Jaxami600zgBB/yGkFckpapUZLEK2O0QdHBu3bjd7+9C0EgYONrbyMMkMoWr8TiX/y0qTHg3SclOEacDqLw3kb0MAe9V9WtE+MKOAM38lkXN1v1J9x2izeEqKBDDuzxMOsRxQwfSlA5MVW1kOiKaQgKl37F5t+msfIsPDlr2DRM4JTsIBaQKMY9E50tQ0cMW+vU7P+kn8UlNdTh53TLwKMjPcU99XG1f95fGAMocAA="
notifications: notifications:
webhooks: https://galaxy.ansible.com/api/v1/notifications/ webhooks: https://galaxy.ansible.com/api/v1/notifications/
branches: branches:

84
README.md

@ -74,7 +74,8 @@ securityDefinitions:
In this Swagger file, the following fields are used: In this Swagger file, the following fields are used:
- `x-threescale-system-name` is used as system_name for the configuration objects in 3scale. - `x-threescale-system-name` is used as a basis for the system_name for the
configuration objects in 3scale.
- `title` is used as the name of the service definition. - `title` is used as the name of the service definition.
- `version` is used for proper versioning and follows the [semver scheme](https://semver.org/). - `version` is used for proper versioning and follows the [semver scheme](https://semver.org/).
- `host` is the DNS name of the existing API backend to expose. - `host` is the DNS name of the existing API backend to expose.
@ -128,12 +129,6 @@ ansible-playbook -i inventory deploy-api.yaml
## Inventory ## Inventory
Three kinds of systems can be declared in the inventory and used with this role:
- 3scale Admin Portal
- Red Hat SSO
- APIcast instances
The 3scale Admin Portal that will be provisionned is the one that is referenced The 3scale Admin Portal that will be provisionned is the one that is referenced
in the playbook that includes this role. For instance, in the previous example, in the playbook that includes this role. For instance, in the previous example,
the provisioned 3scale Admin Portal will be `<TENANT>-admin.3scale.net` because the provisioned 3scale Admin Portal will be `<TENANT>-admin.3scale.net` because
@ -175,60 +170,31 @@ And you can also define it globally, for instance as playbook vars:
threescale_cicd_access_token: 123...456 threescale_cicd_access_token: 123...456
``` ```
The Red Hat SSO instance (currently there can only be one), is taken by convention The Red Hat SSO instance (currently there can only be one), is defined by
from the `sso` group. The `client_id`/`client_secret` used by Zync to synchronize the `threescale_cicd_sso_issuer_endpoint` variable of the `threescale` group.
the 3scale applications are fetched from the inventory variables, as well as the
scheme (`http`/`https`) and the target realm.
Example:
```ini
[sso]
sso.acme.corp client_id=3scale client_secret=123 realm=acme scheme=https
```
Otherwise, if you don't want to follow this convention, you can use the
corresponding extra variable: `threescale_cicd_sso_issuer_endpoint`. For
the previous example, the variable would be:
```ini
threescale_cicd_sso_issuer_endpoint=https://3scale:123@sso.acme.corp/auth/realms/acme
```
If both the `sso` group and the `threescale_cicd_sso_issuer_endpoint` extra Its syntax is `https://<client_id>:<client_secret>@hostname/auth/realms/<realm>`.
variable are specified, the extra variable has precedence over the inventory. The `client_id`/`client_secret` are used by Zync to synchronize the 3scale
applications with Red Hat SSO.
The APIcast instances are fetched from the `apicast-sandbox` and `apicast-production`
groups. There can only be one hostname in each group and it is the public hostname
of each APIcast cluster (not each individual member).
Example: Example:
```ini ```ini
[apicast-sandbox] threescale_cicd_sso_issuer_endpoint=https://3scale:123@sso.acme.corp/auth/realms/acme
api-test.acme.corp scheme=http
[apicast-production]
api.acme.corp scheme=https
``` ```
If you do not want to follow this convention, you can use the corresponding extra The APIcast instances are defined from the following extra variables:
variables:
- `threescale_cicd_apicast_sandbox_endpoint` - `threescale_cicd_apicast_sandbox_endpoint`
- `threescale_cicd_apicast_production_endpoint` - `threescale_cicd_apicast_production_endpoint`
For the previous example, the variables would be: Example:
```ini ```ini
threescale_cicd_apicast_sandbox_endpoint=http://api-test.acme.corp threescale_cicd_apicast_sandbox_endpoint=http://api-test.acme.corp
threescale_cicd_apicast_production_endpoint=https://api.acme.corp threescale_cicd_apicast_production_endpoint=https://api.acme.corp
``` ```
If both the `apicast-*` groups and the `threescale_cicd_apicast_*_endpoint`
extra variables are specified, the extra variables have precedence over the
inventory.
## OpenAPI Specification fields ## OpenAPI Specification fields
This role currently supports only OpenAPI Specifications v2.0 (aka. Swagger 2.0). This role currently supports only OpenAPI Specifications v2.0 (aka. Swagger 2.0).
@ -245,7 +211,7 @@ The following extended fields of the OpenAPI Specifications can be used:
If the extended fields cannot be used (if for instance you do not want to alter If the extended fields cannot be used (if for instance you do not want to alter
your API Contract), you can use the corresponding extra variable: your API Contract), you can use the corresponding extra variable:
- `threescale_cicd_api_system_name` - `threescale_cicd_api_base_system_name`
- `threescale_cicd_openapi_smoketest_operation` - `threescale_cicd_openapi_smoketest_operation`
Here is an example of an OpenAPI Specification using those extended fields: Here is an example of an OpenAPI Specification using those extended fields:
@ -284,7 +250,7 @@ To achieve the same effect without the OpenAPI extended fields, you would have
to pass the following extra variables: to pass the following extra variables:
```ini ```ini
threescale_cicd_api_system_name=echo-api threescale_cicd_api_base_system_name=echo-api
threescale_cicd_openapi_smoketest_operation=Echo # The operationId of the "GET /" method threescale_cicd_openapi_smoketest_operation=Echo # The operationId of the "GET /" method
``` ```
@ -389,14 +355,28 @@ Defines the system_name of the 3scale Service that will be provisioned.
- **Syntax:** lower case alphanumeric + underscore - **Syntax:** lower case alphanumeric + underscore
- **Required:** no - **Required:** no
- **Default value:** if not defined, the system_name is taken from the OpenAPI - **Default value:** if not defined, the system_name is taken from the
Specification `x-threescale-system-name` extended field, suffixed by the `threescale_cicd_api_base_system_name` variable. This base system_name
API major version number. If no `x-threescale-system-name` extended field is then suffixed by the API major version number and prefixed by the
can be found, the `title` field is sanitized and then used. environment name (only if `threescale_cicd_api_environment_name` is defined).
- **Example:** `dev_my_wonderful_service_1`
### `threescale_cicd_api_base_system_name`
Is used as a basis to compute the `threescale_cicd_api_system_name`.
- **Syntax:** lower case alphanumeric + underscore
- **Required:** no
- **Default value:** if not defined, the OpenAPI Specification
`x-threescale-system-name` extended field or as a last resort, the `title`
field is sanitized and then used.
If no title can be found, the default value `API` is used. If no version If no title can be found, the default value `API` is used. If no version
number can be found, `0` is used. number can be found, `0` is used.
- **Example:** `my_wonderful_service` - **Example:** `my_wonderful_service`
Note: If both `threescale_cicd_api_base_system_name` and `threescale_cicd_api_system_name`
are set, the later has precedence.
### `threescale_cicd_wildcard_domain` ### `threescale_cicd_wildcard_domain`
Automatically defines the APIcast public URLs based on a scheme. Automatically defines the APIcast public URLs based on a scheme.
@ -414,7 +394,7 @@ Automatically defines the APIcast public URLs based on a scheme.
```ini ```ini
threescale_cicd_wildcard_domain=acme.corp threescale_cicd_wildcard_domain=acme.corp
threescale_cicd_api_system_name=my_wonderful_service threescale_cicd_api_base_system_name=my_wonderful_service
``` ```
are equivalent to: are equivalent to:
@ -479,7 +459,7 @@ when deploying the same API multiple times on the same 3scale instance.
### Miscellaneous variables ### Miscellaneous variables
Miscellaneous variables defined in [defaults/main.yml](defaults/main.yml]) Miscellaneous variables defined in [defaults/main.yml](defaults/main.yml)
provide sensible defaults. Have a look at them. provide sensible defaults. Have a look at them.
## Dependencies ## Dependencies

2
ansible.cfg

@ -0,0 +1,2 @@
[defaults]
jinja2_extensions = jinja2.ext.do

33
defaults/main.yml

@ -3,8 +3,6 @@ threescale_cicd_openapi_file_format: YAML
threescale_cicd_delay: 10 threescale_cicd_delay: 10
threescale_cicd_retries: 50 threescale_cicd_retries: 50
threescale_cicd_throttling: 2 threescale_cicd_throttling: 2
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_staging_environment_name: sandbox
threescale_cicd_production_environment_name: production threescale_cicd_production_environment_name: production
threescale_cicd_default_staging_suffix: -staging threescale_cicd_default_staging_suffix: -staging
@ -18,3 +16,34 @@ threescale_cicd_application_plans:
default: false default: false
state: hidden state: hidden
name: Ansible Test Plan name: Ansible Test Plan
# APIcast public base URLs
threescale_cicd_apicast_sandbox_endpoint: '{{ lookup(''template'', ''openapi/apicast_sandbox_endpoint.j2'') }}'
threescale_cicd_apicast_production_endpoint: '{{ lookup(''template'', ''openapi/apicast_production_endpoint.j2'') }}'
# SSO Issuer Endpoint
threescale_cicd_sso_issuer_endpoint: '{{ lookup(''template'', ''openapi/sso_issuer_endpoint.j2'') }}'
##
## Default Application (used for Smoke Tests)
##
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.'
# The application plan to pick for the default application (the one used for
# smoke tests)
threescale_cicd_default_application_plan: '{{ (threescale_cicd_application_plans|first).system_name }}'
# Compute the default application's appid. By default, we are using a combination
# of app, api and environment data, hashed toghether to produce a stable id.
threescale_cicd_default_application_appid: '{{ (threescale_cicd_default_application_name ~ threescale_cicd_api_system_name ~ threescale_cicd_access_token)|hash(''sha1'') }}'
threescale_cicd_default_application_appsecret: '{{ (''secret'' ~ threescale_cicd_default_application_name ~ threescale_cicd_api_system_name ~ threescale_cicd_access_token)|hash(''sha1'') }}'
# The OpenAPI Operation to use for the smoketest
threescale_cicd_openapi_smoketest_operation: '{{ threescale_cicd_openapi_file_content|json_query(''paths.*.get[? "x-threescale-smoketests-operation" ].operationId|[0]'')|default("")|regex_replace(''[^0-9a-zA-Z_]+'', ''_'') }}'
##
## OpenAPI Specification File parsing
##
threescale_cicd_api_base_system_name: '{{ lookup(''template'', ''openapi/generate_base_system_name.j2'') }}'
threescale_cicd_api_system_name: '{{ lookup(''template'', ''openapi/generate_final_system_name.j2'') }}'
threescale_cicd_private_base_url: '{{ lookup(''template'', ''openapi/private_base_url.j2'') }}'

23
tasks/api-calls/create_activedoc.yml

@ -0,0 +1,23 @@
---
- debug:
var: threescale_cicd_create_activedoc_payload
verbosity: 1
- name: Create the ActiveDocs
uri:
url: https://{{ inventory_hostname }}/admin/api/active_docs.json
validate_certs: no
method: POST
body: '{{ threescale_cicd_create_activedoc_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- set_fact:
threescale_cicd_existing_activedocs: '{{ threescale_cicd_existing_activedocs|union([ threescale_cicd_tmpresponse.json.api_doc.system_name ]) }}'
threescale_cicd_existing_activedocs_details: '{{ threescale_cicd_existing_activedocs_details|union([ { ''id'': threescale_cicd_tmpresponse.json.api_doc.id, ''system_name'': threescale_cicd_tmpresponse.json.api_doc.system_name } ]) }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

22
tasks/api-calls/create_application.yml

@ -0,0 +1,22 @@
---
- debug:
var: threescale_cicd_create_application_payload
verbosity: 1
- 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_create_application_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- set_fact:
threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

23
tasks/api-calls/create_application_plan.yml

@ -0,0 +1,23 @@
---
- debug:
var: threescale_cicd_create_application_plan_payload
verbosity: 1
- 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_create_application_plan_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- set_fact:
threescale_cicd_existing_application_plans: '{{ threescale_cicd_existing_application_plans|union([ threescale_cicd_application_plan.system_name ]) }}'
threescale_cicd_existing_application_plans_details: '{{ threescale_cicd_existing_application_plans_details|union([{ "system_name": threescale_cicd_application_plan.system_name, "id": threescale_cicd_tmpresponse.json.application_plan.id }]) }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

19
tasks/api-calls/create_mapping_rule.yml

@ -0,0 +1,19 @@
---
- debug:
var: threescale_cicd_create_mapping_rule_payload
verbosity: 1
- 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_create_mapping_rule_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

23
tasks/api-calls/create_method.yml

@ -0,0 +1,23 @@
---
- debug:
var: threescale_cicd_create_method_payload
verbosity: 1
- 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_create_method_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- set_fact:
threescale_cicd_existing_metrics: '{{ threescale_cicd_existing_metrics|union([ threescale_cicd_api_operation.key ]) }}'
threescale_cicd_existing_metrics_details: '{{ threescale_cicd_existing_metrics_details|union([ { "system_name": threescale_cicd_api_operation.key, "id": threescale_cicd_tmpresponse.json|json_query("method.id") } ]) }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

24
tasks/api-calls/create_service.yml

@ -0,0 +1,24 @@
---
- debug:
var: threescale_cicd_create_service_payload
verbosity: 1
- name: Create the service
uri:
url: https://{{ inventory_hostname }}/admin/api/services.json
validate_certs: no
method: POST
body: '{{ threescale_cicd_create_service_payload }}'
status_code: 201
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- set_fact:
threescale_cicd_existing_services: '{{ threescale_cicd_existing_services|union([ threescale_cicd_tmpresponse.json.service.system_name ]) }}'
threescale_cicd_existing_services_details: '{{ threescale_cicd_existing_services_details|union([ { ''id'': threescale_cicd_tmpresponse.json.service.id, ''system_name'': threescale_cicd_tmpresponse.json.service.system_name } ]) }}'
cacheable: true
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

14
tasks/api-calls/delete_mapping_rule.yml

@ -0,0 +1,14 @@
---
- name: Delete the unused mapping rules
uri:
url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules/{{ threescale_cicd_existing_mapping_rules[threescale_cicd_mapping_rule] }}.json?access_token={{ threescale_cicd_access_token|urlencode }}"
validate_certs: no
method: DELETE
status_code: 200,404
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

17
tasks/api-calls/delete_metric.yml

@ -0,0 +1,17 @@
---
- debug:
msg: "Deleting unused metric {{ threescale_cicd_metric.system_name }}..."
- name: Delete the metric
uri:
url: "https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics/{{ threescale_cicd_metric_id }}/methods/{{ threescale_cicd_metric.id }}.json?access_token={{ threescale_cicd_access_token|urlencode }}"
validate_certs: no
method: DELETE
status_code: 200,404
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

13
tasks/api-calls/find_application.yml

@ -0,0 +1,13 @@
---
- name: Check if the default application exists
uri:
url: 'https://{{ inventory_hostname }}/admin/api/applications/find.json?{{ threescale_cicd_find_application_payload }}'
validate_certs: no
method: GET
status_code: 200,404
register: threescale_cicd_tmpresponse
- set_fact:
threescale_cicd_default_application_id: '{{ threescale_cicd_tmpresponse.json.application.id }}'
when: 'threescale_cicd_tmpresponse.status == 200'

10
tasks/api-calls/find_first_account.yml

@ -0,0 +1,10 @@
---
- 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_tmpresponse
- set_fact:
threescale_cicd_default_account_id: '{{ threescale_cicd_tmpresponse.json.accounts[0].account.id }}'

20
tasks/api-calls/get_proxy_version.yml

@ -0,0 +1,20 @@
---
- 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_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
status_code: 200,404
register: threescale_cicd_tmpresponse
- set_fact:
threescale_cicd_production_proxy_version: '{{ threescale_cicd_tmpresponse.json.proxy_config.version if threescale_cicd_tmpresponse.status == 200 else ''NONE'' }}'

22
tasks/api-calls/keycloak/authenticate.yml

@ -0,0 +1,22 @@
---
- debug:
var: threescale_cicd_authenticate_to_keycloak_payload
verbosity: 1
- name: Authenticate to RH-SSO
uri:
url: '{{ threescale_cicd_sso_realm_endpoint }}/protocol/openid-connect/token'
body: '{{ threescale_cicd_authenticate_to_keycloak_payload }}'
method: POST
validate_certs: no
return_content: yes
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
# temporary fix for https://github.com/ansible/ansible/issues/28078
until: 'threescale_cicd_tmpresponse is success'
- name: Extract the access_token
set_fact:
threescale_cicd_keycloak_access_token: '{{ threescale_cicd_tmpresponse.json |json_query("access_token") }}'

23
tasks/api-calls/keycloak/patch_client.yml

@ -0,0 +1,23 @@
---
- debug:
var: threescale_cicd_patch_keycloak_client_payload
verbosity: 1
- name: Patch the client in RH-SSO to support the "client_credentials" and "password" grant_type.
uri:
url: '{{ threescale_cicd_sso_admin_endpoint }}/clients/{{ threescale_cicd_default_application_sso_id|urlencode }}'
method: PUT
validate_certs: no
body: '{{ threescale_cicd_patch_keycloak_client_payload }}'
body_format: json
status_code: '200,204'
headers:
Authorization: 'Bearer {{ threescale_cicd_keycloak_access_token }}'
Content-Type: 'application/json'
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

18
tasks/api-calls/keycloak/wait_for_client.yml

@ -0,0 +1,18 @@
---
- name: Wait for the new client to appear in RH-SSO
uri:
url: '{{ threescale_cicd_sso_admin_endpoint }}/clients?clientId={{ threescale_cicd_default_application_appid|urlencode }}'
method: GET
validate_certs: no
return_content: yes
headers:
Authorization: 'Bearer {{ threescale_cicd_keycloak_access_token }}'
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
until: 'threescale_cicd_tmpresponse is success and threescale_cicd_tmpresponse.json|length > 0'
- set_fact:
threescale_cicd_default_application_sso_id: '{{ threescale_cicd_tmpresponse.json[0].id }}'
threescale_cicd_default_application_sso_body: '{{ threescale_cicd_tmpresponse.json[0] }}'

19
tasks/api-calls/promote_proxy.yml

@ -0,0 +1,19 @@
---
- debug:
var: threescale_cicd_promote_proxy_payload
verbosity: 1
- 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_staging_proxy_version }}/promote.json'
body: '{{ threescale_cicd_promote_proxy_payload }}'
status_code: 201
validate_certs: no
method: POST
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 201'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

20
tasks/api-calls/smoke_test.yml

@ -0,0 +1,20 @@
---
- debug:
var: threescale_cicd_smoke_test_headers
verbosity: 1
- debug:
msg: "Starting a smoke test on '{{ threescale_cicd_smoke_test_url }}'..."
- name: Running smoke tests !
uri:
url: '{{ threescale_cicd_smoke_test_url }}'
headers: '{{ threescale_cicd_smoke_test_headers }}'
validate_certs: no
method: GET
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
# temporary fix for https://github.com/ansible/ansible/issues/28078
until: 'threescale_cicd_tmpresponse is success'

19
tasks/api-calls/update_activedoc.yml

@ -0,0 +1,19 @@
---
- debug:
var: threescale_cicd_update_activedoc_payload
verbosity: 1
- name: Update the ActiveDocs
uri:
url: 'https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_id }}.json'
validate_certs: no
method: PUT
body: '{{ threescale_cicd_update_activedoc_payload }}'
status_code: 200
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

21
tasks/api-calls/update_application.yml

@ -0,0 +1,21 @@
---
- debug:
var: threescale_cicd_update_application_payload
verbosity: 1
- 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_update_application_payload }}'
status_code: 200
register: threescale_cicd_tmpresponse
- set_fact:
threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

19
tasks/api-calls/update_application_plan.yml

@ -0,0 +1,19 @@
---
- debug:
var: threescale_cicd_update_application_plan_payload
verbosity: 1
- 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_application_plan.system_name)|first).id }}.json'
validate_certs: no
method: PUT
body: '{{ threescale_cicd_update_application_plan_payload }}'
status_code: 200
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

21
tasks/api-calls/update_mapping_rule.yml

@ -0,0 +1,21 @@
---
- debug:
var: threescale_cicd_update_mapping_rule_payload
verbosity: 1
- name: Update the mapping rule
uri:
url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules/{{ threescale_cicd_mapping_rule_id }}.json
validate_certs: no
method: PUT
body: '{{ threescale_cicd_update_mapping_rule_payload }}'
status_code: 200
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
vars:
threescale_cicd_mapping_rule_id: '{{ threescale_cicd_existing_mapping_rules[threescale_cicd_mapping_rule] }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

18
tasks/api-calls/update_method.yml

@ -0,0 +1,18 @@
---
- debug:
var: threescale_cicd_update_method_payload
verbosity: 1
- 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_api_operation.key)|first).id }}.json
validate_certs: no
method: PATCH
body: '{{ threescale_cicd_update_method_payload }}'
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

23
tasks/api-calls/update_proxy.yml

@ -0,0 +1,23 @@
---
- debug:
var: threescale_cicd_update_proxy_payload
verbosity: 1
- name: Update the proxy definition
uri:
url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy.json
validate_certs: no
method: PATCH
body: '{{ threescale_cicd_update_proxy_payload }}'
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Extract the staging and production gateway endpoint from the proxy definition
set_fact:
threescale_cicd_apicast_discovered_sandbox_endpoint: '{{ threescale_cicd_tmpresponse.json.proxy.sandbox_endpoint }}'
threescale_cicd_apicast_discovered_production_endpoint: '{{ threescale_cicd_tmpresponse.json.proxy.endpoint }}'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

19
tasks/api-calls/update_service.yml

@ -0,0 +1,19 @@
---
- debug:
var: threescale_cicd_update_service_payload
verbosity: 1
- 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_update_service_payload }}'
status_code: 200
register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

32
tasks/check_requirements.yml

@ -1,32 +0,0 @@
---
- name: Verify that Ansible version is >= 2.4
assert:
that: "ansible_version.full is version_compare('2.4', '>=')"
msg: This module requires at least Ansible 2.4
- name: Check if jmespath is installed locally
debug: msg={{dummy|json_query('@')}}
register: check_jmespath
ignore_errors: yes
vars:
dummy: Hello World
- name: Check if jinja 2.8 is installed locally
debug: msg={{(dummy|selectattr("id", "equalto", "hello")|first)['value']}}
vars:
dummy:
- id: hello
value: Hello World
register: check_jinja28
ignore_errors: yes
- assert:
that:
- 'check_jmespath is success'
msg: "The JMESPath library is required by this role. Please install the JMESPath library with 'pip install jmespath'."
- assert:
that:
- 'check_jinja28 is success'
msg: "At least Jinja v2.8 is required by this role. Please update Jinja with 'pip install -U Jinja2'."

1
tests/cleanup.yaml → tasks/cleanup.yaml

@ -9,6 +9,7 @@
register: threescale_cicd_tmpresponse register: threescale_cicd_tmpresponse
changed_when: 'threescale_cicd_tmpresponse.status == 200' changed_when: 'threescale_cicd_tmpresponse.status == 200'
when: 'threescale_cicd_api_service_id is defined' when: 'threescale_cicd_api_service_id is defined'
- name: Delete the created ActiveDocs - name: Delete the created ActiveDocs
uri: uri:
url: 'https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_id }}.json?access_token={{ threescale_cicd_access_token|urlencode }}' url: 'https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_id }}.json?access_token={{ threescale_cicd_access_token|urlencode }}'

113
tasks/create_activedocs.yml

@ -1,113 +0,0 @@
---
- 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}'') }}'
- name: Get the production 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_apicast_production_endpoint is not defined"
- name: Extract the production gateway endpoint from the proxy definition
set_fact:
threescale_cicd_apicast_production_endpoint: '{{ threescale_cicd_tmpresponse.json|json_query(''proxy.endpoint'') }}'
when: "threescale_cicd_apicast_production_endpoint is not defined"
- set_fact:
threescale_cicd_apicast_production_scheme: '{{ threescale_cicd_apicast_production_endpoint|regex_findall(''^(\w+)://'')|first }}'
threescale_cicd_apicast_production_hostname: '{{ threescale_cicd_apicast_production_endpoint|regex_findall(''^\w+://(.+)$'')|first }}'
- set_fact:
threescale_cicd_openapi_rewritten: '{{ threescale_cicd_openapi_file_content }}'
- name: Rewrite the OpenAPI file (schemes)
set_fact:
threescale_cicd_openapi_rewritten: '{{ threescale_cicd_openapi_rewritten|combine({ ''schemes'': [ threescale_cicd_apicast_production_scheme ] }) }}'
- name: Rewrite the OpenAPI file (host)
set_fact:
threescale_cicd_openapi_rewritten: '{{ threescale_cicd_openapi_rewritten|combine({ ''host'': threescale_cicd_apicast_production_hostname }) }}'
- name: Rewrite the Swagger file (swagger version as string)
set_fact:
threescale_cicd_openapi_rewritten: '{{ threescale_cicd_openapi_rewritten|combine({ ''swagger'': threescale_cicd_openapi_rewritten.swagger ~ "" }) }}'
- name: Add the RH-SSO endpoints to the OpenAPI securityDefinitions
set_fact:
threescale_cicd_api_security_definitions: '{{ threescale_cicd_api_security_definitions|combine({ threescale_cicd_api_security_scheme_name: (threescale_cicd_api_security_definitions[threescale_cicd_api_security_scheme_name]|combine({ ''authorizationUrl'': threescale_cicd_sso_realm_endpoint ~ ''/protocol/openid-connect/auth'', ''tokenUrl'': threescale_cicd_sso_realm_endpoint ~ ''/protocol/openid-connect/token'' })) }) }}'
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
- name: Add the RH-SSO default scope to the OpenAPI securityDefinitions
set_fact:
threescale_cicd_api_security_definitions: '{{ threescale_cicd_api_security_definitions|combine({ threescale_cicd_api_security_scheme_name: (threescale_cicd_api_security_definitions[threescale_cicd_api_security_scheme_name]|combine({ ''scopes'': threescale_cicd_default_oauth_scopes})) }) }}'
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'' and ''scopes'' not in threescale_cicd_api_security_scheme'
- name: Rewrite the OpenAPI file (securityDefinitions)
set_fact:
threescale_cicd_openapi_rewritten: '{{ threescale_cicd_openapi_rewritten|combine({ ''securityDefinitions'': threescale_cicd_api_security_definitions }) }}'
- set_fact:
threescale_cicd_tmp_activedoc_payload:
name: '{{ threescale_cicd_api_name }}'
system_name: '{{ threescale_cicd_api_system_name }}'
body: '{{ threescale_cicd_openapi_rewritten|to_nice_json }}'
description: '{{ threescale_cicd_api_description }}'
published: 'true'
- 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_activedoc_payload }}'
loop_control:
loop_var: threescale_cicd_tmp_param
- set_fact:
threescale_cicd_api_activedocs_id: '{{ (threescale_cicd_existing_activedocs_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first).id }}'
when: 'threescale_cicd_api_system_name in threescale_cicd_existing_activedocs'
- name: Update the ActiveDocs
uri:
url: 'https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_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'
when: 'threescale_cicd_api_system_name in threescale_cicd_existing_activedocs'
- name: Create the ActiveDocs
uri:
url: https://{{ inventory_hostname }}/admin/api/active_docs.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_api_system_name not in threescale_cicd_existing_activedocs'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'
- set_fact:
threescale_cicd_api_activedocs_id: '{{ threescale_cicd_tmpresponse.json.api_doc.id }}'
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_activedocs'
- set_fact:
threescale_cicd_existing_services: '{{ threescale_cicd_existing_activedocs|union([ threescale_cicd_tmpresponse.json.api_doc.system_name ]) }}'
threescale_cicd_existing_services_details: '{{ threescale_cicd_existing_activedocs_details|union([ { ''id'': threescale_cicd_tmpresponse.json.api_doc.id, ''system_name'': threescale_cicd_tmpresponse.json.api_doc.system_name } ]) }}'
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_activedocs'

38
tasks/create_application_plans.yml

@ -1,38 +0,0 @@
---
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

89
tasks/create_default_application.yml

@ -1,89 +0,0 @@
---
- 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'
threescale_cicd_tmp_app_id_field: 'application_id'
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
- set_fact:
threescale_cicd_tmp_search_criteria: 'user_key'
threescale_cicd_tmp_app_id_field: '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_app_id_field ~ "=" ~ (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'
- include_tasks: patch_default_application_for_oauth.yml
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

28
tasks/create_mapping_rule.yml

@ -1,28 +0,0 @@
---
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

58
tasks/create_service.yml

@ -1,58 +0,0 @@
---
- 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
- 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}'') }}'
- 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
- 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_api_system_name in threescale_cicd_existing_services'
- 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_api_system_name in threescale_cicd_existing_services'
- 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
register: threescale_cicd_tmpresponse
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_services'
- set_fact:
threescale_cicd_api_service_id: '{{ threescale_cicd_tmpresponse.json.service.id }}'
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_services'
- set_fact:
threescale_cicd_existing_services: '{{ threescale_cicd_existing_services|union([ threescale_cicd_tmpresponse.json.service.system_name ]) }}'
threescale_cicd_existing_services_details: '{{ threescale_cicd_existing_services_details|union([ { ''id'': threescale_cicd_tmpresponse.json.service.id, ''system_name'': threescale_cicd_tmpresponse.json.service.system_name } ]) }}'
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_services'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

25
tasks/delete_unused_metrics.yml

@ -1,25 +0,0 @@
---
- 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
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

142
tasks/main.yml

@ -1,143 +1,53 @@
--- ---
- import_tasks: check_requirements.yml # Make sure we have everything we need to run this playbook
- import_tasks: steps/requirements.yml
- name: Ensure pre-requisites are met # Warn the user about those deprecated features
assert: - import_tasks: steps/variables_from_inventory.yml
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_sso_realm_endpoint variable from the threescale_cicd_sso_issuer_endpoint
set_fact:
threescale_cicd_sso_realm_endpoint: '{{ (threescale_cicd_sso_issuer_endpoint|urlsplit(''scheme'')) ~ ''://'' ~ (threescale_cicd_sso_issuer_endpoint|urlsplit(''hostname'')) ~ (threescale_cicd_sso_issuer_endpoint|urlsplit(''path'')) }}'
when: 'threescale_cicd_sso_realm_endpoint is not defined and threescale_cicd_sso_issuer_endpoint is defined'
- name: Set the threescale_cicd_sso_admin_endpoint variable from the threescale_cicd_sso_realm_endpoint
set_fact:
threescale_cicd_sso_admin_endpoint: '{{ threescale_cicd_sso_realm_endpoint|replace(''/auth/realms/'', ''/auth/admin/realms/'') }}'
when: 'threescale_cicd_sso_admin_endpoint is not defined and threescale_cicd_sso_realm_endpoint is defined'
- 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 # Load the API definition from the provided OpenAPI file
- import_tasks: read_openapi_file.yml - import_tasks: steps/read_openapi.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: # Discover the current state of the platform
threescale_cicd_api_service_definition: - import_tasks: steps/discover.yml
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 # Create or update the service definition
- import_tasks: create_service.yml - import_tasks: steps/service.yml
- set_fact: # Create or update the methods
threescale_cicd_api_credentials_location: '{{ ''headers'' if threescale_cicd_api_security_scheme.in == ''header'' else threescale_cicd_api_security_scheme.in }}' - import_tasks: steps/methods.yml
when: 'threescale_cicd_api_security_scheme.type == ''apiKey'''
- set_fact: # Create, update or delete the mapping rules
threescale_cicd_api_credentials_location: 'headers' - import_tasks: steps/mapping_rules.yml
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_private_base_url }}'
- 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({ ''oidc_issuer_endpoint'': threescale_cicd_sso_issuer_endpoint }) }}'
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
- 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 metrics
- import_tasks: update_metrics.yml
# Update the mapping rules
- import_tasks: update_mapping_rules.yml
# Update the proxy # Update the proxy
- import_tasks: update_proxy.yml - import_tasks: steps/proxy.yml
- name: Get the list of existing application plans # Create or update application plans
uri: - import_tasks: steps/application_plans.yml
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: # Create or update the default application if smoke tests are needed
threescale_cicd_existing_application_plans: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[*].application_plan.system_name'') }}' - include_tasks: steps/default_application.yml
threescale_cicd_existing_application_plans_details: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[].{"system_name": application_plan.system_name, "id": application_plan.id}'') }}' when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined'
# 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 # Run smoke tests on the staging gateway
- include_tasks: smoke_tests.yml - include_tasks: steps/smoke_test.yml
vars: vars:
threescale_cicd_env: staging threescale_cicd_smoke_test_env: staging
when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined' when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined'
# Promote to production # Promote to production
- import_tasks: promote.yml - import_tasks: steps/promote.yml
# Run smoke tests on the production gateway # Run smoke tests on the production gateway
- include_tasks: smoke_tests.yml - include_tasks: steps/smoke_test.yml
vars: vars:
threescale_cicd_env: production threescale_cicd_smoke_test_env: production
when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined' when: 'threescale_cicd_openapi_smoketest_path is defined and threescale_cicd_application_plans is defined'
# Delete the metrics that are not needed anymore # Delete the metrics that are not needed anymore
- import_tasks: delete_unused_metrics.yml - import_tasks: steps/cleanup_metrics.yml
# Publish the OpenAPI Specifications file on the 3scale Admin Portal # Publish the OpenAPI Specifications file on the 3scale Admin Portal
- import_tasks: create_activedocs.yml - import_tasks: steps/activedoc.yml

65
tasks/patch_default_application_for_oauth.yml

@ -1,65 +0,0 @@
---
- name: Prepare the OAuth Request to RH-SSO (static params)
set_fact:
threescale_cicd_tmp_body: ""
- name: Prepare the OAuth Request to RH-SSO (urlencode dynamic params)
set_fact:
threescale_cicd_tmp_body: '{{ threescale_cicd_tmp_body ~ "&" ~ threescale_cicd_tmp_param.key ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}'
with_dict:
client_id: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''username'') }}'
client_secret: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''password'') }}'
scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}'
grant_type: client_credentials
loop_control:
loop_var: threescale_cicd_tmp_param
- name: Authenticate to RH-SSO using the 3scale service account
uri:
url: '{{ threescale_cicd_sso_realm_endpoint }}/protocol/openid-connect/token'
body: '{{ threescale_cicd_tmp_body }}'
method: POST
validate_certs: no
return_content: yes
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
# temporary fix for https://github.com/ansible/ansible/issues/28078
until: 'threescale_cicd_tmpresponse|success'
- name: Extract the access_token
set_fact:
threescale_cicd_openapi_tmp_access_token: '{{ threescale_cicd_tmpresponse.json |json_query("access_token") }}'
- name: Wait for the new client to appear in RH-SSO
uri:
url: '{{ threescale_cicd_sso_admin_endpoint }}/clients?clientId={{ threescale_cicd_default_application_appid|urlencode }}'
method: GET
validate_certs: no
return_content: yes
headers:
Authorization: 'Bearer {{ threescale_cicd_openapi_tmp_access_token }}'
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
until: 'threescale_cicd_tmpresponse|success and threescale_cicd_tmpresponse.json|length > 0'
- set_fact:
threescale_cicd_default_application_sso_id: '{{ threescale_cicd_tmpresponse.json[0].id }}'
threescale_cicd_tmp_body: '{{ threescale_cicd_tmpresponse.json[0]|combine({ ''serviceAccountsEnabled'': true, ''standardFlowEnabled'': false, ''implicitFlowEnabled'': false, ''directAccessGrantsEnabled'': true }) }}'
- name: Patch the client in RH-SSO to support the "client_credentials" and "password" grant_type.
uri:
url: '{{ threescale_cicd_sso_admin_endpoint }}/clients/{{ threescale_cicd_default_application_sso_id|urlencode }}'
method: PUT
validate_certs: no
body: '{{ threescale_cicd_tmp_body|to_json }}'
status_code: '200,204'
headers:
Authorization: 'Bearer {{ threescale_cicd_openapi_tmp_access_token }}'
Content-Type: 'application/json'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

42
tasks/promote.yml

@ -1,42 +0,0 @@
---
- 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
status_code: 200,404
register: threescale_cicd_tmpresponse
- set_fact:
threescale_cicd_tmp_production_proxy_version: '{{ threescale_cicd_tmpresponse.json.proxy_config.version }}'
when: 'threescale_cicd_tmpresponse.status == 200'
- set_fact:
threescale_cicd_tmp_production_proxy_version: 'NONE'
when: 'threescale_cicd_tmpresponse.status == 404'
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

188
tasks/read_openapi_file.yml

@ -1,188 +0,0 @@
---
- 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_description: '{{ threescale_cicd_openapi_file_content.info.description|default("") }}'
threescale_cicd_api_version: '{{ threescale_cicd_openapi_file_content.info.version|default("0.0.1") }}'
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 components from the version number
set_fact:
threescale_cicd_api_version_components: '{{ threescale_cicd_api_version.split(".") }}'
- name: Find the major version
set_fact:
threescale_cicd_api_version_major: '{{ threescale_cicd_api_version_components|first }}'
- name: Compute the system_name suffix to append to the generated system_name
set_fact:
threescale_cicd_api_system_name_suffix: '{{ (threescale_cicd_api_system_name is not defined)|ternary("_" ~ (threescale_cicd_api_version_major|regex_replace(''[^a-zA-Z0-9_]+'', ''_'')), "") }}'
- name: Compute the system_name prefix to prepend to the generated system_name
set_fact:
threescale_cicd_api_system_name_prefix: '{{ (threescale_cicd_api_system_name is not defined and threescale_cicd_api_environment_name is defined)|ternary((threescale_cicd_api_environment_name|default("")|regex_replace(''[^a-zA-Z0-9_]+'', ''_'')) ~ "_", "") }}'
- 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'']|regex_replace(''[^a-zA-Z0-9_]+'', ''_'')|lower }}'
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_]+'', ''_'')|lower }}'
when: 'threescale_cicd_api_system_name is not defined'
- name: Append the major version to the system_name
set_fact:
threescale_cicd_api_system_name: '{{ threescale_cicd_api_system_name }}{{ threescale_cicd_api_system_name_suffix }}'
- name: Set the threescale_cicd_apicast_{sandbox,production}_endpoint variable from the wildcard domain
set_fact:
threescale_cicd_apicast_sandbox_endpoint: '{{ threescale_cicd_default_apicast_scheme }}://{{ threescale_cicd_api_system_name|regex_replace(''[^a-zA-Z0-9-]+'', ''-'')|lower }}{{ threescale_cicd_default_staging_suffix }}.{{ threescale_cicd_wildcard_domain }}'
threescale_cicd_apicast_production_endpoint: '{{ threescale_cicd_default_apicast_scheme }}://{{ threescale_cicd_api_system_name|regex_replace(''[^a-zA-Z0-9-]+'', ''-'')|lower }}{{ threescale_cicd_default_production_suffix }}.{{ threescale_cicd_wildcard_domain }}'
when: 'threescale_cicd_wildcard_domain is defined'
- name: Prefix the system_name with the environment
set_fact:
threescale_cicd_api_system_name: '{{ threescale_cicd_api_system_name_prefix }}{{ threescale_cicd_api_system_name }}'
- name: Append the full version to the API title
set_fact:
threescale_cicd_api_name: '{{ threescale_cicd_api_name }} (v{{ threescale_cicd_api_version }})'
when: 'threescale_cicd_api_environment_name is not defined'
- name: Append the full version and the environment to the API title
set_fact:
threescale_cicd_api_name: '{{ threescale_cicd_api_name }} ({{ threescale_cicd_api_environment_name|upper }}, v{{ threescale_cicd_api_version }})'
when: 'threescale_cicd_api_environment_name is 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_name: '{{ threescale_cicd_api_security_requirements[0].keys()[0] }}'
- name: Make sure the requested security definition exists
assert:
that:
- 'threescale_cicd_api_security_scheme_name 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] }}'
- 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: 'oidc'
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'
- name: Compute the private base url from the OpenAPI file
set_fact:
threescale_cicd_private_base_url: '{{ threescale_cicd_api_backend_scheme ~ ''://'' ~ threescale_cicd_api_backend_hostname }}'
when: threescale_cicd_api_backend_hostname is defined and threescale_cicd_private_base_url is not defined
- assert:
that:
- 'threescale_cicd_private_base_url is defined'
msg: 'Either the private base url or the tuple backend hostname/scheme must be declared as extra variables (either threescale_cicd_private_base_url or 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_basepath }}{{ 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'

59
tasks/smoke_tests.yml

@ -1,59 +0,0 @@
---
- 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.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 }}'
# temporary fix for https://github.com/ansible/ansible/issues/28078
until: 'threescale_cicd_tmpresponse|success'

9
tasks/smoke_tests_apikey.yml

@ -1,9 +0,0 @@
---
- 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"'

36
tasks/smoke_tests_oauth.yml

@ -1,36 +0,0 @@
---
- name: Prepare the OAuth Request to RH-SSO (static params)
set_fact:
threescale_cicd_tmp_body: ""
- name: Prepare the OAuth Request to RH-SSO (urlencode dynamic params)
set_fact:
threescale_cicd_tmp_body: '{{ threescale_cicd_tmp_body ~ "&" ~ threescale_cicd_tmp_param.key ~ "=" ~ (threescale_cicd_tmp_param.value|urlencode) }}'
with_dict:
client_id: '{{ threescale_cicd_default_application_details.client_id }}'
client_secret: '{{ threescale_cicd_default_application_details.client_secret }}'
scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}'
grant_type: client_credentials
loop_control:
loop_var: threescale_cicd_tmp_param
- name: Authenticate to RH-SSO using the default application credentials
uri:
url: '{{ threescale_cicd_sso_realm_endpoint }}/protocol/openid-connect/token'
body: '{{ threescale_cicd_tmp_body }}'
method: POST
validate_certs: no
return_content: yes
register: threescale_cicd_tmpresponse
retries: '{{ threescale_cicd_retries }}'
delay: '{{ threescale_cicd_delay }}'
# temporary fix for https://github.com/ansible/ansible/issues/28078
until: 'threescale_cicd_tmpresponse|success'
- name: Extract the access_token
set_fact:
threescale_cicd_openapi_smoketest_access_token: '{{ threescale_cicd_tmpresponse.json |json_query("access_token") }}'
- set_fact:
threescale_cicd_openapi_smoketest_headers: "{{ threescale_cicd_openapi_smoketest_headers|combine({ 'Authorization': 'Bearer ' ~ threescale_cicd_openapi_smoketest_access_token }) }}"

10
tasks/steps/activedoc.yml

@ -0,0 +1,10 @@
---
- debug:
var: threescale_cicd_openapi_rewritten
verbosity: 1
- include_tasks: api-calls/update_activedoc.yml
when: 'threescale_cicd_api_system_name in threescale_cicd_existing_activedocs'
- include_tasks: api-calls/create_activedoc.yml
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_activedocs'

8
tasks/steps/application_plan.yml

@ -0,0 +1,8 @@
---
- include_tasks: api-calls/update_application_plan.yml
when: 'threescale_cicd_application_plan.system_name in threescale_cicd_existing_application_plans'
- include_tasks: api-calls/create_application_plan.yml
when: 'threescale_cicd_application_plan.system_name not in threescale_cicd_existing_application_plans'

6
tasks/steps/application_plans.yml

@ -0,0 +1,6 @@
---
- include_tasks: steps/application_plan.yml
with_items: '{{ threescale_cicd_application_plans|default([]) }}'
loop_control:
loop_var: threescale_cicd_application_plan

6
tasks/steps/cleanup_metrics.yml

@ -0,0 +1,6 @@
---
- include_tasks: "api-calls/delete_metric.yml"
with_items: '{{ threescale_cicd_metrics_to_delete }}'
loop_control:
loop_var: threescale_cicd_metric

32
tasks/steps/default_application.yml

@ -0,0 +1,32 @@
---
- import_tasks: "api-calls/find_first_account.yml"
when: 'threescale_cicd_default_account_id is not defined'
- import_tasks: "api-calls/find_application.yml"
- import_tasks: "api-calls/update_application.yml"
when: 'threescale_cicd_default_application_id is defined'
- import_tasks: "api-calls/create_application.yml"
when: 'threescale_cicd_default_application_id is not defined'
##
## When using OAuth / OIDC authentication, we need to patch the Keycloak client
## to support the client_credentials grant.
##
- include_tasks: api-calls/keycloak/authenticate.yml
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
vars:
oauth_payload:
client_id: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''username'') }}'
client_secret: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''password'') }}'
scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}'
grant_type: 'client_credentials'
- include_tasks: api-calls/keycloak/wait_for_client.yml
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
- include_tasks: api-calls/keycloak/patch_client.yml
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''

59
tasks/steps/discover.yml

@ -0,0 +1,59 @@
---
- 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_tmpresponse
when: threescale_cicd_existing_services is not defined
- set_fact:
threescale_cicd_existing_services: '{{ threescale_cicd_tmpresponse.json|json_query(''services[*].service.system_name'') }}'
threescale_cicd_existing_services_details: '{{ threescale_cicd_tmpresponse.json|json_query(''services[].{"system_name": service.system_name, "id": service.id}'') }}'
cacheable: true
when: threescale_cicd_existing_services is not defined
- debug:
msg: "Found {{ threescale_cicd_existing_services|length }} services"
verbosity: 1
- debug:
var: threescale_cicd_existing_services_details
verbosity: 1
- 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
when: threescale_cicd_api_system_name in threescale_cicd_existing_services
- set_fact:
threescale_cicd_existing_application_plans: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[*].application_plan.system_name'') if threescale_cicd_api_system_name in threescale_cicd_existing_services else [] }}'
threescale_cicd_existing_application_plans_details: '{{ threescale_cicd_tmpresponse.json|json_query(''plans[].{"system_name": application_plan.system_name, "id": application_plan.id}'') if threescale_cicd_api_system_name in threescale_cicd_existing_services else [] }}'
- debug:
msg: "Found {{ threescale_cicd_existing_application_plans|length }} application plans"
verbosity: 1
- debug:
var: threescale_cicd_existing_application_plans_details
verbosity: 1
- 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}'') }}'
- debug:
msg: "Found {{ threescale_cicd_existing_activedocs|length }} active docs"
verbosity: 1
- debug:
var: threescale_cicd_existing_activedocs_details
verbosity: 1

25
tasks/steps/mapping_rules.yml

@ -0,0 +1,25 @@
---
- 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_tmpresponse
- set_fact:
threescale_cicd_existing_mapping_rules_details: '{{ threescale_cicd_tmpresponse.json|json_query(''mapping_rules[].{"metric_id": mapping_rule.metric_id, "id": mapping_rule.id}'') }}'
- include_tasks: "api-calls/create_mapping_rule.yml"
with_items: '{{ threescale_cicd_mapping_rules_to_create }}'
loop_control:
loop_var: threescale_cicd_mapping_rule
- include_tasks: "api-calls/update_mapping_rule.yml"
with_items: '{{ threescale_cicd_mapping_rules_to_update }}'
loop_control:
loop_var: threescale_cicd_mapping_rule
- include_tasks: "api-calls/delete_mapping_rule.yml"
with_items: '{{ threescale_cicd_mapping_rules_to_delete }}'
loop_control:
loop_var: threescale_cicd_mapping_rule

7
tasks/steps/method.yml

@ -0,0 +1,7 @@
---
- include_tasks: api-calls/update_method.yml
when: 'threescale_cicd_api_operation.key in threescale_cicd_existing_metrics'
- include_tasks: api-calls/create_method.yml
when: 'threescale_cicd_api_operation.key not in threescale_cicd_existing_metrics'

16
tasks/steps/methods.yml

@ -0,0 +1,16 @@
---
- 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_tmpresponse
- set_fact:
threescale_cicd_existing_metrics: '{{ threescale_cicd_tmpresponse.json|json_query(''metrics[*].metric.system_name'') }}'
threescale_cicd_existing_metrics_details: '{{ threescale_cicd_tmpresponse.json|json_query(''metrics[].{"system_name": metric.system_name, "id": metric.id}'') }}'
- include_tasks: "steps/method.yml"
with_dict: '{{ threescale_cicd_api_operations }}'
loop_control:
loop_var: threescale_cicd_api_operation

6
tasks/steps/promote.yml

@ -0,0 +1,6 @@
---
- import_tasks: "api-calls/get_proxy_version.yml"
- include_tasks: "api-calls/promote_proxy.yml"
when: 'threescale_cicd_staging_proxy_version != threescale_cicd_production_proxy_version'

3
tasks/steps/proxy.yml

@ -0,0 +1,3 @@
---
- import_tasks: api-calls/update_proxy.yml

42
tasks/steps/read_openapi.yml

@ -0,0 +1,42 @@
---
- 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!"
- 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: Make sure the security scheme is consistent with 3scale
assert:
that:
- '''type'' in threescale_cicd_api_security_scheme and threescale_cicd_api_security_scheme.type == ''apiKey'' or (threescale_cicd_api_security_scheme.type == ''oauth2'' and threescale_cicd_sso_issuer_endpoint is defined)'
msg: |-
The embedded security definition {{ threescale_cicd_api_security_scheme_name }} is not compatible with 3scale.
Please make sure you chose an "apiKey" or "oauth2" scheme.
Also, if you chose "oauth2", you will need to pass the threescale_cicd_sso_issuer_endpoint extra variable.
The security definition you chose: {{ threescale_cicd_api_security_scheme|to_nice_json }}
- assert:
that:
- 'threescale_cicd_private_base_url is defined'
msg: 'Either the private base url or the tuple backend hostname/scheme must be declared as extra variables (either threescale_cicd_private_base_url or threescale_cicd_api_backend_scheme / threescale_cicd_api_backend_hostname)'
- 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|length > 0'
- debug:
msg: "Will work on service with system_name = {{ threescale_cicd_api_system_name }}"

64
tasks/steps/requirements.yml

@ -0,0 +1,64 @@
---
- 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: Make sure the OpenAPI File Format is YAML or JSON
assert:
that:
- threescale_cicd_openapi_file_format|upper == 'JSON' or threescale_cicd_openapi_file_format|upper == 'YAML'
msg: |-
The threescale_cicd_openapi_file_format parameter needs to be either 'JSON' or 'YAML'
- name: Verify that Ansible version is >= 2.4
assert:
that: "ansible_version.full is version_compare('2.4', '>=')"
msg: This module requires at least Ansible 2.4
- name: Check if jmespath is installed locally
debug: msg={{dummy|json_query('@')}}
register: check_jmespath
ignore_errors: yes
vars:
dummy: Hello World
- name: Check if jinja 2.8 is installed locally
debug: msg={{(dummy|selectattr("id", "equalto", "hello")|first)['value']}}
vars:
dummy:
- id: hello
value: Hello World
register: check_jinja28
ignore_errors: yes
- name: Check if the "do" jinja extension is enabled
debug: msg={% do {}.update({}) %}{{ success }}
vars:
success: 'The do extension is enabled'
register: check_jinja_do_ext
ignore_errors: yes
- assert:
that:
- 'check_jmespath is success'
msg: "The JMESPath library is required by this role. Please install the JMESPath library with 'pip install jmespath'."
- assert:
that:
- 'check_jinja28 is success'
msg: "At least Jinja v2.8 is required by this role. Please update Jinja with 'pip install -U Jinja2'."
- assert:
that:
- 'check_jinja_do_ext is success'
msg: |-
You need to enable the 'do' extension of Jinja in your ansible.cfg:
[default]
jinja2_extensions = jinja2.ext.do

7
tasks/steps/service.yml

@ -0,0 +1,7 @@
---
- include_tasks: api-calls/update_service.yml
when: 'threescale_cicd_api_system_name in threescale_cicd_existing_services'
- include_tasks: api-calls/create_service.yml
when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_services'

14
tasks/steps/smoke_test.yml

@ -0,0 +1,14 @@
---
# Retrieve a valid access token if the API is secured with OAuth/OIDC
- include_tasks: api-calls/keycloak/authenticate.yml
when: 'threescale_cicd_api_security_scheme.type == ''oauth2'''
vars:
oauth_payload:
client_id: '{{ threescale_cicd_default_application_details.client_id }}'
client_secret: '{{ threescale_cicd_default_application_details.client_secret }}'
scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}'
grant_type: 'client_credentials'
# Do the smoke test
- import_tasks: api-calls/smoke_test.yml

28
tasks/steps/variables_from_inventory.yml

@ -0,0 +1,28 @@
---
- name: Abort on deprecated feature -> the "sso" inventory group
fail:
msg: >
You are currently using a deprecated feature (the 'sso' group in your inventory).
Please replace it with the 'threescale_cicd_sso_issuer_endpoint' variable.
Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features'
extra variable to 'true'.
when: 'threescale_cicd_sso_issuer_endpoint|default("")|length > 0 and ''sso'' in groups and groups[''sso''] > 0 and threescale_cicd_api_backend_version == ''oidc'' and not threescale_cicd_deprecated_features|default(false)|bool'
- name: Abort on deprecated feature -> the "apicast-sandbox" inventory group
fail:
msg: >
You are currently using a deprecated feature (the 'apicast-sandbox' group in your inventory).
Please replace it with the 'threescale_cicd_apicast_sandbox_endpoint' variable.
Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features'
extra variable to 'true'.
when: 'threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 and ''apicast-sandbox'' in groups and groups[''apicast-sandbox''] > 0 and not threescale_cicd_deprecated_features|default(false)|bool'
- name: Abort on deprecated feature -> the "apicast-production" inventory group
fail:
msg: >
You are currently using a deprecated feature (the 'apicast-production' group in your inventory).
Please replace it with the 'threescale_cicd_apicast_production_endpoint' variable.
Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features'
extra variable to 'true'.
when: 'threescale_cicd_apicast_production_endpoint|default("")|length > 0 and ''apicast-production'' in groups and groups[''apicast-production''] > 0 and not threescale_cicd_deprecated_features|default(false)|bool'

30
tasks/update_mapping_rule.yml

@ -1,30 +0,0 @@
---
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

59
tasks/update_mapping_rules.yml

@ -1,59 +0,0 @@
---
- 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_api_basepath ~ 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
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

47
tasks/update_method.yml

@ -1,47 +0,0 @@
---
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

20
tasks/update_metrics.yml

@ -1,20 +0,0 @@
---
- 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

23
tasks/update_proxy.yml

@ -1,23 +0,0 @@
---
- 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'
- name: Wait for a couple seconds
pause:
seconds: '{{ threescale_cicd_throttling }}'

11
templates/api-calls/create_activedoc.j2

@ -0,0 +1,11 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'name=' ~ threescale_cicd_api_name|urlencode,
'description=' ~ threescale_cicd_api_description|urlencode,
'system_name=' ~ threescale_cicd_api_system_name|urlencode,
'body=' ~ threescale_cicd_openapi_rewritten|to_nice_json|urlencode,
'published=true',
]
%}
{{ payload|join("&") }}

16
templates/api-calls/create_application.j2

@ -0,0 +1,16 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'plan_id=' ~ threescale_cicd_default_application_plan_id|urlencode,
'name=' ~ threescale_cicd_default_application_name|urlencode,
'description=' ~ threescale_cicd_default_application_description|urlencode
]
%}
{% if threescale_cicd_api_security_scheme.type == 'oauth2' %}
{% do payload.append("application_id=" ~ threescale_cicd_default_application_appid|urlencode) %}
{% do payload.append("application_key=" ~ threescale_cicd_default_application_appsecret|urlencode) %}
{% endif %}
{% if threescale_cicd_api_security_scheme.type == 'apiKey' %}
{% do payload.append("user_key=" ~ threescale_cicd_default_application_appid|urlencode) %}
{% endif %}
{{ payload|join("&") }}

9
templates/api-calls/create_application_plan.j2

@ -0,0 +1,9 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode
]
%}
{% for key, value in threescale_cicd_application_plan.items() %}
{% do payload.append(key ~ "=" ~ value|urlencode) %}
{% endfor %}
{{ payload|join("&") }}

10
templates/api-calls/create_mapping_rule.j2

@ -0,0 +1,10 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'metric_id=' ~ ((threescale_cicd_existing_metrics_details|selectattr("system_name", "equalto", threescale_cicd_mapping_rule)|first).id|urlencode)
]
%}
{% for key, value in threescale_cicd_wanted_mapping_rules[threescale_cicd_mapping_rule].items() %}
{% do payload.append(key ~ "=" ~ value|urlencode) %}
{% endfor %}
{{ payload|join("&") }}

10
templates/api-calls/create_method.j2

@ -0,0 +1,10 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'friendly_name=' ~ threescale_cicd_api_operation.value.friendly_name|default(threescale_cicd_api_operation.key)|urlencode,
'description=' ~ threescale_cicd_api_operation.value.description|default('')|urlencode,
'system_name=' ~ threescale_cicd_api_operation.key|urlencode,
'unit=hits'
]
%}
{{ payload|join("&") }}

15
templates/api-calls/create_service.j2

@ -0,0 +1,15 @@
{% if threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 or threescale_cicd_apicast_production_endpoint|default("")|length > 0 %}
{% set deployment_type = "self_managed" %}
{% else %}
{% set deployment_type = "hosted" %}
{% endif %}
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'name=' ~ threescale_cicd_api_name|urlencode,
'deployment_option=' ~ deployment_type|urlencode,
'system_name=' ~ threescale_cicd_api_system_name|urlencode,
'backend_version=' ~ threescale_cicd_api_backend_version|urlencode
]
%}
{{ payload|join("&") }}

7
templates/api-calls/find_application.j2

@ -0,0 +1,7 @@
access_token={{ threescale_cicd_access_token|urlencode }}
{%- if threescale_cicd_api_security_scheme.type == 'oauth2' -%}
&app_id={{ threescale_cicd_default_application_appid|urlencode }}
{%- endif -%}
{%- if threescale_cicd_api_security_scheme.type == 'apiKey' -%}
&user_key={{ threescale_cicd_default_application_appid|urlencode }}
{%- endif -%}

5
templates/api-calls/keycloak/authenticate.j2

@ -0,0 +1,5 @@
{% set payload = [ ] %}
{% for key, value in oauth_payload.items() %}
{% do payload.append(key ~ "=" ~ value|urlencode) %}
{% endfor %}
{{ payload|join("&") }}

1
templates/api-calls/keycloak/patch_client.j2

@ -0,0 +1 @@
{{ threescale_cicd_default_application_sso_body|combine({ 'serviceAccountsEnabled': true, 'standardFlowEnabled': false, 'implicitFlowEnabled': false, 'directAccessGrantsEnabled': true }) }}

1
templates/api-calls/promote_proxy.j2

@ -0,0 +1 @@
access_token={{ threescale_cicd_access_token|urlencode }}&to={{ threescale_cicd_production_environment_name|urlencode }}

8
templates/api-calls/smoke-test/headers.j2

@ -0,0 +1,8 @@
{% set headers = {} %}
{% if threescale_cicd_api_security_scheme.type == "apiKey" and threescale_cicd_api_credentials_location == "headers" %}
{% do headers.update({ threescale_cicd_api_security_scheme.name|urlencode: threescale_cicd_default_application_details.user_key }) %}
{% endif %}
{% if threescale_cicd_api_security_scheme.type == "oauth2" and threescale_cicd_api_credentials_location == "headers" %}
{% do headers.update({ 'Authorization': 'Bearer ' ~ threescale_cicd_keycloak_access_token }) %}
{% endif %}
{{ headers }}

10
templates/api-calls/smoke-test/url.j2

@ -0,0 +1,10 @@
{%- if threescale_cicd_smoke_test_env == "staging" -%}
{{ threescale_cicd_apicast_discovered_sandbox_endpoint }}
{%- endif -%}
{%- if threescale_cicd_smoke_test_env == "production" -%}
{{ threescale_cicd_apicast_discovered_production_endpoint }}
{%- endif -%}
{{ threescale_cicd_openapi_smoketest_path }}
{%- if threescale_cicd_api_security_scheme.type == "apiKey" and threescale_cicd_api_credentials_location == "query" -%}
?{{ threescale_cicd_api_security_scheme.name|urlencode }}={{ threescale_cicd_default_application_details.user_key }}
{%- endif -%}

1
templates/api-calls/update_activedoc.j2

@ -0,0 +1 @@
create_activedoc.j2

1
templates/api-calls/update_application.j2

@ -0,0 +1 @@
create_application.j2

1
templates/api-calls/update_application_plan.j2

@ -0,0 +1 @@
create_application_plan.j2

1
templates/api-calls/update_mapping_rule.j2

@ -0,0 +1 @@
create_mapping_rule.j2

1
templates/api-calls/update_method.j2

@ -0,0 +1 @@
create_method.j2

20
templates/api-calls/update_proxy.j2

@ -0,0 +1,20 @@
{%
set payload = [
'access_token=' ~ threescale_cicd_access_token|urlencode,
'credentials_location=' ~ threescale_cicd_api_credentials_location|urlencode,
'api_backend=' ~ threescale_cicd_private_base_url|urlencode
]
%}
{% if threescale_cicd_api_security_scheme.type == 'apiKey' %}
{% do payload.append('auth_user_key=' ~ threescale_cicd_api_security_scheme.name|urlencode) %}
{% endif %}
{% if threescale_cicd_api_security_scheme.type == 'oauth2' %}
{% do payload.append('oidc_issuer_endpoint=' ~ threescale_cicd_sso_issuer_endpoint|urlencode) %}
{% endif %}
{% if threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 %}
{% do payload.append('sandbox_endpoint=' ~ threescale_cicd_apicast_sandbox_endpoint|urlencode) %}
{% endif %}
{% if threescale_cicd_apicast_production_endpoint|default("")|length > 0 %}
{% do payload.append('endpoint=' ~ threescale_cicd_apicast_production_endpoint|urlencode) %}
{% endif %}
{{ payload|join("&") }}

1
templates/api-calls/update_service.j2

@ -0,0 +1 @@
create_service.j2

5
templates/existing_mapping_rules.j2

@ -0,0 +1,5 @@
{% set mapping_rules = {} %}
{% for value in threescale_cicd_existing_mapping_rules_details %}
{% do mapping_rules.update({ (threescale_cicd_existing_metrics_details|selectattr("id", "equalto", value.metric_id)|first).system_name: value.id }) %}
{% endfor %}
{{ mapping_rules }}

7
templates/metrics_to_delete.j2

@ -0,0 +1,7 @@
{% set to_delete = [] %}
{% for metric in threescale_cicd_existing_metrics_details %}
{% if metric.system_name != "hits" and metric.system_name not in threescale_cicd_api_operations %}
{% do to_delete.append(metric) %}
{% endif %}
{% endfor %}
{{ to_delete }}

5
templates/openapi/apicast_production_endpoint.j2

@ -0,0 +1,5 @@
{%- if threescale_cicd_wildcard_domain is defined -%}
{{ threescale_cicd_default_apicast_scheme }}://{{ (threescale_cicd_api_base_system_name ~ "-" ~ threescale_cicd_api_version_major)|regex_replace('[^a-zA-Z0-9-]+', '-')|lower }}{{ threescale_cicd_default_production_suffix }}.{{ threescale_cicd_wildcard_domain }}
{%- elif 'apicast-production' in groups and groups['apicast-production'] > 0 -%}
{{ (hostvars[groups['apicast-production'][0]].scheme|default('https')) ~ '://' ~ groups['apicast-production'][0] }}
{%- endif -%}

5
templates/openapi/apicast_sandbox_endpoint.j2

@ -0,0 +1,5 @@
{%- if threescale_cicd_wildcard_domain is defined -%}
{{ threescale_cicd_default_apicast_scheme }}://{{ (threescale_cicd_api_base_system_name ~ "-" ~ threescale_cicd_api_version_major)|regex_replace('[^a-zA-Z0-9-]+', '-')|lower }}{{ threescale_cicd_default_staging_suffix }}.{{ threescale_cicd_wildcard_domain }}
{%- elif 'apicast-sandbox' in groups and groups['apicast-sandbox'] > 0 -%}
{{ (hostvars[groups['apicast-sandbox'][0]].scheme|default('https')) ~ '://' ~ groups['apicast-sandbox'][0] }}
{%- endif -%}

6
templates/openapi/generate_base_system_name.j2

@ -0,0 +1,6 @@
{% if 'x-threescale-system-name' in threescale_cicd_openapi_file_content.info %}
{% set extracted_system_name = threescale_cicd_openapi_file_content.info['x-threescale-system-name']|regex_replace('[^a-zA-Z0-9_]+', '_')|lower %}
{% else %}
{% set extracted_system_name = threescale_cicd_openapi_file_content.info['title']|default('api')|regex_replace('[^a-zA-Z0-9_]+', '_')|lower %}
{% endif %}
{{ extracted_system_name }}

4
templates/openapi/generate_final_system_name.j2

@ -0,0 +1,4 @@
{%- if threescale_cicd_api_environment_name is defined -%}
{%- set system_name_prefix = threescale_cicd_api_environment_name|default("")|regex_replace('[^a-zA-Z0-9_]+', '_') ~ "_" -%}
{%- endif -%}
{{ system_name_prefix|default("") }}{{ threescale_cicd_api_base_system_name }}_{{ threescale_cicd_api_version_major|regex_replace('[^a-zA-Z0-9_]+', '_') }}

26
templates/openapi/openapi_operations.j2

@ -0,0 +1,26 @@
{% 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 -%}
{% do operation[operation_id].update({ 'friendly_name': method_description.summary }) -%}
{% endif -%}
{% if 'description' in method_description -%}
{% do operation[operation_id].update({ 'description': method_description.description }) -%}
{% endif -%}
{% do operations.update(operation) -%}
{% endif -%}
{% endfor -%}
{% endif -%}
{% endfor -%}
{% endif -%}
{{ operations }}

11
templates/openapi/private_base_url.j2

@ -0,0 +1,11 @@
{%- if threescale_cicd_api_backend_hostname is not defined and 'host' in threescale_cicd_openapi_file_content -%}
{%- set backend_hostname = threescale_cicd_openapi_file_content.host -%}
{%- else -%}
{%- set backend_hostname = threescale_cicd_api_backend_hostname -%}
{%- endif -%}
{%- if threescale_cicd_api_backend_scheme is not defined -%}
{%- set backend_scheme = threescale_cicd_openapi_file_content.schemes|default(["http"])|first -%}
{%- else -%}
{%- set backend_scheme = threescale_cicd_api_backend_scheme -%}
{%- endif -%}
{{ backend_scheme }}://{{ backend_hostname }}

5
templates/openapi/service_name.j2

@ -0,0 +1,5 @@
{%- if threescale_cicd_api_environment_name is defined -%}
{{ threescale_cicd_api_default_name }} ({{ threescale_cicd_api_environment_name|upper }}, v{{ threescale_cicd_api_version }})
{%- else -%}
{{ threescale_cicd_api_default_name }} (v{{ threescale_cicd_api_version }})
{%- endif -%}

3
templates/openapi/sso_issuer_endpoint.j2

@ -0,0 +1,3 @@
{%- if 'sso' in groups and groups['sso'] > 0 -%}
{{ (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 }}
{%- endif -%}

22
templates/rewritten_openapi.j2

@ -0,0 +1,22 @@
{% set security_definitions = threescale_cicd_api_security_definitions %}
{% set new_openapi = threescale_cicd_openapi_file_content %}
{# Add the RH-SSO endpoints to the OpenAPI securityDefinitions #}
{% if threescale_cicd_api_security_scheme.type == "oauth2" %}
{% do security_definitions[threescale_cicd_api_security_scheme_name].update({ "authorizationUrl": threescale_cicd_sso_realm_endpoint ~ "/protocol/openid-connect/auth", "tokenUrl": threescale_cicd_sso_realm_endpoint ~ "/protocol/openid-connect/token" }) %}
{% endif %}
{# Add the RH-SSO default scope to the OpenAPI securityDefinitions #}
{% if threescale_cicd_api_security_scheme.type == "oauth2" and "scopes" not in threescale_cicd_api_security_scheme %}
{% do security_definitions[threescale_cicd_api_security_scheme_name].update({ "scopes": threescale_cicd_default_oauth_scopes }) %}
{% endif %}
{# Update the security definitions #}
{% do new_openapi.update({ "securityDefinitions": security_definitions }) %}
{# Update the "schemes" and "hostname" fields with the public apicast production URL #}
{% set apicast_production_scheme = threescale_cicd_apicast_discovered_production_endpoint|urlsplit('scheme') %}
{% set apicast_production_hostname = threescale_cicd_apicast_discovered_production_endpoint|urlsplit('hostname') %}
{% do new_openapi.update({
"schemes": [ apicast_production_scheme ],
"host": apicast_production_hostname
}) %}
{# Make sure the swagger version is a string and not a number #}
{% do new_openapi.update({ "swagger": new_openapi.swagger ~ "" }) %}
{{ new_openapi }}

5
templates/wanted_mapping_rules.j2

@ -0,0 +1,5 @@
{% set mapping_rules = {} %}
{% for key, value in threescale_cicd_api_operations.items() %}
{% do mapping_rules.update({ key: { "http_method": value.verb.upper(), "pattern": threescale_cicd_api_basepath ~ value.path ~ "$", "delta": 1 } }) %}
{% endfor %}
{{ mapping_rules }}

16
tests/3scale-saas-with-hosted-apicast-apikey.yml

@ -8,10 +8,14 @@
threescale_cicd_openapi_file_format: 'JSON' threescale_cicd_openapi_file_format: 'JSON'
threescale_cicd_api_backend_hostname: echo-api.3scale.net threescale_cicd_api_backend_hostname: echo-api.3scale.net
threescale_cicd_openapi_smoketest_operation: GET_beer threescale_cicd_openapi_smoketest_operation: GET_beer
roles: tasks:
# Test first deployment # Test a first deployment
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 1 } } - import_role:
name: 'nmasse-itix.threescale-cicd'
# Verify idempotence # Verify idempotence
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 2 } } - import_role:
post_tasks: name: 'nmasse-itix.threescale-cicd'
- import_tasks: 'cleanup.yaml' # Delete the service
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'

45
tests/3scale-saas-with-hosted-apicast-multi-environment.yml

@ -0,0 +1,45 @@
---
- name: Deploy the Beer Catalog API to a 3scale SaaS instance in multi environment
hosts: threescale
gather_facts: no
vars:
threescale_cicd_openapi_file: '{{ playbook_dir }}/api/beer-catalog-api.json'
threescale_cicd_openapi_file_format: 'JSON'
threescale_cicd_api_backend_hostname: echo-api.3scale.net
threescale_cicd_openapi_smoketest_operation: GET_beer
threescale_cicd_api_base_system_name: beer_catalog
tasks:
# Deploy in DEV
- import_role:
name: 'nmasse-itix.threescale-cicd'
vars:
threescale_cicd_api_environment_name: dev
# Deploy in TEST
- import_role:
name: 'nmasse-itix.threescale-cicd'
vars:
threescale_cicd_api_environment_name: test
# Deploy in PROD
- import_role:
name: 'nmasse-itix.threescale-cicd'
vars:
threescale_cicd_api_environment_name: prod
# Cleanup the DEV
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'
vars:
threescale_cicd_api_environment_name: dev
# Cleanup the TEST
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'
vars:
threescale_cicd_api_environment_name: test
# Cleanup the PROD
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'
vars:
threescale_cicd_api_environment_name: prod

16
tests/3scale-saas-with-hosted-apicast-oidc.yml

@ -5,10 +5,14 @@
gather_facts: no gather_facts: no
vars: vars:
threescale_cicd_openapi_file: '{{ playbook_dir }}/api/echo-api-oidc.yaml' threescale_cicd_openapi_file: '{{ playbook_dir }}/api/echo-api-oidc.yaml'
roles: tasks:
# Test first deployment # Test a first deployment
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 1 } } - import_role:
name: 'nmasse-itix.threescale-cicd'
# Verify idempotence # Verify idempotence
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 2 } } - import_role:
post_tasks: name: 'nmasse-itix.threescale-cicd'
- import_tasks: 'cleanup.yaml' # Delete the service
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'

16
tests/3scale-saas-with-hosted-apicast-with-basePath.yml

@ -5,10 +5,14 @@
gather_facts: no gather_facts: no
vars: vars:
threescale_cicd_openapi_file: '{{ playbook_dir }}/api/echo-api-with-basePath.yaml' threescale_cicd_openapi_file: '{{ playbook_dir }}/api/echo-api-with-basePath.yaml'
roles: tasks:
# Test first deployment # Test a first deployment
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 1 } } - import_role:
name: 'nmasse-itix.threescale-cicd'
# Verify idempotence # Verify idempotence
- { role: 'nmasse-itix.threescale-cicd', vars: { 'round': 2 } } - import_role:
post_tasks: name: 'nmasse-itix.threescale-cicd'
- import_tasks: 'cleanup.yaml' # Delete the service
- import_role:
name: 'nmasse-itix.threescale-cicd'
tasks_from: 'cleanup'

10
tests/inventory.j2

@ -6,12 +6,4 @@ ansible_connection=local
[threescale:vars] [threescale:vars]
threescale_cicd_access_token={{ threescale_inventory.threescale_hosted.access_token }} threescale_cicd_access_token={{ threescale_inventory.threescale_hosted.access_token }}
threescale_cicd_sso_issuer_endpoint=https://{{ threescale_inventory.sso.client_id }}:{{ threescale_inventory.sso.client_secret }}@{{ threescale_inventory.sso.host }}/auth/realms/{{ threescale_inventory.sso.realm }}
[sso]
{{ threescale_inventory.sso.host }}
[sso:vars]
realm={{ threescale_inventory.sso.realm }}
client_id={{ threescale_inventory.sso.client_id }}
client_secret={{ threescale_inventory.sso.client_secret }}
scheme=https

92
vars/main.yml

@ -0,0 +1,92 @@
---
# Credentials are expected to be passed in HTTP headers unless stated otherwise
# and only for API Keys
threescale_cicd_api_credentials_location: '{{ ''headers'' if threescale_cicd_api_security_scheme.in|default(''header'') == ''header'' or threescale_cicd_api_security_scheme.type == ''oauth2'' else ''query'' }}'
# A list of unused metrics to delete
threescale_cicd_metrics_to_delete: '{{ lookup(''template'', ''metrics_to_delete.j2'') }}'
# The OpenAPI file to be pushed to 3scale as an ActiveDocs
threescale_cicd_openapi_rewritten: '{{ lookup(''template'', ''rewritten_openapi.j2'') }}'
# Compute the Keycloak Realm endpoint from the threescale_cicd_sso_issuer_endpoint
threescale_cicd_sso_realm_endpoint: '{{ (threescale_cicd_sso_issuer_endpoint|urlsplit(''scheme'')) ~ ''://'' ~ (threescale_cicd_sso_issuer_endpoint|urlsplit(''hostname'')) ~ (threescale_cicd_sso_issuer_endpoint|urlsplit(''path'')) }}'
# Compute the Keycloak REST Admin Endpoint from the threescale_cicd_sso_realm_endpoint
threescale_cicd_sso_admin_endpoint: '{{ threescale_cicd_sso_realm_endpoint|replace(''/auth/realms/'', ''/auth/admin/realms/'') }}'
##
## OpenAPI Specification File parsing
##
threescale_cicd_openapi_file_content: '{{ lookup(''file'', threescale_cicd_openapi_file)|from_json if threescale_cicd_openapi_file_format|upper == ''JSON'' else lookup(''file'', threescale_cicd_openapi_file)|from_yaml }}'
threescale_cicd_openapi_file_version: '{{ threescale_cicd_openapi_file_content.swagger }}'
threescale_cicd_api_default_name: '{{ threescale_cicd_openapi_file_content.info.title|default("API") }}'
threescale_cicd_api_name: '{{ lookup(''template'', ''openapi/service_name.j2'') }}'
threescale_cicd_api_description: '{{ threescale_cicd_openapi_file_content.info.description|default("") }}'
threescale_cicd_api_version: '{{ threescale_cicd_openapi_file_content.info.version|default("0.0.1") }}'
threescale_cicd_api_basepath: '{{ threescale_cicd_openapi_file_content.basePath|default("") }}'
threescale_cicd_api_operations: '{{ lookup(''template'', ''openapi/openapi_operations.j2'') }}'
threescale_cicd_api_version_components: '{{ threescale_cicd_api_version.split(".") }}'
threescale_cicd_api_version_major: '{{ threescale_cicd_api_version_components|first }}'
threescale_cicd_api_security_requirements: '{{ threescale_cicd_openapi_file_content.security|default([]) }}'
threescale_cicd_api_security_definitions: '{{ threescale_cicd_openapi_file_content.securityDefinitions|default({}) }}'
threescale_cicd_api_security_scheme_name: '{{ threescale_cicd_api_security_requirements[0].keys()[0]|default(''none'') }}'
threescale_cicd_api_security_scheme: '{{ threescale_cicd_api_security_definitions[threescale_cicd_api_security_scheme_name] if threescale_cicd_api_security_scheme_name in threescale_cicd_api_security_definitions else {} }}'
threescale_cicd_api_backend_version: '{{ threescale_cicd_backend_version_mapping[threescale_cicd_api_security_scheme.type] }}'
threescale_cicd_backend_version_mapping:
apiKey: '1'
oauth2: 'oidc'
threescale_cicd_openapi_smoketest_path: '{{ threescale_cicd_api_basepath }}{{ threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].path }}'
##
## ID Lookup Variables
##
# The id of the current service is fetched from the threescale_cicd_existing_services_details fact
threescale_cicd_api_service_id: '{{ (threescale_cicd_existing_services_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first)[''id''] }}'
# The id of the 'hits' metric is fetched from the threescale_cicd_existing_metrics_details fact
threescale_cicd_metric_id: '{{ (threescale_cicd_existing_metrics_details|selectattr(''system_name'', ''equalto'', ''hits'')|first).id }}'
# Find the default application plan id from its system name
threescale_cicd_default_application_plan_id: '{{ (threescale_cicd_existing_application_plans_details|selectattr("system_name", "equalto", threescale_cicd_default_application_plan)|first).id }}'
# Find the id of the existing activedocs from the threescale_cicd_existing_activedocs_details fact
threescale_cicd_api_activedocs_id: '{{ (threescale_cicd_existing_activedocs_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first).id }}'
##
## Mapping Rules computation
##
# what we want
threescale_cicd_wanted_mapping_rules: '{{ lookup(''template'', ''wanted_mapping_rules.j2'') }}'
# what we have
threescale_cicd_existing_mapping_rules: '{{ lookup(''template'', ''existing_mapping_rules.j2'') }}'
# create the items that we want but don't have yet
threescale_cicd_mapping_rules_to_create: '{{ threescale_cicd_wanted_mapping_rules.keys()|difference(threescale_cicd_existing_mapping_rules.keys()) }}'
# delete the items that we don't want but we have
threescale_cicd_mapping_rules_to_delete: '{{ threescale_cicd_existing_mapping_rules.keys()|difference(threescale_cicd_wanted_mapping_rules.keys()) }}'
# update the items that we want and we have
threescale_cicd_mapping_rules_to_update: '{{ threescale_cicd_existing_mapping_rules.keys()|intersect(threescale_cicd_wanted_mapping_rules.keys()) }}'
##
## 3scale API Payload definition
##
threescale_cicd_update_proxy_payload: '{{ lookup(''template'', ''api-calls/update_proxy.j2'') }}'
threescale_cicd_update_service_payload: '{{ lookup(''template'', ''api-calls/update_service.j2'') }}'
threescale_cicd_create_service_payload: '{{ lookup(''template'', ''api-calls/create_service.j2'') }}'
threescale_cicd_update_method_payload: '{{ lookup(''template'', ''api-calls/update_method.j2'') }}'
threescale_cicd_create_method_payload: '{{ lookup(''template'', ''api-calls/create_method.j2'') }}'
threescale_cicd_update_mapping_rule_payload: '{{ lookup(''template'', ''api-calls/update_mapping_rule.j2'') }}'
threescale_cicd_create_mapping_rule_payload: '{{ lookup(''template'', ''api-calls/create_mapping_rule.j2'') }}'
threescale_cicd_update_application_plan_payload: '{{ lookup(''template'', ''api-calls/update_application_plan.j2'') }}'
threescale_cicd_create_application_plan_payload: '{{ lookup(''template'', ''api-calls/create_application_plan.j2'') }}'
threescale_cicd_find_application_payload: '{{ lookup(''template'', ''api-calls/find_application.j2'') }}'
threescale_cicd_update_application_payload: '{{ lookup(''template'', ''api-calls/update_application.j2'') }}'
threescale_cicd_create_application_payload: '{{ lookup(''template'', ''api-calls/create_application.j2'') }}'
threescale_cicd_authenticate_to_keycloak_payload: '{{ lookup(''template'', ''api-calls/keycloak/authenticate.j2'') }}'
threescale_cicd_patch_keycloak_client_payload: '{{ lookup(''template'', ''api-calls/keycloak/patch_client.j2'') }}'
threescale_cicd_smoke_test_headers: '{{ lookup(''template'', ''api-calls/smoke-test/headers.j2'') }}'
threescale_cicd_smoke_test_url: '{{ lookup(''template'', ''api-calls/smoke-test/url.j2'') }}'
threescale_cicd_promote_proxy_payload: '{{ lookup(''template'', ''api-calls/promote_proxy.j2'') }}'
threescale_cicd_update_activedoc_payload: '{{ lookup(''template'', ''api-calls/update_activedoc.j2'') }}'
threescale_cicd_create_activedoc_payload: '{{ lookup(''template'', ''api-calls/create_activedoc.j2'') }}'
Loading…
Cancel
Save