Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ OAS_VALIDATION_RULESET=basic-ruleset
GWA_PROD_ENV_SLUG=E0000000
GWA_RES_SVR_CLIENT_ID=gwa-api
GWA_RES_SVR_CLIENT_SECRET=18900468-3db1-43f7-a8af-e75f079eb742
STEP_TOKEN_URL=http://step-token-api.localtest.me:2020
KEYCLOAK_AUTH_URL=http://keycloak.localtest.me:9081/auth
KEYCLOAK_REALM=master
EMAIL_ENABLED=true
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/.build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
on:
workflow_call:
inputs:
name:
required: true
type: string
context:
required: true
type: string
default: '.'

jobs:
build-image:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v7

- name: Image meta
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}/${{ inputs.name }}

- name: Set DEPLOY_ID
run: |
echo "DEPLOY_ID=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT"
echo "APP_VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" >> "$GITHUB_OUTPUT"
echo "APP_REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}" >> "$GITHUB_OUTPUT"
id: set-deploy-id

- name: Get deploy ID
run: echo "The DEPLOY_ID is ${{ steps.set-deploy-id.outputs.DEPLOY_ID }}"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
logout: false

- name: Create image
uses: docker/build-push-action@v7
with:
context: ${{ inputs.context }}
file: ${{ inputs.context }}/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: |
APP_VERSION=${{ steps.set-deploy-id.outputs.APP_VERSION }}
APP_REVISION=${{ steps.set-deploy-id.outputs.APP_REVISION }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
14 changes: 11 additions & 3 deletions .github/workflows/ci-build-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@ name: Build and Deploy Refactor

on:
push:
branches: [hotfix/*]
branches: [hotfix/*, task/*]

env:
REGISTRY: ghcr.io
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}

jobs:
build:
build-provisioner:
uses: ./.github/workflows/.build.yml
permissions:
contents: read
packages: write # 🚀 Crucial permission needed to push to ghcr.io
with:
name: provisioner-svc
context: ./provisioner-api

build-others:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -84,4 +93,3 @@ jobs:
GITHUB_API_TOKEN=${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
APP_VERSION=${{ steps.set-deploy-id.outputs.APP_VERSION }}
APP_REVISION=${{ steps.set-deploy-id.outputs.APP_REVISION }}

63 changes: 20 additions & 43 deletions .github/workflows/ci-build-provisioner-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,40 @@ name: Build Provisioner API

on:
push:
branches: [feature/*, dev, test]
branches: [tmp/*, feature/*, dev, test]

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}

jobs:
build:
uses: ./.github/workflows/.build.yml
permissions:
contents: read
packages: write # 🚀 Crucial permission needed to push to ghcr.io
with:
name: provisioner-svc
context: ./provisioner-api

deploy:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Docker meta
id: docker_meta
- uses: actions/checkout@v2

- name: Image meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/bcgov/api-services-portal/provisioner
images: ghcr.io/${{ github.repository }}/provisioner-svc

- name: Set DEPLOY_ID
run: |
echo '::set-output name=DEPLOY_ID::${{ steps.docker_meta.outputs.version }}'
echo '::set-output name=APP_VERSION::${{ fromJSON(steps.docker_meta.outputs.json).labels['org.opencontainers.image.version'] }}'
echo '::set-output name=APP_REVISION::${{ fromJSON(steps.docker_meta.outputs.json).labels['org.opencontainers.image.revision'] }}'
echo '::set-output name=DEPLOY_ID::${{ steps.meta.outputs.version }}'
echo '::set-output name=APP_VERSION::${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}'
echo '::set-output name=APP_REVISION::${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}'
id: set-deploy-id

- name: Get deploy ID
run: echo "The DEPLOY_ID is ${{ steps.set-deploy-id.outputs.DEPLOY_ID }}"

- uses: actions/checkout@v2

- name: Install oc
uses: redhat-actions/oc-installer@v1
with:
Expand All @@ -51,30 +52,6 @@ jobs:

namespace: ${{ env.OPENSHIFT_NAMESPACE }}

- name: Log in to Container Registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USERNAME }}
password: ${{ env.REGISTRY_PASSWORD }}

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3

- name: Build
uses: docker/build-push-action@v6
with:
context: provisioner-api
file: provisioner-api/Dockerfile
push: true
build-args: |
GITHUB_API_TOKEN=${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
APP_VERSION=${{ steps.set-deploy-id.outputs.APP_VERSION }}
APP_REVISION=${{ steps.set-deploy-id.outputs.APP_REVISION }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

- name: 'Get Helm'
if: github.ref != 'refs/heads/dev'
run: |
Expand All @@ -97,7 +74,7 @@ jobs:
maxSurge: 50%

image:
repository: ${{ env.REGISTRY }}/bcgov/api-services-portal/provisioner
repository: ghcr.io/bcgov/api-services-portal/provisioner-svc
tag: ${{ steps.set-deploy-id.outputs.DEPLOY_ID }}
pullPolicy: Always

Expand Down
29 changes: 29 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ services:
- --allow-write
- --allow-read
- /app/main.ts
- '2020'
# Needed when running portal outside of docker compose
ports:
- 2020:2020
Expand All @@ -514,6 +515,34 @@ services:
aliases:
- step-token-api.localtest.me

# Second, independent instance of the same CA token mock so that
# per-environment routing through the provisioner (SP136) is observable
# locally: each environment's `ca_token_url` can point at a distinct CA.
# Runs on its own internal port (2022) rather than sharing 2020 with the
# first instance, since containers reached from inside the docker network
# (the provisioner) use the container's internal port, not the host
# port mapping.
mocks-step-token-api-2:
image: denoland/deno:2.7.12
container_name: mocks-step-token-api-2
command:
- deno
- run
- --allow-net=:2022,deno.land
- --allow-write
- --allow-read
- /app/main.ts
- '2022'
# Needed when running portal outside of docker compose
ports:
- 2022:2022
volumes:
- ./local/mocks/step-token-api/main.ts:/app/main.ts
networks:
aps-net:
aliases:
- step-token-api-2.localtest.me

mocks-sdx-edge-api:
image: denoland/deno:2.7.12
container_name: mocks-sdx-edge-api
Expand Down
1 change: 1 addition & 0 deletions e2e/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default defineConfig({
'./cypress/tests/21-*/**/*.ts',
'./cypress/tests/22-*/*.ts',
'./cypress/tests/23-*/*.ts',
'./cypress/tests/99-*/*.ts',
]
return config
},
Expand Down
145 changes: 145 additions & 0 deletions e2e/cypress/tests/99-sp136/01-cert-token-provisioner-routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { v4 as uuidv4 } from 'uuid'

// SP136: Runtime group certificate-signing token generation must call
// step-ca through the provisioner (which resolves the target CA per
// environment) rather than the portal calling a single, global step-ca
// directly.
//
// The local stack runs two independent CA mocks so that "per-environment
// routing" is something this test can actually observe from the outside:
// - environment 'dev' -> step-token-api.localtest.me:2020 (internally)
// - environment 'cyp' -> step-token-api-2.localtest.me:2022 (internally)
// (see local/provisioner/environments.json - those internal hostname:port
// combinations are what the *provisioner* resolves and calls; each mock
// listens on its own internal port so the two are genuinely distinct
// endpoints on the docker network, not just distinguished by host port
// mapping). This test itself runs on the host, where both mocks resolve to
// 127.0.0.1 and are reached via their published host ports, which happen to
// match the internal ones (2020 and 2022, see docker-compose.yml). Each mock
// exposes `GET /tokens` returning how many tokens it has issued so far.
//
// Under the bug, the portal ignores the runtime group's environment and
// always calls a single global CA, so a token request for the 'cyp'
// runtime group never reaches the second mock - its count never moves.

const DEV_CA_URL = 'http://localhost:2020'
const CYP_CA_URL = 'http://localhost:2022'

function tokenCount(caUrl: string) {
return cy
.request({ url: `${caUrl}/tokens`, method: 'GET' })
.then((res) => {
expect(res.status).to.be.equal(200)
return res.body.count as number
})
}

function shortId(): string {
return uuidv4().replace(/-/g, '').toLowerCase().substring(0, 6)
}

describe('SP136 - Runtime group certificate-signing token routes through the provisioner', () => {
let workingData: any
const runtimeGroupDev = `d${shortId()}`
const runtimeGroupCyp = `c${shortId()}`

before(() => {
cy.buildOrgGatewayDatasetAndProduct().then((data) => {
workingData = data
})
})

it('creates runtime groups in two different environments', () => {
const { org } = workingData

cy.setRequestBody({ name: runtimeGroupDev, environment: 'dev' })
cy.callAPI(`ds/api/sdx/v1/organizations/${org.name}/runtime-groups`, 'PUT').then(
({ apiRes: { status, body } }: any) => {
expect(status, JSON.stringify(body)).to.be.equal(200)
}
)

cy.setRequestBody({ name: runtimeGroupCyp, environment: 'cyp' })
cy.callAPI(`ds/api/sdx/v1/organizations/${org.name}/runtime-groups`, 'PUT').then(
({ apiRes: { status, body } }: any) => {
expect(status, JSON.stringify(body)).to.be.equal(200)
}
)
})

it("routes the 'dev' runtime group's token request to the 'dev' environment's CA only", () => {
const { org } = workingData

let devBaseline: number
let cypBaseline: number

tokenCount(DEV_CA_URL).then((count) => {
devBaseline = count
})
tokenCount(CYP_CA_URL).then((count) => {
cypBaseline = count
})

cy.clearRequestBody()
cy.setQueryString({})
cy.callAPI(
`ds/api/sdx/v1/organizations/${org.name}/runtime-groups/${runtimeGroupDev}/environments/dev/tokens`,
'POST'
).then(({ apiRes: { status, body } }: any) => {
expect(status, JSON.stringify(body)).to.be.equal(200)
expect(body.token).to.be.a('string')
})

tokenCount(DEV_CA_URL).then((count) => {
expect(count, "dev CA should have issued the 'dev' runtime group's token").to.be.equal(
devBaseline + 1
)
})
tokenCount(CYP_CA_URL).then((count) => {
expect(
count,
"cyp CA should not have been called for the 'dev' runtime group's token"
).to.be.equal(cypBaseline)
})
})

it("routes the 'cyp' runtime group's token request to the 'cyp' environment's CA only", () => {
const { org } = workingData

let devBaseline: number
let cypBaseline: number

tokenCount(DEV_CA_URL).then((count) => {
devBaseline = count
})
tokenCount(CYP_CA_URL).then((count) => {
cypBaseline = count
})

cy.clearRequestBody()
cy.setQueryString({})
cy.callAPI(
`ds/api/sdx/v1/organizations/${org.name}/runtime-groups/${runtimeGroupCyp}/environments/cyp/tokens`,
'POST'
).then(({ apiRes: { status, body } }: any) => {
expect(status, JSON.stringify(body)).to.be.equal(200)
expect(body.token).to.be.a('string')
})

// This is the assertion that fails on unfixed code: today every token
// request - regardless of the runtime group's environment - lands on the
// single global STEP_TOKEN_URL (the 'dev' CA), so the 'cyp' CA's count
// never moves and this expectation fails.
tokenCount(CYP_CA_URL).then((count) => {
expect(count, "cyp CA should have issued the 'cyp' runtime group's token").to.be.equal(
cypBaseline + 1
)
})
tokenCount(DEV_CA_URL).then((count) => {
expect(
count,
"dev CA should not have been called for the 'cyp' runtime group's token"
).to.be.equal(devBaseline)
})
})
})
Loading
Loading