├── .gitignore ├── .travis.yml ├── CUSTOM.md ├── LICENSE ├── README.md ├── ansible.cfg ├── defaults └── main.yml ├── meta └── main.yml ├── support ├── README.md ├── awx │ ├── README.md │ ├── ansible.cfg │ ├── deploy-api.yaml │ ├── roles │ │ └── nmasse-itix.threescale-cicd │ └── tower-assets.yaml ├── docker │ ├── Dockerfile │ ├── README.md │ ├── ansible.cfg │ ├── deploy-api.yaml │ ├── install.yaml │ └── roles │ │ └── nmasse-itix.threescale-cicd ├── jenkins │ ├── Dockerfile │ ├── Jenkinsfile │ ├── README.md │ ├── deploy-3scale-api-pipeline.yaml │ ├── deploy-api.yaml │ ├── jenkins-slave-template-rhel.yaml │ └── roles │ │ └── nmasse-itix.threescale-cicd ├── kubernetes │ ├── README.md │ └── job.yaml └── openshift │ ├── README.md │ └── openshift-template.yaml ├── tasks ├── api-calls │ ├── create_activedoc.yml │ ├── create_application.yml │ ├── create_application_plan.yml │ ├── create_mapping_rule.yml │ ├── create_method.yml │ ├── create_service.yml │ ├── delete_mapping_rule.yml │ ├── delete_metric.yml │ ├── find_application.yml │ ├── find_first_account.yml │ ├── get_proxy_version.yml │ ├── keycloak │ │ ├── authenticate.yml │ │ ├── patch_client.yml │ │ └── wait_for_client.yml │ ├── promote_proxy.yml │ ├── smoke_test.yml │ ├── update_activedoc.yml │ ├── update_application.yml │ ├── update_application_plan.yml │ ├── update_mapping_rule.yml │ ├── update_method.yml │ ├── update_oidc_configuration.yml │ ├── update_policies.yml │ ├── update_proxy.yml │ └── update_service.yml ├── cleanup.yaml ├── install_prerequisites.yaml ├── main.yml └── steps │ ├── activedoc.yml │ ├── ansible_requirements.yml │ ├── application_plan.yml │ ├── application_plans.yml │ ├── cleanup_metrics.yml │ ├── default_application.yml │ ├── discover_platform.yml │ ├── discover_service.yml │ ├── find_goswagger.yml │ ├── install_goswagger.yml │ ├── mapping_rules.yml │ ├── method.yml │ ├── methods.yml │ ├── oidc_configuration.yml │ ├── policies.yml │ ├── promote.yml │ ├── proxy.yml │ ├── read_openapi.yml │ ├── requirements.yml │ ├── service.yml │ ├── smoke_test.yml │ ├── validate_openapi.yml │ └── variables_from_inventory.yml ├── templates ├── api-calls │ ├── create_activedoc.j2 │ ├── create_application.j2 │ ├── create_application_plan.j2 │ ├── create_mapping_rule.j2 │ ├── create_method.j2 │ ├── create_service.j2 │ ├── find_application.j2 │ ├── keycloak │ │ ├── authenticate.j2 │ │ └── patch_client.j2 │ ├── promote_proxy.j2 │ ├── smoke-test │ │ ├── headers.j2 │ │ └── url.j2 │ ├── update_activedoc.j2 │ ├── update_application.j2 │ ├── update_application_plan.j2 │ ├── update_mapping_rule.j2 │ ├── update_method.j2 │ ├── update_oidc_configuration.j2 │ ├── update_policies.j2 │ ├── update_proxy.j2 │ └── update_service.j2 ├── existing_mapping_rules.j2 ├── existing_policies.j2 ├── metrics_to_delete.j2 ├── openapi │ ├── apicast_production_endpoint.j2 │ ├── apicast_sandbox_endpoint.j2 │ ├── generate_base_system_name.j2 │ ├── generate_final_system_name.j2 │ ├── openapi_operations.j2 │ ├── private_base_url.j2 │ ├── service_name.j2 │ └── sso_issuer_endpoint.j2 ├── rewritten_openapi.j2 ├── wanted_mapping_rules.j2 └── wanted_policies.j2 ├── tests ├── 3scale-inventory.yaml.enc ├── ansible.cfg ├── environments │ └── .gitignore ├── inventory.j2 ├── results │ └── .gitignore ├── run-tests.sh ├── setup │ ├── README.md │ ├── cleanup.yml │ ├── common │ │ └── create-sso-client.yml │ ├── delete-travis-logs.yml │ └── setup-sso.yml ├── test-cases │ ├── 01-beer-catalog-apikey.yml │ ├── 02-echo-api-oidc.yml │ ├── 03-multi-environment.yml │ ├── 04-one-gateway.yml │ ├── 05-echo-api-with-basePath.yml │ ├── 06-echo-api-with-cors-policy.yml │ ├── 07-echo-api-with-smoketest-in-extra-vars.yml │ ├── 08-echo-api-without-smoketest.yml │ ├── api-contracts │ │ ├── beer-catalog-api.json │ │ ├── echo-api-bare.yaml │ │ ├── echo-api-oidc.yaml │ │ ├── echo-api-with-basePath.yaml │ │ └── echo-api.yaml │ ├── common │ │ └── random-system-name.yml │ └── roles │ │ └── nmasse-itix.threescale-cicd └── write-inventory-files.yml └── vars └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | inventory 3 | 3scale-inventory.yaml 4 | bin 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: '2.7' 5 | env: ANSIBLE_VERSION=2.4.6 THREESCALE_ENV=saas 6 | - python: '2.7' 7 | env: ANSIBLE_VERSION=2.4.6 THREESCALE_ENV=saas-apicast-selfmanaged 8 | - python: '2.7' 9 | env: ANSIBLE_VERSION=2.4.6 THREESCALE_ENV=onpremise-2.4 10 | - python: '2.7' 11 | env: ANSIBLE_VERSION=2.4.6 THREESCALE_ENV=onpremise-2.5 12 | - python: '3.6' 13 | env: ANSIBLE_VERSION=2.7.5 THREESCALE_ENV=saas 14 | - python: '3.6' 15 | env: ANSIBLE_VERSION=2.7.5 THREESCALE_ENV=saas-apicast-selfmanaged 16 | - python: '3.6' 17 | env: ANSIBLE_VERSION=2.7.5 THREESCALE_ENV=onpremise-2.4 18 | - python: '3.6' 19 | env: ANSIBLE_VERSION=2.7.5 THREESCALE_ENV=onpremise-2.5 20 | install: 21 | - pip install ansible==$ANSIBLE_VERSION 22 | - pip install jmespath 23 | # Pre-install go-swagger locally since it cannot be fetched from the Travis-CI 24 | # infrastructures because of rate limits imposed by GitHub on its API. 25 | - mkdir -p tests/test-cases/bin/ && curl -L -o tests/test-cases/bin/swagger https://github.com/go-swagger/go-swagger/releases/download/0.16.0/swagger_linux_amd64 && chmod 755 tests/test-cases/bin/swagger 26 | script: 27 | - tests/run-tests.sh 28 | notifications: 29 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ 30 | branches: 31 | only: 32 | - master 33 | before_install: 34 | # travis encrypt-file tests/3scale-inventory.yaml tests/3scale-inventory.yaml.enc 35 | - openssl aes-256-cbc -K $encrypted_5ba3c614c7e1_key -iv $encrypted_5ba3c614c7e1_iv -in tests/3scale-inventory.yaml.enc -out tests/3scale-inventory.yaml -d 36 | -------------------------------------------------------------------------------- /CUSTOM.md: -------------------------------------------------------------------------------- 1 | # How to customize the behavior of this role 2 | 3 | ## Speed-up the deployments 4 | 5 | Go save precious seconds during deployments, you can override the 6 | `threescale_cicd_throttling` variable. 7 | 8 | **WARNING:** the throttling is there to let the 3scale Admin Portal 9 | digest your changes. Use at your own risk! 10 | 11 | ```yaml 12 | - hosts: threescale 13 | gather_facts: no 14 | vars: 15 | threescale_cicd_throttling: 0 16 | roles: 17 | - nmasse-itix.threescale-cicd 18 | ``` 19 | 20 | ## Customize the 3scale Service display name 21 | 22 | To have the 3scale Service display name generated from a different pattern 23 | (`ENV[name-vX.Y]` in the following example), override the `threescale_cicd_api_name` 24 | variable. 25 | 26 | ```yaml 27 | - hosts: threescale 28 | gather_facts: no 29 | vars: 30 | myenv: TEST 31 | threescale_cicd_api_name: '{{ myenv }}[{{ threescale_cicd_api_default_name }}-v{{ threescale_cicd_api_version }}]' 32 | roles: 33 | - nmasse-itix.threescale-cicd 34 | ``` 35 | 36 | ## Provision a custom policy chain 37 | 38 | To provision a custom policy chain, you would need to store your custom policy 39 | in a file and reference it from the `threescale_cicd_policies_to_update` variable. 40 | 41 | **custom-policy-chain.json**: 42 | 43 | ```json 44 | [ 45 | { "name": "cors", "version": "builtin", "configuration": {}, "enabled": true }, 46 | { "name": "headers", "version": "builtin", "configuration": { "request": [ { "op": "set", "header": "X-TEST", "value_type": "plain", "value": "foo" } ] }, "enabled": true }, 47 | { "name": "apicast", "version": "builtin", "configuration": {}, "enabled": true } 48 | ] 49 | ``` 50 | 51 | **deploy-api.yaml**: 52 | 53 | ```yaml 54 | - hosts: threescale 55 | gather_facts: no 56 | vars: 57 | threescale_cicd_policies_to_update: '{{ lookup(''file'', playbook_dir ~ ''/custom-policy-chain.json'')|from_json }}' 58 | roles: 59 | - nmasse-itix.threescale-cicd 60 | ``` 61 | 62 | ## Implement the url_rewriting policy 63 | 64 | If you want to deploy an API that is not yet implemented and would like to route requests to a mock such as [Microcks](http://microcks.github.io/), you will need to implement the `url_rewriting` policy. 65 | 66 | The `url_rewriting` policy will help you add a prefix to the URL before calling the Mock server: 67 | `GET /beers` becomes `GET /rest/Echo+API/1.0/beers`. 68 | 69 | **custom-policy-chain.json.j2**: 70 | 71 | ```json 72 | [ 73 | { "name": "apicast", "version": "builtin", "configuration": {}, "enabled": true }, 74 | { "name": "url_rewriting", "version": "builtin", "configuration": { "query_args_commands": [], "commands": [ { "op": "sub", "regex": "^/", "replace": "/rest/{{ api_name|urlencode }}/{{ threescale_cicd_api_version }}/" } ] }, "enabled": true } 75 | ] 76 | ``` 77 | 78 | **deploy-api.yaml**: 79 | 80 | ```yaml 81 | - hosts: threescale 82 | gather_facts: no 83 | vars: 84 | api_name: Echo API 85 | threescale_cicd_policies_to_update: '{{ lookup(''template'', playbook_dir ~ ''/custom-policy-chain.json.j2'') }}' 86 | roles: 87 | - nmasse-itix.threescale-cicd 88 | ``` 89 | 90 | ## Choose the Account in which the smoke test application is created 91 | 92 | By default, the playbook will create a client application in the default first 93 | account of your tenant (the Account that contains "john"). But you can choose 94 | the Account to use by overriding the `threescale_cicd_default_account_id` 95 | variable. 96 | 97 | ```yaml 98 | - hosts: threescale 99 | gather_facts: no 100 | vars: 101 | threescale_cicd_default_account_id: '2445582535751' 102 | roles: 103 | - nmasse-itix.threescale-cicd 104 | ``` 105 | 106 | You can find the Account id by navigating to **Audience** > **Accounts** > 107 | **Listing** and clicking on the Account of your choice. The ID is the 108 | last part of the URL (`/buyers/accounts/{id}`). 109 | 110 | ## Override the default versioning scheme 111 | 112 | By default, the playbook will version your APIs based on the 113 | [Semantic Versioning](https://semver.org/) scheme. This means minor versions 114 | (1.0, 1.1, 1.2, etc) will be deployed continously to the same 3scale service. 115 | Major versions are deployed side-by-side (one service for each major version). 116 | 117 | If you want to deploy all versions to the same service, no matter what: 118 | 119 | ```yaml 120 | - hosts: threescale 121 | gather_facts: no 122 | vars: 123 | threescale_cicd_api_version_major: '0' # or whatever you want as long as it remains static 124 | roles: 125 | - nmasse-itix.threescale-cicd 126 | ``` 127 | 128 | If you want to release minor versions as major versions: 129 | 130 | ```yaml 131 | - hosts: threescale 132 | gather_facts: no 133 | vars: 134 | threescale_cicd_api_version_major: '{{ threescale_cicd_api_version_components|first }}-{{ threescale_cicd_api_version_components[1] if threescale_cicd_api_version_components|length > 1 else 0 }}' 135 | roles: 136 | - nmasse-itix.threescale-cicd 137 | ``` 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nicolas MASSE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-cicd 2 | 3 | [![Build Status](https://travis-ci.org/nmasse-itix/threescale-cicd.svg?branch=master)](https://travis-ci.org/nmasse-itix/threescale-cicd) 4 | [![MIT licensed][mit-badge]][mit-link] 5 | [![Galaxy Role][role-badge]][galaxy-link] 6 | 7 | Enables Continuous Delivery with Red Hat 3scale API Management Platform (3scale AMP). 8 | 9 | ## Requirements 10 | 11 | This role requires: 12 | 13 | - an instance of 3scale API Management Platform (hosted or on-premise) 14 | - an instance of Red Hat SSO if you plan to use OpenID Connect authentication 15 | - two APIcast gateways (staging and production), either hosted or self-managed 16 | - a Swagger 2.0 file describing the API you want to publish 17 | 18 | All the components are driven through APIs, so no SSH connection is required! 19 | 20 | On the control node, the `jmespath` library is required. If it is not already there, 21 | you can install it with: 22 | 23 | ```sh 24 | pip install jmespath 25 | ``` 26 | 27 | A recent version of Jinja (2.8) is also required. You can upgrade your Jinja version with: 28 | 29 | ```sh 30 | pip install -U Jinja2 31 | ``` 32 | 33 | If your control node runs on RHEL7, you can run 34 | [this playbook](https://github.com/nmasse-itix/OpenShift-Lab/blob/master/common/verify-local-requirements.yml) 35 | to install the missing dependencies. 36 | 37 | ## Example: Deploy an API on 3scale SaaS with hosted APIcast gateways 38 | 39 | If you want to deploy the classic "Echo API" on a SaaS 3scale instance using API Keys, 40 | you can do it in three steps: 41 | 42 | 1. Craft a Swagger file for your Echo API 43 | 2. Build your inventory file 44 | 3. Write the playbook 45 | 4. Run the playbook! 46 | 47 | First, make sure your swagger file (`api-swagger.yaml`) has the required information: 48 | 49 | ```yaml 50 | swagger: '2.0' 51 | info: 52 | x-threescale-system-name: 'echo-api' 53 | title: 'Echo API' 54 | version: '1.0' 55 | host: 'echo-api.3scale.net' 56 | paths: 57 | /: 58 | get: 59 | operationId: Echo 60 | summary: 'Get an echo' 61 | description: 'Get an echo from the server' 62 | x-threescale-smoketests-operation: true 63 | responses: 64 | 200: 65 | description: 'An Echo from the server' 66 | security: 67 | - apikey: [] 68 | securityDefinitions: 69 | apikey: 70 | name: api-key 71 | in: header 72 | type: apiKey 73 | ``` 74 | 75 | In this Swagger file, the following fields are used: 76 | 77 | - `x-threescale-system-name` is used as a basis for the system_name for the 78 | configuration objects in 3scale. 79 | - `title` is used as the name of the service definition. 80 | - `version` is used for proper versioning and follows the [semver scheme](https://semver.org/). 81 | - `host` is the DNS name of the existing API backend to expose. 82 | - the `operationId` fields are used as the system_name for the methods/metrics. 83 | - the `summary` and `description` fields are used as name and description for the methods/metrics. 84 | - `x-threescale-smoketests-operation` is used to flag one operation as usable for smoke tests. The method needs to be idempotent, read-only and without parameters. If no method is flagged as smoke tests, the smoke tests are just skipped. 85 | - the `security` and `securityDefinitions` are used to determine the security scheme of the exposed API. In this example, we are using the API Keys scheme. 86 | 87 | Then, write the `inventory` file: 88 | 89 | ```ini 90 | [all:vars] 91 | ansible_connection=local 92 | 93 | [threescale] 94 | -admin.3scale.net 95 | 96 | [threescale:vars] 97 | threescale_cicd_access_token= 98 | ``` 99 | 100 | The important bits of the inventory file are: 101 | 102 | - the 3scale admin portal needs to be declared in a group named `threescale`. 103 | - the [3scale access token](https://access.redhat.com/documentation/en-us/red_hat_3scale/2.saas/html-single/accounts/index#access_tokens) needs to be set in the `threescale_cicd_access_token` variable. 104 | - since no SSH connection is needed (we only use the 3scale Admin APIs), `ansible_connection=local` is set to the whole inventory. 105 | 106 | You can now write the playbook (`deploy-api.yaml`): 107 | 108 | ```yaml 109 | - hosts: threescale 110 | gather_facts: no 111 | vars: 112 | threescale_cicd_openapi_file: 'api-swagger.yaml' 113 | roles: 114 | - nmasse-itix.threescale-cicd 115 | ``` 116 | 117 | The main parts are: 118 | 119 | - `threescale_cicd_openapi_file` is the path to the swagger file defined in step 1. 120 | - the `nmasse-itix.threescale-cicd` role is used. 121 | - `gather_facts: no` needs to be used since there is no SSH connection to the target systems. 122 | 123 | Finally, you can run the playbook: 124 | 125 | ```sh 126 | ansible-galaxy install nmasse-itix.threescale-cicd 127 | ansible-playbook -i inventory deploy-api.yaml 128 | ``` 129 | 130 | ## Inventory 131 | 132 | The 3scale Admin Portal that will be provisionned is the one that is referenced 133 | in the playbook that includes this role. For instance, in the previous example, 134 | the provisioned 3scale Admin Portal will be `-admin.3scale.net` because 135 | the main playbook specifies `hosts: threescale` and the `threescale` group 136 | contains only one host: `-admin.3scale.net`. 137 | 138 | If you specifies multiple hosts for the 3scale Admin Portal, they all will be 139 | provisionned with the exact same configuration (useful for multi-site deployments). 140 | 141 | To connect to the 3scale Admin Portal, you will have to provide an Access Token 142 | having read/write privileges on the Account Management API. You can provide this 143 | token at the host level, group level or globally with the 144 | `threescale_cicd_access_token` variable. 145 | 146 | At the host level, it is defined as such: 147 | 148 | ```ini 149 | [threescale] 150 | tenant1-admin.3scale.net threescale_cicd_access_token=123...456 151 | tenant2-admin.3scale.net threescale_cicd_access_token=789...012 152 | ``` 153 | 154 | At the group level, you can define it as such: 155 | 156 | ```ini 157 | [threescale:vars] 158 | threescale_cicd_access_token=123...456 159 | 160 | [threescale] 161 | tenant1-admin.3scale.net 162 | tenant2-admin.3scale.net 163 | ``` 164 | 165 | And you can also define it globally, for instance as playbook vars: 166 | 167 | ```yaml 168 | - hosts: threescale 169 | vars: 170 | threescale_cicd_access_token: 123...456 171 | ``` 172 | 173 | The Red Hat SSO instance (currently there can only be one), is defined by 174 | the `threescale_cicd_sso_issuer_endpoint` variable of the `threescale` group. 175 | 176 | Its syntax is `https://:@hostname/auth/realms/`. 177 | The `client_id`/`client_secret` are used by Zync to synchronize the 3scale 178 | applications with Red Hat SSO. 179 | 180 | Example: 181 | 182 | ```ini 183 | threescale_cicd_sso_issuer_endpoint=https://3scale:123@sso.acme.corp/auth/realms/acme 184 | ``` 185 | 186 | The APIcast instances are defined from the following extra variables: 187 | 188 | - `threescale_cicd_apicast_sandbox_endpoint` 189 | - `threescale_cicd_apicast_production_endpoint` 190 | 191 | Example: 192 | 193 | ```ini 194 | threescale_cicd_apicast_sandbox_endpoint=http://api-test.acme.corp 195 | threescale_cicd_apicast_production_endpoint=https://api.acme.corp 196 | ``` 197 | 198 | ## OpenAPI Specification fields 199 | 200 | This role currently supports only OpenAPI Specifications v2.0 (aka. Swagger 2.0). 201 | 202 | The following extended fields of the OpenAPI Specifications can be used: 203 | 204 | - `x-threescale-system-name`, in the `info` structure is used as basis 205 | to construct the system_name for the configuration objects in 3scale. 206 | - `x-threescale-smoketests-operation` in a method definition is used to flag 207 | this operation as usable for smoke tests. The method needs to be idempotent, 208 | read-only and without parameters. If no method is flagged as smoke tests, 209 | the smoke tests are just skipped. 210 | 211 | If the extended fields cannot be used (if for instance you do not want to alter 212 | your API Contract), you can use the corresponding extra variable: 213 | 214 | - `threescale_cicd_api_base_system_name` 215 | - `threescale_cicd_openapi_smoketest_operation` 216 | 217 | Here is an example of an OpenAPI Specification using those extended fields: 218 | 219 | ```yaml 220 | swagger: '2.0' 221 | info: 222 | x-threescale-system-name: 'echo-api' 223 | title: 'Echo API' 224 | version: '1.0' 225 | host: 'echo-api.3scale.net' 226 | paths: 227 | /: 228 | get: 229 | operationId: Echo 230 | summary: 'Get an echo' 231 | description: 'Get an echo from the server' 232 | x-threescale-smoketests-operation: true 233 | responses: 234 | 200: 235 | description: 'An Echo from the server' 236 | security: 237 | - apikey: [] 238 | securityDefinitions: 239 | apikey: 240 | name: api-key 241 | in: header 242 | type: apiKey 243 | ``` 244 | 245 | Namely, `echo-api` would be used as a basis to construct the system_name 246 | of the 3scale service definition and a `GET` on `/` would be used as 247 | smoketests. 248 | 249 | To achieve the same effect without the OpenAPI extended fields, you would have 250 | to pass the following extra variables: 251 | 252 | ```ini 253 | threescale_cicd_api_base_system_name=echo-api 254 | threescale_cicd_openapi_smoketest_operation=Echo # The operationId of the "GET /" method 255 | ``` 256 | 257 | The following standard fields of the OpenAPI Specifications are used. 258 | 259 | In the `info` section: 260 | 261 | - `title` is used as the display name of the 3scale service definition. 262 | - `version` is used for proper versioning and follows the [semver scheme](https://semver.org/). 263 | - `host` is the DNS name of the existing API backend to expose. 264 | 265 | For each defined method: 266 | 267 | - the `operationId` fields is used as the system_name for the corresponding 268 | methods/metrics. 269 | - the `summary` and `description` fields are used as name and description 270 | for the methods/metrics. 271 | - the `security` and `securityDefinitions` are used to determine the security 272 | scheme of the exposed API. 273 | 274 | To have a one-to-one mapping between the OpenAPI Specifications and the 3scale features, 275 | some restrictions are applied on the `security`/`securityDefinitions` structures. 276 | Namely, there must be one and exactly one security requirement in the `security` 277 | structure. The security requirement needs to be applied globally (not on a per 278 | method basis). 279 | 280 | The security definitions also have restrictions: you can choose between only two 281 | security schemes: 282 | 283 | - OAuth / OpenID Connect 284 | - API Key 285 | 286 | The App Key Pair scheme proposed by 3scale has no corresponding definition in the 287 | OpenAPI Specifications and is currently not supported by this role. 288 | 289 | So to be more concrete, to secure your API with API Key, use this excerpt in your 290 | OpenAPI Specification file: 291 | 292 | ```yaml 293 | securityDefinitions: 294 | apikey: 295 | name: api-key 296 | in: header 297 | type: apiKey 298 | security: 299 | - apikey: [] 300 | ``` 301 | 302 | You can of course, choose the HTTP header name that will be used to send the 303 | API Key by changing the `name` field (in this example: `api-key`). 304 | 305 | And to secure it with OpenID Connect use this excerpt in your OpenAPI 306 | Specification file: 307 | 308 | ```yaml 309 | securityDefinitions: 310 | oidc: 311 | type: oauth2 312 | flow: accessCode 313 | authorizationUrl: http://dummy/placeholder 314 | tokenUrl: http://dummy/placeholder 315 | scopes: 316 | openid: Get an OpenID Connect token 317 | security: 318 | - oidc: 319 | - openid 320 | ``` 321 | 322 | You can of course use the OpenID Connect flow of your choice: 323 | 324 | - `implicit` 325 | - `password` 326 | - `application` 327 | - `accessCode` 328 | 329 | ## Role Variables 330 | 331 | This section presents extensively all the variables used by this role. As a 332 | foreword, this role adopt a convention-over-configuration scheme. This means 333 | that sensible defaults and opinionated naming schemes are provided out-of-the-box. 334 | 335 | ### `threescale_cicd_openapi_file` 336 | 337 | Specifies the OpenAPI Specification file to read. 338 | 339 | - **Syntax:** Complete path to the OpenAPI Specification, on the local filesystem. 340 | Avoid relative paths, prefer absolute ones. If you need to read a file that is 341 | relative to your playbook, use the `{{ playbook_dir }}` placeholder. 342 | - **Required:** yes 343 | - **Examples:** `/tmp/openapi.yaml` or `{{ playbook_dir }}/git/openapi.json` 344 | 345 | ### `threescale_cicd_openapi_file_format` 346 | 347 | Specifies the format (JSON or YAML) of the OpenAPI Specification file to read. 348 | 349 | - **Syntax:** `JSON` or `YAML` 350 | - **Required:** no 351 | - **Default value:** `YAML` 352 | - **Example:** `YAML` 353 | 354 | ### `threescale_cicd_api_system_name` 355 | 356 | Defines the system_name of the 3scale Service that will be provisioned. 357 | 358 | - **Syntax:** lower case alphanumeric + underscore 359 | - **Required:** no 360 | - **Default value:** if not defined, the system_name is taken from the 361 | `threescale_cicd_api_base_system_name` variable. This base system_name 362 | is then suffixed by the API major version number and prefixed by the 363 | environment name (only if `threescale_cicd_api_environment_name` is defined). 364 | - **Example:** `dev_my_wonderful_service_1` 365 | 366 | ### `threescale_cicd_api_base_system_name` 367 | 368 | Is used as a basis to compute the `threescale_cicd_api_system_name`. 369 | 370 | - **Syntax:** lower case alphanumeric + underscore 371 | - **Required:** no 372 | - **Default value:** if not defined, the OpenAPI Specification 373 | `x-threescale-system-name` extended field or as a last resort, the `title` 374 | field is sanitized and then used. 375 | If no title can be found, the default value `API` is used. If no version 376 | number can be found, `0` is used. 377 | - **Example:** `my_wonderful_service` 378 | 379 | Note: If both `threescale_cicd_api_base_system_name` and `threescale_cicd_api_system_name` 380 | are set, the later has precedence. 381 | 382 | ### `threescale_cicd_wildcard_domain` 383 | 384 | Automatically defines the APIcast public URLs based on a scheme. 385 | 386 | - **Syntax:** DNS domain suffix 387 | - **Required:** no 388 | - **Default value:** if defined, computes the `threescale_cicd_apicast_sandbox_endpoint` 389 | and `threescale_cicd_apicast_production_endpoint` from the API system_name. 390 | The sandbox APIcast will be `-staging.` and the 391 | production APIcast will be `.`. The suffix for the 392 | staging (`-staging`) and the production (empty) can be customized with the 393 | `threescale_cicd_default_staging_suffix` and `threescale_cicd_default_production_suffix` 394 | variables. 395 | - **Example:** the following two variables 396 | 397 | ```ini 398 | threescale_cicd_wildcard_domain=acme.corp 399 | threescale_cicd_api_base_system_name=my_wonderful_service 400 | ``` 401 | 402 | are equivalent to: 403 | 404 | ```ini 405 | threescale_cicd_apicast_sandbox_endpoint=https://my-wonderful-service-staging.acme.corp/ 406 | threescale_cicd_apicast_production_endpoint=https://my-wonderful-service.acme.corp/ 407 | ``` 408 | 409 | ### `threescale_cicd_api_basepath` 410 | 411 | Defines a `basePath` on which is deployed the backend API, overriding the `basePath` field 412 | of the OpenAPI Specification. The resulting value is used to define the mapping rules of the 413 | 3scale API Gateway, prepending this base path to paths of different methods/operations. 414 | 415 | - **Syntax:** URI part with starting / 416 | - **Required:** no 417 | - **Default value:** the `basePath` field of the OpenAPI Specification. 418 | - **Examples:** `/api` or `/context` 419 | 420 | ### `threescale_cicd_api_backend_hostname` 421 | 422 | Defines the backend hostname, overriding the `host` field of the OpenAPI Specification. 423 | The resulting value is used to define the `threescale_cicd_private_base_url` variable 424 | if missing. 425 | 426 | - **Syntax:** FQDN with an optional port 427 | - **Required:** no 428 | - **Default value:** the `host` field of the OpenAPI Specification. 429 | - **Examples:** `mybackend.acme.corp` or `mybackend.acme.corp:8080` 430 | 431 | ### `threescale_cicd_api_backend_scheme` 432 | 433 | Defines the scheme to use to connect to the backend, overriding the `schemes` field of the OpenAPI Specification. 434 | The resulting value is used to define the `threescale_cicd_private_base_url` variable 435 | if missing. 436 | 437 | - **Syntax:** `http` or `https` 438 | - **Required:** no 439 | - **Default value:** the first item of the `scheme` field of the OpenAPI Specification, 440 | defaulting to `http` if missing. 441 | - **Example:** `https` 442 | 443 | ### `threescale_cicd_private_base_url` 444 | 445 | Defines the 3scale Private Base URL. 446 | 447 | - **Syntax:** `://:` 448 | - **Required:** no 449 | - **Default value:** `://` 450 | - **Example:** `http://mybackend.acme.corp:8080` 451 | 452 | ### `threescale_cicd_apicast_policies_cors` 453 | 454 | Allows to enable the CORS policy onto APICast gateway. In case your API should support cross-origin 455 | and browser based invocations and you do not have included the `OPTIONS` verb on correct path into 456 | your OpenAPI Specification file... 457 | 458 | - **Syntax:** boolean `yes` or `no` 459 | - **Required:** no 460 | - **Default value:** `no` 461 | - **Example:** `yes` if you want to activate CORS policy on APICast 462 | 463 | ### `threescale_cicd_openapi_smoketest_operation` 464 | 465 | Defines the OpenAPI Specification method to use for smoke tests. 466 | 467 | - **Syntax:** the `operationId` of the OpenAPI Specification method 468 | - **Required:** no 469 | - **Default value:** none. If this variable is undefined and if there is no operation 470 | flagged with `x-threescale-smoketests-operation` in the OpenAPI Specification, the 471 | smoke tests are skipped. 472 | - **Example:** `GetName` 473 | 474 | ### `threescale_cicd_api_environment_name` 475 | 476 | Prefixes all services with an environment name to prevent any name collision 477 | when deploying the same API multiple times on the same 3scale instance. 478 | 479 | - **Syntax:** lowercase, alphanumeric + underscore 480 | - **Required:** no 481 | - **Default value:** none, no prefixing is performed. 482 | - **Examples:** `dev`, `test` or `prod` 483 | 484 | ### `threescale_cicd_validate_openapi` 485 | 486 | Validates the OpenAPI Specification file against the official schema. To do this, 487 | the [go-swagger](https://goswagger.io/) tool is used. 488 | 489 | You can pre-install this tool somewhere in your `PATH`. Alternatively, you can 490 | also point the complete path to the `swagger` command with the 491 | `threescale_cicd_goswagger_command` extra variable. 492 | 493 | If the tool is missing, it will be automatically downloaded from GitHub and 494 | installed in `{{ threescale_cicd_local_bin_path }}`. 495 | 496 | - **Syntax:** boolean (`yes`, `no`, `true`, `false`) 497 | - **Required:** no 498 | - **Default value:** `yes` 499 | - **Examples:** 500 | - `threescale_cicd_validate_openapi=no` 501 | - `threescale_cicd_goswagger_command=/usr/local/bin/swagger` 502 | - `threescale_cicd_local_bin_path=/tmp` 503 | 504 | ### `threescale_cicd_oicd_flows` 505 | 506 | Override or update the list of supported OAuth flows for this API. 507 | 508 | - **Syntax:** array of strings (`[ 'application', 'accessCode' ]`) 509 | - **Required:** no 510 | - **Default value:** the `flow` field of the `securityScheme` object in your 511 | OpenAPI Specification file. 512 | - **Examples:** 513 | - `threescale_cicd_oicd_flows="{{ [ 'application', 'accessCode' ] }}"` (override the flow list) 514 | - `threescale_cicd_oicd_flows="{{ [ 'application', threescale_cicd_api_security_scheme.flow ] }}"` (add a flow) 515 | 516 | ### `threescale_cicd_create_default_application` 517 | 518 | Allows to create a test application with the default application plan, whether smoke tests are enabled or not. 519 | 520 | - **Syntax:** boolean (`yes`, `no`, `true`, `false`) 521 | - **Required:** no 522 | - **Default value:** `no` 523 | - **Example:** `yes` if you want a default application to be created 524 | 525 | ### Miscellaneous variables 526 | 527 | Miscellaneous variables defined in [defaults/main.yml](defaults/main.yml) 528 | provide sensible defaults. Have a look at them. 529 | 530 | ## Dependencies 531 | 532 | This role has no dependencies on other roles, but it has dependencies on: 533 | 534 | - Ansible (at least version 2.4) 535 | - JMESPath 536 | - Jinja (at least version 2.8) 537 | - 3scale API Management 2.3 538 | 539 | ## Integration with other technologies 540 | 541 | Support for major technologies is available [in the support folder](support). 542 | There is support for Jenkins, Kubernetes, Docker, OpenShift, including a 543 | [pre-built docker image](https://hub.docker.com/r/nmasse/threescale-cicd). 544 | 545 | ## License 546 | 547 | MIT 548 | 549 | ## Author Information 550 | 551 | - Nicolas Massé, Red Hat 552 | - Laurent Broudoux, Red Hat 553 | - Daria Mayorova, Red Hat 554 | 555 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 556 | [mit-link]: https://raw.githubusercontent.com/nmasse-itix/threescale-cicd/master/LICENSE 557 | [role-badge]: https://img.shields.io/badge/role-threescale--cicd-green.svg 558 | [galaxy-link]: https://galaxy.ansible.com/nmasse-itix/threescale-cicd/ 559 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | jinja2_extensions = jinja2.ext.do 3 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # By default, the OpenAPI file is read as YAML 4 | threescale_cicd_openapi_file_format: YAML 5 | 6 | # Controls how much time to wait for smoke tests to be OK and OIDC client to 7 | # appear in RH-SSO. 8 | # 9 | # Max time is threescale_cicd_delay x threescale_cicd_retries 10 | threescale_cicd_delay: 10 11 | threescale_cicd_retries: 50 12 | 13 | # How much time to wait between each write call to the 3scale Admin API 14 | threescale_cicd_throttling: 2 15 | 16 | # The two 3scale standard environments are named "sandbox" and "production" 17 | threescale_cicd_staging_environment_name: sandbox 18 | threescale_cicd_production_environment_name: production 19 | 20 | # The staging gateway has a "-staging" suffix... 21 | threescale_cicd_default_staging_suffix: -staging 22 | 23 | # ... while the production one does not. 24 | threescale_cicd_default_production_suffix: "" 25 | 26 | # APIcast instances are expected to be configured for TLS 27 | threescale_cicd_default_apicast_scheme: https 28 | 29 | # The OIDC scopes to use for smoke tests 30 | threescale_cicd_openapi_smoketest_default_scope: openid 31 | threescale_cicd_default_oauth_scopes: 32 | openid: Any OpenID Connect token 33 | 34 | # The application plans to create with each service 35 | threescale_cicd_application_plans: 36 | - system_name: ansible 37 | default: false 38 | state: hidden 39 | name: Ansible Test Plan 40 | 41 | # Controls when to log sensitive information. Can be set to false for 42 | # production environments. 43 | # 44 | # By default, log sensitive information only when Ansible is called with 45 | # A verbosity level of at least one "-v". 46 | threescale_cicd_nolog: '{{ ansible_verbosity|default(0) == 0 }}' 47 | 48 | # A folder where to download dependencies, when required 49 | threescale_cicd_local_bin_path: '{{ playbook_dir }}/bin' 50 | 51 | # Enable the OpenAPI Specification validation 52 | threescale_cicd_validate_openapi: yes 53 | 54 | ## 55 | ## APIcast policies 56 | ## 57 | 58 | # CORS 59 | threescale_cicd_apicast_policies_cors: no 60 | 61 | ## 62 | ## Policies computation 63 | ## 64 | ## what we want 65 | threescale_cicd_wanted_policies: '{{ lookup(''template'', ''wanted_policies.j2'') }}' 66 | ## what we have 67 | threescale_cicd_existing_policies: '{{ lookup(''template'', ''existing_policies.j2'') }}' 68 | # update the items that we want and we have 69 | threescale_cicd_policies_to_update: '{{ threescale_cicd_wanted_policies|union(threescale_cicd_existing_policies) }}' 70 | 71 | # APIcast public base URLs 72 | threescale_cicd_apicast_sandbox_endpoint: '{{ lookup(''template'', ''openapi/apicast_sandbox_endpoint.j2'') }}' 73 | threescale_cicd_apicast_production_endpoint: '{{ lookup(''template'', ''openapi/apicast_production_endpoint.j2'') }}' 74 | 75 | # SSO Issuer Endpoint 76 | threescale_cicd_sso_issuer_endpoint: '{{ lookup(''template'', ''openapi/sso_issuer_endpoint.j2'') }}' 77 | 78 | ## 79 | ## Default Application (used for Smoke Tests) 80 | ## 81 | threescale_cicd_default_application_name: 'Ansible smoke-tests default application' 82 | 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.' 83 | 84 | # The application plan to pick for the default application (the one used for 85 | # smoke tests) 86 | threescale_cicd_default_application_plan: '{{ (threescale_cicd_application_plans|first).system_name }}' 87 | 88 | # Compute the default application's appid. By default, we are using a combination 89 | # of app, api and environment data, hashed toghether to produce a stable id. 90 | threescale_cicd_default_application_appid: '{{ (threescale_cicd_default_application_name ~ threescale_cicd_api_system_name ~ threescale_cicd_access_token)|hash(''sha1'') }}' 91 | threescale_cicd_default_application_appsecret: '{{ (''secret'' ~ threescale_cicd_default_application_name ~ threescale_cicd_api_system_name ~ threescale_cicd_access_token)|hash(''sha1'') }}' 92 | 93 | ## 94 | ## Create test application whether or not smoke tests are enabled 95 | ## 96 | threescale_cicd_create_default_application: no 97 | 98 | # The OpenAPI Operation to use for the smoketest 99 | threescale_cicd_openapi_smoketest_operation: '{{ threescale_cicd_openapi_file_content|json_query(''paths.*.get[? "x-threescale-smoketests-operation" ].operationId'')|first|default("")|regex_replace(''[^0-9a-zA-Z_]+'', ''_'') }}' 100 | 101 | ## 102 | ## OpenAPI Specification File parsing 103 | ## 104 | threescale_cicd_api_basepath: '{{ threescale_cicd_openapi_file_content.basePath|default("") }}' 105 | threescale_cicd_api_base_system_name: '{{ lookup(''template'', ''openapi/generate_base_system_name.j2'') }}' 106 | threescale_cicd_api_system_name: '{{ lookup(''template'', ''openapi/generate_final_system_name.j2'') }}' 107 | threescale_cicd_private_base_url: '{{ lookup(''template'', ''openapi/private_base_url.j2'') }}' 108 | 109 | # Credentials are expected to be passed in HTTP headers unless stated otherwise 110 | # and only for API Keys 111 | 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'' }}' 112 | 113 | 114 | # The OpenAPI file to be pushed to 3scale as an ActiveDocs 115 | threescale_cicd_openapi_rewritten: '{{ lookup(''template'', ''rewritten_openapi.j2'') }}' 116 | 117 | # Compute the Keycloak Realm endpoint from the threescale_cicd_sso_issuer_endpoint 118 | 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'')) }}' 119 | 120 | # Compute the Keycloak REST Admin Endpoint from the threescale_cicd_sso_realm_endpoint 121 | threescale_cicd_sso_admin_endpoint: '{{ threescale_cicd_sso_realm_endpoint|replace(''/auth/realms/'', ''/auth/admin/realms/'') }}' 122 | 123 | ## 124 | ## OpenAPI Specification File parsing 125 | ## 126 | 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 }}' 127 | threescale_cicd_openapi_file_version: '{{ threescale_cicd_openapi_file_content.swagger }}' 128 | threescale_cicd_api_default_name: '{{ threescale_cicd_openapi_file_content.info.title|default("API") }}' 129 | threescale_cicd_api_name: '{{ lookup(''template'', ''openapi/service_name.j2'') }}' 130 | threescale_cicd_api_description: '{{ threescale_cicd_openapi_file_content.info.description|default("") }}' 131 | threescale_cicd_api_version: '{{ threescale_cicd_openapi_file_content.info.version|default("0.0.1") }}' 132 | threescale_cicd_api_version_components: '{{ threescale_cicd_api_version.split(".") }}' 133 | threescale_cicd_api_version_major: '{{ threescale_cicd_api_version_components|first }}' 134 | threescale_cicd_api_security_requirements: '{{ threescale_cicd_openapi_file_content.security|default([]) }}' 135 | threescale_cicd_api_security_definitions: '{{ threescale_cicd_openapi_file_content.securityDefinitions|default({}) }}' 136 | threescale_cicd_api_security_scheme_name: '{{ (threescale_cicd_api_security_requirements|first|default(''{ "none": {} }'')).keys()|list|first }}' 137 | 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 {} }}' 138 | 139 | ## 140 | ## Mapping Rules computation 141 | ## 142 | # what we want 143 | threescale_cicd_wanted_mapping_rules: '{{ lookup(''template'', ''wanted_mapping_rules.j2'') }}' 144 | # what we have 145 | threescale_cicd_existing_mapping_rules: '{{ lookup(''template'', ''existing_mapping_rules.j2'') }}' 146 | 147 | ## 148 | ## 3scale API Payload definition 149 | ## 150 | threescale_cicd_update_proxy_payload: '{{ lookup(''template'', ''api-calls/update_proxy.j2'') }}' 151 | threescale_cicd_update_service_payload: '{{ lookup(''template'', ''api-calls/update_service.j2'') }}' 152 | threescale_cicd_create_service_payload: '{{ lookup(''template'', ''api-calls/create_service.j2'') }}' 153 | threescale_cicd_update_method_payload: '{{ lookup(''template'', ''api-calls/update_method.j2'') }}' 154 | threescale_cicd_create_method_payload: '{{ lookup(''template'', ''api-calls/create_method.j2'') }}' 155 | threescale_cicd_update_mapping_rule_payload: '{{ lookup(''template'', ''api-calls/update_mapping_rule.j2'') }}' 156 | threescale_cicd_create_mapping_rule_payload: '{{ lookup(''template'', ''api-calls/create_mapping_rule.j2'') }}' 157 | threescale_cicd_update_policies_payload: '{{ lookup(''template'', ''api-calls/update_policies.j2'') }}' 158 | threescale_cicd_update_application_plan_payload: '{{ lookup(''template'', ''api-calls/update_application_plan.j2'') }}' 159 | threescale_cicd_create_application_plan_payload: '{{ lookup(''template'', ''api-calls/create_application_plan.j2'') }}' 160 | threescale_cicd_find_application_payload: '{{ lookup(''template'', ''api-calls/find_application.j2'') }}' 161 | threescale_cicd_update_application_payload: '{{ lookup(''template'', ''api-calls/update_application.j2'') }}' 162 | threescale_cicd_create_application_payload: '{{ lookup(''template'', ''api-calls/create_application.j2'') }}' 163 | threescale_cicd_authenticate_to_keycloak_payload: '{{ lookup(''template'', ''api-calls/keycloak/authenticate.j2'') }}' 164 | threescale_cicd_patch_keycloak_client_payload: '{{ lookup(''template'', ''api-calls/keycloak/patch_client.j2'') }}' 165 | threescale_cicd_smoke_test_headers: '{{ lookup(''template'', ''api-calls/smoke-test/headers.j2'') }}' 166 | threescale_cicd_smoke_test_url: '{{ lookup(''template'', ''api-calls/smoke-test/url.j2'') }}' 167 | threescale_cicd_promote_proxy_payload: '{{ lookup(''template'', ''api-calls/promote_proxy.j2'') }}' 168 | threescale_cicd_update_activedoc_payload: '{{ lookup(''template'', ''api-calls/update_activedoc.j2'') }}' 169 | threescale_cicd_create_activedoc_payload: '{{ lookup(''template'', ''api-calls/create_activedoc.j2'') }}' 170 | threescale_cicd_update_oidc_configuration_payload: '{{ lookup(''template'', ''api-calls/update_oidc_configuration.j2'') }}' -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: Nicolas Massé 3 | description: Enables Continuous Deployment with 3scale API Management Platform 4 | company: Red Hat 5 | license: MIT 6 | min_ansible_version: 2.4 7 | galaxy_tags: 8 | - api 9 | - automation 10 | - ci 11 | - cd 12 | - deployment 13 | - deploy 14 | - redhat 15 | - management 16 | - 3scale 17 | - threescale 18 | platforms: 19 | - name: GenericLinux 20 | versions: 21 | - any 22 | dependencies: [] 23 | -------------------------------------------------------------------------------- /support/README.md: -------------------------------------------------------------------------------- 1 | # Support for popular technologies 2 | 3 | You can use this Ansible role from other popular technologies: 4 | 5 | - [Jenkins](jenkins/): if you want to deploy your APIs on 3scale from a Jenkins pipeline, using a custom slave. 6 | - [Ansible Tower / AWX](awx/): if you want to enforce RBAC, have audit, etc. 7 | - [Docker](docker/): deploy an API to 3scale from the command line with minimal dependencies 8 | - [Kubernetes](kubernetes/): deploy an API to 3scale from a Kubernetes Job 9 | - [OpenShift](openshift/): deploying an API to 3scale is as easy as starting a Build from a BuildConfig 10 | -------------------------------------------------------------------------------- /support/awx/README.md: -------------------------------------------------------------------------------- 1 | # Using this Ansible role from AWX / Ansible Tower 2 | 3 | Ansible has powerful concepts to separate the HOW (how to deploy an API) from 4 | the WHERE (where to deploy an API). Ansible Tower / AWX can push this to the 5 | next level by enforcing RBAC, having audit logs and providing to developers 6 | *Deploy an API to 3scale* as-a-service. 7 | 8 | You can have a look at those two blog posts that present this approach in greater 9 | details: 10 | 11 | - [Integrating Ansible with Jenkins in a CI/CD process](https://www.redhat.com/en/blog/integrating-ansible-jenkins-cicd-process) 12 | - [Take Ansible and Jenkins Integration to the next level: CI/CD with Ansible Tower](https://www.redhat.com/en/blog/take-ansible-and-jenkins-integration-next-level-cicd-ansible-tower) 13 | 14 | There is also a webinar showing this approach as a demo: [Take Ansible and Jenkins integration to the next level: CI/CD with Ansible Tower](https://www.redhat.com/en/events/webinar/take-ansible-and-jenkins-integration-next-level-cicd-ansible-tower). 15 | 16 | ## Setup 17 | 18 | Install the Tower CLI: 19 | 20 | ```sh 21 | sudo yum install python2-ansible-tower-cli 22 | ``` 23 | 24 | Review the [tower-assets.yaml](tower-assets.yaml) and adjust it to match your environment. 25 | Search for those placeholders to replace: 26 | 27 | - YOUR_ACCESS_TOKEN 28 | - CLIENT_ID 29 | - CLIENT_SECRET 30 | - SSO_HOST 31 | - REALM 32 | - TENANT 33 | 34 | Use the tower-cli to create the resources in Tower: 35 | 36 | ```sh 37 | tower-cli send -h tower.hostname -u admin -p secret support/awx/tower-assets.yaml 38 | ``` 39 | 40 | You can now provision an API from your favourite CI/CD tool. For example, from Jenkins you could use: 41 | 42 | ```groovy 43 | def towerExtraVars = [ 44 | git_repository: "https://github.com/nmasse-itix/rhte-api.git", 45 | git_ref: "master", 46 | openapi_file: "openapi-spec.yaml", 47 | threescale_cicd_api_base_system_name: "event_api", 48 | threescale_cicd_private_base_url: "https://echo-api.3scale.net", 49 | threescale_cicd_api_environment_name: "prod", 50 | threescale_cicd_wildcard_domain: "prod.app.openshift.test" 51 | ] 52 | 53 | ansibleTower towerServer: "tower", 54 | inventory: "3scale", 55 | jobTemplate: "Deploy an API to 3scale", 56 | extraVars: JsonOutput.toJson(towerExtraVars) 57 | ``` 58 | 59 | ## Advanced usage 60 | 61 | If you need to customize the playbooks, the inventory or both, you can follow this guide: 62 | 63 | - define an inventory for each of your environments (dev, test, prod, etc.) 64 | - in those inventories, define a group (let's say `threescale`) containing 65 | the 3scale Admin Portal(s) of this environment 66 | - set all the variables that depends on the environment (`threescale_cicd_wildcard_domain`, `threescale_cicd_api_environment_name`, etc.) as 67 | group variables 68 | - create a playbook, committed in your GIT repository and reference it as a Project 69 | in Tower 70 | - in this playbook, use the `assert` module to do some surface checks and set the variables that depends on the API being provisioned (such as `threescale_cicd_private_base_url`) 71 | - create the corresponding Job Template 72 | 73 | A very minimalistic playbook could be: 74 | 75 | ```yaml 76 | --- 77 | - name: Deploy an API on a 3scale instance 78 | hosts: threescale 79 | gather_facts: no 80 | pre_tasks: 81 | - assert: 82 | that: 83 | - "git_repo is defined" 84 | - name: Clone the git repo containing the API Definition 85 | git: 86 | repo: '{{ git_repo }}' 87 | dest: '{{ playbook_dir }}/api' 88 | version: '{{ git_branch|default(''master'') }}' 89 | delegate_to: localhost 90 | - set_fact: 91 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api/{{ openapi_file|default(''openapi-spec.yaml'') }}' 92 | roles: 93 | - nmasse-itix.threescale-cicd 94 | ``` 95 | 96 | Then, make sure to reference this module in your `roles/requirements.yml` file: 97 | 98 | ```yaml 99 | --- 100 | - src: nmasse-itix.threescale-cicd 101 | version: 1.1.0 102 | ``` 103 | 104 | You can reference a specific version like in this example or leave the `version` 105 | field out. This will pick the latest version available. 106 | 107 | **Caution:** once the role has been installed locally, it will never be 108 | automatically updated, even if you change the `version` field. 109 | 110 | To update this role to a more recent version use: 111 | 112 | ```sh 113 | ansible-galaxy install -f nmasse-itix.threescale-cicd,1.1.0 -p roles/ 114 | ``` 115 | -------------------------------------------------------------------------------- /support/awx/ansible.cfg: -------------------------------------------------------------------------------- 1 | ../../ansible.cfg -------------------------------------------------------------------------------- /support/awx/deploy-api.yaml: -------------------------------------------------------------------------------- 1 | ../docker/deploy-api.yaml -------------------------------------------------------------------------------- /support/awx/roles/nmasse-itix.threescale-cicd: -------------------------------------------------------------------------------- 1 | ../../../ -------------------------------------------------------------------------------- /support/awx/tower-assets.yaml: -------------------------------------------------------------------------------- 1 | - ask_variables_on_launch: true 2 | asset_relation: 3 | extra_credentials: [] 4 | notification_templates_error: [] 5 | notification_templates_success: [] 6 | roles: 7 | - name: Read 8 | team: [] 9 | user: [] 10 | - name: Execute 11 | team: [] 12 | user: [] 13 | - name: Admin 14 | team: [] 15 | user: [] 16 | schedules: [] 17 | survey_spec: {} 18 | asset_type: job_template 19 | name: Deploy an API to 3scale 20 | playbook: support/awx/deploy-api.yaml 21 | inventory: 3scale 22 | project: Deploy API to 3scale 23 | verbosity: 1 24 | - asset_relation: 25 | notification_templates_error: [] 26 | notification_templates_success: [] 27 | roles: 28 | - name: Admin 29 | team: [] 30 | user: [] 31 | - name: Read 32 | team: [] 33 | user: [] 34 | - name: Use 35 | team: [] 36 | user: [] 37 | - name: Update 38 | team: [] 39 | user: [] 40 | schedules: [] 41 | asset_type: project 42 | description: Enable continuous deployment of an API to 3scale AMP 43 | name: Deploy API to 3scale 44 | organization: Default 45 | scm_branch: master 46 | scm_type: git 47 | scm_update_cache_timeout: 60 48 | scm_update_on_launch: true 49 | scm_url: https://github.com/nmasse-itix/threescale-cicd.git 50 | - asset_relation: 51 | group: 52 | - hosts: 53 | - TENANT-admin.3scale.net 54 | name: threescale 55 | sub_groups: [] 56 | variables: '--- 57 | 58 | threescale_cicd_access_token: YOUR_ACCESS_TOKEN 59 | 60 | threescale_cicd_sso_issuer_endpoint: https://CLIENT_ID:CLIENT_SECRET@SSO_HOST/auth/realms/REALM' 61 | host: 62 | - name: TENANT-admin.3scale.net 63 | inventory_source: [] 64 | roles: 65 | - name: Ad Hoc 66 | team: [] 67 | user: [] 68 | - name: Admin 69 | team: [] 70 | user: [] 71 | - name: Read 72 | team: [] 73 | user: [] 74 | - name: Use 75 | team: [] 76 | user: [] 77 | - name: Update 78 | team: [] 79 | user: [] 80 | asset_type: inventory 81 | name: 3scale 82 | organization: Default 83 | variables: '--- 84 | 85 | ansible_connection: local' 86 | -------------------------------------------------------------------------------- /support/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | MAINTAINER Nicolas Masse 4 | 5 | LABEL io.k8s.display-name="Ansible role nmasse-itix.threescale-cicd" \ 6 | io.k8s.description="Deploys an API to 3scale API Management." \ 7 | io.openshift.tags="3scale" 8 | 9 | ARG THREESCALE_CICD_GIT_REPOSITORY=https://github.com/nmasse-itix/threescale-cicd.git 10 | 11 | # This one is by convention used by the Docker Build services. 12 | # See https://docs.docker.com/docker-hub/builds/advanced/ 13 | ARG SOURCE_BRANCH=master 14 | 15 | RUN yum --enablerepo=extras install -y epel-release centos-release-scl && \ 16 | yum-config-manager --enable rhel-server-rhscl-7-rpms && \ 17 | yum install -y ansible git python27-python-pip && \ 18 | # Remove the existing jinja2 library and its dependencies before re-installing it 19 | # This is mandatory to prevent any leftover from a previous install 20 | rm -rf /usr/lib/python2.7/site-packages/markupsafe /usr/lib/python2.7/site-packages/jinja2 && \ 21 | scl enable python27 "pip install --install-option='--install-purelib=/usr/lib/python2.7/site-packages/' jinja2" && \ 22 | yum clean all && \ 23 | rm -rf /var/cache/yum && \ 24 | mkdir -p /opt/ansible/threescale-cicd && \ 25 | git clone -b ${SOURCE_BRANCH} -- ${THREESCALE_CICD_GIT_REPOSITORY} /opt/ansible/threescale-cicd && \ 26 | cd /opt/ansible/threescale-cicd/support/docker && mkdir api && \ 27 | ansible-playbook install.yaml 28 | 29 | WORKDIR /opt/ansible/threescale-cicd/support/docker 30 | VOLUME [ "/opt/ansible/threescale-cicd/support/docker/api" ] 31 | 32 | ENTRYPOINT [ "/usr/bin/ansible-playbook", "deploy-api.yaml" ] 33 | CMD [ ] 34 | -------------------------------------------------------------------------------- /support/docker/README.md: -------------------------------------------------------------------------------- 1 | # Using this Ansible role from Docker 2 | 3 | You can use this Ansible role as a container image and provision an API in 4 | 3scale very quickly. 5 | 6 | You would first need to provision your 3scale Admin Portal hostname, access token 7 | and optionally the Red Hat SSO Issuer Endpoint in the same format as a Kubernetes 8 | secret: 9 | 10 | ```sh 11 | mkdir -p /tmp/secrets 12 | cat > /tmp/secrets/hostname < /tmp/secrets/access_token < /tmp/secrets/sso_issuer_endpoint < 0 else lookup(''env'', ''threescale_portal_hostname'') }}' 12 | threescale_cicd_access_token: '{{ lookup(''env'', ''THREESCALE_CICD_ACCESS_TOKEN'') if lookup(''env'', ''THREESCALE_CICD_ACCESS_TOKEN'')|length > 0 else lookup(''env'', ''threescale_cicd_access_token'') }}' 13 | tasks: 14 | - block: 15 | - name: Check if /tmp/secrets/hostname exists 16 | stat: 17 | path: /tmp/secrets/hostname 18 | register: secrets 19 | 20 | - name: Fetch the threescale_portal_hostname variable from /tmp/secrets/hostname 21 | set_fact: 22 | threescale_portal_hostname: '{{ lookup(''file'', ''/tmp/secrets/hostname'') }}' 23 | when: secrets.stat.exists 24 | 25 | - name: Check if /tmp/secrets/access_token exists 26 | stat: 27 | path: /tmp/secrets/access_token 28 | register: secrets 29 | 30 | - name: Fetch the threescale_cicd_access_token variable from /tmp/secrets/access_token 31 | set_fact: 32 | threescale_cicd_access_token: '{{ lookup(''file'', ''/tmp/secrets/access_token'') }}' 33 | when: secrets.stat.exists 34 | 35 | - assert: 36 | that: threescale_portal_hostname|length > 0 37 | msg: > 38 | Please pass the hostname of your 3scale Admin Portal in "hostname" key of the 39 | "3scale-admin-portal" secret. 40 | 41 | - assert: 42 | that: threescale_cicd_access_token|length > 0 43 | msg: > 44 | Please pass the access token of your 3scale Admin Portal in "access_token" key of the 45 | "3scale-admin-portal" secret. 46 | 47 | # Generate dynamically a one host inventory 48 | - add_host: 49 | hostname: '{{ threescale_portal_hostname }}' 50 | groups: 51 | - threescale 52 | threescale_cicd_access_token: '{{ threescale_cicd_access_token }}' 53 | when: groups['threescale']|default([])|length == 0 54 | 55 | - name: Deploy an API to 3scale 56 | hosts: threescale 57 | gather_facts: no 58 | vars: 59 | # Support for OpenShift custom build 60 | # 61 | # The git_repository, git_context_dir and git_ref are taken from the OpenShift build definition 62 | # but they can be overriden from the command line as extra vars (-e git_repository=... 63 | # -e git_ref=... -e git_context_dir=...) or environment variables (GIT_REPOSITORY=..., GIT_REF=..., 64 | # GIT_CONTEXT_DIR=...) 65 | build: '{{ lookup(''env'', ''BUILD'')|from_json if lookup(''env'', ''BUILD'')|length > 0 else {} }}' 66 | git_repository: '{{ build.spec.source.git.uri if ''spec'' in build and ''uri'' in build.spec.source.git else '''' }}' 67 | git_context_dir: '{{ build.spec.source.contextDir if ''spec'' in build and ''contextDir'' in build.spec.source else '''' }}' 68 | git_ref: '{{ build.spec.source.git.ref if ''spec'' in build and ''ref'' in build.spec.source.git else ''master'' }}' 69 | openapi_file: openapi-spec.yaml 70 | 71 | ansible_connection: local 72 | threescale_cicd_openapi_file: '{{ playbook_dir ~ "/api/" ~ git_context_dir ~ "/" ~ openapi_file if git_repository|length > 0 else playbook_dir ~ "/api/" ~ openapi_file }}' 73 | parameter_whitelist: 74 | - git_repository 75 | - git_ref 76 | - git_context_dir 77 | - openapi_file # relative path to the OpenAPI file 78 | - threescale_cicd_openapi_file # absolute path to the OpenAPI file 79 | - threescale_cicd_openapi_file_format 80 | - threescale_cicd_api_system_name 81 | - threescale_cicd_api_base_system_name 82 | - threescale_cicd_wildcard_domain 83 | - threescale_cicd_api_basepath 84 | - threescale_cicd_api_backend_hostname 85 | - threescale_cicd_api_backend_scheme 86 | - threescale_cicd_private_base_url 87 | - threescale_cicd_apicast_policies_cors 88 | - threescale_cicd_openapi_smoketest_operation 89 | - threescale_cicd_api_environment_name 90 | - threescale_cicd_validate_openapi 91 | - threescale_cicd_apicast_sandbox_endpoint 92 | - threescale_cicd_apicast_production_endpoint 93 | - threescale_cicd_sso_issuer_endpoint 94 | - threescale_cicd_create_default_application 95 | pre_tasks: 96 | - name: Accept threescale_cicd_* variables from environment variables (lowercase) 97 | set_fact: 98 | '{{ item|lower }}': '{{ lookup(''env'', item|lower) }}' 99 | with_items: '{{ parameter_whitelist }}' 100 | when: 'lookup(''env'', item|lower)|length > 0' 101 | 102 | - name: Accept threescale_cicd_* variables from environment variables (uppercase) 103 | set_fact: 104 | '{{ item|lower }}': '{{ lookup(''env'', item|upper) }}' 105 | with_items: '{{ parameter_whitelist }}' 106 | when: 'lookup(''env'', item|upper)|length > 0' 107 | 108 | - name: Clone the git repo containing the API Definition 109 | git: 110 | repo: '{{ git_repository }}' 111 | dest: '{{ playbook_dir }}/api' 112 | version: '{{ git_ref }}' 113 | when: 'git_repository|length > 0' 114 | 115 | - name: Check if /tmp/secrets/sso_issuer_endpoint exists 116 | stat: 117 | path: /tmp/secrets/sso_issuer_endpoint 118 | register: secrets 119 | 120 | - name: Fetch the threescale_cicd_sso_issuer_endpoint variable from /tmp/secrets/sso_issuer_endpoint 121 | set_fact: 122 | threescale_cicd_sso_issuer_endpoint: '{{ lookup(''file'', ''/tmp/secrets/sso_issuer_endpoint'') }}' 123 | when: secrets.stat.exists 124 | 125 | roles: 126 | - nmasse-itix.threescale-cicd 127 | -------------------------------------------------------------------------------- /support/docker/install.yaml: -------------------------------------------------------------------------------- 1 | - name: Install the pre-requisites 2 | hosts: localhost 3 | gather_facts: no 4 | vars: 5 | ansible_connection: local 6 | tasks: 7 | - import_role: 8 | name: 'nmasse-itix.threescale-cicd' 9 | tasks_from: 'install_prerequisites' 10 | -------------------------------------------------------------------------------- /support/docker/roles/nmasse-itix.threescale-cicd: -------------------------------------------------------------------------------- 1 | ../../.. -------------------------------------------------------------------------------- /support/jenkins/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openshift/jenkins-slave-base-centos7:v3.11 2 | 3 | MAINTAINER Nicolas Masse 4 | 5 | # Labels consumed by Red Hat build service 6 | LABEL name="openshift3/jenkins-agent-ansible-26-centos7" \ 7 | version="v3.11" \ 8 | architecture="x86_64" \ 9 | io.k8s.display-name="Jenkins Agent Ansible" \ 10 | io.k8s.description="The jenkins agent ansible image has the Ansible engine on top of the jenkins slave base image." \ 11 | io.openshift.tags="openshift,jenkins,agent,ansible" 12 | 13 | USER root 14 | 15 | # Set a safe value for the temporary directory. Otherwise the ansible-playbook command fails when run from a jenkins slave: 16 | # AnsibleError: Unable to create local directories(/home/jenkins/.ansible/tmp): [Errno 13] Permission denied: '/home/jenkins/.ansible/tmp' 17 | ENV DEFAULT_LOCAL_TMP=/tmp 18 | 19 | RUN yum install -y epel-release && \ 20 | yum install -y 'ansible >= 2.6' && \ 21 | yum install -y python27-python-pip && \ 22 | # Remove the existing jinja2 library and its dependencies before re-installing it 23 | # This is mandatory to prevent any leftover from a previous install 24 | rm -rf /usr/lib/python2.7/site-packages/markupsafe /usr/lib/python2.7/site-packages/jinja2 && \ 25 | scl enable python27 "pip install --install-option='--install-purelib=/usr/lib/python2.7/site-packages/' jinja2" && \ 26 | yum clean all && \ 27 | rm -rf /var/cache/yum && \ 28 | chown -R 1001:0 $HOME && \ 29 | chmod -R g+rw $HOME 30 | 31 | USER 1001 32 | -------------------------------------------------------------------------------- /support/jenkins/Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | /* 4 | * This Jenkins Pipeline depends on the following plugins : 5 | * - Credentials Binding (https://plugins.jenkins.io/credentials-binding) 6 | * - Ansible (https://plugins.jenkins.io/ansible) 7 | */ 8 | 9 | pipeline { 10 | agent { label 'jenkins-ansible-slave.latest' } 11 | 12 | parameters { 13 | // Environment parameters 14 | credentials(name: 'THREESCALE_CICD_ACCESS_TOKEN', description: 'The 3scale Access Token', credentialType: "Secret text", required: true) 15 | credentials(name: 'THREESCALE_CICD_SSO_ISSUER_ENDPOINT', description: 'The SSO Issuer Endpoint when deploying an API with OpenID Connect', credentialType: "Secret text", required: false) 16 | string(name: 'THREESCALE_PORTAL_HOSTNAME', description: 'The 3scale Admin Portal hostname') 17 | string(name: 'THREESCALE_CICD_PRIVATE_BASE_URL', description: 'The 3scale private base URL', defaultValue: 'https://echo-api.3scale.net') 18 | string(name: 'THREESCALE_CICD_API_SYSTEM_NAME', description: 'Override the 3scale Service system_name') 19 | string(name: 'THREESCALE_CICD_API_BASE_SYSTEM_NAME', description: 'Define the base name to compute the final system_name') 20 | string(name: 'THREESCALE_CICD_WILDCARD_DOMAIN', description: 'Automatically defines the APIcast public URLs based on a scheme') 21 | string(name: 'THREESCALE_CICD_API_ENVIRONMENT_NAME', description: 'Prefixes all services with an environment name to prevent any name collision when deploying the same API multiple times on the same 3scale instance') 22 | string(name: 'THREESCALE_CICD_APICAST_SANDBOX_ENDPOINT', description: 'Defines the Public Staging Base URL') 23 | string(name: 'THREESCALE_CICD_APICAST_PRODUCTION_ENDPOINT', description: 'Defines the Public Production Base URL') 24 | 25 | // Details about the API to deploy 26 | string(name: 'API_REPOSITORY', description: 'The GIT repository to checkout, containing the OpenAPI Specifications') 27 | string(name: 'API_BRANCH', description: 'The GIT branch or tag to checkout, containing the OpenAPI Specifications', defaultValue: 'master') 28 | string(name: 'OPENAPI_FILE', description: 'The path to the OpenAPI Specification within the GIT Repository') 29 | booleanParam(name: 'THREESCALE_CICD_APICAST_POLICIES_CORS', description: 'Allows to enable the CORS policy onto APICast gateway', defaultValue: false) 30 | string(name: 'THREESCALE_CICD_OPENAPI_SMOKETEST_OPERATION', description: 'Defines the OpenAPI Specification method to use for smoke tests') 31 | string(name: 'THREESCALE_CICD_API_BASEPATH', description: 'Overrides the OpenAPI basePath field') 32 | choice(name: 'THREESCALE_CICD_OPENAPI_FILE_FORMAT', description: 'Read the OpenAPI file as YAML or JSON', choices: [ "YAML", "JSON" ]) 33 | 34 | // Misc. params 35 | booleanParam(name: 'THREESCALE_CICD_VALIDATE_OPENAPI', description: 'Validates the OpenAPI Specification file against the official schema', defaultValue: true) 36 | booleanParam(name: 'ANSIBLE_VERBOSE', description: 'Run Ansible in verbose mode (-v)', defaultValue: false) 37 | booleanParam(name: 'THREESCALE_CICD_CREATE_DEFAULT_APPLICATION', description: 'Create a test application with the default application plan, whether smoke tests are enabled or not', defaultValue: true) 38 | } 39 | 40 | stages { 41 | stage("Pre-requisites") { 42 | steps { 43 | script { 44 | if (paramIsEmpty(params.THREESCALE_CICD_ACCESS_TOKEN)) { 45 | currentBuild.result = 'ABORTED' 46 | error("The 3scale Access Token cannot be left empty") 47 | } 48 | if (paramIsEmpty(params.THREESCALE_PORTAL_HOSTNAME)) { 49 | currentBuild.result = 'ABORTED' 50 | error("The 3scale Admin Portal hostname cannot be left empty") 51 | } 52 | if (params.THREESCALE_PORTAL_HOSTNAME.startsWith("http://") || params.THREESCALE_PORTAL_HOSTNAME.startsWith("https://")) { 53 | currentBuild.result = 'ABORTED' 54 | error("The 3scale Admin Portal hostname must be the bare hostname (no http:// or https://)") 55 | } 56 | if (paramIsEmpty(params.API_REPOSITORY)) { 57 | currentBuild.result = 'ABORTED' 58 | error("The GIT Repository URL cannot be left empty") 59 | } 60 | if (paramIsEmpty(params.API_BRANCH)) { 61 | currentBuild.result = 'ABORTED' 62 | error("The GIT Repository Branch / Tag name cannot be left empty") 63 | } 64 | if (paramIsEmpty(params.OPENAPI_FILE)) { 65 | currentBuild.result = 'ABORTED' 66 | error("The path to the OpenAPI Specification within the GIT Repository cannot be left empty") 67 | } 68 | 69 | } 70 | } 71 | } 72 | stage("GIT Checkout") { 73 | steps { 74 | // Checkout the GIT repository containing the Ansible Playbook 75 | checkout scm 76 | //git url: 'https://github.com/nmasse-itix/threescale-cicd.git' 77 | 78 | // Checkout the GIT repository containing the OpenAPI Specification to provision 79 | checkout([ $class: 'GitSCM', 80 | branches: [[name: '*/'+params.API_BRANCH]], 81 | doGenerateSubmoduleConfigurations: false, 82 | extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'support/jenkins/api']], 83 | submoduleCfg: [], 84 | userRemoteConfigs: [[url: params.API_REPOSITORY]]]) 85 | } 86 | } 87 | 88 | stage("Deploy an API to 3scale") { 89 | steps { 90 | script { 91 | // Generate the credential bindings: 92 | // - THREESCALE_CICD_ACCESS_TOKEN is mandatory 93 | // - THREESCALE_CICD_SSO_ISSUER_ENDPOINT is optional 94 | credentialBindings = [ string(credentialsId: params.THREESCALE_CICD_ACCESS_TOKEN, variable: 'THREESCALE_CICD_ACCESS_TOKEN') ]; 95 | if (! paramIsEmpty(params.THREESCALE_CICD_SSO_ISSUER_ENDPOINT)) { 96 | credentialBindings.add(string(credentialsId: params.THREESCALE_CICD_SSO_ISSUER_ENDPOINT, variable: 'THREESCALE_CICD_SSO_ISSUER_ENDPOINT')) 97 | } 98 | } 99 | 100 | // Run the Ansible Playbook 101 | withCredentials(credentialBindings) { 102 | ansiblePlaybook(playbook: 'support/jenkins/deploy-api.yaml', 103 | extraVars: [ openapi_file: params.OPENAPI_FILE ], 104 | extras: paramIsEmpty(params.ANSIBLE_VERBOSE) ? '' : '-v') 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | def credentialBindings = null; 112 | def paramIsEmpty(param) { 113 | return param == null || param == ""; 114 | } -------------------------------------------------------------------------------- /support/jenkins/README.md: -------------------------------------------------------------------------------- 1 | # \[DEPRECATED\] 2 | 3 | Since we received too much negative feedbacks about this Jenkins integration (complex to setup, not stable enough, hard to debug, etc.), we need to accept the truth: it is not suitable for customer use and long term support. 4 | 5 | Hopefully, we came up with a different approach that proved (at least until now) much more stable and easier to setup: the [OpenShift integration](../openshift/). 6 | 7 | With the [OpenShift integration](../openshift/), you can still provision an API to 3scale from a Jenkins pipeline but it also brings several advantages : 8 | 9 | - Credentials (3scale Access Token, SSO Issuer Endpoint) are managed by OpenShift and thus subject to RBAC (**more secure**) 10 | - Playbooks logs are stored in OpenShift (**easier to debug**) 11 | - Environmental parameters (admin portal url, public base URL, etc.) are stored and managed in OpenShift (**easier to manage**) 12 | - All artefacts are stored in OpenShift: you can use "Configuration-as-Code" principles to define the target state of your infrastructure without any manual setup in Jenkins (**easier to configure**) 13 | 14 | Have a try to the [OpenShift integration](../openshift/) and let us know what you think! 15 | 16 | # Using this Ansible role from Jenkins 17 | 18 | You can use this Ansible role from Jenkins to include 3scale in your Continuous 19 | Deployment pipeline. 20 | 21 | To use this role from Jenkins, you will need to: 22 | 23 | - Create the Jenkins Slave image for Ansible 24 | - Install the Ansible Jenkins plugin 25 | - Create the pipeline that calls Ansible 26 | - Give your 3scale Access Token to Jenkins 27 | - Run the pipeline! 28 | 29 | ## Create the Jenkins Slave image for Ansible 30 | 31 | You can create the Jenkins Slave image for Ansible by executing the following command **in the same project as your Jenkins master**: 32 | 33 | ```sh 34 | oc import-image jenkins-ansible-slave:master --from=docker.io/nmasse/jenkins-ansible-slave:master --confirm 35 | oc annotate is jenkins-ansible-slave role=jenkins-slave --overwrite 36 | oc tag jenkins-ansible-slave:master jenkins-ansible-slave:latest --alias 37 | oc patch is/jenkins-ansible-slave -p '{"spec":{"tags":[{"name":"latest","annotations":{"role": "jenkins-slave"}}]}}' 38 | ``` 39 | 40 | Alternatively, if you are a Red Hat customer, you can build your images based on RHEL with the following commands: 41 | 42 | ```sh 43 | oc create -f https://raw.githubusercontent.com/nmasse-itix/threescale-cicd/master/support/jenkins/jenkins-slave-template-rhel.yaml 44 | oc new-app --template=jenkins-ansible-slave 45 | oc logs -f bc/jenkins-ansible-slave 46 | ``` 47 | 48 | ## Install the Ansible Jenkins plugin 49 | 50 | - Connect to your Jenkins instance 51 | - Click **Manage Jenkins** > **Manage Plugins** 52 | - Go to the **Available** tab 53 | - In the **Filter** text field, type `Ansible` 54 | - In the list, find the **Ansible plugin** and check its box in the **Enabled** column 55 | - Click **Install without restart** 56 | 57 | ## Create the pipeline that calls Ansible 58 | 59 | You can create the Jenkins pipeline that calls Ansible with the following command: 60 | 61 | ```sh 62 | oc create -f https://raw.githubusercontent.com/nmasse-itix/threescale-cicd/master/support/jenkins/deploy-3scale-api-pipeline.yaml 63 | oc new-app --template=deploy-3scale-api 64 | ``` 65 | 66 | ## Give your 3scale Access Token to Jenkins 67 | 68 | You can give your 3scale Access Token to Jenkins with the following command: 69 | 70 | ```sh 71 | oc create secret generic 3scale-access-token --from-literal="secrettext=1234...5678" 72 | oc label secret 3scale-access-token credential.sync.jenkins.openshift.io=true 73 | ``` 74 | 75 | Replace `1234...5678` with your actual 3scale token. Do not change the name of the key (`secrettext=`) since is used by the OpenShift Jenkins Sync plugin to create the correct credentials in Jenkins. 76 | 77 | If you plan to deploy APIs secured with OpenID Connect, also give your *OpenID Connect Issuer Endpoint* to Jenkins with: 78 | 79 | ```sh 80 | oc create secret generic oidc-issuer-endpoint --from-literal="secrettext=https://:@/auth/realms/" 81 | oc label secret oidc-issuer-endpoint credential.sync.jenkins.openshift.io=true 82 | ``` 83 | 84 | ## Run the pipeline! 85 | 86 | - Connect to your Jenkins master 87 | - Click on the name of your OpenShift project 88 | - Click on **deploy-3scale-api** 89 | - Click on **Build with Parameters** 90 | - For the first run, do not enter any information. This step is mandatory to [initialize the pipeline parameters](https://dev.to/pencillr/jenkins-pipelines-and-their-dirty-secrets-2). 91 | - Wait for the pipeline to finish. **An error is normal at this step.** 92 | - Click on **Build with Parameters** 93 | - This time you can fill-in the relevant information: 94 | - **THREESCALE_CICD_ACCESS_TOKEN** is your 3scale Access Token (`*-3scale-access-token`) 95 | - **THREESCALE_CICD_SSO_ISSUER_ENDPOINT** is your OpenID Connect Issuer Endpoint (`*-oidc-issuer-endpoint`, required only if you are deploying APIs secured with OpenID Connect) 96 | - **THREESCALE_PORTAL_HOSTNAME** is the hostname of your 3scale admin portal (`-admin.3scale.net`) 97 | - **GIT_REPOSITORY** is the URL of the GIT repository that contains the OpenAPI Specification (`https://github.com/nmasse-itix/rhte-api.git`) 98 | - **GIT_BRANCH** is the branch or tag of the GIT repository that contains the OpenAPI Specification (`master`) 99 | - **OPENAPI_FILE** is the path to the OpenAPI Specification file in the GIT repository (`openapi-spec.yaml`) 100 | - **THREESCALE_CICD_PRIVATE_BASE_URL** is the URL of your backend to protect with 3scale (`https://echo-api.3scale.net`) 101 | 102 | ## Use this pipeline from another pipeline 103 | 104 | When you need to provision an API from within a Jenkins Pipeline, you can use the `build` step to call the `deploy-3scale-api` Pipeline: 105 | 106 | ```groovy 107 | build(job: '-deploy-3scale-api', 108 | parameters: [ credentials(name: 'THREESCALE_CICD_ACCESS_TOKEN', value: '-3scale-access-token'), 109 | credentials(name: 'THREESCALE_CICD_SSO_ISSUER_ENDPOINT', value: '-oidc-issuer-endpoint'), 110 | string(name: 'THREESCALE_PORTAL_HOSTNAME', value: '-admin.3scale.net'), 111 | string(name: 'GIT_REPOSITORY', value: 'https://github.com/nmasse-itix/rhte-api.git'), 112 | string(name: 'GIT_BRANCH', value: 'master'), 113 | string(name: 'OPENAPI_FILE', value: 'openapi-spec.yaml'), 114 | string(name: 'THREESCALE_CICD_PRIVATE_BASE_URL', value: 'https://echo-api.3scale.net') ]) 115 | ``` 116 | 117 | ## How to troubleshoot issues when running ansible playbooks from Jenkins 118 | 119 | Run a container using the `jenkins-ansible-slave` docker image and override the default entrypoint: 120 | 121 | ```sh 122 | docker run -it --rm nmasse/jenkins-ansible-slave:master /bin/bash 123 | ``` 124 | 125 | Clone this repository in the current directory (usually `/var/lib/origin`): 126 | 127 | ```sh 128 | git clone https://github.com/nmasse-itix/threescale-cicd.git . 129 | ``` 130 | 131 | Clone the target API repository: 132 | 133 | ```sh 134 | export API_BRANCH=master 135 | export API_REPOSITORY=https://github.com/nmasse-itix/rhte-api.git 136 | export OPENAPI_FILE=openapi-spec.yaml 137 | git clone -b "$API_BRANCH" -- "$API_REPOSITORY" support/jenkins/api 138 | ``` 139 | 140 | Set the Jenkins job parameters as environment variables: 141 | 142 | ```sh 143 | export THREESCALE_CICD_ACCESS_TOKEN=1234...5678 144 | export THREESCALE_CICD_SSO_ISSUER_ENDPOINT=http://client_id:client_secret@sso.hostname/auth/realms/realm # only for OIDC 145 | export THREESCALE_PORTAL_HOSTNAME=-admin.3scale.net 146 | export THREESCALE_CICD_API_BASE_SYSTEM_NAME=my_test_123 147 | export THREESCALE_CICD_PRIVATE_BASE_URL=http://echo-api.3scale.net 148 | ``` 149 | 150 | Run the playbook: 151 | 152 | ```sh 153 | ansible-playbook support/jenkins/deploy-api.yaml -e "openapi_file=$OPENAPI_FILE" -v 154 | ``` 155 | 156 | ## How to customize the Jenkins pipeline 157 | 158 | - Connect to your Jenkins master 159 | - Click **New Item** 160 | - Fill out the item name 161 | - Click on **Pipeline** 162 | - Click on **OK** 163 | - Check **This project is parameterized** 164 | - Copy/paste the pipeline definition from [this file](Jenkinsfile) 165 | - Comment the line: 166 | 167 | ```raw 168 | checkout scm 169 | ``` 170 | 171 | - Uncomment the line: 172 | 173 | ```raw 174 | //git url: 'https://github.com/nmasse-itix/threescale-cicd.git' 175 | ``` 176 | 177 | - It should look like this: 178 | 179 | ```raw 180 | stage("GIT Checkout") { 181 | steps { 182 | // Checkout the GIT repository containing the Ansible Playbook 183 | //checkout scm 184 | git url: 'https://github.com/nmasse-itix/threescale-cicd.git' 185 | ``` 186 | 187 | - Edit the `parameters` section of the pipeline to add comfortable default values so that you can run the pipeline with just a few clicks. Set default values for: 188 | 189 | ```groovy 190 | parameters { 191 | credentials(name: 'THREESCALE_CICD_ACCESS_TOKEN', description: 'The 3scale Access Token', credentialType: "Secret text", required: true, defaultValue: "-3scale-access-token") 192 | string(name: 'THREESCALE_PORTAL_HOSTNAME', description: 'The 3scale Admin Portal hostname', defaultValue: "-admin.3scale.net") 193 | string(name: 'THREESCALE_CICD_API_BASE_SYSTEM_NAME', description: 'Define the base name to compute the final system_name', defaultValue: "my_test_123") 194 | string(name: 'API_REPOSITORY', description: 'The GIT repository to checkout, containing the OpenAPI Specifications', defaultValue: "https://github.com/nmasse-itix/rhte-api.git") 195 | string(name: 'OPENAPI_FILE', description: 'The path to the OpenAPI Specification within the GIT Repository', defaultValue: "openapi-spec.yaml") 196 | booleanParam(name: 'ANSIBLE_VERBOSE', description: 'Run Ansible in verbose mode (-v)', defaultValue: true) 197 | } 198 | ``` 199 | 200 | - Click **Save** 201 | - Click **Build** or **Build with Parameters** 202 | - Wait for the pipeline to complete 203 | 204 | Congratulation, you can now customize the pipeline as will! 205 | -------------------------------------------------------------------------------- /support/jenkins/deploy-3scale-api-pipeline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | labels: 4 | template: deploy-3scale-api 5 | metadata: 6 | annotations: 7 | description: |- 8 | Deploy an API to 3scale 9 | openshift.io/display-name: Jenkins Pipeline to deploy an API to 3scale 10 | tags: jenkins 11 | template.openshift.io/documentation-url: https://github.com/nmasse-itix/threescale-cicd 12 | template.openshift.io/long-description: Jenkins Pipeline to deploy an API to 3scale 13 | template.openshift.io/provider-display-name: Nicolas Massé 14 | template.openshift.io/support-url: https://github.com/nmasse-itix/threescale-cicd/issues 15 | name: deploy-3scale-api 16 | parameters: 17 | objects: 18 | - kind: "BuildConfig" 19 | apiVersion: "v1" 20 | metadata: 21 | name: "deploy-3scale-api" 22 | spec: 23 | source: 24 | git: 25 | uri: https://github.com/nmasse-itix/threescale-cicd.git 26 | strategy: 27 | type: "JenkinsPipeline" 28 | jenkinsPipelineStrategy: 29 | jenkinsfilePath: support/jenkins/Jenkinsfile 30 | -------------------------------------------------------------------------------- /support/jenkins/deploy-api.yaml: -------------------------------------------------------------------------------- 1 | ../docker/deploy-api.yaml -------------------------------------------------------------------------------- /support/jenkins/jenkins-slave-template-rhel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | labels: 4 | template: jenkins-ansible-slave 5 | metadata: 6 | annotations: 7 | description: |- 8 | A Jenkins slave that embeds Ansible 2.6 on RHEL 9 | openshift.io/display-name: Jenkins Slave for Ansible 10 | tags: jenkins 11 | template.openshift.io/documentation-url: https://github.com/nmasse-itix/threescale-cicd 12 | template.openshift.io/long-description: 13 | template.openshift.io/provider-display-name: Nicolas Massé 14 | template.openshift.io/support-url: https://github.com/nmasse-itix/threescale-cicd/issues 15 | name: jenkins-ansible-slave 16 | parameters: 17 | - name: OPENSHIFT_VERSION 18 | value: v3.11 19 | required: true 20 | objects: 21 | - apiVersion: v1 22 | kind: ImageStream 23 | metadata: 24 | annotations: 25 | openshift.io/display-name: Jenkins Slave for Ansible 26 | name: jenkins-ansible-slave 27 | spec: 28 | tags: 29 | - name: latest 30 | annotations: 31 | role: jenkins-slave 32 | 33 | - apiVersion: v1 34 | kind: ImageStream 35 | metadata: 36 | name: jenkins-slave-base 37 | spec: 38 | tags: 39 | - name: ${OPENSHIFT_VERSION} 40 | referencePolicy: 41 | type: Local 42 | from: 43 | kind: DockerImage 44 | name: registry.access.redhat.com/openshift3/jenkins-slave-base-rhel7:${OPENSHIFT_VERSION} 45 | importPolicy: 46 | scheduled: true 47 | 48 | - apiVersion: v1 49 | kind: BuildConfig 50 | metadata: 51 | name: jenkins-ansible-slave 52 | spec: 53 | output: 54 | to: 55 | kind: ImageStreamTag 56 | name: jenkins-ansible-slave:latest 57 | runPolicy: Serial 58 | source: 59 | dockerfile: |- 60 | FROM openshift3/jenkins-slave-base-rhel7:${OPENSHIFT_VERSION} 61 | 62 | MAINTAINER Nicolas Masse 63 | 64 | # Labels consumed by Red Hat build service 65 | LABEL name="openshift3/jenkins-agent-ansible-26-rhel7" \ 66 | version="${OPENSHIFT_VERSION}" \ 67 | architecture="x86_64" \ 68 | io.k8s.display-name="Jenkins Agent Ansible" \ 69 | io.k8s.description="The jenkins agent ansible image has the Ansible engine on top of the jenkins slave base image." \ 70 | io.openshift.tags="openshift,jenkins,agent,ansible" 71 | 72 | USER root 73 | RUN yum install -y --enablerepo=rhel-7-server-ansible-2.6-rpms 'ansible >= 2.6' && \ 74 | yum install -y --enablerepo=rhel-server-rhscl-7-rpms python27-python-pip && \ 75 | # Remove the existing jinja2 library and its dependencies before re-installing it 76 | # This is mandatory to prevent any leftover from a previous install 77 | rm -rf /usr/lib/python2.7/site-packages/markupsafe /usr/lib/python2.7/site-packages/jinja2 && \ 78 | scl enable python27 "pip install --install-option='--install-purelib=/usr/lib/python2.7/site-packages/' jinja2" && \ 79 | yum clean all && \ 80 | rm -rf /var/cache/yum && \ 81 | chown -R 1001:0 $HOME && \ 82 | chmod -R g+rw $HOME 83 | 84 | USER 1001 85 | type: Dockerfile 86 | strategy: 87 | dockerStrategy: 88 | noCache: true 89 | from: 90 | kind: ImageStreamTag 91 | name: jenkins-slave-base:${OPENSHIFT_VERSION} 92 | type: Docker 93 | triggers: 94 | - type: ConfigChange 95 | - type: ImageChange 96 | -------------------------------------------------------------------------------- /support/jenkins/roles/nmasse-itix.threescale-cicd: -------------------------------------------------------------------------------- 1 | ../../../ -------------------------------------------------------------------------------- /support/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Using this Ansible role from Kubernetes 2 | 3 | Open [job.yaml](job.yaml) and to tailor it to your environment. Update at least 4 | the `hostname` and `access_token` fields of the `Secret` object. 5 | 6 | You can also edit the `sso_issuer_endpoint` field if your API needs to be secured 7 | with OpenID Connect. 8 | 9 | Once finished, you can create the job: 10 | 11 | ```sh 12 | kubectl create -f job.yaml 13 | ``` 14 | 15 | And wait for the job to complete: 16 | 17 | ```sh 18 | kubectl get pods -l job-name=deploy-3scale-api -w 19 | ``` 20 | -------------------------------------------------------------------------------- /support/kubernetes/job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: batch/v1 5 | kind: Job 6 | metadata: 7 | name: deploy-3scale-api 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: threescale-cicd 13 | image: docker.io/nmasse/threescale-cicd:stable 14 | imagePullPolicy: Always 15 | env: 16 | - name: GIT_REPOSITORY 17 | value: https://github.com/nmasse-itix/rhte-api.git 18 | - name: GIT_REF 19 | value: master 20 | - name: OPENAPI_FILE 21 | value: openapi-spec.yaml 22 | - name: THREESCALE_CICD_OPENAPI_FILE_FORMAT 23 | value: YAML 24 | - name: THREESCALE_CICD_API_BASE_SYSTEM_NAME 25 | value: echo-api 26 | - name: THREESCALE_CICD_API_SYSTEM_NAME 27 | value: "" 28 | - name: THREESCALE_CICD_WILDCARD_DOMAIN 29 | value: "" 30 | - name: THREESCALE_CICD_API_BASEPATH 31 | value: "" 32 | - name: THREESCALE_CICD_PRIVATE_BASE_URL 33 | value: https://echo-api.3scale.net 34 | - name: THREESCALE_CICD_APICAST_POLICIES_CORS 35 | value: "false" 36 | - name: THREESCALE_CICD_OPENAPI_SMOKETEST_OPERATION 37 | value: "" 38 | - name: THREESCALE_CICD_API_ENVIRONMENT_NAME 39 | value: "" 40 | - name: THREESCALE_CICD_VALIDATE_OPENAPI 41 | value: "true" 42 | - name: THREESCALE_CICD_APICAST_SANDBOX_ENDPOINT 43 | value: "" 44 | - name: THREESCALE_CICD_APICAST_PRODUCTION_ENDPOINT 45 | value: "" 46 | - name: THREESCALE_CICD_CREATE_DEFAULT_APPLICATION 47 | value: "true" 48 | volumeMounts: 49 | - name: 3scale-secrets 50 | mountPath: /tmp/secrets 51 | restartPolicy: OnFailure 52 | volumes: 53 | - name: 3scale-secrets 54 | secret: 55 | secretName: 3scale-admin-portal 56 | backoffLimit: 1 57 | completions: 1 58 | parallelism: 1 59 | 60 | - apiVersion: v1 61 | kind: Secret 62 | metadata: 63 | name: 3scale-admin-portal 64 | type: Opaque 65 | stringData: 66 | hostname: TENANT-admin.3scale.net 67 | access_token: "1234..5678" 68 | sso_issuer_endpoint: "" 69 | -------------------------------------------------------------------------------- /support/openshift/README.md: -------------------------------------------------------------------------------- 1 | # Using this Ansible role on OpenShift 2 | 3 | You can use the provided OpenShift template to create all the mandatory objects: 4 | 5 | - An ImageStream to keep track of the [provided docker image](https://hub.docker.com/r/nmasse/threescale-cicd). 6 | - A custom BuildConfig that checkout the GIT repository containing the OpenAPI Specification and runs the Ansible role on it 7 | - A Secret holding the credentials to access the 3scale Admin Portal 8 | 9 | ```sh 10 | oc create -f https://raw.githubusercontent.com/nmasse-itix/threescale-cicd/master/support/openshift/openshift-template.yaml 11 | oc new-app --template=deploy-3scale-api -p THREESCALE_CICD_VERSION=stable -p THREESCALE_ADMIN_PORTAL_ACCESS_TOKEN=1234..5678 -p THREESCALE_ADMIN_PORTAL_HOSTNAME=TENANT-admin.3scale.net -p API_NAME=echo-api -p THREESCALE_CICD_PRIVATE_BASE_URL=https://echo-api.3scale.net -p API_GIT_URI=https://github.com/nmasse-itix/rhte-api.git 12 | ``` 13 | 14 | You will have to change at least the value of: 15 | 16 | - the `THREESCALE_ADMIN_PORTAL_ACCESS_TOKEN` parameter to match the Access Token of your 3scale Admin Portal 17 | - the `THREESCALE_ADMIN_PORTAL_HOSTNAME` parameter to match the hostname of your 3scale Admin Portal 18 | 19 | This template will create a BuildConfig with the name of your API: 20 | 21 | ```raw 22 | $ oc get bc 23 | NAME TYPE FROM LATEST 24 | deploy-3scale-api-echo-api Custom threescale-cicd:stable 7 25 | ``` 26 | 27 | Start the build to deploy the API to 3scale: 28 | 29 | ```sh 30 | oc start-build deploy-3scale-api-echo-api 31 | ``` 32 | 33 | Wait for the build to complete: 34 | 35 | ```sh 36 | oc logs -f bc/deploy-3scale-api-echo-api 37 | ``` 38 | 39 | To start this build from a Jenkins pipeline, you will need to give the `system:build-strategy-custom` role to the `jenkins` Service Account: 40 | 41 | ```sh 42 | oc adm policy add-role-to-user system:build-strategy-custom -z jenkins 43 | ``` 44 | 45 | Then, from your Jenkins pipeline you can use: 46 | 47 | ```sh 48 | openshift.withCluster() { 49 | openshift.withProject() { 50 | def bc = openshift.selector('bc', "deploy-3scale-api-echo-api"); 51 | bc.startBuild("--wait=true"); 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /support/openshift/openshift-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | labels: 4 | template: deploy-3scale-api 5 | metadata: 6 | annotations: 7 | description: |- 8 | Deploy an API to 3scale 9 | openshift.io/display-name: OpenShift BuildConfig to deploy an API to 3scale 10 | tags: builder 11 | template.openshift.io/documentation-url: https://github.com/nmasse-itix/threescale-cicd 12 | template.openshift.io/long-description: OpenShift BuildConfig to deploy an API to 3scale 13 | template.openshift.io/provider-display-name: Nicolas Massé 14 | template.openshift.io/support-url: https://github.com/nmasse-itix/threescale-cicd/issues 15 | name: deploy-3scale-api 16 | parameters: 17 | - name: API_NAME 18 | description: 'The name of your API (subject to OpenShift name syntax: [a-zA-Z0-9_])' 19 | value: example 20 | required: true 21 | - name: API_GIT_URI 22 | description: GIT Repository containing the OpenAPI Specification to deploy 23 | required: true 24 | - name: API_GIT_REF 25 | description: The branch or tag to use when checking out the GIT repository 26 | value: master 27 | required: true 28 | - name: API_GIT_CONTEXT_DIR 29 | description: The context directory to cd after the git checkout 30 | required: false 31 | - name: API_OPENAPI_FILE 32 | description: The OpenAPI Specification file to use to deploy the API 33 | value: openapi-spec.yaml 34 | required: true 35 | - name: API_OPENAPI_FILE_FORMAT 36 | description: The format (JSON or YAML) of the OpenAPI Specification file 37 | value: YAML 38 | required: false 39 | - name: THREESCALE_ADMIN_PORTAL_HOSTNAME 40 | description: The hostname of the 3scale admin portal (bare hostname, without https://) 41 | required: true 42 | - name: THREESCALE_ADMIN_PORTAL_ACCESS_TOKEN 43 | description: The 3scale access token 44 | required: true 45 | - name: SSO_ISSUER_ENDPOINT 46 | description: For OpenID Connect APIs only, the Red Hat SSO OIDC Issuer Endpoint (https://client_id:client_secret@sso.hostname/auth/realms/my-realm) 47 | required: false 48 | - name: THREESCALE_CICD_VERSION 49 | description: The version of the Ansible playbooks to use 50 | value: stable 51 | required: true 52 | - name: THREESCALE_CICD_API_SYSTEM_NAME 53 | description: Override the 3scale Service system_name 54 | required: false 55 | - name: THREESCALE_CICD_API_BASE_SYSTEM_NAME 56 | description: Define the base name to compute the final system_name 57 | required: false 58 | value: api 59 | - name: THREESCALE_CICD_WILDCARD_DOMAIN 60 | description: Automatically defines the APIcast public URLs based on a scheme 61 | required: false 62 | - name: THREESCALE_CICD_API_BASEPATH 63 | description: Overrides the OpenAPI basePath field 64 | required: false 65 | - name: THREESCALE_CICD_PRIVATE_BASE_URL 66 | description: The 3scale private base URL 67 | required: false 68 | - name: THREESCALE_CICD_APICAST_POLICIES_CORS 69 | description: Allows to enable the CORS policy onto APICast gateway 70 | required: false 71 | - name: THREESCALE_CICD_OPENAPI_SMOKETEST_OPERATION 72 | description: Defines the OpenAPI Specification method to use for smoke tests 73 | required: false 74 | - name: THREESCALE_CICD_API_ENVIRONMENT_NAME 75 | description: Prefixes all services with an environment name to prevent any name collision when deploying the same API multiple times on the same 3scale instance 76 | required: false 77 | - name: THREESCALE_CICD_VALIDATE_OPENAPI 78 | description: Validates the OpenAPI Specification file against the official schema 79 | required: false 80 | - name: THREESCALE_CICD_APICAST_SANDBOX_ENDPOINT 81 | description: Defines the Public Staging Base URL 82 | required: false 83 | - name: THREESCALE_CICD_APICAST_PRODUCTION_ENDPOINT 84 | description: Defines the Public Production Base URL 85 | required: false 86 | - name: THREESCALE_CICD_CREATE_DEFAULT_APPLICATION 87 | description: Create a test application with the default application plan, whether smoke tests are enabled or not 88 | required: false 89 | value: "true" 90 | 91 | objects: 92 | - apiVersion: v1 93 | kind: ImageStream 94 | metadata: 95 | name: threescale-cicd 96 | spec: 97 | tags: 98 | - name: latest 99 | annotations: 100 | referencePolicy: 101 | type: Local 102 | from: 103 | kind: ImageStreamTag 104 | name: ${THREESCALE_CICD_VERSION} 105 | 106 | - name: ${THREESCALE_CICD_VERSION} 107 | annotations: 108 | referencePolicy: 109 | type: Local 110 | from: 111 | kind: DockerImage 112 | name: docker.io/nmasse/threescale-cicd:${THREESCALE_CICD_VERSION} 113 | 114 | - kind: "BuildConfig" 115 | apiVersion: "v1" 116 | metadata: 117 | name: "deploy-3scale-api-${API_NAME}" 118 | spec: 119 | source: 120 | type: "Git" 121 | git: 122 | uri: "${API_GIT_URI}" 123 | ref: "${API_GIT_REF}" 124 | contextDir: "${API_GIT_CONTEXT_DIR}" 125 | strategy: 126 | type: "Custom" 127 | customStrategy: 128 | from: 129 | kind: "ImageStreamTag" 130 | name: "threescale-cicd:${THREESCALE_CICD_VERSION}" 131 | env: 132 | - name: THREESCALE_CICD_OPENAPI_FILE_FORMAT 133 | value: ${API_OPENAPI_FILE_FORMAT} 134 | - name: OPENAPI_FILE 135 | value: ${API_OPENAPI_FILE} 136 | - name: THREESCALE_CICD_API_BASE_SYSTEM_NAME 137 | value: ${THREESCALE_CICD_API_BASE_SYSTEM_NAME} 138 | - name: THREESCALE_CICD_API_SYSTEM_NAME 139 | value: ${THREESCALE_CICD_API_SYSTEM_NAME} 140 | - name: THREESCALE_CICD_WILDCARD_DOMAIN 141 | value: ${THREESCALE_CICD_WILDCARD_DOMAIN} 142 | - name: THREESCALE_CICD_API_BASEPATH 143 | value: ${THREESCALE_CICD_API_BASEPATH} 144 | - name: THREESCALE_CICD_PRIVATE_BASE_URL 145 | value: ${THREESCALE_CICD_PRIVATE_BASE_URL} 146 | - name: THREESCALE_CICD_APICAST_POLICIES_CORS 147 | value: ${THREESCALE_CICD_APICAST_POLICIES_CORS} 148 | - name: THREESCALE_CICD_OPENAPI_SMOKETEST_OPERATION 149 | value: ${THREESCALE_CICD_OPENAPI_SMOKETEST_OPERATION} 150 | - name: THREESCALE_CICD_API_ENVIRONMENT_NAME 151 | value: ${THREESCALE_CICD_API_ENVIRONMENT_NAME} 152 | - name: THREESCALE_CICD_VALIDATE_OPENAPI 153 | value: ${THREESCALE_CICD_VALIDATE_OPENAPI} 154 | - name: THREESCALE_CICD_APICAST_SANDBOX_ENDPOINT 155 | value: ${THREESCALE_CICD_APICAST_SANDBOX_ENDPOINT} 156 | - name: THREESCALE_CICD_APICAST_PRODUCTION_ENDPOINT 157 | value: ${THREESCALE_CICD_APICAST_PRODUCTION_ENDPOINT} 158 | - name: THREESCALE_CICD_CREATE_DEFAULT_APPLICATION 159 | value: ${THREESCALE_CICD_CREATE_DEFAULT_APPLICATION} 160 | secrets: 161 | - secretSource: 162 | name: "3scale-admin-portal-${API_NAME}" 163 | mountPath: "/tmp/secrets" 164 | triggers: 165 | - type: ConfigChange 166 | 167 | - apiVersion: v1 168 | kind: Secret 169 | metadata: 170 | name: 3scale-admin-portal-${API_NAME} 171 | type: Opaque 172 | stringData: 173 | hostname: ${THREESCALE_ADMIN_PORTAL_HOSTNAME} 174 | access_token: ${THREESCALE_ADMIN_PORTAL_ACCESS_TOKEN} 175 | sso_issuer_endpoint: ${SSO_ISSUER_ENDPOINT} -------------------------------------------------------------------------------- /tasks/api-calls/create_activedoc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_activedoc_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the ActiveDocs 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/active_docs.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_activedoc_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Store the list of existing ActiveDocs as fact 20 | set_fact: 21 | threescale_cicd_existing_activedocs: >- 22 | {{ threescale_cicd_existing_activedocs|union([ threescale_cicd_tmpresponse.json.api_doc.system_name ]) }} 23 | threescale_cicd_existing_activedocs_details: >- 24 | {{ threescale_cicd_existing_activedocs_details|union([ 25 | { 26 | 'id': threescale_cicd_tmpresponse.json.api_doc.id, 27 | 'system_name': threescale_cicd_tmpresponse.json.api_doc.system_name 28 | } 29 | ]) }} 30 | 31 | - name: Wait for a couple seconds 32 | pause: 33 | seconds: '{{ threescale_cicd_throttling }}' 34 | -------------------------------------------------------------------------------- /tasks/api-calls/create_application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_application_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the application 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/accounts/{{ threescale_cicd_default_account_id }}/applications.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_application_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Store the default application details as fact 20 | set_fact: 21 | threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}' 22 | 23 | - name: Wait for a couple seconds 24 | pause: 25 | seconds: '{{ threescale_cicd_throttling }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/create_application_plan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_application_plan_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the application plan 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/application_plans.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_application_plan_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Store the list of existing Application Plans as fact 20 | set_fact: 21 | threescale_cicd_existing_application_plans: >- 22 | {{ threescale_cicd_existing_application_plans|union([ threescale_cicd_application_plan.system_name ]) }} 23 | threescale_cicd_existing_application_plans_details: >- 24 | {{ threescale_cicd_existing_application_plans_details|union([ 25 | { 26 | "system_name": threescale_cicd_application_plan.system_name, 27 | "id": threescale_cicd_tmpresponse.json.application_plan.id 28 | } 29 | ]) }} 30 | 31 | - name: Wait for a couple seconds 32 | pause: 33 | seconds: '{{ threescale_cicd_throttling }}' 34 | -------------------------------------------------------------------------------- /tasks/api-calls/create_mapping_rule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_mapping_rule_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the mapping rule 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_mapping_rule_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Wait for a couple seconds 20 | pause: 21 | seconds: '{{ threescale_cicd_throttling }}' 22 | -------------------------------------------------------------------------------- /tasks/api-calls/create_method.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_method_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the method 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/metrics/{{ threescale_cicd_metric_id }}/methods.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_method_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Store the metrics details as fact 20 | set_fact: 21 | threescale_cicd_existing_metrics: >- 22 | {{ threescale_cicd_existing_metrics|union([ threescale_cicd_api_operation.key ]) }} 23 | threescale_cicd_existing_metrics_details: >- 24 | {{ threescale_cicd_existing_metrics_details|union([ 25 | { 26 | "system_name": threescale_cicd_api_operation.key, 27 | "id": threescale_cicd_tmpresponse.json|json_query("method.id") 28 | } 29 | ]) }} 30 | 31 | - name: Wait for a couple seconds 32 | pause: 33 | seconds: '{{ threescale_cicd_throttling }}' 34 | -------------------------------------------------------------------------------- /tasks/api-calls/create_service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_create_service_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Create the service 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services.json 11 | validate_certs: no 12 | method: POST 13 | body: '{{ threescale_cicd_create_service_payload }}' 14 | status_code: 201 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Add the new service to the existing service list 20 | set_fact: 21 | threescale_cicd_existing_services: >- 22 | {{ threescale_cicd_existing_services|union([ threescale_cicd_tmpresponse.json.service.system_name ]) }} 23 | threescale_cicd_existing_services_details: >- 24 | {{ threescale_cicd_existing_services_details|union([ 25 | { 26 | "id": threescale_cicd_tmpresponse.json.service.id, 27 | "system_name": threescale_cicd_tmpresponse.json.service.system_name 28 | } 29 | ]) }} 30 | cacheable: true 31 | 32 | - name: Wait for a couple seconds 33 | pause: 34 | seconds: '{{ threescale_cicd_throttling }}' 35 | -------------------------------------------------------------------------------- /tasks/api-calls/delete_mapping_rule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | msg: "Deleting unused mapping rule {{ threescale_cicd_mapping_rule }}..." 5 | verbosity: 1 6 | 7 | - name: Delete the unused mapping rules 8 | uri: 9 | url: "{{ service_url }}/proxy/mapping_rules/{{ rule_id }}.json?access_token={{ threescale_cicd_access_token|urlencode }}" 10 | validate_certs: no 11 | method: DELETE 12 | status_code: 200,404 13 | register: threescale_cicd_tmpresponse 14 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 15 | no_log: '{{ threescale_cicd_nolog }}' 16 | vars: 17 | rule_id: '{{ threescale_cicd_existing_mapping_rules[threescale_cicd_mapping_rule] }}' 18 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 19 | 20 | - name: Wait for a couple seconds 21 | pause: 22 | seconds: '{{ threescale_cicd_throttling }}' 23 | -------------------------------------------------------------------------------- /tasks/api-calls/delete_metric.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | msg: "Deleting unused metric {{ threescale_cicd_metric.system_name }}..." 5 | verbosity: 1 6 | 7 | - name: Delete the metric 8 | uri: 9 | url: "{{ service_url }}{{ metric_path }}?access_token={{ threescale_cicd_access_token|urlencode }}" 10 | validate_certs: no 11 | method: DELETE 12 | status_code: 200,404 13 | register: threescale_cicd_tmpresponse 14 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 15 | no_log: '{{ threescale_cicd_nolog }}' 16 | vars: 17 | metric_path: /metrics/{{ threescale_cicd_metric_id }}/methods/{{ threescale_cicd_metric.id }}.json 18 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 19 | 20 | - name: Wait for a couple seconds 21 | pause: 22 | seconds: '{{ threescale_cicd_throttling }}' 23 | -------------------------------------------------------------------------------- /tasks/api-calls/find_application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Check if the default application exists 4 | uri: 5 | url: 'https://{{ inventory_hostname }}/admin/api/applications/find.json?{{ threescale_cicd_find_application_payload }}' 6 | validate_certs: no 7 | method: GET 8 | status_code: 200,404 9 | register: threescale_cicd_tmpresponse 10 | no_log: '{{ threescale_cicd_nolog }}' 11 | 12 | - name: Set the default application id as a fact 13 | set_fact: 14 | threescale_cicd_default_application_id: '{{ threescale_cicd_tmpresponse.json.application.id }}' 15 | when: 'threescale_cicd_tmpresponse.status == 200' 16 | -------------------------------------------------------------------------------- /tasks/api-calls/find_first_account.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Get the default (first) account 4 | uri: 5 | url: https://{{ inventory_hostname }}/admin/api/accounts.json?access_token={{ threescale_cicd_access_token|urlencode }}&state=approved&page=1&per_page=1 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | no_log: '{{ threescale_cicd_nolog }}' 9 | 10 | - name: Set the default first account id as a fact 11 | set_fact: 12 | threescale_cicd_default_account_id: '{{ threescale_cicd_tmpresponse.json.accounts[0].account.id }}' 13 | -------------------------------------------------------------------------------- /tasks/api-calls/get_proxy_version.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Get the version of the staging proxy definition 4 | uri: 5 | url: '{{ service_url }}{{ proxy_path }}?access_token={{ threescale_cicd_access_token|urlencode }}' 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | no_log: '{{ threescale_cicd_nolog }}' 9 | vars: 10 | proxy_path: /proxy/configs/{{ threescale_cicd_staging_environment_name }}/latest.json 11 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 12 | 13 | - name: Set the staging proxy version number as a fact 14 | set_fact: 15 | threescale_cicd_staging_proxy_version: >- 16 | {{ threescale_cicd_tmpresponse.json.proxy_config.version }} 17 | 18 | - debug: # TODO 19 | var: threescale_cicd_staging_proxy_version 20 | 21 | - name: Get the version of the production proxy definition 22 | uri: 23 | url: '{{ service_url }}{{ proxy_path }}?access_token={{ threescale_cicd_access_token|urlencode }}' 24 | validate_certs: no 25 | status_code: 200,404 26 | register: threescale_cicd_tmpresponse 27 | no_log: '{{ threescale_cicd_nolog }}' 28 | vars: 29 | proxy_path: /proxy/configs/{{ threescale_cicd_production_environment_name }}/latest.json 30 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 31 | 32 | - name: Set the production proxy version number as a fact 33 | set_fact: 34 | threescale_cicd_production_proxy_version: >- 35 | {{ threescale_cicd_tmpresponse.json.proxy_config.version 36 | if threescale_cicd_tmpresponse.status == 200 37 | else 'NONE' }} 38 | -------------------------------------------------------------------------------- /tasks/api-calls/keycloak/authenticate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_authenticate_to_keycloak_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Authenticate to RH-SSO 9 | uri: 10 | url: '{{ threescale_cicd_sso_realm_endpoint }}/protocol/openid-connect/token' 11 | body: '{{ threescale_cicd_authenticate_to_keycloak_payload }}' 12 | method: POST 13 | validate_certs: no 14 | return_content: yes 15 | register: threescale_cicd_tmpresponse 16 | retries: '{{ threescale_cicd_retries }}' 17 | delay: '{{ threescale_cicd_delay }}' 18 | # temporary fix for https://github.com/ansible/ansible/issues/28078 19 | until: 'threescale_cicd_tmpresponse is success' 20 | no_log: '{{ threescale_cicd_nolog }}' 21 | 22 | - name: Extract the access_token 23 | set_fact: 24 | threescale_cicd_keycloak_access_token: '{{ threescale_cicd_tmpresponse.json |json_query("access_token") }}' 25 | no_log: '{{ threescale_cicd_nolog }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/keycloak/patch_client.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_patch_keycloak_client_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Patch the client in RH-SSO to support the "client_credentials" and "password" grant_type. 9 | uri: 10 | url: '{{ threescale_cicd_sso_admin_endpoint }}/clients/{{ threescale_cicd_default_application_sso_id|urlencode }}' 11 | method: PUT 12 | validate_certs: no 13 | body: '{{ threescale_cicd_patch_keycloak_client_payload }}' 14 | body_format: json 15 | status_code: '200,204' 16 | headers: 17 | Authorization: 'Bearer {{ threescale_cicd_keycloak_access_token }}' 18 | Content-Type: 'application/json' 19 | register: threescale_cicd_tmpresponse 20 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 21 | no_log: '{{ threescale_cicd_nolog }}' 22 | 23 | - name: Wait for a couple seconds 24 | pause: 25 | seconds: '{{ threescale_cicd_throttling }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/keycloak/wait_for_client.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Wait for the new client to appear in RH-SSO 4 | uri: 5 | url: '{{ threescale_cicd_sso_admin_endpoint }}/clients?clientId={{ threescale_cicd_default_application_appid|urlencode }}' 6 | method: GET 7 | validate_certs: no 8 | return_content: yes 9 | headers: 10 | Authorization: 'Bearer {{ threescale_cicd_keycloak_access_token }}' 11 | register: threescale_cicd_tmpresponse 12 | retries: '{{ threescale_cicd_retries }}' 13 | delay: '{{ threescale_cicd_delay }}' 14 | until: 'threescale_cicd_tmpresponse is success and threescale_cicd_tmpresponse.json|length > 0' 15 | no_log: '{{ threescale_cicd_nolog }}' 16 | 17 | - name: Set the new client details and id as a fact 18 | set_fact: 19 | threescale_cicd_default_application_sso_id: '{{ threescale_cicd_tmpresponse.json[0].id }}' 20 | threescale_cicd_default_application_sso_body: '{{ threescale_cicd_tmpresponse.json[0] }}' 21 | -------------------------------------------------------------------------------- /tasks/api-calls/promote_proxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_promote_proxy_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Promote to production 9 | uri: 10 | url: '{{ service_url }}{{ promote_path }}' 11 | body: '{{ threescale_cicd_promote_proxy_payload }}' 12 | status_code: 201 13 | validate_certs: no 14 | method: POST 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 201' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | vars: 19 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 20 | promote_path: /proxy/configs/{{ threescale_cicd_staging_environment_name }}/{{ threescale_cicd_staging_proxy_version }}/promote.json 21 | 22 | - name: Wait for a couple seconds 23 | pause: 24 | seconds: '{{ threescale_cicd_throttling }}' 25 | -------------------------------------------------------------------------------- /tasks/api-calls/smoke_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_smoke_test_headers 5 | verbosity: 1 6 | 7 | - debug: 8 | msg: "Starting a smoke test on '{{ threescale_cicd_smoke_test_url }}'..." 9 | 10 | - name: Running smoke tests ! 11 | uri: 12 | url: '{{ threescale_cicd_smoke_test_url }}' 13 | headers: '{{ threescale_cicd_smoke_test_headers }}' 14 | validate_certs: no 15 | method: GET 16 | register: threescale_cicd_tmpresponse 17 | retries: '{{ threescale_cicd_retries }}' 18 | delay: '{{ threescale_cicd_delay }}' 19 | # temporary fix for https://github.com/ansible/ansible/issues/28078 20 | until: 'threescale_cicd_tmpresponse is success' 21 | 22 | - name: Running smoke tests (CORS) ! 23 | uri: 24 | url: '{{ threescale_cicd_smoke_test_url }}' 25 | headers: '{{ threescale_cicd_smoke_test_headers|combine({ ''Origin'': threescale_cicd_smoke_test_url, ''Access-Control-Request-Method'': ''GET'' }) }}' 26 | validate_certs: no 27 | method: OPTIONS 28 | status_code: 200,204 29 | register: threescale_cicd_tmpresponse 30 | retries: '{{ threescale_cicd_retries }}' 31 | delay: '{{ threescale_cicd_delay }}' 32 | # temporary fix for https://github.com/ansible/ansible/issues/28078 33 | until: 'threescale_cicd_tmpresponse is success' 34 | when: 'threescale_cicd_apicast_policies_cors|bool' 35 | -------------------------------------------------------------------------------- /tasks/api-calls/update_activedoc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_activedoc_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the ActiveDocs 9 | uri: 10 | url: 'https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_id }}.json' 11 | validate_certs: no 12 | method: PUT 13 | body: '{{ threescale_cicd_update_activedoc_payload }}' 14 | status_code: 200 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Wait for a couple seconds 20 | pause: 21 | seconds: '{{ threescale_cicd_throttling }}' 22 | -------------------------------------------------------------------------------- /tasks/api-calls/update_application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_application_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the application 9 | uri: 10 | url: '{{ default_account_url }}/applications/{{ threescale_cicd_default_application_id }}.json' 11 | validate_certs: no 12 | method: PUT 13 | body: '{{ threescale_cicd_update_application_payload }}' 14 | status_code: 200 15 | register: threescale_cicd_tmpresponse 16 | no_log: '{{ threescale_cicd_nolog }}' 17 | vars: 18 | default_account_url: https://{{ inventory_hostname }}/admin/api/accounts/{{ threescale_cicd_default_account_id }} 19 | 20 | - name: Set the default application details as a fact 21 | set_fact: 22 | threescale_cicd_default_application_details: '{{ threescale_cicd_tmpresponse.json.application }}' 23 | 24 | - name: Wait for a couple seconds 25 | pause: 26 | seconds: '{{ threescale_cicd_throttling }}' 27 | -------------------------------------------------------------------------------- /tasks/api-calls/update_application_plan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_application_plan_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the application plan 9 | uri: 10 | url: '{{ service_url }}/application_plans/{{ application_plan_id }}.json' 11 | validate_certs: no 12 | method: PUT 13 | body: '{{ threescale_cicd_update_application_plan_payload }}' 14 | status_code: 200 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | vars: 19 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 20 | application_plan_id: >- 21 | {{ (threescale_cicd_existing_application_plans_details|selectattr("system_name", "equalto", threescale_cicd_application_plan.system_name)|first).id }} 22 | 23 | - name: Wait for a couple seconds 24 | pause: 25 | seconds: '{{ threescale_cicd_throttling }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/update_mapping_rule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_mapping_rule_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the mapping rule 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/mapping_rules/{{ threescale_cicd_mapping_rule_id }}.json 11 | validate_certs: no 12 | method: PUT 13 | body: '{{ threescale_cicd_update_mapping_rule_payload }}' 14 | status_code: 200 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 17 | vars: 18 | threescale_cicd_mapping_rule_id: '{{ threescale_cicd_existing_mapping_rules[threescale_cicd_mapping_rule] }}' 19 | no_log: '{{ threescale_cicd_nolog }}' 20 | 21 | - name: Wait for a couple seconds 22 | pause: 23 | seconds: '{{ threescale_cicd_throttling }}' 24 | -------------------------------------------------------------------------------- /tasks/api-calls/update_method.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_method_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the method 9 | uri: 10 | url: '{{ service_url }}/metrics/{{ threescale_cicd_metric_id }}/methods/{{ method_id }}.json' 11 | validate_certs: no 12 | method: PATCH 13 | body: '{{ threescale_cicd_update_method_payload }}' 14 | register: threescale_cicd_tmpresponse 15 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 16 | no_log: '{{ threescale_cicd_nolog }}' 17 | vars: 18 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 19 | method_id: "{{ (threescale_cicd_existing_metrics_details|selectattr('system_name', 'equalto', threescale_cicd_api_operation.key)|first).id }}" 20 | 21 | - name: Wait for a couple seconds 22 | pause: 23 | seconds: '{{ threescale_cicd_throttling }}' 24 | -------------------------------------------------------------------------------- /tasks/api-calls/update_oidc_configuration.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_oidc_configuration_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the proxy definition 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/oidc_configuration.json 11 | validate_certs: no 12 | method: PATCH 13 | body: '{{ threescale_cicd_update_oidc_configuration_payload }}' 14 | register: threescale_cicd_tmpresponse 15 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 16 | no_log: '{{ threescale_cicd_nolog }}' 17 | 18 | - name: Wait for a couple seconds 19 | pause: 20 | seconds: '{{ threescale_cicd_throttling }}' 21 | -------------------------------------------------------------------------------- /tasks/api-calls/update_policies.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_policies_to_update 5 | verbosity: 1 6 | 7 | - debug: 8 | var: threescale_cicd_update_policies_payload 9 | verbosity: 1 10 | no_log: '{{ threescale_cicd_nolog }}' 11 | 12 | - name: Update the policies chain 13 | uri: 14 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy/policies.json 15 | validate_certs: no 16 | method: PUT 17 | body: '{{ threescale_cicd_update_policies_payload }}' 18 | status_code: 200 19 | register: threescale_cicd_tmpresponse 20 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 21 | no_log: '{{ threescale_cicd_nolog }}' 22 | 23 | - name: Wait for a couple seconds 24 | pause: 25 | seconds: '{{ threescale_cicd_throttling }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/update_proxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_proxy_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the proxy definition 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}/proxy.json 11 | validate_certs: no 12 | method: PATCH 13 | body: '{{ threescale_cicd_update_proxy_payload }}' 14 | register: threescale_cicd_tmpresponse 15 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 16 | no_log: '{{ threescale_cicd_nolog }}' 17 | 18 | - name: Extract the staging and production gateway endpoint from the proxy definition 19 | set_fact: 20 | threescale_cicd_apicast_discovered_sandbox_endpoint: '{{ threescale_cicd_tmpresponse.json.proxy.sandbox_endpoint }}' 21 | threescale_cicd_apicast_discovered_production_endpoint: '{{ threescale_cicd_tmpresponse.json.proxy.endpoint }}' 22 | 23 | - name: Wait for a couple seconds 24 | pause: 25 | seconds: '{{ threescale_cicd_throttling }}' 26 | -------------------------------------------------------------------------------- /tasks/api-calls/update_service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | var: threescale_cicd_update_service_payload 5 | verbosity: 1 6 | no_log: '{{ threescale_cicd_nolog }}' 7 | 8 | - name: Update the service 9 | uri: 10 | url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}.json 11 | validate_certs: no 12 | method: PUT 13 | body: '{{ threescale_cicd_update_service_payload }}' 14 | status_code: 200 15 | register: threescale_cicd_tmpresponse 16 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 17 | no_log: '{{ threescale_cicd_nolog }}' 18 | 19 | - name: Wait for a couple seconds 20 | pause: 21 | seconds: '{{ threescale_cicd_throttling }}' 22 | -------------------------------------------------------------------------------- /tasks/cleanup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Delete the created service and any dependent object 4 | uri: 5 | url: '{{ service_url }}?access_token={{ threescale_cicd_access_token|urlencode }}' 6 | validate_certs: no 7 | method: DELETE 8 | status_code: 200,404 9 | register: threescale_cicd_tmpresponse 10 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 11 | when: 'threescale_cicd_api_service_id is defined' 12 | no_log: '{{ threescale_cicd_nolog }}' 13 | vars: 14 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }}.json 15 | 16 | - name: Delete the created ActiveDocs 17 | uri: 18 | url: '{{ activedocs_url }}?access_token={{ threescale_cicd_access_token|urlencode }}' 19 | validate_certs: no 20 | method: DELETE 21 | status_code: 200,404 22 | register: threescale_cicd_tmpresponse 23 | changed_when: 'threescale_cicd_tmpresponse.status == 200' 24 | when: 'threescale_cicd_api_activedocs_id is defined' 25 | no_log: '{{ threescale_cicd_nolog }}' 26 | vars: 27 | activedocs_url: https://{{ inventory_hostname }}/admin/api/active_docs/{{ threescale_cicd_api_activedocs_id }}.json 28 | -------------------------------------------------------------------------------- /tasks/install_prerequisites.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - import_tasks: steps/ansible_requirements.yml 4 | - include_tasks: steps/install_goswagger.yml 5 | vars: 6 | threescale_cicd_local_bin_path: /usr/local/bin 7 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Make sure we have everything we need to run this playbook 4 | - import_tasks: steps/requirements.yml 5 | - import_tasks: steps/ansible_requirements.yml 6 | 7 | # Validate the API definition against the Swagger 2.0 / OAS 3.0 8 | - include_tasks: steps/validate_openapi.yml 9 | when: threescale_cicd_validate_openapi|bool 10 | 11 | # Warn the user about those deprecated features 12 | - import_tasks: steps/variables_from_inventory.yml 13 | 14 | # Discover the current state of the platform 15 | - import_tasks: steps/discover_platform.yml 16 | 17 | # Load the API definition from the provided OpenAPI file 18 | - import_tasks: steps/read_openapi.yml 19 | 20 | # Discover the current state of the service 21 | - import_tasks: steps/discover_service.yml 22 | 23 | # Create or update the service definition 24 | - import_tasks: steps/service.yml 25 | 26 | # Create or update the methods 27 | - import_tasks: steps/methods.yml 28 | 29 | # Create, update or delete the mapping rules 30 | - import_tasks: steps/mapping_rules.yml 31 | 32 | # Update the proxy 33 | - import_tasks: steps/proxy.yml 34 | 35 | # Update the OIDC configuration 36 | - import_tasks: steps/oidc_configuration.yml 37 | 38 | # Create or update policies 39 | - import_tasks: steps/policies.yml 40 | 41 | # Create or update application plans 42 | - import_tasks: steps/application_plans.yml 43 | 44 | # Create or update the default application if smoke tests are needed 45 | - include_tasks: steps/default_application.yml 46 | when: (threescale_cicd_openapi_smoketest_operation|length > 0 or threescale_cicd_create_default_application|bool) and threescale_cicd_application_plans is defined 47 | 48 | # Run smoke tests on the staging gateway 49 | - include_tasks: steps/smoke_test.yml 50 | vars: 51 | threescale_cicd_smoke_test_env: staging 52 | when: >- 53 | threescale_cicd_openapi_smoketest_operation|length > 0 and threescale_cicd_application_plans is defined 54 | and threescale_cicd_apicast_discovered_sandbox_endpoint != threescale_cicd_apicast_discovered_production_endpoint 55 | 56 | # Promote to production 57 | - import_tasks: steps/promote.yml 58 | 59 | # Run smoke tests on the production gateway 60 | - include_tasks: steps/smoke_test.yml 61 | vars: 62 | threescale_cicd_smoke_test_env: production 63 | when: 'threescale_cicd_openapi_smoketest_operation|length > 0 and threescale_cicd_application_plans is defined' 64 | 65 | # Delete the metrics that are not needed anymore 66 | - import_tasks: steps/cleanup_metrics.yml 67 | 68 | # Publish the OpenAPI Specifications file on the 3scale Admin Portal 69 | - import_tasks: steps/activedoc.yml 70 | -------------------------------------------------------------------------------- /tasks/steps/activedoc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - debug: 3 | var: threescale_cicd_openapi_rewritten 4 | verbosity: 1 5 | 6 | - include_tasks: api-calls/update_activedoc.yml 7 | when: 'threescale_cicd_api_system_name in threescale_cicd_existing_activedocs' 8 | 9 | - include_tasks: api-calls/create_activedoc.yml 10 | when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_activedocs' 11 | -------------------------------------------------------------------------------- /tasks/steps/ansible_requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Verify that Ansible version is >= 2.4.6 4 | assert: 5 | that: "ansible_version.full is version_compare('2.4.6', '>=')" 6 | msg: >- 7 | This module requires at least Ansible 2.4.6. The version that comes 8 | with RHEL and CentOS by default (2.4.2) has a known bug that prevent 9 | this role from running properly. 10 | 11 | - name: Check if jmespath is installed locally 12 | debug: msg={{ dummy|json_query('@') }} 13 | register: check_jmespath 14 | ignore_errors: yes 15 | vars: 16 | dummy: Hello World 17 | 18 | - name: Check if jinja 2.8 is installed locally 19 | debug: msg={{ (dummy|selectattr("id", "equalto", "hello")|first)['value'] }} 20 | vars: 21 | dummy: 22 | - id: hello 23 | value: Hello World 24 | register: check_jinja28 25 | ignore_errors: yes 26 | 27 | - name: Check if the "do" jinja extension is enabled 28 | debug: msg={% do {}.update({}) %}{{ success }} 29 | vars: 30 | success: 'The do extension is enabled' 31 | register: check_jinja_do_ext 32 | ignore_errors: yes 33 | 34 | - name: Ensure JMESPath is installed 35 | assert: 36 | that: 37 | - 'check_jmespath is success' 38 | msg: "The JMESPath library is required by this role. Please install the JMESPath library with 'pip install jmespath'." 39 | 40 | - name: Ensure at least Jinja 2.8 is installed 41 | assert: 42 | that: 43 | - 'check_jinja28 is success' 44 | msg: "At least Jinja v2.8 is required by this role. Please update Jinja with 'pip install -U Jinja2'." 45 | 46 | - name: Ensure the "do" extension of Jinja is enabled 47 | assert: 48 | that: 49 | - 'check_jinja_do_ext is success' 50 | msg: |- 51 | You need to enable the 'do' extension of Jinja in your ansible.cfg: 52 | [default] 53 | jinja2_extensions = jinja2.ext.do 54 | -------------------------------------------------------------------------------- /tasks/steps/application_plan.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: api-calls/update_application_plan.yml 4 | when: 'threescale_cicd_application_plan.system_name in threescale_cicd_existing_application_plans' 5 | 6 | - include_tasks: api-calls/create_application_plan.yml 7 | when: 'threescale_cicd_application_plan.system_name not in threescale_cicd_existing_application_plans' 8 | -------------------------------------------------------------------------------- /tasks/steps/application_plans.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: steps/application_plan.yml 4 | with_items: '{{ threescale_cicd_application_plans|default([]) }}' 5 | loop_control: 6 | loop_var: threescale_cicd_application_plan 7 | -------------------------------------------------------------------------------- /tasks/steps/cleanup_metrics.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: "api-calls/delete_metric.yml" 4 | with_items: '{{ threescale_cicd_metrics_to_delete }}' 5 | loop_control: 6 | loop_var: threescale_cicd_metric 7 | -------------------------------------------------------------------------------- /tasks/steps/default_application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - import_tasks: "api-calls/find_first_account.yml" 4 | when: 'threescale_cicd_default_account_id is not defined' 5 | 6 | - import_tasks: "api-calls/find_application.yml" 7 | 8 | - import_tasks: "api-calls/update_application.yml" 9 | when: 'threescale_cicd_default_application_id is defined' 10 | 11 | - import_tasks: "api-calls/create_application.yml" 12 | when: 'threescale_cicd_default_application_id is not defined' 13 | 14 | ## 15 | ## When using OAuth / OIDC authentication, we need to patch the Keycloak client 16 | ## to support the client_credentials grant. 17 | ## 18 | ## Note: only happens on 3scale AMP < 2.5. Later versions do not need this. 19 | ## 20 | 21 | - include_tasks: api-calls/keycloak/authenticate.yml 22 | when: >- 23 | threescale_cicd_api_security_scheme.type == 'oauth2' 24 | and not threescale_cicd_capabilities.oidc_configuration_api|bool 25 | vars: 26 | oauth_payload: 27 | client_id: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''username'') }}' 28 | client_secret: '{{ threescale_cicd_sso_issuer_endpoint|urlsplit(''password'') }}' 29 | scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}' 30 | grant_type: 'client_credentials' 31 | 32 | - include_tasks: api-calls/keycloak/wait_for_client.yml 33 | when: >- 34 | threescale_cicd_api_security_scheme.type == 'oauth2' 35 | and not threescale_cicd_capabilities.oidc_configuration_api|bool 36 | 37 | - include_tasks: api-calls/keycloak/patch_client.yml 38 | when: >- 39 | threescale_cicd_api_security_scheme.type == 'oauth2' 40 | and not threescale_cicd_capabilities.oidc_configuration_api|bool 41 | -------------------------------------------------------------------------------- /tasks/steps/discover_platform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Retrieve existing Services from the 3scale Admin Portal 4 | uri: 5 | url: "https://{{ inventory_hostname }}/admin/api/services.json?access_token={{ threescale_cicd_access_token|urlencode }}" 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | when: threescale_cicd_existing_services is not defined 9 | no_log: '{{ threescale_cicd_nolog }}' 10 | 11 | - name: Set the list of existing service as a fact 12 | set_fact: 13 | threescale_cicd_existing_services: >- 14 | {{ threescale_cicd_tmpresponse.json|json_query('services[*].service.system_name') }} 15 | threescale_cicd_existing_services_details: >- 16 | {{ threescale_cicd_tmpresponse.json|json_query('services[].{"system_name": service.system_name, "id": service.id}') }} 17 | cacheable: true 18 | when: threescale_cicd_existing_services is not defined 19 | 20 | - debug: 21 | msg: "Found {{ threescale_cicd_existing_services|length }} services" 22 | verbosity: 1 23 | 24 | - debug: 25 | var: threescale_cicd_existing_services_details 26 | verbosity: 1 27 | 28 | - name: Retrieve existing ActiveDocs from the 3scale Admin Portal 29 | uri: 30 | url: "https://{{ inventory_hostname }}/admin/api/active_docs.json?access_token={{ threescale_cicd_access_token|urlencode }}" 31 | validate_certs: no 32 | register: threescale_cicd_tmp_allactivedocs 33 | no_log: '{{ threescale_cicd_nolog }}' 34 | 35 | - name: Set the list of existing active docs as a fact 36 | set_fact: 37 | threescale_cicd_existing_activedocs: >- 38 | {{ threescale_cicd_tmp_allactivedocs.json|json_query('api_docs[*].api_doc.system_name') }} 39 | threescale_cicd_existing_activedocs_details: >- 40 | {{ threescale_cicd_tmp_allactivedocs.json|json_query('api_docs[].{"system_name": api_doc.system_name, "id": api_doc.id}') }} 41 | 42 | - debug: 43 | msg: "Found {{ threescale_cicd_existing_activedocs|length }} active docs" 44 | verbosity: 1 45 | 46 | - debug: 47 | var: threescale_cicd_existing_activedocs_details 48 | verbosity: 1 49 | 50 | - name: Poke around for the APIcast Policies Registry API 51 | uri: 52 | url: "https://{{ inventory_hostname }}/admin/api/policies.json?access_token={{ threescale_cicd_access_token|urlencode }}" 53 | validate_certs: no 54 | status_code: 200,404 55 | register: threescale_cicd_tmp_policy_response 56 | changed_when: false 57 | no_log: '{{ threescale_cicd_nolog }}' 58 | when: threescale_cicd_capabilities is not defined 59 | 60 | - name: Poke around for the OIDC Configuration Show API 61 | uri: 62 | url: "https://{{ inventory_hostname }}/admin/api/services/0/proxy/oidc_configuration.json?access_token={{ threescale_cicd_access_token|urlencode }}" 63 | validate_certs: no 64 | status_code: 404 65 | register: threescale_cicd_tmp_oidc_config_response 66 | changed_when: false 67 | no_log: '{{ threescale_cicd_nolog }}' 68 | when: threescale_cicd_capabilities is not defined 69 | 70 | - name: Set the list of 3scale capabilities as a fact 71 | set_fact: 72 | threescale_cicd_capabilities: 73 | policy_registry_api: '{{ threescale_cicd_tmp_policy_response.status == 200 }}' 74 | oidc_configuration_api: '{{ threescale_cicd_tmp_oidc_config_response.content_type.startswith(''application/json'') }}' 75 | cacheable: true 76 | when: threescale_cicd_capabilities is not defined 77 | 78 | - debug: 79 | var: threescale_cicd_capabilities 80 | verbosity: 1 81 | -------------------------------------------------------------------------------- /tasks/steps/discover_service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Get the list of existing application plans 4 | uri: 5 | url: '{{ service_url }}/application_plans.json?access_token={{ threescale_cicd_access_token|urlencode }}' 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | when: threescale_cicd_api_system_name in threescale_cicd_existing_services 9 | no_log: '{{ threescale_cicd_nolog }}' 10 | vars: 11 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 12 | 13 | - name: Set the list of existing application plans as a fact 14 | set_fact: 15 | threescale_cicd_existing_application_plans: >- 16 | {{ threescale_cicd_tmpresponse.json|json_query('plans[*].application_plan.system_name') 17 | if threescale_cicd_api_system_name in threescale_cicd_existing_services 18 | else [] }} 19 | threescale_cicd_existing_application_plans_details: >- 20 | {{ threescale_cicd_tmpresponse.json|json_query('plans[].{"system_name": application_plan.system_name, "id": application_plan.id}') 21 | if threescale_cicd_api_system_name in threescale_cicd_existing_services 22 | else [] }} 23 | 24 | - debug: 25 | msg: "Found {{ threescale_cicd_existing_application_plans|length }} application plans" 26 | verbosity: 1 27 | 28 | - debug: 29 | var: threescale_cicd_existing_application_plans_details 30 | verbosity: 1 31 | -------------------------------------------------------------------------------- /tasks/steps/find_goswagger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Check if go-swagger is installed globally 4 | command: swagger version 5 | register: check_global_goswagger_version 6 | changed_when: false 7 | ignore_errors: yes 8 | 9 | - name: Set the 'swagger' command path as fact 10 | set_fact: 11 | threescale_cicd_goswagger_command: 'swagger' 12 | cacheable: true 13 | when: check_global_goswagger_version is success 14 | 15 | - name: Check if go-swagger is installed locally 16 | command: '{{ threescale_cicd_local_bin_path }}/swagger version' 17 | register: check_local_goswagger_version 18 | changed_when: false 19 | ignore_errors: yes 20 | when: check_global_goswagger_version is failed 21 | 22 | - name: Set the 'swagger' command path as fact 23 | set_fact: 24 | threescale_cicd_goswagger_command: '{{ threescale_cicd_local_bin_path }}/swagger' 25 | cacheable: true 26 | when: check_local_goswagger_version is success and check_local_goswagger_version is not skipped 27 | -------------------------------------------------------------------------------- /tasks/steps/install_goswagger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Find the OS and architecture of the control node 4 | setup: 5 | gather_subset: '!all' 6 | 7 | - name: Find the latest version of go-swagger 8 | uri: 9 | url: https://api.github.com/repos/go-swagger/go-swagger/releases/latest 10 | register: latest_goswagger_version 11 | 12 | - debug: 13 | msg: 'Found go-swagger remotely, version {{ latest_goswagger_version.json.tag_name }}' 14 | 15 | - name: Create the folder to download go-swagger 16 | file: 17 | path: '{{ threescale_cicd_local_bin_path }}' 18 | state: directory 19 | 20 | - name: Download go-swagger 21 | get_url: 22 | url: '{{ goswagger_download_url }}' 23 | dest: '{{ threescale_cicd_local_bin_path }}/swagger' 24 | mode: 0755 25 | vars: 26 | goswagger_download_url: '{{ goswagger_asset.browser_download_url }}' 27 | goswagger_asset: '{{ goswagger_assets|selectattr(''name'', ''equalto'', artifact_name)|first }}' 28 | goswagger_assets: '{{ latest_goswagger_version.json.assets }}' 29 | artifact_name: 'swagger_{{ ansible_system|lower }}_{{ swagger_architecture }}' 30 | swagger_architecture: '{{ swagger_architecture_mapping[ansible_architecture] }}' 31 | swagger_architecture_mapping: 32 | x86_64: amd64 33 | armv7l: arm 34 | 35 | - name: Set the 'swagger' command path as fact 36 | set_fact: 37 | threescale_cicd_goswagger_command: '{{ threescale_cicd_local_bin_path }}/swagger' 38 | cacheable: true 39 | -------------------------------------------------------------------------------- /tasks/steps/mapping_rules.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Retrieve existing mapping rules from the 3scale Admin Portal 4 | uri: 5 | url: "{{ service_url }}/proxy/mapping_rules.json?access_token={{ threescale_cicd_access_token|urlencode }}" 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | no_log: '{{ threescale_cicd_nolog }}' 9 | vars: 10 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 11 | 12 | - name: Set the list of existing mapping rules as a fact 13 | set_fact: 14 | threescale_cicd_existing_mapping_rules_details: >- 15 | {{ threescale_cicd_tmpresponse.json|json_query('mapping_rules[].{"metric_id": mapping_rule.metric_id, "id": mapping_rule.id}') }} 16 | 17 | - include_tasks: "api-calls/create_mapping_rule.yml" 18 | with_items: '{{ threescale_cicd_mapping_rules_to_create }}' 19 | loop_control: 20 | loop_var: threescale_cicd_mapping_rule 21 | 22 | - include_tasks: "api-calls/update_mapping_rule.yml" 23 | with_items: '{{ threescale_cicd_mapping_rules_to_update }}' 24 | loop_control: 25 | loop_var: threescale_cicd_mapping_rule 26 | 27 | - include_tasks: "api-calls/delete_mapping_rule.yml" 28 | with_items: '{{ threescale_cicd_mapping_rules_to_delete }}' 29 | loop_control: 30 | loop_var: threescale_cicd_mapping_rule 31 | -------------------------------------------------------------------------------- /tasks/steps/method.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: api-calls/update_method.yml 4 | when: 'threescale_cicd_api_operation.key in threescale_cicd_existing_metrics' 5 | 6 | - include_tasks: api-calls/create_method.yml 7 | when: 'threescale_cicd_api_operation.key not in threescale_cicd_existing_metrics' 8 | -------------------------------------------------------------------------------- /tasks/steps/methods.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Retrieve existing metrics from the 3scale Admin Portal 4 | uri: 5 | url: "{{ service_url }}/metrics.json?access_token={{ threescale_cicd_access_token|urlencode }}" 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | no_log: '{{ threescale_cicd_nolog }}' 9 | vars: 10 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 11 | 12 | - name: Set the list of existing metrics as facts 13 | set_fact: 14 | threescale_cicd_existing_metrics: >- 15 | {{ threescale_cicd_tmpresponse.json|json_query('metrics[*].metric.system_name') }} 16 | threescale_cicd_existing_metrics_details: >- 17 | {{ threescale_cicd_tmpresponse.json|json_query('metrics[].{"system_name": metric.system_name, "id": metric.id}') }} 18 | 19 | - include_tasks: "steps/method.yml" 20 | with_dict: '{{ threescale_cicd_api_operations }}' 21 | loop_control: 22 | loop_var: threescale_cicd_api_operation 23 | -------------------------------------------------------------------------------- /tasks/steps/oidc_configuration.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: api-calls/update_oidc_configuration.yml 4 | when: >- 5 | threescale_cicd_api_security_scheme.type == 'oauth2' 6 | and threescale_cicd_capabilities.oidc_configuration_api|bool 7 | -------------------------------------------------------------------------------- /tasks/steps/policies.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Retrieve existing policies from the 3scale Admin Portal 4 | uri: 5 | url: "{{ service_url }}/proxy/policies.json?access_token={{ threescale_cicd_access_token|urlencode }}" 6 | validate_certs: no 7 | register: threescale_cicd_tmpresponse 8 | no_log: '{{ threescale_cicd_nolog }}' 9 | vars: 10 | service_url: https://{{ inventory_hostname }}/admin/api/services/{{ threescale_cicd_api_service_id }} 11 | 12 | - name: Set the list of existing policies as a fact 13 | set_fact: 14 | threescale_cicd_existing_policies_details: '{{ threescale_cicd_tmpresponse.json|json_query(''policies_config[]'') }}' 15 | 16 | - include_tasks: "api-calls/update_policies.yml" 17 | with_items: '{{ threescale_cicd_policies_to_update }}' 18 | -------------------------------------------------------------------------------- /tasks/steps/promote.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - import_tasks: "api-calls/get_proxy_version.yml" 4 | 5 | - include_tasks: "api-calls/promote_proxy.yml" 6 | when: 'threescale_cicd_staging_proxy_version != threescale_cicd_production_proxy_version' 7 | -------------------------------------------------------------------------------- /tasks/steps/proxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - import_tasks: api-calls/update_proxy.yml 4 | -------------------------------------------------------------------------------- /tasks/steps/read_openapi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Check the OpenAPI format version 4 | assert: 5 | that: 6 | - "threescale_cicd_openapi_file_version == '2.0'" 7 | msg: "Currently only the OpenAPI/Swagger 2.0 is handled. If needed, fill an issue or submit a pull request!" 8 | 9 | - name: Make sure there is one and exactly one security requirement 10 | assert: 11 | that: 12 | - 'threescale_cicd_api_security_requirements|length == 1' 13 | msg: >- 14 | You have {{ threescale_cicd_api_security_requirements|length }} global security requirements. 15 | There must be one and only one security requirement. 16 | 17 | - name: Make sure the security scheme is consistent with 3scale 18 | assert: 19 | that: 20 | - >- 21 | 'type' in threescale_cicd_api_security_scheme and threescale_cicd_api_security_scheme.type == 'apiKey' 22 | or (threescale_cicd_api_security_scheme.type == 'oauth2' and threescale_cicd_sso_issuer_endpoint is defined) 23 | msg: >- 24 | The embedded security definition {{ threescale_cicd_api_security_scheme_name }} is not compatible with 3scale. 25 | Please make sure you chose an "apiKey" or "oauth2" scheme. 26 | Also, if you chose "oauth2", you will need to pass the threescale_cicd_sso_issuer_endpoint extra variable. 27 | The security definition you chose: {{ threescale_cicd_api_security_scheme|to_nice_json }} 28 | 29 | - name: Make sure the Private Base URL is defined 30 | assert: 31 | that: 32 | - 'threescale_cicd_private_base_url is defined' 33 | msg: >- 34 | Either the private base url or the tuple backend hostname/scheme must be declared as extra variables 35 | (either threescale_cicd_private_base_url or threescale_cicd_api_backend_scheme / threescale_cicd_api_backend_hostname) 36 | 37 | - name: Smoketests consistency 38 | assert: 39 | that: 40 | # Operation must exists 41 | - 'threescale_cicd_openapi_smoketest_operation in threescale_cicd_api_operations' 42 | # Must be a GET 43 | - "threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].verb == 'get'" 44 | # Must NOT have a placeholder in the path 45 | - 'threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].path.find("{") == -1' 46 | msg: "The smoketest operation {{ threescale_cicd_openapi_smoketest_operation }} must be a GET and cannot have a placeholder in its path." 47 | when: 'threescale_cicd_openapi_smoketest_operation|length > 0' 48 | 49 | - name: Make sure the 'application' OAuth flow is enabled if smoke tests are required 50 | assert: 51 | that: 52 | - "'application' in threescale_cicd_oicd_flows|default([ threescale_cicd_api_security_scheme.flow ])" 53 | msg: >- 54 | Since 3scale AMP 2.5, you need to explicitely add the "application" flow to the supported 55 | OAuth flows if you want to run smoke tests. You can either disable smoke tests 56 | (-e threescale_cicd_openapi_smoketest_operation="") or enable the application flow 57 | (-e threescale_cicd_oicd_flows="{{ [ 'application', threescale_cicd_api_security_scheme.flow ] }}"). 58 | when: >- 59 | threescale_cicd_api_security_scheme.type == 'oauth2' 60 | and threescale_cicd_openapi_smoketest_operation|length > 0 61 | and threescale_cicd_capabilities.oidc_configuration_api|bool 62 | 63 | - debug: 64 | msg: "Will work on service with system_name = {{ threescale_cicd_api_system_name }}" 65 | -------------------------------------------------------------------------------- /tasks/steps/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ensure pre-requisites are met 4 | assert: 5 | that: 6 | - "threescale_cicd_access_token is defined" 7 | - "threescale_cicd_openapi_file is defined" 8 | msg: |- 9 | This module requires at least two variables: 10 | - threescale_cicd_access_token that contains an Access Token with Read/Write privileges on the 3scale Account Management API. 11 | This variable is usually set in your inventory file. 12 | - threescale_cicd_openapi_file that is the path to the OpenAPI file you want to deploy in 3scale. 13 | This variable is usually passed as an extra variable (-e threescale_cicd_openapi_file=...) 14 | 15 | - name: Make sure the OpenAPI File Format is YAML or JSON 16 | assert: 17 | that: 18 | - threescale_cicd_openapi_file_format|upper == 'JSON' or threescale_cicd_openapi_file_format|upper == 'YAML' 19 | msg: |- 20 | The threescale_cicd_openapi_file_format parameter needs to be either 'JSON' or 'YAML' 21 | -------------------------------------------------------------------------------- /tasks/steps/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - include_tasks: api-calls/update_service.yml 4 | when: 'threescale_cicd_api_system_name in threescale_cicd_existing_services' 5 | 6 | - include_tasks: api-calls/create_service.yml 7 | when: 'threescale_cicd_api_system_name not in threescale_cicd_existing_services' 8 | -------------------------------------------------------------------------------- /tasks/steps/smoke_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Retrieve a valid access token if the API is secured with OAuth/OIDC 4 | - include_tasks: api-calls/keycloak/authenticate.yml 5 | when: "threescale_cicd_api_security_scheme.type == 'oauth2'" 6 | vars: 7 | oauth_payload: 8 | client_id: '{{ threescale_cicd_default_application_details.client_id }}' 9 | client_secret: '{{ threescale_cicd_default_application_details.client_secret }}' 10 | scope: '{{ threescale_cicd_openapi_smoketest_default_scope }}' 11 | grant_type: 'client_credentials' 12 | 13 | # Do the smoke test 14 | - import_tasks: api-calls/smoke_test.yml 15 | -------------------------------------------------------------------------------- /tasks/steps/validate_openapi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - debug: 4 | msg: >- 5 | Will use go-swagger at '{{ threescale_cicd_goswagger_command }}' as instructed. 6 | Auto-detection and download is DISABLED. 7 | when: threescale_cicd_goswagger_command is defined 8 | 9 | - include_tasks: "steps/find_goswagger.yml" 10 | when: threescale_cicd_goswagger_command is not defined 11 | 12 | - include_tasks: "steps/install_goswagger.yml" 13 | when: threescale_cicd_goswagger_command is not defined 14 | 15 | - name: Validate the provided OpenAPI Specification file 16 | command: '{{ threescale_cicd_goswagger_command }} validate {{ threescale_cicd_openapi_file }}' 17 | changed_when: false 18 | -------------------------------------------------------------------------------- /tasks/steps/variables_from_inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Abort on deprecated feature -> the "sso" inventory group 4 | fail: 5 | msg: >- 6 | You are currently using a deprecated feature (the 'sso' group in your inventory). 7 | Please replace it with the 'threescale_cicd_sso_issuer_endpoint' variable. 8 | Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features' 9 | extra variable to 'true'. 10 | when: >- 11 | threescale_cicd_sso_issuer_endpoint|default("")|length > 0 and 'sso' in groups 12 | and groups['sso'] > 0 and threescale_cicd_api_backend_version == 'oidc' 13 | and not threescale_cicd_deprecated_features|default(false)|bool 14 | 15 | - name: Abort on deprecated feature -> the "apicast-sandbox" inventory group 16 | fail: 17 | msg: >- 18 | You are currently using a deprecated feature (the 'apicast-sandbox' group in your inventory). 19 | Please replace it with the 'threescale_cicd_apicast_sandbox_endpoint' variable. 20 | Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features' 21 | extra variable to 'true'. 22 | when: >- 23 | threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 and 'apicast-sandbox' in groups 24 | and groups['apicast-sandbox'] > 0 and not threescale_cicd_deprecated_features|default(false)|bool 25 | 26 | - name: Abort on deprecated feature -> the "apicast-production" inventory group 27 | fail: 28 | msg: >- 29 | You are currently using a deprecated feature (the 'apicast-production' group in your inventory). 30 | Please replace it with the 'threescale_cicd_apicast_production_endpoint' variable. 31 | Alternatively, you can also bypass this warning by setting the 'threescale_cicd_deprecated_features' 32 | extra variable to 'true'. 33 | when: >- 34 | threescale_cicd_apicast_production_endpoint|default("")|length > 0 and 'apicast-production' in groups 35 | and groups['apicast-production'] > 0 and not threescale_cicd_deprecated_features|default(false)|bool 36 | -------------------------------------------------------------------------------- /templates/api-calls/create_activedoc.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'name=' ~ threescale_cicd_api_name|urlencode, 5 | 'description=' ~ threescale_cicd_api_description|urlencode, 6 | 'system_name=' ~ threescale_cicd_api_system_name|urlencode, 7 | 'body=' ~ threescale_cicd_openapi_rewritten|to_nice_json|urlencode, 8 | 'published=true', 9 | 'service_id=' ~ threescale_cicd_api_service_id|urlencode, 10 | ] 11 | %} 12 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/create_application.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'plan_id=' ~ threescale_cicd_default_application_plan_id|urlencode, 5 | 'name=' ~ threescale_cicd_default_application_name|urlencode, 6 | 'description=' ~ threescale_cicd_default_application_description|urlencode 7 | ] 8 | %} 9 | {% if threescale_cicd_api_security_scheme.type == 'oauth2' %} 10 | {% do payload.append("application_id=" ~ threescale_cicd_default_application_appid|urlencode) %} 11 | {% do payload.append("application_key=" ~ threescale_cicd_default_application_appsecret|urlencode) %} 12 | {% endif %} 13 | {% if threescale_cicd_api_security_scheme.type == 'apiKey' %} 14 | {% do payload.append("user_key=" ~ threescale_cicd_default_application_appid|urlencode) %} 15 | {% endif %} 16 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/create_application_plan.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode 4 | ] 5 | %} 6 | {% for key, value in threescale_cicd_application_plan.items() %} 7 | {% do payload.append(key ~ "=" ~ value|urlencode) %} 8 | {% endfor %} 9 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/create_mapping_rule.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'metric_id=' ~ ((threescale_cicd_existing_metrics_details|selectattr("system_name", "equalto", threescale_cicd_mapping_rule)|first).id|urlencode) 5 | ] 6 | %} 7 | {% for key, value in threescale_cicd_wanted_mapping_rules[threescale_cicd_mapping_rule].items() %} 8 | {% do payload.append(key ~ "=" ~ value|urlencode) %} 9 | {% endfor %} 10 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/create_method.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'friendly_name=' ~ threescale_cicd_api_operation.value.friendly_name|default(threescale_cicd_api_operation.key)|urlencode, 5 | 'description=' ~ threescale_cicd_api_operation.value.description|default('')|urlencode, 6 | 'system_name=' ~ threescale_cicd_api_operation.key|urlencode, 7 | 'unit=hits' 8 | ] 9 | %} 10 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/create_service.j2: -------------------------------------------------------------------------------- 1 | {% if inventory_hostname is match(".*[.]3scale[.]net") and (threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 or threescale_cicd_apicast_production_endpoint|default("")|length > 0) %} 2 | {% set deployment_type = "self_managed" %} 3 | {% else %} 4 | {% set deployment_type = "hosted" %} 5 | {% endif %} 6 | {% 7 | set payload = [ 8 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 9 | 'name=' ~ threescale_cicd_api_name|urlencode, 10 | 'deployment_option=' ~ deployment_type|urlencode, 11 | 'system_name=' ~ threescale_cicd_api_system_name|urlencode, 12 | 'backend_version=' ~ threescale_cicd_api_backend_version|urlencode 13 | ] 14 | %} 15 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/find_application.j2: -------------------------------------------------------------------------------- 1 | access_token={{ threescale_cicd_access_token|urlencode }} 2 | {%- if threescale_cicd_api_security_scheme.type == 'oauth2' -%} 3 | &app_id={{ threescale_cicd_default_application_appid|urlencode }} 4 | {%- endif -%} 5 | {%- if threescale_cicd_api_security_scheme.type == 'apiKey' -%} 6 | &user_key={{ threescale_cicd_default_application_appid|urlencode }} 7 | {%- endif -%} -------------------------------------------------------------------------------- /templates/api-calls/keycloak/authenticate.j2: -------------------------------------------------------------------------------- 1 | {% set payload = [ ] %} 2 | {% for key, value in oauth_payload.items() %} 3 | {% do payload.append(key ~ "=" ~ value|urlencode) %} 4 | {% endfor %} 5 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/keycloak/patch_client.j2: -------------------------------------------------------------------------------- 1 | {{ threescale_cicd_default_application_sso_body|combine({ 'serviceAccountsEnabled': true, 'standardFlowEnabled': false, 'implicitFlowEnabled': false, 'directAccessGrantsEnabled': true }) }} 2 | -------------------------------------------------------------------------------- /templates/api-calls/promote_proxy.j2: -------------------------------------------------------------------------------- 1 | access_token={{ threescale_cicd_access_token|urlencode }}&to={{ threescale_cicd_production_environment_name|urlencode }} -------------------------------------------------------------------------------- /templates/api-calls/smoke-test/headers.j2: -------------------------------------------------------------------------------- 1 | {% set headers = {} %} 2 | {% if threescale_cicd_api_security_scheme.type == "apiKey" and threescale_cicd_api_credentials_location == "headers" %} 3 | {% do headers.update({ threescale_cicd_api_security_scheme.name|urlencode: threescale_cicd_default_application_details.user_key }) %} 4 | {% endif %} 5 | {% if threescale_cicd_api_security_scheme.type == "oauth2" and threescale_cicd_api_credentials_location == "headers" %} 6 | {% do headers.update({ 'Authorization': 'Bearer ' ~ threescale_cicd_keycloak_access_token }) %} 7 | {% endif %} 8 | {{ headers }} -------------------------------------------------------------------------------- /templates/api-calls/smoke-test/url.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_smoke_test_env == "staging" -%} 2 | {{ threescale_cicd_apicast_discovered_sandbox_endpoint }} 3 | {%- endif -%} 4 | {%- if threescale_cicd_smoke_test_env == "production" -%} 5 | {{ threescale_cicd_apicast_discovered_production_endpoint }} 6 | {%- endif -%} 7 | {{ threescale_cicd_openapi_smoketest_path }} 8 | {%- if threescale_cicd_api_security_scheme.type == "apiKey" and threescale_cicd_api_credentials_location == "query" -%} 9 | ?{{ threescale_cicd_api_security_scheme.name|urlencode }}={{ threescale_cicd_default_application_details.user_key }} 10 | {%- endif -%} -------------------------------------------------------------------------------- /templates/api-calls/update_activedoc.j2: -------------------------------------------------------------------------------- 1 | create_activedoc.j2 -------------------------------------------------------------------------------- /templates/api-calls/update_application.j2: -------------------------------------------------------------------------------- 1 | create_application.j2 -------------------------------------------------------------------------------- /templates/api-calls/update_application_plan.j2: -------------------------------------------------------------------------------- 1 | create_application_plan.j2 -------------------------------------------------------------------------------- /templates/api-calls/update_mapping_rule.j2: -------------------------------------------------------------------------------- 1 | create_mapping_rule.j2 -------------------------------------------------------------------------------- /templates/api-calls/update_method.j2: -------------------------------------------------------------------------------- 1 | create_method.j2 -------------------------------------------------------------------------------- /templates/api-calls/update_oidc_configuration.j2: -------------------------------------------------------------------------------- 1 | {% set flows = threescale_cicd_oicd_flows|default([ threescale_cicd_api_security_scheme.flow ]) %} 2 | {% 3 | set payload = [ 4 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 5 | 'implicit_flow_enabled=' ~ ('true' if 'implicit' in flows else 'false'), 6 | 'direct_access_grants_enabled=' ~ ('true' if 'password' in flows else 'false'), 7 | 'service_accounts_enabled=' ~ ('true' if 'application' in flows else 'false'), 8 | 'standard_flow_enabled=' ~ ('true' if 'accessCode' in flows else 'false'), 9 | ] 10 | %} 11 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/update_policies.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'policies_config=' ~ threescale_cicd_policies_to_update|to_json|urlencode 5 | ] 6 | %} 7 | {{ payload|join("&") }} 8 | -------------------------------------------------------------------------------- /templates/api-calls/update_proxy.j2: -------------------------------------------------------------------------------- 1 | {% 2 | set payload = [ 3 | 'access_token=' ~ threescale_cicd_access_token|urlencode, 4 | 'credentials_location=' ~ threescale_cicd_api_credentials_location|urlencode, 5 | 'api_backend=' ~ threescale_cicd_private_base_url|urlencode 6 | ] 7 | %} 8 | {% if threescale_cicd_api_security_scheme.type == 'apiKey' %} 9 | {% do payload.append('auth_user_key=' ~ threescale_cicd_api_security_scheme.name|urlencode) %} 10 | {% endif %} 11 | {% if threescale_cicd_api_security_scheme.type == 'oauth2' %} 12 | {% do payload.append('oidc_issuer_endpoint=' ~ threescale_cicd_sso_issuer_endpoint|urlencode) %} 13 | {% endif %} 14 | {% if threescale_cicd_apicast_sandbox_endpoint|default("")|length > 0 %} 15 | {% do payload.append('sandbox_endpoint=' ~ threescale_cicd_apicast_sandbox_endpoint|urlencode) %} 16 | {% endif %} 17 | {% if threescale_cicd_apicast_production_endpoint|default("")|length > 0 %} 18 | {% do payload.append('endpoint=' ~ threescale_cicd_apicast_production_endpoint|urlencode) %} 19 | {% endif %} 20 | {{ payload|join("&") }} -------------------------------------------------------------------------------- /templates/api-calls/update_service.j2: -------------------------------------------------------------------------------- 1 | create_service.j2 -------------------------------------------------------------------------------- /templates/existing_mapping_rules.j2: -------------------------------------------------------------------------------- 1 | {% set mapping_rules = {} %} 2 | {% for value in threescale_cicd_existing_mapping_rules_details %} 3 | {% do mapping_rules.update({ (threescale_cicd_existing_metrics_details|selectattr("id", "equalto", value.metric_id)|first).system_name: value.id }) %} 4 | {% endfor %} 5 | {{ mapping_rules }} -------------------------------------------------------------------------------- /templates/existing_policies.j2: -------------------------------------------------------------------------------- 1 | {% set policies = [] %} 2 | {% for value in threescale_cicd_existing_policies_details %} 3 | {% do policies.append( value ) %} 4 | {% endfor %} 5 | {{ policies }} 6 | -------------------------------------------------------------------------------- /templates/metrics_to_delete.j2: -------------------------------------------------------------------------------- 1 | {% set to_delete = [] %} 2 | {% for metric in threescale_cicd_existing_metrics_details %} 3 | {% if metric.system_name != "hits" and metric.system_name not in threescale_cicd_api_operations %} 4 | {% do to_delete.append(metric) %} 5 | {% endif %} 6 | {% endfor %} 7 | {{ to_delete }} -------------------------------------------------------------------------------- /templates/openapi/apicast_production_endpoint.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_wildcard_domain is defined -%} 2 | {{ 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 }} 3 | {%- elif 'apicast-production' in groups and groups['apicast-production'] > 0 -%} 4 | {{ (hostvars[groups['apicast-production'][0]].scheme|default('https')) ~ '://' ~ groups['apicast-production'][0] }} 5 | {%- endif -%} -------------------------------------------------------------------------------- /templates/openapi/apicast_sandbox_endpoint.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_wildcard_domain is defined -%} 2 | {{ 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 }} 3 | {%- elif 'apicast-sandbox' in groups and groups['apicast-sandbox'] > 0 -%} 4 | {{ (hostvars[groups['apicast-sandbox'][0]].scheme|default('https')) ~ '://' ~ groups['apicast-sandbox'][0] }} 5 | {%- endif -%} -------------------------------------------------------------------------------- /templates/openapi/generate_base_system_name.j2: -------------------------------------------------------------------------------- 1 | {% if 'x-threescale-system-name' in threescale_cicd_openapi_file_content.info %} 2 | {% set extracted_system_name = threescale_cicd_openapi_file_content.info['x-threescale-system-name']|regex_replace('[^a-zA-Z0-9_]+', '_')|lower %} 3 | {% else %} 4 | {% set extracted_system_name = threescale_cicd_openapi_file_content.info['title']|default('api')|regex_replace('[^a-zA-Z0-9_]+', '_')|lower %} 5 | {% endif %} 6 | {{ extracted_system_name }} -------------------------------------------------------------------------------- /templates/openapi/generate_final_system_name.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_api_environment_name is defined -%} 2 | {%- set system_name_prefix = threescale_cicd_api_environment_name|default("")|regex_replace('[^a-zA-Z0-9_]+', '_') ~ "_" -%} 3 | {%- endif -%} 4 | {{ system_name_prefix|default("") }}{{ threescale_cicd_api_base_system_name }}_{{ threescale_cicd_api_version_major|regex_replace('[^a-zA-Z0-9_]+', '_') }} -------------------------------------------------------------------------------- /templates/openapi/openapi_operations.j2: -------------------------------------------------------------------------------- 1 | {% set operations = {} -%} 2 | {% if 'paths' in threescale_cicd_openapi_file_content -%} 3 | {% for path, verbs in threescale_cicd_openapi_file_content['paths'].items() -%} 4 | {% if path.startswith('/') -%} 5 | {% for verb, method_description in verbs.items() -%} 6 | {% if verb != '$ref' and verb != 'parameters' -%} 7 | {% if 'operationId' in method_description -%} 8 | {% set operation_id = method_description['operationId'] -%} 9 | {% else -%} 10 | {% set operation_id = verb.upper() + path -%} 11 | {% endif -%} 12 | {% set operation_id = operation_id|regex_replace('[^0-9a-zA-Z_]+', '_') -%} 13 | {% set operation = { operation_id: { 'path': path, 'verb': verb } } -%} 14 | {% if 'summary' in method_description -%} 15 | {% do operation[operation_id].update({ 'friendly_name': method_description.summary }) -%} 16 | {% endif -%} 17 | {% if 'description' in method_description -%} 18 | {% do operation[operation_id].update({ 'description': method_description.description }) -%} 19 | {% endif -%} 20 | {% do operations.update(operation) -%} 21 | {% endif -%} 22 | {% endfor -%} 23 | {% endif -%} 24 | {% endfor -%} 25 | {% endif -%} 26 | {{ operations }} -------------------------------------------------------------------------------- /templates/openapi/private_base_url.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_api_backend_hostname is not defined and 'host' in threescale_cicd_openapi_file_content -%} 2 | {%- set backend_hostname = threescale_cicd_openapi_file_content.host -%} 3 | {%- else -%} 4 | {%- set backend_hostname = threescale_cicd_api_backend_hostname -%} 5 | {%- endif -%} 6 | {%- if threescale_cicd_api_backend_scheme is not defined -%} 7 | {%- set backend_scheme = threescale_cicd_openapi_file_content.schemes|default(["http"])|first -%} 8 | {%- else -%} 9 | {%- set backend_scheme = threescale_cicd_api_backend_scheme -%} 10 | {%- endif -%} 11 | {{ backend_scheme }}://{{ backend_hostname }} -------------------------------------------------------------------------------- /templates/openapi/service_name.j2: -------------------------------------------------------------------------------- 1 | {%- if threescale_cicd_api_environment_name is defined -%} 2 | {{ threescale_cicd_api_default_name }} ({{ threescale_cicd_api_environment_name|upper }}, v{{ threescale_cicd_api_version }}) 3 | {%- else -%} 4 | {{ threescale_cicd_api_default_name }} (v{{ threescale_cicd_api_version }}) 5 | {%- endif -%} -------------------------------------------------------------------------------- /templates/openapi/sso_issuer_endpoint.j2: -------------------------------------------------------------------------------- 1 | {%- if 'sso' in groups and groups['sso'] > 0 -%} 2 | {{ (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 }} 3 | {%- endif -%} -------------------------------------------------------------------------------- /templates/rewritten_openapi.j2: -------------------------------------------------------------------------------- 1 | {% set security_definitions = threescale_cicd_api_security_definitions %} 2 | {% set new_openapi = threescale_cicd_openapi_file_content %} 3 | {# Add the RH-SSO endpoints to the OpenAPI securityDefinitions #} 4 | {% if threescale_cicd_api_security_scheme.type == "oauth2" %} 5 | {% if threescale_cicd_api_security_scheme.flow == "implicit" or threescale_cicd_api_security_scheme.flow == "accessCode" %} 6 | {% do security_definitions[threescale_cicd_api_security_scheme_name].update({ "authorizationUrl": threescale_cicd_sso_realm_endpoint ~ "/protocol/openid-connect/auth" }) %} 7 | {% endif %} 8 | {% if threescale_cicd_api_security_scheme.flow == "password" or threescale_cicd_api_security_scheme.flow == "application" or threescale_cicd_api_security_scheme.flow == "accessCode" %} 9 | {% do security_definitions[threescale_cicd_api_security_scheme_name].update({ "tokenUrl": threescale_cicd_sso_realm_endpoint ~ "/protocol/openid-connect/token" }) %} 10 | {% endif %} 11 | {% endif %} 12 | {# Add the RH-SSO default scope to the OpenAPI securityDefinitions #} 13 | {% if threescale_cicd_api_security_scheme.type == "oauth2" and "scopes" not in threescale_cicd_api_security_scheme %} 14 | {% do security_definitions[threescale_cicd_api_security_scheme_name].update({ "scopes": threescale_cicd_default_oauth_scopes }) %} 15 | {% endif %} 16 | {# Update the security definitions #} 17 | {% do new_openapi.update({ "securityDefinitions": security_definitions }) %} 18 | {# Update the "schemes" and "hostname" fields with the public apicast production URL #} 19 | {% set apicast_production_scheme = threescale_cicd_apicast_discovered_production_endpoint|urlsplit('scheme') %} 20 | {% set apicast_production_hostname = threescale_cicd_apicast_discovered_production_endpoint|urlsplit('hostname') %} 21 | {% do new_openapi.update({ 22 | "schemes": [ apicast_production_scheme ], 23 | "host": apicast_production_hostname 24 | }) %} 25 | {# Make sure the swagger version is a string and not a number #} 26 | {% do new_openapi.update({ "swagger": new_openapi.swagger ~ "" }) %} 27 | {# Patch the basePath in case it has been overwritten #} 28 | {% do new_openapi.update({ "basePath": (threescale_cicd_api_basepath if threescale_cicd_api_basepath|length > 0 else "/") }) %} 29 | {{ new_openapi }} -------------------------------------------------------------------------------- /templates/wanted_mapping_rules.j2: -------------------------------------------------------------------------------- 1 | {% set mapping_rules = {} %} 2 | {% for key, value in threescale_cicd_api_operations.items() %} 3 | {% do mapping_rules.update({ key: { "http_method": value.verb.upper(), "pattern": threescale_cicd_api_basepath ~ value.path ~ "$", "delta": 1 } }) %} 4 | {% endfor %} 5 | {{ mapping_rules }} -------------------------------------------------------------------------------- /templates/wanted_policies.j2: -------------------------------------------------------------------------------- 1 | {% set policies = [] %} 2 | {% if threescale_cicd_apicast_policies_cors %} 3 | {% do policies.append( {"name": "cors", "version": "builtin", "configuration": {"allow_credentials": true}, "enabled": true} ) %} 4 | {% endif %} 5 | {{ policies }} 6 | -------------------------------------------------------------------------------- /tests/3scale-inventory.yaml.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmasse-itix/threescale-cicd/ed21a12738cf35c7d7931e7ed288008e2331f9e2/tests/3scale-inventory.yaml.enc -------------------------------------------------------------------------------- /tests/ansible.cfg: -------------------------------------------------------------------------------- 1 | ../ansible.cfg -------------------------------------------------------------------------------- /tests/environments/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /tests/inventory.j2: -------------------------------------------------------------------------------- 1 | [all:vars] 2 | ansible_connection=local 3 | 4 | [threescale] 5 | {{ test_environment.threescale.admin_portal }} 6 | 7 | [threescale:vars] 8 | threescale_cicd_access_token={{ test_environment.threescale.access_token }} 9 | threescale_cicd_sso_issuer_endpoint=https://{{ test_environment.sso.client_id }}:{{ test_environment.sso.client_secret }}@{{ test_environment.sso.host }}/auth/realms/{{ test_environment.sso.realm }} 10 | {% if 'wildcard_domain' in test_environment.threescale %} 11 | threescale_cicd_wildcard_domain={{ test_environment.threescale.wildcard_domain }} 12 | threescale_cicd_apicast_sandbox_endpoint='{% raw %}{{ 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 }}{% endraw %}' 13 | threescale_cicd_apicast_production_endpoint='{% raw %}{{ 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 }}{% endraw %}' 14 | {% endif %} 15 | {% if 'apicast_staging_domain' in test_environment.threescale %} 16 | threescale_cicd_apicast_sandbox_endpoint='{% raw %}{{ threescale_cicd_default_apicast_scheme }}://{{ ((threescale_cicd_api_environment_name ~ "-" if threescale_cicd_api_environment_name is defined else "") ~ threescale_cicd_api_system_name)|regex_replace("[^a-zA-Z0-9-]+", "-")|lower }}{{ threescale_cicd_default_staging_suffix }}{% endraw %}.{{ test_environment.threescale.apicast_staging_domain }}' 17 | {% endif %} 18 | {% if 'apicast_production_domain' in test_environment.threescale %} 19 | threescale_cicd_apicast_production_endpoint='{% raw %}{{ threescale_cicd_default_apicast_scheme }}://{{ ((threescale_cicd_api_environment_name ~ "-" if threescale_cicd_api_environment_name is defined else "") ~ threescale_cicd_api_system_name)|regex_replace("[^a-zA-Z0-9-]+", "-")|lower }}{{ threescale_cicd_default_production_suffix }}{% endraw %}.{{ test_environment.threescale.apicast_production_domain }}' 20 | {% endif %} 21 | {% if test_environment.threescale.admin_portal is match(".*[.]3scale[.]net") %} 22 | # The 3scale SaaS environment is sometimes slow. Make sure to give enough time 23 | # to the SaaS environment to synchronise the OIDC clients in RH-SSO. 24 | threescale_cicd_delay=15 25 | threescale_cicd_retries=200 26 | {% endif %} 27 | -------------------------------------------------------------------------------- /tests/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /tests/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure all Ansible failed tasks go to the stderr. Failed tasks usually 4 | # output sensitive informations, by routing them to stderr we can filter 5 | # them out. 6 | export ANSIBLE_DISPLAY_FAILED_STDERR=yes 7 | 8 | cd "${0%/*}" || exit 1 9 | 10 | echo "--> Generating the Ansible inventory files..." 11 | ansible-playbook -i /dev/null write-inventory-files.yml &>results/write-inventory-files 12 | ret=$? 13 | if [ "$ret" -gt 0 ]; then 14 | echo "--> Ansible inventory files generation FAILED !" 15 | exit 1 16 | else 17 | echo "--> Ansible inventory files generation SUCCEEDED !" 18 | fi 19 | 20 | # Because of a bug in Ansible, we need to move one directory upper before running 21 | # the playbooks. 22 | # 23 | # The bug makes the playbooks fail after the Application Plans creation/update 24 | # with this error message: 25 | # 26 | # ERROR! Unexpected Exception, this is probably a bug: expected str, bytes or os.PathLike object, not NoneType 27 | # 28 | cd ".." || exit 1 29 | 30 | for environment in tests/environments/3scale-${THREESCALE_ENV:-*}; do 31 | for testcase in tests/test-cases/*.y*ml; do 32 | echo "--> Running $testcase against $environment..." 33 | if [ -z "$THREESCALE_VERBOSE" ] || [ "$THREESCALE_VERBOSE" == "no" ]; then 34 | # reduce output verbosity and make sure not to output sensitive information 35 | logfile="tests/results/$(basename "$environment")-$(basename "$testcase")" 36 | DISPLAY_SKIPPED_HOSTS=no ANSIBLE_DISPLAY_OK_HOSTS=no ansible-playbook -i "$environment" "$testcase" 2>"$logfile" 37 | else 38 | ansible-playbook -i "$environment" -v "$testcase" 39 | fi 40 | ret=$? 41 | if [ "$ret" -gt 0 ]; then 42 | echo "--> $testcase against $environment FAILED !" 43 | exit 1 44 | else 45 | echo "--> $testcase against $environment SUCCEEDED !" 46 | fi 47 | done 48 | done -------------------------------------------------------------------------------- /tests/setup/README.md: -------------------------------------------------------------------------------- 1 | # Test Environment setup 2 | 3 | ## 3scale SaaS with APIcast Self-Managed 4 | 5 | Create a project in an OpenShift cluster: 6 | 7 | ```sh 8 | oc new-project apicast-3scale-ci 9 | ``` 10 | 11 | Deploy two 3scale gateways (staging and production): 12 | 13 | ```sh 14 | oc create secret generic 3scale-tenant- --from-literal=password=https://@-admin.3scale.net 15 | oc create -f https://raw.githubusercontent.com/3scale/apicast/v3.4.0/openshift/apicast-template.yml 16 | oc new-app --template=3scale-gateway --name=apicast--staging -p CONFIGURATION_URL_SECRET=3scale-tenant- -p CONFIGURATION_CACHE=0 -p RESPONSE_CODES=true -p LOG_LEVEL=info -p CONFIGURATION_LOADER=lazy -p APICAST_NAME=apicast--staging -p DEPLOYMENT_ENVIRONMENT=sandbox -p IMAGE_NAME=quay.io/3scale/apicast:v3.4.0 17 | oc new-app --template=3scale-gateway --name=apicast--production -p CONFIGURATION_URL_SECRET=3scale-tenant- -p CONFIGURATION_CACHE=60 -p RESPONSE_CODES=true -p LOG_LEVEL=info -p CONFIGURATION_LOADER=boot -p APICAST_NAME=apicast--production -p DEPLOYMENT_ENVIRONMENT=production -p IMAGE_NAME=quay.io/3scale/apicast:v3.4.0 18 | oc scale dc/apicast--staging --replicas=1 19 | oc scale dc/apicast--production --replicas=1 20 | oc create route edge apicast--staging --service=apicast--staging --hostname=wildcard.-staging.app.itix.fr --insecure-policy=Allow --wildcard-policy=Subdomain 21 | oc create route edge apicast--production --service=apicast--production --hostname=wildcard.-production.app... --insecure-policy=Allow --wildcard-policy=Subdomain 22 | ``` 23 | 24 | Provision the Red Hat SSO tenants with the included playbooks: 25 | 26 | ```sh 27 | ansible-playbook tests/setup/setup-sso.yml -e sso_admin_password=secret -e sso_hostname=sso.app.example.test 28 | ``` 29 | 30 | ## 3scale on-premise 31 | 32 | Create a project in an OpenShift cluster: 33 | 34 | ```sh 35 | oc new-project 3scale-ci-23 --display-name="3scale CI 2.3" 36 | ``` 37 | 38 | Deploy 3scale AMP 2.3: 39 | 40 | ```sh 41 | oc create -f https://raw.githubusercontent.com/3scale/3scale-amp-openshift-templates/2.3.0.GA/amp/amp.yml 42 | oc new-app --template=3scale-api-management -p WILDCARD_DOMAIN=3scale-ci-23.app.example.test -p WILDCARD_POLICY=Subdomain 43 | ``` 44 | 45 | Create two tenants: `pool1` and `pool2` and expose them: 46 | 47 | ```sh 48 | oc expose svc/system-provider --hostname pool1-admin.3scale-ci-23.app.example.test --overrides='{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "spec": { "tls": { "insecureEdgeTerminationPolicy": "Allow", "termination": "edge" } } }' --name=pool1-admin 49 | oc expose svc/system-provider --hostname pool2-admin.3scale-ci-23.app.example.test --overrides='{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "spec": { "tls": { "insecureEdgeTerminationPolicy": "Allow", "termination": "edge" } } }' --name=pool2-admin 50 | ``` 51 | 52 | Provision the Red Hat SSO tenants with the included playbooks: 53 | 54 | ```sh 55 | ansible-playbook tests/setup/setup-sso.yml -e sso_admin_password=secret -e sso_hostname=sso.app.example.test 56 | ``` 57 | 58 | Delete the wildcard route and recreate it with two more routes: 59 | 60 | ```sh 61 | oc delete route apicast-wildcard-router 62 | oc expose svc/apicast-wildcard-router --wildcard-policy=Subdomain --overrides='{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "spec": { "tls": { "insecureEdgeTerminationPolicy": "Allow", "termination": "edge" } } }' --hostname=apicast-wildcard.pool1.3scale-ci-23.app.example.test --name=pool1-apicast-wildcard-router 63 | oc expose svc/apicast-wildcard-router --wildcard-policy=Subdomain --overrides='{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "spec": { "tls": { "insecureEdgeTerminationPolicy": "Allow", "termination": "edge" } } }' --hostname=apicast-wildcard.pool2.3scale-ci-23.app.example.test --name=pool2-apicast-wildcard-router 64 | oc expose svc/apicast-wildcard-router --wildcard-policy=Subdomain --overrides='{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "spec": { "tls": { "insecureEdgeTerminationPolicy": "Allow", "termination": "edge" } } }' --hostname=apicast-wildcard.3scale-ci-23.app.example.test --name=apicast-wildcard-router 65 | ``` 66 | 67 | Do the same with other versions of 3scale. 68 | -------------------------------------------------------------------------------- /tests/setup/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Delete all services of a 3scale tenant 4 | hosts: localhost 5 | gather_facts: no 6 | vars: 7 | ansible_connection: local 8 | threescale_api: https://nmasse-redhat-admin.3scale.net/admin/api 9 | tasks: 10 | - assert: 11 | that: 12 | - threescale_token is defined 13 | msg: > 14 | Please pass your 3scale Access Token in the 'threescale_token' extra var 15 | 16 | - name: Find Services 17 | uri: 18 | url: '{{ threescale_api }}/services.json?access_token={{ threescale_token }}' 19 | register: find_services_response 20 | changed_when: false 21 | 22 | - name: Delete services (except the default 'api' service) 23 | uri: 24 | url: '{{ threescale_api }}/services/{{ item }}.json?access_token={{ threescale_token }}' 25 | method: DELETE 26 | status_code: "200,404" 27 | register: delete_service_response 28 | changed_when: delete_service_response.status == 200 29 | with_items: '{{ services }}' 30 | vars: 31 | services: '{{ find_services_response.json|json_query(query) }}' 32 | query: services[?service.system_name != `api`].service.id 33 | 34 | - name: Find ActiveDocs 35 | uri: 36 | url: '{{ threescale_api }}/active_docs.json?access_token={{ threescale_token }}' 37 | register: find_active_docs_response 38 | changed_when: false 39 | 40 | - name: Delete ActiveDocs (except the default 'api' ActiveDoc) 41 | uri: 42 | url: '{{ threescale_api }}/active_docs/{{ item }}.json?access_token={{ threescale_token }}' 43 | method: DELETE 44 | status_code: "200,404" 45 | register: delete_active_docs_response 46 | changed_when: delete_active_docs_response.status == 200 47 | with_items: '{{ active_docs }}' 48 | vars: 49 | active_docs: '{{ find_active_docs_response.json|json_query(query) }}' 50 | query: api_docs[?api_doc.system_name != `api`].api_doc.id 51 | -------------------------------------------------------------------------------- /tests/setup/common/create-sso-client.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Create the SSO client 4 | keycloak_client: 5 | auth_keycloak_url: 'https://{{ sso_hostname }}/auth' 6 | auth_password: '{{ sso_admin_password }}' 7 | auth_realm: '{{ sso_admin_realm }}' 8 | auth_username: '{{ sso_admin_username }}' 9 | name: '{{ item.client_id }}' 10 | description: 'Zync account for 3scale instance {{ item.admin_portal }}' 11 | realm: '{{ item.realm }}' 12 | enabled: true 13 | state: present 14 | protocol: openid-connect 15 | client_id: '{{ item.client_id }}' 16 | secret: '{{ item.client_secret }}' 17 | direct_access_grants_enabled: false 18 | full_scope_allowed: true 19 | implicit_flow_enabled: false 20 | public_client: false 21 | service_accounts_enabled: true 22 | standard_flow_enabled: false 23 | validate_certs: no 24 | register: create_client_response 25 | 26 | - name: Get the service account user tied to the client 27 | uri: 28 | url: 'https://{{ sso_hostname }}/auth/admin/realms/{{ item.realm }}/clients/{{ client_uuid }}/service-account-user' 29 | validate_certs: no 30 | headers: 31 | Authorization: 'Bearer {{ access_token }}' 32 | register: service_account_response 33 | changed_when: false 34 | vars: 35 | client_uuid: '{{ create_client_response.end_state.id }}' 36 | 37 | - name: Get the "realm-management" client details 38 | keycloak_client: 39 | auth_keycloak_url: 'https://{{ sso_hostname }}/auth' 40 | auth_password: '{{ sso_admin_password }}' 41 | auth_realm: '{{ sso_admin_realm }}' 42 | auth_username: '{{ sso_admin_username }}' 43 | realm: '{{ item.realm }}' 44 | state: present 45 | client_id: realm-management 46 | validate_certs: no 47 | check_mode: yes 48 | register: realm_management_client_response 49 | 50 | - name: Get the role description of the "realm-management" client 51 | uri: 52 | url: 'https://{{ sso_hostname }}/auth/admin/realms/{{ item.realm }}/clients/{{ realm_management_client_uuid }}/roles/manage-clients' 53 | validate_certs: no 54 | headers: 55 | Authorization: 'Bearer {{ access_token }}' 56 | register: get_role_response 57 | changed_when: false 58 | vars: 59 | realm_management_client_uuid: '{{ realm_management_client_response.existing.id }}' 60 | 61 | - name: Add the 'manage-clients' role mapping to the service account user 62 | uri: 63 | url: 'https://{{ sso_hostname }}/auth/admin/realms/{{ item.realm }}/users/{{ service_account_uuid }}/role-mappings/clients/{{ realm_management_client_uuid }}' 64 | body: 65 | - '{{ manage_clients_role }}' 66 | body_format: json 67 | method: POST 68 | validate_certs: no 69 | headers: 70 | Authorization: 'Bearer {{ access_token }}' 71 | status_code: "204" 72 | register: set_role_mapping_response 73 | changed_when: set_role_mapping_response.status == 204 74 | vars: 75 | service_account_uuid: '{{ service_account_response.json.id }}' 76 | realm_management_client_uuid: '{{ realm_management_client_response.existing.id }}' 77 | manage_clients_role: '{{ get_role_response.json }}' 78 | -------------------------------------------------------------------------------- /tests/setup/delete-travis-logs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Delete the Travis logs of a build 4 | hosts: localhost 5 | gather_facts: no 6 | vars: 7 | ansible_connection: local 8 | travis_repo: nmasse-itix/threescale-cicd 9 | travis_api: https://api.travis-ci.org 10 | tasks: 11 | - assert: 12 | that: 13 | - travis_token is defined 14 | msg: > 15 | Please pass your Travis Token in the 'travis_token' extra var 16 | 17 | - assert: 18 | that: 19 | - travis_build is defined 20 | msg: > 21 | Please pass Travis build number in the 'travis_build' extra var 22 | 23 | - name: Find Build 24 | uri: 25 | url: '{{ travis_api }}/repos/{{ travis_repo }}/builds?number={{ travis_build }}' 26 | headers: 27 | Authorization: "token {{ travis_token }}" 28 | register: find_build_response 29 | changed_when: false 30 | 31 | - name: Get Build 32 | uri: 33 | url: '{{ travis_api }}/repos/{{ travis_repo }}/builds/{{ travis_build_id }}' 34 | headers: 35 | Authorization: "token {{ travis_token }}" 36 | register: get_build_response 37 | changed_when: false 38 | vars: 39 | travis_build_id: '{{ find_build_response.json|json_query(''[0].id'') }}' 40 | 41 | - name: Delete logs 42 | uri: 43 | url: '{{ travis_api }}/jobs/{{ item }}/log' 44 | headers: 45 | Authorization: "token {{ travis_token }}" 46 | body_format: form-urlencoded 47 | body: 48 | reason: "Logs removed because it contains sensitive data" 49 | method: PATCH 50 | status_code: "200,409" 51 | register: delete_logs_response 52 | changed_when: delete_logs_response.status == 200 53 | with_items: '{{ travis_jobs }}' 54 | vars: 55 | travis_jobs: '{{ get_build_response.json|json_query(''@.matrix[].id'') }}' 56 | -------------------------------------------------------------------------------- /tests/setup/setup-sso.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Configure Red Hat SSO according to the 3scale inventory file 4 | hosts: localhost 5 | gather_facts: no 6 | vars: 7 | ansible_connection: local 8 | sso_admin_username: admin 9 | sso_admin_realm: master 10 | sso_admin_client_id: admin-cli 11 | tasks: 12 | - assert: 13 | that: 14 | - sso_admin_password is defined 15 | - sso_hostname is defined 16 | msg: > 17 | Please pass the SSO admin credentials as extra vars 18 | 19 | - set_fact: 20 | threescale_inventory: '{{ lookup(''env'', ''THREESCALE_INVENTORY'')|b64decode|from_json }}' 21 | when: 'threescale_inventory is not defined and lookup(''env'', ''THREESCALE_INVENTORY'')|length > 0' 22 | 23 | - set_fact: 24 | threescale_inventory: '{{ lookup(''file'', ''{{ playbook_dir }}/../3scale-inventory.yaml'')|from_yaml }}' 25 | when: 'threescale_inventory is not defined' 26 | 27 | - name: Authenticate to RH-SSO 28 | uri: 29 | url: 'https://{{ sso_hostname }}/auth/realms/{{ sso_admin_realm }}/protocol/openid-connect/token' 30 | body: 'grant_type=password&client_id={{ sso_admin_client_id|urlencode }}&username={{ sso_admin_username|urlencode }}&password={{ sso_admin_password|urlencode }}' 31 | method: POST 32 | validate_certs: no 33 | register: auth_response 34 | changed_when: false 35 | 36 | - name: Delete the RH-SSO realm 37 | uri: 38 | url: 'https://{{ sso_hostname }}/auth/admin/realms/{{ item }}' 39 | method: DELETE 40 | validate_certs: no 41 | headers: 42 | Authorization: 'Bearer {{ access_token }}' 43 | status_code: "204,404" 44 | register: delete_realm_response 45 | changed_when: delete_realm_response.status == 204 46 | with_items: '{{ realms }}' 47 | vars: 48 | realms: '{{ threescale_inventory|json_query(''@.*[].sso.realm'')|unique }}' 49 | access_token: '{{ auth_response.json.access_token }}' 50 | 51 | - name: Create the RH-SSO realm 52 | uri: 53 | url: 'https://{{ sso_hostname }}/auth/admin/realms' 54 | body: 55 | id: '{{ item }}' 56 | enabled: true 57 | realm: '{{ item }}' 58 | displayName: '{{ item }}' 59 | notBefore: 0 60 | revokeRefreshToken: false 61 | refreshTokenMaxReuse: 0 62 | registrationAllowed: false 63 | registrationEmailAsUsername: false 64 | rememberMe: false 65 | verifyEmail: false 66 | loginWithEmailAllowed: false 67 | duplicateEmailsAllowed: false 68 | resetPasswordAllowed: false 69 | bruteForceProtected: false 70 | permanentLockout: false 71 | roles: 72 | realm: [] 73 | defaultRoles: [] 74 | requiredCredentials: 75 | - password 76 | scopeMappings: [] 77 | editUsernameAllowed: false 78 | accessTokenLifespanForImplicitFlow: 86400 # 1 day 79 | accessTokenLifespan: 86400 # 1 day 80 | accessCodeLifespanUserAction: 86400 # 1 day 81 | accessCodeLifespanLogin: 86400 # 1 day 82 | accessCodeLifespan: 86400 # 1 day 83 | ssoSessionIdleTimeout: 86400 # 1 day 84 | ssoSessionMaxLifespan: 86400 # 1 day 85 | offlineSessionIdleTimeout: 2592000 # 30 days 86 | actionTokenGeneratedByAdminLifespan: 86400 # 1 day 87 | actionTokenGeneratedByUserLifespan: 86400 # 1 day 88 | sslRequired: none 89 | body_format: json 90 | method: POST 91 | validate_certs: no 92 | headers: 93 | Authorization: 'Bearer {{ access_token }}' 94 | status_code: "201,409" 95 | register: create_realm_response 96 | changed_when: create_realm_response.status == 201 97 | with_items: '{{ realms }}' 98 | vars: 99 | realms: '{{ threescale_inventory|json_query(''@.*[].sso.realm'')|unique }}' 100 | access_token: '{{ auth_response.json.access_token }}' 101 | 102 | - include_tasks: "common/create-sso-client.yml" 103 | with_items: '{{ clients }}' 104 | vars: 105 | clients: '{{ threescale_inventory|json_query(''@.*[].{client_id: sso.client_id, client_secret: sso.client_secret, realm: sso.realm, admin_portal: threescale.admin_portal }'')|unique }}' 106 | access_token: '{{ auth_response.json.access_token }}' 107 | -------------------------------------------------------------------------------- /tests/test-cases/01-beer-catalog-apikey.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Beer Catalog API with API Key 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/beer-catalog-api.json' 8 | threescale_cicd_openapi_file_format: 'JSON' 9 | threescale_cicd_api_backend_hostname: echo-api.3scale.net 10 | threescale_cicd_openapi_smoketest_operation: GET_beer 11 | tasks: 12 | - name: Generate a random system_name for this test run 13 | import_tasks: "common/random-system-name.yml" 14 | 15 | # Test a first deployment 16 | - import_role: 17 | name: 'nmasse-itix.threescale-cicd' 18 | # Verify idempotence 19 | - import_role: 20 | name: 'nmasse-itix.threescale-cicd' 21 | # Delete the service 22 | - import_role: 23 | name: 'nmasse-itix.threescale-cicd' 24 | tasks_from: 'cleanup' 25 | -------------------------------------------------------------------------------- /tests/test-cases/02-echo-api-oidc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Echo API with OpenID Connect 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/echo-api-oidc.yaml' 8 | threescale_cicd_oicd_flows: "{{ [ 'application', threescale_cicd_api_security_scheme.flow ] }}" 9 | tasks: 10 | - name: Generate a random system_name for this test run 11 | import_tasks: "common/random-system-name.yml" 12 | 13 | # Test a first deployment 14 | - import_role: 15 | name: 'nmasse-itix.threescale-cicd' 16 | # Verify idempotence 17 | - import_role: 18 | name: 'nmasse-itix.threescale-cicd' 19 | # Delete the service 20 | - import_role: 21 | name: 'nmasse-itix.threescale-cicd' 22 | tasks_from: 'cleanup' 23 | -------------------------------------------------------------------------------- /tests/test-cases/03-multi-environment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Beer Catalog API in multi environment 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/beer-catalog-api.json' 8 | threescale_cicd_openapi_file_format: 'JSON' 9 | threescale_cicd_api_backend_hostname: echo-api.3scale.net 10 | threescale_cicd_openapi_smoketest_operation: GET_beer 11 | tasks: 12 | - name: Generate a random system_name for this test run 13 | import_tasks: "common/random-system-name.yml" 14 | 15 | # Deploy in DEV 16 | - import_role: 17 | name: 'nmasse-itix.threescale-cicd' 18 | vars: 19 | threescale_cicd_api_environment_name: dev 20 | # Deploy in TEST 21 | - import_role: 22 | name: 'nmasse-itix.threescale-cicd' 23 | vars: 24 | threescale_cicd_api_environment_name: test 25 | # Deploy in PROD 26 | - import_role: 27 | name: 'nmasse-itix.threescale-cicd' 28 | vars: 29 | threescale_cicd_api_environment_name: prod 30 | # Cleanup the DEV 31 | - import_role: 32 | name: 'nmasse-itix.threescale-cicd' 33 | tasks_from: 'cleanup' 34 | vars: 35 | threescale_cicd_api_environment_name: dev 36 | # Cleanup the TEST 37 | - import_role: 38 | name: 'nmasse-itix.threescale-cicd' 39 | tasks_from: 'cleanup' 40 | vars: 41 | threescale_cicd_api_environment_name: test 42 | # Cleanup the PROD 43 | - import_role: 44 | name: 'nmasse-itix.threescale-cicd' 45 | tasks_from: 'cleanup' 46 | vars: 47 | threescale_cicd_api_environment_name: prod 48 | -------------------------------------------------------------------------------- /tests/test-cases/04-one-gateway.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Beer Catalog API with only one gateway 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/beer-catalog-api.json' 8 | threescale_cicd_openapi_file_format: 'JSON' 9 | threescale_cicd_api_backend_hostname: echo-api.3scale.net 10 | threescale_cicd_openapi_smoketest_operation: GET_beer 11 | # Both Public Base URL are the same 12 | threescale_cicd_apicast_sandbox_endpoint: '{{ threescale_cicd_apicast_production_endpoint }}' 13 | tasks: 14 | - name: Generate a random system_name for this test run 15 | import_tasks: "common/random-system-name.yml" 16 | 17 | # Deploy the service 18 | - import_role: 19 | name: 'nmasse-itix.threescale-cicd' 20 | # Delete the service 21 | - import_role: 22 | name: 'nmasse-itix.threescale-cicd' 23 | tasks_from: 'cleanup' 24 | -------------------------------------------------------------------------------- /tests/test-cases/05-echo-api-with-basePath.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Echo API with a basepath 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/echo-api-with-basePath.yaml' 8 | tasks: 9 | - name: Generate a random system_name for this test run 10 | import_tasks: "common/random-system-name.yml" 11 | 12 | # Test a first deployment 13 | - import_role: 14 | name: 'nmasse-itix.threescale-cicd' 15 | # Verify idempotence 16 | - import_role: 17 | name: 'nmasse-itix.threescale-cicd' 18 | # Delete the service 19 | - import_role: 20 | name: 'nmasse-itix.threescale-cicd' 21 | tasks_from: 'cleanup' 22 | -------------------------------------------------------------------------------- /tests/test-cases/06-echo-api-with-cors-policy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Echo API with CORS 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_smoketest_operation: Echo 8 | threescale_cicd_apicast_policies_cors: yes 9 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/echo-api-bare.yaml' 10 | tasks: 11 | - name: Generate a random system_name for this test run 12 | import_tasks: "common/random-system-name.yml" 13 | 14 | # Deploy the service 15 | - import_role: 16 | name: 'nmasse-itix.threescale-cicd' 17 | # Delete the service 18 | - import_role: 19 | name: 'nmasse-itix.threescale-cicd' 20 | tasks_from: 'cleanup' 21 | -------------------------------------------------------------------------------- /tests/test-cases/07-echo-api-with-smoketest-in-extra-vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Echo API with smoketests in extra vars 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | threescale_cicd_openapi_smoketest_operation: Echo 8 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/echo-api-bare.yaml' 9 | tasks: 10 | - name: Generate a random system_name for this test run 11 | import_tasks: "common/random-system-name.yml" 12 | 13 | # Deploy the service 14 | - import_role: 15 | name: 'nmasse-itix.threescale-cicd' 16 | # Delete the service 17 | - import_role: 18 | name: 'nmasse-itix.threescale-cicd' 19 | tasks_from: 'cleanup' 20 | -------------------------------------------------------------------------------- /tests/test-cases/08-echo-api-without-smoketest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy the Echo API with no smoketests 4 | hosts: threescale 5 | gather_facts: no 6 | vars: 7 | # There is no "threescale_cicd_openapi_smoketest_operation" variable 8 | threescale_cicd_openapi_file: '{{ playbook_dir }}/api-contracts/echo-api-bare.yaml' 9 | tasks: 10 | - name: Generate a random system_name for this test run 11 | import_tasks: "common/random-system-name.yml" 12 | 13 | # Deploy the service 14 | - import_role: 15 | name: 'nmasse-itix.threescale-cicd' 16 | # Delete the service 17 | - import_role: 18 | name: 'nmasse-itix.threescale-cicd' 19 | tasks_from: 'cleanup' 20 | -------------------------------------------------------------------------------- /tests/test-cases/api-contracts/beer-catalog-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Beer Catalog API", 5 | "description": "An API for querying beer catalog of Acme Inc.", 6 | "contact": { 7 | "name": "Laurent Broudoux", 8 | "url": "http://github.com/lbroudoux", 9 | "email": "laurent.broudoux@gmail.com" 10 | }, 11 | "license": { 12 | "name": "MIT License", 13 | "url": "https://opensource.org/licenses/MIT" 14 | }, 15 | "version": "1.0" 16 | }, 17 | "basePath": "/api", 18 | "paths": { 19 | "/beer/{name}": { 20 | "get": { 21 | "tags": [ 22 | "beer" 23 | ], 24 | "summary": "Get beer having name", 25 | "description": "Get beer having name", 26 | "responses": { 27 | "200": { 28 | "description": "Beer having requested name", 29 | "schema": { 30 | "$ref": "#/definitions/Beer" 31 | } 32 | } 33 | } 34 | }, 35 | "parameters": [ 36 | { 37 | "name": "name", 38 | "in": "path", 39 | "description": "Name of beer to retrieve", 40 | "required": true, 41 | "type": "string" 42 | } 43 | ] 44 | }, 45 | "/beer/findByStatus/{status}": { 46 | "get": { 47 | "tags": [ 48 | "beer" 49 | ], 50 | "summary": "Get beers having status", 51 | "description": "Get beers having status", 52 | "responses": { 53 | "200": { 54 | "description": "List of beers having requested status", 55 | "schema": { 56 | "type": "array", 57 | "items": { 58 | "$ref": "#/definitions/Beer" 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "parameters": [ 65 | { 66 | "name": "status", 67 | "in": "path", 68 | "description": "Status of beers to retrieve", 69 | "required": true, 70 | "type": "string" 71 | }, 72 | { 73 | "name": "page", 74 | "in": "query", 75 | "description": "Number of page to retrieve", 76 | "type": "number" 77 | } 78 | ] 79 | }, 80 | "/beer": { 81 | "get": { 82 | "tags": [ 83 | "beer" 84 | ], 85 | "summary": "List beers within catalog", 86 | "description": "List beers within catalog", 87 | "responses": { 88 | "200": { 89 | "description": "Array of beers", 90 | "schema": { 91 | "type": "array", 92 | "items": { 93 | "$ref": "#/definitions/Beer" 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | "parameters": [ 100 | { 101 | "name": "page", 102 | "in": "query", 103 | "description": "Number of page to retrieve", 104 | "type": "number" 105 | } 106 | ] 107 | } 108 | }, 109 | "tags": [ 110 | { 111 | "name": "beer", 112 | "description": "Beer resource" 113 | } 114 | ], 115 | "definitions": { 116 | "Beer": { 117 | "properties": { 118 | "name": { 119 | "description": "Name of Beer", 120 | "type": "string" 121 | }, 122 | "country": { 123 | "description": "Origin country of Beer", 124 | "type": "string" 125 | }, 126 | "type": { 127 | "description": "Type of Beer", 128 | "type": "string" 129 | }, 130 | "rating": { 131 | "description": "Rating from customers", 132 | "type": "number" 133 | }, 134 | "status": { 135 | "description": "Stock status", 136 | "type": "string" 137 | } 138 | } 139 | } 140 | }, 141 | "securityDefinitions": { 142 | "apikey": { 143 | "name": "api-key", 144 | "in":"header", 145 | "type":"apiKey" 146 | } 147 | }, 148 | "security": [ 149 | { 150 | "apikey": [] 151 | } 152 | ] 153 | } 154 | -------------------------------------------------------------------------------- /tests/test-cases/api-contracts/echo-api-bare.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: 'Echo API' 4 | description: 'A very simple API.' 5 | contact: 6 | name: 'Nicolas MASSE' 7 | url: 'http://github.com/nmasse-itix' 8 | email: nmasse@redhat.com 9 | license: 10 | name: 'MIT License' 11 | url: 'https://opensource.org/licenses/MIT' 12 | version: '1.0' 13 | host: 'echo-api.3scale.net' 14 | schemes: 15 | - http 16 | paths: 17 | /: 18 | get: 19 | operationId: Echo 20 | tags: 21 | - echo 22 | summary: 'Get an echo' 23 | description: 'Get an echo from the server' 24 | responses: 25 | 200: 26 | description: 'An Echo from the server' 27 | security: 28 | - apikey: [] 29 | securityDefinitions: 30 | apikey: 31 | name: api-key 32 | in: header 33 | type: apiKey 34 | -------------------------------------------------------------------------------- /tests/test-cases/api-contracts/echo-api-oidc.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | x-threescale-system-name: 'echo-api-oidc' 4 | title: 'Echo API' 5 | description: 'A very simple API.' 6 | contact: 7 | name: 'Nicolas MASSE' 8 | url: 'http://github.com/nmasse-itix' 9 | email: nmasse@redhat.com 10 | license: 11 | name: 'MIT License' 12 | url: 'https://opensource.org/licenses/MIT' 13 | version: '1.0' 14 | host: 'echo-api.3scale.net' 15 | schemes: 16 | - http 17 | paths: 18 | /: 19 | get: 20 | operationId: Echo 21 | tags: 22 | - echo 23 | summary: 'Get an echo' 24 | description: 'Get an echo from the server' 25 | x-threescale-smoketests-operation: true 26 | responses: 27 | 200: 28 | description: 'An Echo from the server' 29 | security: 30 | - oidc: 31 | - openid 32 | securityDefinitions: 33 | oidc: 34 | type: oauth2 35 | flow: accessCode 36 | scopes: 37 | openid: Get an OpenID Connect token 38 | authorizationUrl: http://dummy/placeholder 39 | tokenUrl: http://dummy/placeholder 40 | -------------------------------------------------------------------------------- /tests/test-cases/api-contracts/echo-api-with-basePath.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | x-threescale-system-name: 'echo-api-with-basepath' 4 | title: 'Echo API' 5 | description: 'A very simple API.' 6 | contact: 7 | name: 'Nicolas MASSE' 8 | url: 'http://github.com/nmasse-itix' 9 | email: nmasse@redhat.com 10 | license: 11 | name: 'MIT License' 12 | url: 'https://opensource.org/licenses/MIT' 13 | version: '1.0' 14 | host: 'echo-api.3scale.net' 15 | basePath: '/test-base' 16 | schemes: 17 | - http 18 | paths: 19 | /echo: 20 | get: 21 | operationId: Echo 22 | tags: 23 | - echo 24 | summary: 'Get an echo' 25 | description: 'Get an echo from the server' 26 | x-threescale-smoketests-operation: true 27 | responses: 28 | 200: 29 | description: 'An Echo from the server' 30 | security: 31 | - apikey: [] 32 | securityDefinitions: 33 | apikey: 34 | name: api-key 35 | in: header 36 | type: apiKey 37 | -------------------------------------------------------------------------------- /tests/test-cases/api-contracts/echo-api.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | x-threescale-system-name: 'echo-api' 4 | title: 'Echo API' 5 | description: 'A very simple API.' 6 | contact: 7 | name: 'Nicolas MASSE' 8 | url: 'http://github.com/nmasse-itix' 9 | email: nmasse@redhat.com 10 | license: 11 | name: 'MIT License' 12 | url: 'https://opensource.org/licenses/MIT' 13 | version: '1.0' 14 | host: 'echo-api.3scale.net' 15 | schemes: 16 | - http 17 | paths: 18 | /: 19 | get: 20 | operationId: Echo 21 | tags: 22 | - echo 23 | summary: 'Get an echo' 24 | description: 'Get an echo from the server' 25 | x-threescale-smoketests-operation: true 26 | responses: 27 | 200: 28 | description: 'An Echo from the server' 29 | security: 30 | - apikey: [] 31 | securityDefinitions: 32 | apikey: 33 | name: api-key 34 | in: header 35 | type: apiKey 36 | -------------------------------------------------------------------------------- /tests/test-cases/common/random-system-name.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Generate a random system_name for this test run 4 | set_fact: 5 | threescale_cicd_api_base_system_name: 'testcase_{{ lookup(''password'', ''/dev/null length=12 chars=hexdigits'')|lower }}' -------------------------------------------------------------------------------- /tests/test-cases/roles/nmasse-itix.threescale-cicd: -------------------------------------------------------------------------------- 1 | ../../.. -------------------------------------------------------------------------------- /tests/write-inventory-files.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Write the inventory files for the included test cases 4 | hosts: localhost 5 | gather_facts: no 6 | vars: 7 | ansible_connection: local 8 | tasks: 9 | - set_fact: 10 | threescale_inventory: '{{ lookup(''env'', ''THREESCALE_INVENTORY'')|b64decode|from_json }}' 11 | when: 'threescale_inventory is not defined and lookup(''env'', ''THREESCALE_INVENTORY'')|length > 0' 12 | 13 | - set_fact: 14 | threescale_inventory: '{{ lookup(''file'', ''{{ playbook_dir }}/3scale-inventory.yaml'')|from_yaml }}' 15 | when: 'threescale_inventory is not defined' 16 | 17 | - name: Process the Jinja2 templates 18 | template: 19 | src: '{{ playbook_dir }}/inventory.j2' 20 | dest: '{{ playbook_dir }}/environments/{{ item.key }}' 21 | with_dict: '{{ threescale_inventory }}' 22 | vars: 23 | test_environment: '{{ item.value }}' 24 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # A list of unused metrics to delete 4 | threescale_cicd_metrics_to_delete: '{{ lookup(''template'', ''metrics_to_delete.j2'') }}' 5 | 6 | ## 7 | ## OpenAPI Specification File parsing 8 | ## 9 | threescale_cicd_api_operations: '{{ lookup(''template'', ''openapi/openapi_operations.j2'') }}' 10 | threescale_cicd_api_backend_version: '{{ threescale_cicd_backend_version_mapping[threescale_cicd_api_security_scheme.type] }}' 11 | threescale_cicd_backend_version_mapping: 12 | apiKey: '1' 13 | oauth2: 'oidc' 14 | threescale_cicd_openapi_smoketest_path: '{{ threescale_cicd_api_basepath }}{{ threescale_cicd_api_operations[threescale_cicd_openapi_smoketest_operation].path }}' 15 | 16 | ## 17 | ## ID Lookup Variables 18 | ## 19 | 20 | # The id of the current service is fetched from the threescale_cicd_existing_services_details fact 21 | threescale_cicd_api_service_id: '{{ (threescale_cicd_existing_services_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first)[''id''] }}' 22 | 23 | # The id of the 'hits' metric is fetched from the threescale_cicd_existing_metrics_details fact 24 | threescale_cicd_metric_id: '{{ (threescale_cicd_existing_metrics_details|selectattr(''system_name'', ''equalto'', ''hits'')|first).id }}' 25 | 26 | # Find the default application plan id from its system name 27 | threescale_cicd_default_application_plan_id: '{{ (threescale_cicd_existing_application_plans_details|selectattr("system_name", "equalto", threescale_cicd_default_application_plan)|first).id }}' 28 | 29 | # Find the id of the existing activedocs from the threescale_cicd_existing_activedocs_details fact 30 | threescale_cicd_api_activedocs_id: '{{ (threescale_cicd_existing_activedocs_details|selectattr(''system_name'', ''equalto'', threescale_cicd_api_system_name)|first).id }}' 31 | 32 | ## 33 | ## Mapping Rules computation 34 | ## 35 | # create the items that we want but don't have yet 36 | threescale_cicd_mapping_rules_to_create: '{{ threescale_cicd_wanted_mapping_rules.keys()|difference(threescale_cicd_existing_mapping_rules.keys()) }}' 37 | # delete the items that we don't want but we have 38 | threescale_cicd_mapping_rules_to_delete: '{{ threescale_cicd_existing_mapping_rules.keys()|difference(threescale_cicd_wanted_mapping_rules.keys()) }}' 39 | # update the items that we want and we have 40 | threescale_cicd_mapping_rules_to_update: '{{ threescale_cicd_existing_mapping_rules.keys()|intersect(threescale_cicd_wanted_mapping_rules.keys()) }}' 41 | --------------------------------------------------------------------------------