Skip to content

Commit b6784a4

Browse files
authored
Merge pull request #10 from bcgov/dev
feat: release main
2 parents e2c881b + a041c9f commit b6784a4

File tree

23 files changed

+521
-24
lines changed

23 files changed

+521
-24
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ terraform
66
/playwright-report/
77
/blob-report/
88
/playwright/.cache/
9+
.env
10+
k6

.github/workflows/terraform.yml

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ jobs:
105105
TF_VAR_grafana_kc_url=${{ vars.GRAFANA_OAUTH_BASE_URL }}
106106
TF_VAR_grafana_kc_client_id=${{ secrets.GRAFANA_OAUTH_CLIENT_ID }}
107107
TF_VAR_grafana_kc_client_secret=${{ secrets.GRAFANA_OAUTH_CLIENT_SECRET }}
108+
109+
TF_VAR_dev_desired_tasks=1
110+
TF_VAR_dev_enable_autoscale=true
111+
TF_VAR_test_desired_tasks=1
112+
TF_VAR_test_enable_autoscale=false
113+
TF_VAR_prod_desired_tasks=1
114+
TF_VAR_prod_enable_autoscale=false
115+
116+
TF_VAR_cpu_target_use=15
117+
TF_VAR_autoscale_max_capacity=2
118+
TF_VAR_autoscale_min_capacity=1
119+
108120
EOF
109121
110122
- name: Set production environment variables
@@ -143,10 +155,10 @@ jobs:
143155
TF_VAR_prod_app_url=https://otp.loginproxy.gov.bc.ca
144156
TF_VAR_prod_custom_domain_name=otp.loginproxy.gov.bc.ca
145157
TF_VAR_prod_cors_origins=https://loginproxy.gov.bc.ca
146-
TF_VAR_prod_task_cpu=256
147-
TF_VAR_prod_task_memory=512
148-
TF_VAR_prod_task_container_cpu=256
149-
TF_VAR_prod_task_container_memory=512
158+
TF_VAR_prod_task_cpu=512
159+
TF_VAR_prod_task_memory=1024
160+
TF_VAR_prod_task_container_cpu=512
161+
TF_VAR_prod_task_container_memory=1024
150162
TF_VAR_prod_task_container_port=3000
151163
TF_VAR_prod_rds_min_capacity=0.5
152164
TF_VAR_prod_rds_max_capacity=2
@@ -155,6 +167,18 @@ jobs:
155167
TF_VAR_grafana_kc_url=${{ vars.GRAFANA_OAUTH_BASE_URL }}
156168
TF_VAR_grafana_kc_client_id=${{ secrets.GRAFANA_OAUTH_CLIENT_ID }}
157169
TF_VAR_grafana_kc_client_secret=${{ secrets.GRAFANA_OAUTH_CLIENT_SECRET }}
170+
171+
TF_VAR_dev_desired_tasks=1
172+
TF_VAR_dev_enable_autoscale=false
173+
TF_VAR_test_desired_tasks=1
174+
TF_VAR_test_enable_autoscale=false
175+
TF_VAR_prod_desired_tasks=2
176+
TF_VAR_prod_enable_autoscale=true
177+
178+
TF_VAR_cpu_target_use=15
179+
TF_VAR_autoscale_max_capacity=4
180+
TF_VAR_autoscale_min_capacity=2
181+
158182
EOF
159183
160184
- name: Configure AWS credentials

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ tflint 0.43.0
55
terraform 1.11.0
66
python 3.13.1
77
terraform-docs 0.21.0
8+
k6 1.4.0
9+
oc 4.7.5

Dockerfile

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
FROM node:22.18.0-alpine
1+
FROM node:22.18.0-alpine as builder
22

33
# install yarn
44
RUN apk add --no-cache yarn
55

66
# Set the working directory
77
WORKDIR /app
88

9-
# Copy the rest of the application code
10-
COPY . .
11-
129
# Install dependencies
13-
RUN yarn install
10+
COPY package.json yarn.lock ./
11+
RUN yarn install --frozen-lockfile
1412

15-
RUN yarn tailwind:build
13+
# Copy remaining files for build step. Note only the build dir will be copied into the runner
14+
COPY . .
1615

17-
# Build the application
16+
# Build the app
17+
RUN yarn tailwind:build
1818
RUN yarn build
1919

20+
# Create fresh runner image
21+
FROM node:22.18.0-alpine as runner
22+
23+
WORKDIR /app
24+
25+
# Copy only the built assets and dependency lock into a new runner image
26+
COPY --from=builder /app/build ./build
27+
COPY package.json yarn.lock ./
28+
29+
# Only install prod dependencies
30+
RUN yarn install --production --frozen-lockfile
31+
2032
ENTRYPOINT [ "yarn", "start" ]

k6/.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

k6/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# See https://grafana.com/docs/k6/latest/results-output/real-time/timescaledb/. Need a custom build to support timescalDB.
2+
FROM golang:1.25 AS builder
3+
4+
# Install xk6
5+
RUN go install go.k6.io/xk6/cmd/xk6@latest
6+
7+
# Build custom k6 with the TimescaleDB output
8+
RUN xk6 build --with github.com/grafana/xk6-output-timescaledb
9+
10+
# ---------- Stage 2: Runtime image ----------
11+
FROM debian:bookworm-slim
12+
13+
# Install minimal dependencies (CA certificates for HTTPS, etc.)
14+
RUN apt-get update && apt-get install -y --no-install-recommends \
15+
ca-certificates \
16+
&& rm -rf /var/lib/apt/lists/*
17+
18+
# Copy k6 binary from builder
19+
COPY --from=builder /go/k6 /usr/bin/k6
20+
21+
COPY load-test.js /scripts/load-test.js
22+
23+
ENTRYPOINT ["k6"]
24+
CMD ["run", "/scripts/load-test.js"]

k6/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# SETUP
2+
3+
These load tests can be run against OTP. To work, the running image for the OTP server must have the env var TEST_MODE=true, this bypasses the email and sets a known code for the test to use. In dev sandbox you can use the task definition "otp-provider-test-mode" which has it set.
4+
5+
You will also need to ensure the otp server has a client setup in the "ClientConfig" table with a client id, secret, and redirect uris you can use. The test is setup to use a confidential client.
6+
7+
## Config
8+
9+
You can set the following environment variables to adjust the test configuration:
10+
11+
- **CLIENT_ID**: The client id to use in the OTP server
12+
- **CLIENT_SECRET**: The client secret to use in the OTP server
13+
- **REDIRECT_URI**: The allowed redirect uri configured for the client in the OTP server
14+
- **OTP_BASE_URL**: The base url of the otp server, e.g. for dev sandbox `https://dev.sandbox.otp.loginproxy.gov.bc.ca`.
15+
- **SCENARIO**: The scenario to use, one of `smoke` or `load`.
16+
17+
The following environment variables are to set test tags, which help to organize them and know what configuration the OTP server was using for the test run.
18+
19+
- **RDS_MIN_ACU**
20+
- **RDS_MAX_ACU**
21+
- **FARGATE_TASKS**
22+
- **FARGATE_CPU**
23+
- **FARGATE_MEM**
24+
- **TEST_ID**: The ID for the test. Follow the format `OTP:<timestamp>`, e.g. `OTP:2025-12-14T12:12:12`. This helps organize the test by range and search metrics near its timestamp.
25+
26+
## Usage
27+
28+
The test can be run locally for development, but is best run in openshift namespace c6af30-dev where the results can be kept in our grafana dashboard for reference and the test runner has the same settings. To run in openshift, adjust the environment variables in `job.yaml` for your desired configuration. Ensure to update the `args` field to add in the timescaleDB connection string. Then run `oc apply -f job.yaml`. To cleanup `oc delete job k6-test`.

k6/job.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
apiVersion: batch/v1
2+
kind: Job
3+
metadata:
4+
name: k6-test
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- name: k6-test
10+
imagePullPolicy: Always
11+
image: ghcr.io/bcgov/otp-provider/k6-tester:v1
12+
args: ['run', '-o', 'timescaledb=<replace-me>', '/scripts/load-test.js']
13+
resources:
14+
limits:
15+
cpu: '1'
16+
requests:
17+
cpu: '0.5'
18+
env:
19+
- name: CLIENT_ID
20+
value: <replace-me>
21+
- name: CLIENT_SECRET
22+
value: <replace-me>
23+
- name: REDIRECT_URI
24+
value: <replace-me>
25+
- name: OTP_BASE_URL
26+
value: <replace-me>
27+
- name: SCENARIO
28+
value: <replace-me>
29+
- name: RDS_MIN_ACU
30+
value: <replace-me>
31+
- name: RDS_MAX_ACU
32+
value: <replace-me>
33+
- name: FARGATE_TASKS
34+
value: <replace-me>
35+
- name: FARGATE_CPU
36+
value: <replace-me>
37+
- name: FARGATE_MEM
38+
value: <replace-me>
39+
- name: TEST_ID
40+
value: <replace-me>
41+
restartPolicy: Never
42+
backoffLimit: 1

k6/load-test.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import http from 'k6/http';
2+
import { sleep } from 'k6';
3+
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
4+
import { expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js';
5+
6+
const CLIENT_ID = __ENV.CLIENT_ID;
7+
const CLIENT_SECRET = __ENV.CLIENT_SECRET;
8+
const REDIRECT_URI = __ENV.REDIRECT_URI;
9+
const OTP_BASE_URL = __ENV.OTP_BASE_URL;
10+
11+
const SCENARIO = __ENV.SCENARIO || 'smoke';
12+
13+
const scenarios = {
14+
smoke: {
15+
executor: 'constant-vus',
16+
vus: 1,
17+
duration: '10s',
18+
tags: { test_type: 'smoke' },
19+
},
20+
load: {
21+
executor: 'ramping-arrival-rate',
22+
startRate: 1,
23+
timeUnit: '1s',
24+
preAllocatedVUs: 100,
25+
stages: [
26+
{ target: 5, duration: '10s' },
27+
{ target: 10, duration: '10s' },
28+
{ target: 15, duration: '10s' },
29+
{ target: 15, duration: '30s' },
30+
],
31+
tags: { test_type: 'load' },
32+
},
33+
};
34+
35+
export let options = {
36+
thresholds: {
37+
// Fail if any request takes longer than 5s
38+
http_req_duration: [{ threshold: 'max<5000', abortOnFail: true }],
39+
http_req_failed: [{ threshold: 'rate<0.001', abortOnFail: true }], // fail if more than 0.1% fail
40+
},
41+
tags: {
42+
testid: __ENV.TEST_ID,
43+
rds_min_acu: __ENV.RDS_MIN_ACU,
44+
rds_max_acu: __ENV.RDS_MAX_ACU,
45+
fargate_tasks: __ENV.FARGATE_TASKS,
46+
fargate_cpu: __ENV.FARGATE_CPU,
47+
fargate_mem: __ENV.FARGATE_MEM,
48+
},
49+
scenarios: {},
50+
};
51+
52+
options.scenarios[SCENARIO] = scenarios[SCENARIO];
53+
54+
export default function () {
55+
console.log(`starting with vu ${__VU}`);
56+
const authParams = {
57+
client_id: CLIENT_ID,
58+
redirect_uri: REDIRECT_URI,
59+
response_type: 'code',
60+
scope: 'openid',
61+
};
62+
63+
const jar = http.cookieJar();
64+
65+
const authUrl =
66+
`${OTP_BASE_URL}/auth?` +
67+
Object.entries(authParams)
68+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
69+
.join('&');
70+
71+
let res = http.get(authUrl, { redirects: 0, jar });
72+
let cookies = res.cookies;
73+
74+
const loginPath = `${OTP_BASE_URL}${cookies._interaction[0].path}/otp`;
75+
76+
// Generating randId to avoid email dupes
77+
const randID = Math.floor(Math.random() * 100000000000);
78+
79+
res = http.post(
80+
loginPath,
81+
{
82+
email: `test${randID}@mail.com`,
83+
},
84+
{
85+
jar,
86+
redirects: 0,
87+
headers: {
88+
'Content-Type': 'application/x-www-form-urlencoded',
89+
},
90+
},
91+
);
92+
93+
const authnPath = `${OTP_BASE_URL}${cookies._interaction[0].path}/login`;
94+
95+
res = http.post(
96+
authnPath,
97+
{
98+
code1: '1',
99+
code2: '1',
100+
code3: '1',
101+
code4: '1',
102+
code5: '1',
103+
code6: '1',
104+
},
105+
{
106+
jar,
107+
redirects: 0,
108+
headers: {
109+
'Content-Type': 'application/x-www-form-urlencoded',
110+
},
111+
},
112+
);
113+
114+
// Adjust to https in case location header is http
115+
const redirectUri = res.headers.Location.replace('http://', 'https://');
116+
117+
const newRes = http.get(redirectUri, {
118+
redirects: 0,
119+
jar,
120+
});
121+
122+
const url = new URL(newRes.headers.Location);
123+
const authCode = url.searchParams.get('code');
124+
125+
const tokenUrl = `${OTP_BASE_URL}/token`;
126+
const payload = {
127+
grant_type: 'authorization_code',
128+
code: authCode,
129+
redirect_uri: REDIRECT_URI,
130+
client_id: CLIENT_ID,
131+
client_secret: CLIENT_SECRET,
132+
};
133+
134+
const tokenRes = http.post(tokenUrl, payload, {
135+
redirects: 0,
136+
headers: {
137+
'Content-Type': 'application/x-www-form-urlencoded',
138+
},
139+
});
140+
141+
expect(tokenRes.status).to.equal(200);
142+
expect(JSON.parse(tokenRes.body).access_token).to.be.a('string');
143+
sleep(1);
144+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@types/jest": "^29.5.14",
4646
"@types/lodash.isempty": "^4.4.9",
4747
"@types/morgan": "^1.9.9",
48-
"@types/node": "^24.0.13",
48+
"@types/node": "^25.0.2",
4949
"@types/nodemailer": "^6.4.17",
5050
"@types/oidc-provider": "^8.8.1",
5151
"@types/pg": "^8.15.2",

0 commit comments

Comments
 (0)