Skip to content

Commit f883bee

Browse files
author
ikethecoder
authored
Redis and rate-limiting plugin added (#6)
1 parent 489acb8 commit f883bee

File tree

9 files changed

+170
-45
lines changed

9 files changed

+170
-45
lines changed

.github/workflows/gh-pages.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Build and Deploy
2+
on:
3+
push:
4+
branches: [ dev ]
5+
jobs:
6+
build-and-deploy:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout 🛎️
10+
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
11+
with:
12+
persist-credentials: false
13+
14+
- name: Deploy
15+
uses: peaceiris/actions-gh-pages@v3
16+
with:
17+
github_token: ${{ secrets.GITHUB_TOKEN }}
18+
publish_dir: ./docs/static

README.md

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
# GWA APIs
22

3-
For self-service of the APIs, a set of microservices are used to coordinate updates by the API Owners.
3+
For self-service of APIs, a set of microservices are used to coordinate updates by the providers of APIs.
44

5-
* Gateway API : Provides a way for API Owners to update their Kong configuration (and internally the OCP Edge Router)
6-
* Authz API : Provides a way for API Owners to update Keycloak for access to the API Services Portal
7-
* Catalog API : Providers a way for API Owners to update the API details in the BC Data Catalog
5+
* `Gateway` : Provides a way for API Owners to update their Kong configuration (and internally the OCP Edge Router)
6+
* `Authz` : Provides a way for API Owners to update Keycloak for access to functionality on the API Services Portal
7+
* `Catalog` : Provides a way for API Owners to update the API details in the BC Data Catalog
88

99
All APIs are protected by an OIDC JWT Token with the following claims:
1010

11-
* `aud` : https://gwa-qwzrwc-dev.pathfinder.gov.bc.ca/
12-
* `namespace` : Identifies the namespace that the APIs belong to, used to scope what changes are synced with Kong
13-
* `scope` : `manage:config`
11+
* `aud` : `gwa`
12+
* `namespace` : Identifies the namespace that the APIs belong to, used to scope what changes are allowed.
1413

1514
**Configuration:**
1615

@@ -33,30 +32,9 @@ All APIs are protected by an OIDC JWT Token with the following claims:
3332
| `KC_PASSWORD` | Keycloak access for administrative rights to manage groups for namespaces | `xxx`
3433
| `HOST_TRANSFORM_ENABLED` | For Dev and Test a way to transform the host for working in these environments | `false`
3534
| `HOST_TRANSFORM_BASE_URL` | For Dev and Test a way to transform the host for working in these environments |
35+
| `PLUGINS_RATELIMITING_REDIS_PASSWORD` | The Redis credential added to the rate-limiting Kong plugin during publish |
3636

37-
38-
## Gateway API
39-
40-
The `Gateway API` has a `dry-run` and `sync` of Kong and OCP configuration.
41-
42-
The token must have a valid scope for managing the config.
43-
44-
45-
# Access
46-
47-
```
48-
scope := permission/resource.access
49-
50-
permission, resource, access := any string (without space, period, slash, or asterisk) | asterisk
51-
```
52-
53-
permission type is based on single or bulk records.
54-
55-
resource types: GatewayConfig, Catalog
56-
57-
access: read, write
58-
59-
# Flow
37+
# API Provider Flow
6038

6139
## 1. Register a new namespace
6240

@@ -119,9 +97,23 @@ services:
11997

12098
Run: `gwa new` and follow the prompts.
12199

100+
Example:
101+
102+
```
103+
gwa new -o sample.yaml https://bcgov.github.io/gwa-api/openapi/simple.yaml
104+
```
105+
106+
> The current beta version of `gwa new` results in Kong configuration that needs to be edited before it is ready to be applied.
107+
108+
> Make the following edits:
109+
> * Add a `hosts` list under each `route` with the external URL of your service on the gateway (i.e./ a value that is: `$NAME.api.gov.bc.ca`)
110+
> * The `service` `url` might need to be edited to equal your upstream URL
111+
> * Optionally: Add a qualifier to the namespace tags if you are separating your configuration into different pipelines
112+
113+
122114
## 4. Apply gateway configuration
123115

124-
The Swagger console for the `gwa-api` can be used to publish Kong Gateway configuration, or the `gwa-cli` can be used.
116+
The Swagger console for the `gwa-api` can be used to publish Kong Gateway configuration, or the `gwa Command Line` can be used.
125117

126118
### Swagger Console
127119

@@ -139,7 +131,7 @@ Select a `configFile` file.
139131

140132
Send the request.
141133

142-
### Command Line
134+
### gwa Command Line
143135

144136
**Install**
145137

@@ -175,6 +167,12 @@ gwa init -T --namespace=$NS --client-id=<YOUR SERVICE ACCOUNT ID> --client-secre
175167
gwa pg sample.yaml
176168
```
177169

170+
If you want to see the expected changes but not actually apply them, you can run:
171+
172+
```
173+
gwa pg --dry-run sample.yaml
174+
```
175+
178176
## 5. Verify routes
179177

180178
In our test environment, the hosts that you defined in the routes get altered; to see the actual hosts, log into the <a href="https://gwa-qwzrwc-test.pathfinder.gov.bc.ca/int" target="_blank">API Services Portal</a> and view the hosts under `Services`.
@@ -188,24 +186,34 @@ ab -n 20 -c 2 https://${NAME}-api-gov-bc-ca.test.189768.xyz/headers
188186

189187
## 6. View metrics
190188

191-
Go to <a href="https://grafana-qwzrwc-test.pathfinder.gov.bc.ca/" target="_blank">Grafana</a> to view metrics for your configured services.
189+
The following metrics can be viewed in real-time for the Services that you configure on the Gateway:
190+
191+
* Request Rate : Requests / Second (by Service/Route, by HTTP Status)
192+
* Latency : Standard deviations measured for latency inside Kong and on the Upstream Service (by Service/Route)
193+
* Bandwidth : Ingress/egress bandwidth (by Service/Route)
194+
* Total Requests : In 5 minute windows (by Consumer, by User Agent, by Service, by HTTP Status)
192195

196+
All metrics can be viewed by an arbitrary time window - defaults to `Last 24 Hours`.
197+
198+
Go to <a href="https://grafana-qwzrwc-test.pathfinder.gov.bc.ca/" target="_blank">Grafana</a> to view metrics for your configured services.
193199

194200
## 7. Grant access to others
195201

196-
The `acl` command is an all-inclusive membership list, so the `--users` should have the full list of members. Any user that is a member but not in the `--users` list will be removed from the namespace.
202+
The `acl` command provides a way to update the access for the namespace. It expects an all-inclusive membership list, so the `--users` should have the full list of members. Any user that is a member but not in the `--users` list will be removed from the namespace.
197203

198-
For administrative privileges (such as managing Service Accounts), add the usernames to the `--managers` argument.
204+
For elevated privileges (such as managing Service Accounts), add the usernames to the `--managers` argument.
199205

200206
```
201-
gwa acl --managers acope@idir --users acope@idir jjones@idir
207+
gwa acl --users acope@idir jjones@idir --managers acope@idir
202208
```
203209

210+
The result will show the ACL changes. The Add/Delete counts represent the membership changes of registered users. The Missing count represents the users that will automatically be added to the namespace once they have logged into the `APS Services Portal`.
211+
204212
## 8. Add to your CI/CD Pipeline
205213

206214
Update your CI/CD pipelines to run the `gwa-cli` to keep your services updated on the gateway.
207215

208-
### Github Actions
216+
### Github Actions Example
209217

210218
In the repository that you maintain your CI/CD Pipeline configuration, use the Service Account details from `Step 2` to set up two `Secrets`:
211219

docs/static/openapi/simple.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Sample API
4+
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
5+
version: 0.0.1
6+
servers:
7+
- url: https://httpbin.org
8+
description: The upstream service
9+
paths:
10+
"/headers":
11+
get:
12+
summary: Returns the request headers that the upstream receives.
13+
description: Optional extended description in CommonMark or HTML.
14+
responses:
15+
'200': # status code
16+
description: A JSON object of request header name and values
17+
content:
18+
application/json:
19+
schema:
20+
type: object

microservices/gatewayApi/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ hostip=$(ifconfig en0 | awk '$1 == "inet" {print $2}')
2626

2727
docker run -ti --rm \
2828
-e CONFIG_PATH=/tmp/production.json -e ENVIRONMENT=production \
29-
-e OIDC_BASE_URL=https://auth-qwzrwc-dev.pathfinder.gov.bc.ca/auth/realms/aps \
29+
-e OIDC_BASE_URL=https://auth.cloud/auth/realms/aps \
3030
-e TOKEN_MATCH_AUD=account \
3131
-e WORKING_FOLDER=/tmp \
32-
-e KONG_ADMIN_URL=https://adminapi-qwzrwc-dev.pathfinder.gov.bc.ca \
33-
-e KC_SERVER_URL=https://auth-qwzrwc-dev.pathfinder.gov.bc.ca/auth/ \
32+
-e KONG_ADMIN_URL=https://adminapi.cloud \
33+
-e KC_SERVER_URL=https://auth.cloud/auth/ \
3434
-e KC_REALM=aps \
3535
-e KC_USERNAME=kcadmin \
36-
-e KC_PASSWORD="SdufuSYnFAANnluWrAH0waHavE9YWdCu" \
36+
-e KC_PASSWORD="" \
3737
-e KC_USER_REALM=master \
3838
-e KC_CLIENT_ID=admin-cli \
39+
-e HOST_TRANSFORM_ENABLED=true \
40+
-e HOST_TRANSFORM_BASE_URL=api.cloud \
41+
-e PLUGINS_RATELIMITING_REDIS_PASSWORD="" \
3942
-v `pwd`/_tmp:/ssl \
4043
-v ~/.kube/config:/root/.kube/config \
4144
--add-host=docker:$hostip -p 2000:2000 gwa_kong_api

microservices/gatewayApi/entrypoint.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ cat > "${CONFIG_PATH:-./config/default.json}" <<EOF
2424
"hostTransformation": {
2525
"enabled": ${HOST_TRANSFORM_ENABLED:-false},
2626
"baseUrl": "${HOST_TRANSFORM_BASE_URL}"
27+
},
28+
"plugins": {
29+
"rate_limiting": {
30+
"redis_database": 0,
31+
"redis_host": "redis-master",
32+
"redis_port": 6379,
33+
"redis_password": "${PLUGINS_RATELIMITING_REDIS_PASSWORD}",
34+
"redis_timeout": 2000,
35+
"policy": "redis"
36+
}
2737
}
2838
}
2939
EOF
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import config
2+
3+
conf = config.Config().data
4+
5+
def mask (payload):
6+
# Need to mask the redis details
7+
redis_pswd = conf['plugins']['rate_limiting']['redis_password']
8+
redis_host = conf['plugins']['rate_limiting']['redis_host']
9+
returned = payload
10+
for val in [redis_pswd, redis_host]:
11+
returned = returned.replace(val, '****')
12+
return returned
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import config
2+
import logging
3+
from flask import current_app as app
4+
5+
conf = config.Config().data
6+
7+
# Some plugins will need to be automatically transformed, such as the
8+
# rate-limiting plugin
9+
def plugins_transformations (namespace, yaml):
10+
traverse_plugins (yaml)
11+
12+
def rate_limiting (plugin):
13+
log = app.logger
14+
override_config = conf['plugins']['rate_limiting']
15+
16+
if 'config' not in plugin:
17+
plugin['config'] = {}
18+
19+
plugin_config = plugin['config']
20+
21+
for k, v in override_config.items():
22+
plugin_config[k] = v
23+
24+
# Add null values to the following if they are not specified
25+
for nval in ['second', 'minute', 'hour', 'day', 'month', 'year', 'header_name']:
26+
if nval not in plugin_config:
27+
plugin_config[nval] = None
28+
29+
# Add these defaults if not defined
30+
defaults = {
31+
"enabled": True,
32+
"protocols": [
33+
"http",
34+
"https"
35+
]
36+
}
37+
for k, v in defaults.items():
38+
if k not in plugin:
39+
plugin[k] = v
40+
41+
def traverse_plugins (yaml):
42+
traversables = ['services', 'routes', 'plugins', 'upstreams', 'consumers', 'certificates']
43+
for k in yaml:
44+
if k in traversables:
45+
for item in yaml[k]:
46+
if k == 'plugins':
47+
if item['name'] == 'rate-limiting':
48+
rate_limiting(item)
49+
traverse_plugins (item)

microservices/gatewayApi/utils/validators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ def host_valid(input_string):
1515
# regex = re.compile(host_validation_rule)
1616
# match = regex.match(str(input_string))
1717
# return bool(match is not None)
18+

microservices/gatewayApi/v1/routes/gateway.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from clients.openshift import prepare_apply_routes, prepare_delete_routes, apply_routes, delete_routes
1414

1515
from utils.validators import host_valid
16+
from utils.transforms import plugins_transformations
17+
from utils.masking import mask
18+
1619

1720
gw = Blueprint('gwa', 'gateway')
1821

@@ -60,6 +63,7 @@ def write_config(namespace: str) -> object:
6063

6164
#
6265
# Enrich the rate-limiting plugin with the appropriate Redis details
66+
plugins_transformations (namespace, gw_config)
6367

6468
with open("%s/%s" % (tempFolder, 'config-%02d.yaml' % index), 'w') as file:
6569
yaml.dump(gw_config, file)
@@ -109,7 +113,7 @@ def write_config(namespace: str) -> object:
109113
if deck_run.returncode != 0:
110114
cleanup (tempFolder)
111115
log.warn("%s - %s" % (namespace, out.decode('utf-8')))
112-
abort(make_response(jsonify(error="Sync Failed.", results=out.decode('utf-8')), 400))
116+
abort(make_response(jsonify(error="Sync Failed.", results=mask(out.decode('utf-8'))), 400))
113117

114118
elif cmd == "sync":
115119
route_count = prepare_apply_routes (namespace, selectTag, tempFolder)
@@ -127,7 +131,7 @@ def write_config(namespace: str) -> object:
127131
if cmd == 'diff':
128132
message = "Dry-run. No changes applied."
129133

130-
return make_response(jsonify(message=message, results=out.decode('utf-8')))
134+
return make_response(jsonify(message=message, results=mask(out.decode('utf-8'))))
131135
else:
132136
log.error("Missing input")
133137
log.error(request.get_data())
@@ -183,7 +187,7 @@ def host_transformation (namespace, yaml):
183187
transforms = 0
184188
conf = app.config['hostTransformation']
185189
if conf['enabled'] is True:
186-
if 'service' in yaml:
190+
if 'services' in yaml:
187191
for service in yaml['services']:
188192
if 'routes' in service:
189193
for route in service['routes']:
@@ -199,7 +203,7 @@ def validate_hosts (yaml):
199203
log = app.logger
200204
errors = []
201205

202-
if 'service' in yaml:
206+
if 'services' in yaml:
203207
for service in yaml['services']:
204208
if 'routes' in service:
205209
for route in service['routes']:

0 commit comments

Comments
 (0)