CI Workflow¶
Prerequisites¶
- Consumer tests
- Provider tests
- Helm deployment
- Pact Broker credentials
- CircleCI token
Overview¶
One of the major benefits of Pact and contract testing is being able to ensure compatibility between applications without needing to deploy and run end-to-end tests. This requires setting up a proper CI workflow that both execute contract testing and verification of newly published pacts. Since publishing the pact happens on the consumer side and verification happens on the provider side, collaboration between the consumer and provider is key to having a good workflow.
The high level overview of the e2e CI workflow is:
- Consumer executes its contract tests and publishes the generated pact file. Consumer CI still continues with pipeline execution.
- Pact Broker detects that a new version of the pact has been published. This event triggers a webhook to kick off the contract tests on the provider side.
- Provider contract tests execute and publish results back to Pact Broker.
- NOTE: Contract tests should be executed against the versions of the provider running in each of the different environments. This is critical for us to know which versions of the provider the consumer is compatible with.
- Consumer CI workflow, which has still been running, fetches the verification results from Pact Broker just prior to deployment.
What's important to note is the collaboration between the consumer and provider and the concurrent CI workflows. The consumer may face a race condition where it attempts to fetch the verification results before the provider tests have finished executing. Pact Broker has ways to mitigate this.
Provider¶
1. Label the Kubernetes Deployment¶
You will need to update your Helm chart to add a git tag reference in the labels of the deployment resource. This is what is read during the provider test execution to know which version is deployed in each environment.
deployment/helm/templates/_helpers.tpl
git tag added to deployment labels
{{/*
Common labels
...
{{- if .Values.gitTag }}
tag: {{ .Values.gitTag | quote }}
{{- end }}
...
{{- end }}
2. Tests in the Pipeline¶
On the provider side, there are a few extra steps to executing the contract tests. We would like to be able to execute the tests against the deployed versions of the provider.
We first need to determine the version of the provider running in a particular environment by reading it from the label of the Kubernetes deployment.
After executing the tests and publishing the results, we must also tag the version in Pact Broker with the particular environment. This is done using the Pact Broker CLI.
job definition
run-contract-tests:
working_directory: ~/<PROVIDER APP NAME>
executor: java-agent # $ECR_URL/client-di-circleci-java-11-agent:7 [update the client's path](liatrio-tag)
parameters:
environment:
type: string
team-name:
description: team name (target k8s namespace prefix); exported to DEPLOY_TEAM env variable
type: string
cluster-name:
description: target cluster for the deploy
type: string
aws-region:
description: which aws region we want
type: string
steps:
- checkout:
path: ~/<PROVIDER APP NAME>
- common-tasks/vault-login:
<<: *circle-ci-service-account
- common-tasks/setup-aws-credentials:
aws-region: << parameters.aws-region >>
team-name: << parameters.team-name >>
environment: << parameters.environment >>
- common-tasks/setup-eks-credentials:
aws-region: << parameters.aws-region >>
cluster-name: << parameters.cluster-name >>
- run:
name: Install Dependencies
command: |
apk --no-cache add ca-certificates wget bash ruby-json wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.29-r0/glibc-2.29-r0.apk
apk add glibc-2.29-r0.apk
gem install pact_broker-client
- run:
name: Run provider contract tests
command: |
export PACT_BROKER_URL="pact-broker.clientcloud.com" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
export PACT_BROKER_PORT="443"
export PACT_BROKER_SCHEME="https"
export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
export DEPLOYMENT_NAME=$(kubectl get deployments -n <TEAM NAME>-<< parameters.environment >> | grep <PROVIDER APP NAME> | awk '{print $1}' | head -n 1)
export PACT_PROVIDER_VERSION=$(kubectl get deployments -n <TEAM NAME>-<< parameters.environment >> $DEPLOYMENT_NAME -o json | jq -r .metadata.labels.tag)
git checkout $PACT_PROVIDER_VERSION
./gradlew contractTest
- run:
name: Tag provider version
command: |
pact-broker create-version-tag --tag=<< parameters.environment >> --pacticipant=<PROVIDER PACTICIPANT NAME> --broker-base-url=https://$PACT_BROKER_URL --version=$PACT_PROVIDER_VERSION || true
3. Pipeline parameters¶
We will add a couple pipeline parameters to be able to filter which workflow is executed by the webhook. We would like the pact workflow to be disabled by default as to only execute from the webhook, and the dev-release workflow to be triggered normally by default.
pipeline parameters
parameters:
run_workflow_pact:
default: false
type: boolean
run_workflow_dev_release:
default: true
type: boolean
Then to use the pipeline parameters, update the dev-release workflow by adding the when
key.
dev-release workflow
workflows:
version: 2
dev-release:
when: << pipeline.parameters.run_workflow_dev_release >>
4. Add the Contract Testing Workflow¶
Add a new workflow for the contract testing. Here we use the same job definition for running the contract tests in 3 parallel executions- for dev, QA, and prod.
contract test workflow
contract-tests:
when: << pipeline.parameters.run_workflow_pact >>
jobs:
- run-contract-tests:
name: Pact Contract Tests for Dev
<<: *circle-ci-context
environment: dev
team-name: *team
cluster-name: di-nonprod-cluster
aws-region: *nonprod-aws-region
- run-contract-tests:
name: Pact Contract Tests for QA
<<: *circle-ci-context
environment: qa
team-name: *team
cluster-name: di-nonprod-cluster
aws-region: *nonprod-aws-region
- run-contract-tests:
name: Pact Contract Tests for Prod
<<: *circle-ci-context
environment: prod
team-name: *team
cluster-name: di-prod-cluster
aws-region: *prod-aws-region
Consumer¶
5. Tests in the Pipeline¶
You will need to add a new job definition for executing contract tests. For consumers, this job should handle both the exeuction of the tests as well as the publishing.
job definition
run-contract-tests:
working_directory: ~/<CONSUMER REPO NAME>
executor: platform-agent # $ECR_URL/client-di-circleci-base-agent:9 [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
steps:
- checkout:
path: ~/<CONSUMER REPO NAME>
- common-pipeline-tasks/vault-login:
<<: *circle-ci-service-account
- setup-aws-credentials
- run:
name: Run contract tests
command: |
<npm run testContract || ./gradlew clean contractTest>
- run:
name: Publish pact
command: |
export PACT_BROKER_URL="https://pact-broker.clientcloud.com" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
<npm run publish --workspace test/contract || ./gradlew pactPublish>
6. can-i-deploy¶
Another job definition will need to be added for checking the verification results. This job will use the Pact Broker CLI to run the can-i-deploy
command to check the result.
Here we only have one parameter being passed in, the environment. The environment parameter is being used to determine which tagged version of the provider to check against.
Note also that there are retry flags being passed as well. This is due to the concurrency between the consumer and provider CI pipelines and the potential for the provider tests to still be executing by the time this stage is started. Here we are saying the retry 6 times with an interval of 30 seconds.
job definition
can-i-deploy:
working_directory: ~/<CONSUMER REPO NAME>
executor: platform-agent # $ECR_URL/client-di-circleci-base-agent:9 [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
parameters:
environment:
type: string
steps:
- common-pipeline-tasks/vault-login:
<<: *circle-ci-service-account
- run:
name: Install Dependencies
command: |
gem install pact_broker-client
- run:
name: Check validation status
command: |
export PACT_BROKER_URL="https://pact-broker.clientcloud.com/" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
pact-broker can-i-deploy -a "DKS UI" -b $PACT_BROKER_URL --latest --to=<< parameters.environment >> --retry-while-unknown=6 --retry-interval=30
7. Setting up the webhook¶
To setup the webhook in Pact Broker, you may configure it through the homepage in the UI, or another way to do so would be to use cURL.
NOTE: To setup the webhook, you must have an initial pact published. The reason for this is that Pact Broker needs to be aware of the consumer and provider this webhook corresponds to.
payload.json
{
"consumer": {
"name": "<CONSUMER NAME>"
},
"provider": {
"name": "<PROVIDER NAME>"
},
"events": [
{
"name": "contract_content_changed"
}
],
"request": {
"method": "POST",
"url": "https://circleci.com/api/v2/project/bitbucket/client/<CIRCLECI PROVIDER PROJECT NAME>/pipeline", [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
"headers": {
"Content-Type": "application/json",
"Circle-Token": "<CIRCLECI TOKEN>"
},
"body": {
"branch": "main",
"parameters": {
"run_workflow_pact": true,
"run_workflow_dev_release": false
}
}
}
}
cURL command
curl -H 'Content-Type: application/json' -u $PACT_BROKER_USERNAME:$PACT_BROKER_PASSWORD -d @payload.json https://pact-broker.clientcloud.com/webhooks