Skip to content

Commit 6a12912

Browse files
authored
ci: use a wildcard subdomain for PR deployments (#8801)
1 parent 336e663 commit 6a12912

File tree

3 files changed

+132
-52
lines changed

3 files changed

+132
-52
lines changed

.github/workflows/pr-cleanup.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,20 @@ jobs:
4848
- name: "Remove PR namespace"
4949
run: |
5050
kubectl delete namespace "pr${{ steps.pr_number.outputs.PR_NUMBER }}" || echo "namespace not found"
51+
52+
- name: "Remove DNS records"
53+
run: |
54+
set -euxo pipefail
55+
# Get identifier for the record
56+
record_id=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records?name=%2A.pr${{ steps.pr_number.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" \
57+
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
58+
-H "Content-Type:application/json" | jq -r '.result[0].id') || echo "DNS record not found"
59+
60+
echo "::add-mask::$record_id"
61+
62+
# Delete the record
63+
(
64+
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records/$record_id" \
65+
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
66+
-H "Content-Type:application/json"
67+
) || echo "DNS record not found"

.github/workflows/pr-deploy.yaml

+83-45
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ on:
77
inputs:
88
pr_number:
99
description: "PR number"
10+
type: number
1011
required: true
1112
skip_build:
1213
description: "Skip build job"
1314
required: false
15+
type: boolean
1416
default: false
17+
experiments:
18+
description: "Experiments to enable"
19+
required: false
20+
type: string
21+
default: "*"
1522

1623
env:
1724
REPO: ghcr.io/coder/coder-preview
@@ -22,8 +29,8 @@ permissions:
2229
pull-requests: write
2330

2431
concurrency:
25-
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
26-
cancel-in-progress: false
32+
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.ref }}
33+
cancel-in-progress: true
2734

2835
jobs:
2936
pr_commented:
@@ -136,7 +143,7 @@ jobs:
136143
PR_TITLE: ${{ needs.pr_commented.outputs.PR_TITLE }}
137144
PR_URL: ${{ needs.pr_commented.outputs.PR_URL }}
138145
PR_BRANCH: ${{ needs.pr_commented.outputs.PR_BRANCH }}
139-
PR_DEPLOYMENT_ACCESS_URL: "https://pr${{ needs.pr_commented.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
146+
PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.pr_commented.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
140147
steps:
141148
- name: Check if image exists
142149
run: |
@@ -145,10 +152,19 @@ jobs:
145152
if [ -z "$foundTag" ]; then
146153
echo "Image not found"
147154
echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview"
148-
echo "Please remove --skip-build from the comment or ./scripts/deploy-pr.sh"
155+
echo "Please remove --skip-build from the comment and try again"
149156
exit 1
150157
fi
151158
159+
- name: Add DNS record to Cloudflare
160+
run: |
161+
(
162+
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \
163+
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
164+
-H "Content-Type:application/json" \
165+
--data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}'
166+
)
167+
152168
- name: Checkout
153169
uses: actions/checkout@v3
154170
with:
@@ -168,35 +184,36 @@ jobs:
168184
kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true
169185
kubectl create namespace "pr${{ env.PR_NUMBER }}"
170186
171-
- name: Setup ingress
187+
- name: Check and Create Certificate
172188
run: |
173-
cat <<EOF > ingress.yaml
174-
apiVersion: networking.k8s.io/v1
175-
kind: Ingress
176-
metadata:
177-
name: pr${{ env.PR_NUMBER }}
178-
namespace: pr${{ env.PR_NUMBER }}
179-
annotations:
180-
cert-manager.io/cluster-issuer: letsencrypt
181-
spec:
182-
tls:
183-
- hosts:
184-
- "${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
185-
- "*.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
189+
# Using kubectl to check if a Certificate resource already exists
190+
# we are doing this to avoid letsenrypt rate limits
191+
if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then
192+
echo "Certificate doesn't exist. Creating a new one."
193+
cat <<EOF | kubectl apply -f -
194+
apiVersion: cert-manager.io/v1
195+
kind: Certificate
196+
metadata:
197+
name: pr${{ env.PR_NUMBER }}-tls
198+
namespace: pr-deployment-certs
199+
spec:
186200
secretName: pr${{ env.PR_NUMBER }}-tls
187-
rules:
188-
- host: "pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
189-
http:
190-
paths:
191-
- pathType: Prefix
192-
path: "/"
193-
backend:
194-
service:
195-
name: coder
196-
port:
197-
number: 80
201+
issuerRef:
202+
name: letsencrypt
203+
kind: ClusterIssuer
204+
dnsNames:
205+
- "${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
206+
- "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
198207
EOF
199-
kubectl apply -f ingress.yaml
208+
else
209+
echo "Certificate exists. Skipping certificate creation."
210+
echo "Copy certificate from pr-deployment-certs to pr${{ env.PR_NUMBER }} namespace"
211+
(
212+
kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs -o json |
213+
jq 'del(.metadata.namespace,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid,.metadata.managedFields)' |
214+
kubectl -n pr${{ env.PR_NUMBER }} apply -f -
215+
)
216+
fi
200217
201218
- name: Set up PostgreSQL database
202219
run: |
@@ -210,6 +227,17 @@ jobs:
210227
kubectl create secret generic coder-db-url -n pr${{ env.PR_NUMBER }} \
211228
--from-literal=url="postgres://coder:coder@coder-db-postgresql.pr${{ env.PR_NUMBER }}.svc.cluster.local:5432/coder?sslmode=disable"
212229
230+
- name: Get experiments
231+
id: get_experiments
232+
run: |
233+
set -euxo pipefail
234+
if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
235+
experiments=${{ github.event.inputs.experiments }}
236+
else
237+
experiments=$(echo "${{ github.event.comment.body }}" | grep -oP '(?<=--experiments )[^ ]+')
238+
fi
239+
echo "experiments=$experiments" >> $GITHUB_OUTPUT
240+
213241
- name: Create values.yaml
214242
run: |
215243
cat <<EOF > pr-deploy-values.yaml
@@ -220,13 +248,22 @@ jobs:
220248
pullPolicy: Always
221249
service:
222250
type: ClusterIP
251+
ingress:
252+
enable: true
253+
className: traefik
254+
host: ${{ env.PR_DEPLOYMENT_ACCESS_URL }}
255+
wildcardHost: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
256+
tls:
257+
enable: true
258+
secretName: pr${{ env.PR_NUMBER }}-tls
259+
wildcardSecretName: pr${{ env.PR_NUMBER }}-tls
223260
env:
224261
- name: "CODER_ACCESS_URL"
225-
value: "https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
262+
value: "https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
226263
- name: "CODER_WILDCARD_ACCESS_URL"
227-
value: "*--pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
264+
value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
228265
- name: "CODER_EXPERIMENTS"
229-
value: "*"
266+
value: "*,${{ steps.get_experiments.outputs.experiments }}"
230267
- name: CODER_PG_CONNECTION_URL
231268
valueFrom:
232269
secretKeyRef:
@@ -261,7 +298,7 @@ jobs:
261298
set -euxo pipefail
262299
263300
DEST="${HOME}/coder"
264-
URL="${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64"
301+
URL="https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64"
265302
266303
mkdir -p "$(dirname ${DEST})"
267304
@@ -279,6 +316,7 @@ jobs:
279316
curl -fsSL "$URL" -o "${DEST}"
280317
chmod +x "${DEST}"
281318
"${DEST}" version
319+
mv "${DEST}" /usr/local/bin/coder
282320
283321
- name: Create first user, template and workspace
284322
id: setup_deployment
@@ -294,16 +332,16 @@ jobs:
294332
echo "::add-mask::$password"
295333
echo "password=$password" >> $GITHUB_OUTPUT
296334
297-
/home/runner/coder login \
298-
--first-user-username pr${{ env.PR_NUMBER }} \
299-
--first-user-email ${{ env.PR_NUMBER }}@coder.com \
335+
coder login \
336+
--first-user-username test \
337+
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
300338
--first-user-password $password \
301339
--first-user-trial \
302340
--use-token-as-session \
303-
${{ env.PR_DEPLOYMENT_ACCESS_URL }}
341+
https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}
304342
305343
# Create template
306-
/home/runner/coder templates init --id kubernetes && cd ./kubernetes/ && /home/runner/coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
344+
coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
307345
308346
# Create workspace
309347
cat <<EOF > workspace.yaml
@@ -312,8 +350,8 @@ jobs:
312350
home_disk_size: "2"
313351
EOF
314352
315-
/home/runner/coder create --template="kubernetes" pr${{ env.PR_NUMBER }} --rich-parameter-file ./workspace.yaml -y
316-
/home/runner/coder stop pr${{ env.PR_NUMBER }} -y
353+
coder create --template="kubernetes" test --rich-parameter-file ./workspace.yaml -y
354+
coder stop test -y
317355
318356
- name: Send Slack notification
319357
run: |
@@ -323,9 +361,9 @@ jobs:
323361
"pr_number": "'"${{ env.PR_NUMBER }}"'",
324362
"pr_url": "'"${{ env.PR_URL }}"'",
325363
"pr_title": "'"${{ env.PR_TITLE }}"'",
326-
"pr_access_url": "'"${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
327-
"pr_username": "'"pr${{ env.PR_NUMBER }}"'",
328-
"pr_email": "'"${{ env.PR_NUMBER }}@coder.com"'",
364+
"pr_access_url": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
365+
"pr_username": "'"test"'",
366+
"pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
329367
"pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",
330368
"pr_actor": "'"${{ github.actor }}"'"
331369
}' \
@@ -351,6 +389,6 @@ jobs:
351389
comment-id: ${{ steps.fc.outputs.comment-id }}
352390
body: |
353391
:heavy_check_mark: Deployed PR ${{ env.PR_NUMBER }} successfully.
354-
:rocket: Access the deployment link [here](${{ env.PR_DEPLOYMENT_ACCESS_URL }}).
392+
:rocket: Access the deployment link [here](https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}).
355393
:warning: This deployment will be deleted when the PR is closed.
356394
reactions: rocket

scripts/deploy-pr.sh

+32-7
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,43 @@ set -euo pipefail
99
skipBuild=false
1010
dryRun=false
1111
confirm=true
12+
experiments=""
1213

1314
# parse arguments
14-
for arg in "$@"; do
15-
case $arg in
15+
while (("$#")); do
16+
case "$1" in
1617
-s | --skip-build)
1718
skipBuild=true
18-
shift # Remove --skip-build from processing
19+
shift
1920
;;
2021
-n | --dry-run)
2122
dryRun=true
22-
shift # Remove --dry-run from processing
23+
shift
24+
;;
25+
-e | --experiments)
26+
if [ -n "$2" ] && [ "${2:0:1}" != "-" ]; then
27+
experiments="$2"
28+
shift
29+
else
30+
echo "Error: Argument for $1 is missing" >&2
31+
exit 1
32+
fi
33+
shift
2334
;;
2435
-y | --yes)
2536
confirm=false
26-
shift # Remove --yes from processing
37+
shift
38+
;;
39+
--)
40+
shift
41+
break
42+
;;
43+
--*)
44+
echo "Error: Unsupported flag $1" >&2
45+
exit 1
2746
;;
2847
*)
29-
shift # Remove generic argument from processing
48+
shift
3049
;;
3150
esac
3251
done
@@ -61,7 +80,13 @@ if $dryRun; then
6180
echo "branchName: ${branchName}"
6281
echo "prNumber: ${prNumber}"
6382
echo "skipBuild: ${skipBuild}"
83+
echo "experiments: ${experiments}"
6484
exit 0
6585
fi
6686

67-
gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "skip_build=${skipBuild}"
87+
echo "branchName: ${branchName}"
88+
echo "prNumber: ${prNumber}"
89+
echo "skipBuild: ${skipBuild}"
90+
echo "experiments: ${experiments}"
91+
92+
gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "skip_build=${skipBuild}" -f "experiments=${experiments}"

0 commit comments

Comments
 (0)