Skip to content

Commit e1339e7

Browse files
committed
Merge branch 'main' into colin/pnpm
2 parents fefad21 + 4456d0b commit e1339e7

32 files changed

+5356
-9222
lines changed

.github/workflows/pr-cleanup.yaml

Lines changed: 17 additions & 0 deletions
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

Lines changed: 83 additions & 45 deletions
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

.github/workflows/release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ jobs:
407407
- name: Comment on PR
408408
if: ${{ !inputs.dry_run }}
409409
run: |
410+
# wait 30 seconds
411+
Start-Sleep -Seconds 30.0
410412
# Find the PR that wingetcreate just made.
411413
$version = "${{ needs.release.outputs.version }}".Trim('v')
412414
$pr_list = gh pr list --repo microsoft/winget-pkgs --search "author:cdrci Coder.Coder version ${version}" --limit 1 --json number | `

agent/agent.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ type Client interface {
8585

8686
type Agent interface {
8787
HTTPDebug() http.Handler
88+
// TailnetConn may be nil.
89+
TailnetConn() *tailnet.Conn
8890
io.Closer
8991
}
9092

@@ -200,6 +202,10 @@ type agent struct {
200202
metrics *agentMetrics
201203
}
202204

205+
func (a *agent) TailnetConn() *tailnet.Conn {
206+
return a.network
207+
}
208+
203209
func (a *agent) init(ctx context.Context) {
204210
sshSrv, err := agentssh.NewServer(ctx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, a.sshMaxTimeout, "")
205211
if err != nil {

agent/agent_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,15 @@ func TestAgent_UpdatedDERP(t *testing.T) {
18181818
})
18191819
require.NoError(t, err)
18201820

1821+
require.Eventually(t, func() bool {
1822+
conn := closer.TailnetConn()
1823+
if conn == nil {
1824+
return false
1825+
}
1826+
regionIDs := conn.DERPMap().RegionIDs()
1827+
return len(regionIDs) == 1 && regionIDs[0] == 2 && conn.Node().PreferredDERP == 2
1828+
}, testutil.WaitLong, testutil.IntervalFast)
1829+
18211830
// Connect from a second client and make sure it uses the new DERP map.
18221831
conn2 := newClientConn(newDerpMap)
18231832
require.Equal(t, []int{2}, conn2.DERPMap().RegionIDs())

coderd/coderdtest/coderdtest.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,21 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
303303
accessURL = serverURL
304304
}
305305

306-
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
307-
stunAddr.IP = net.ParseIP("127.0.0.1")
308-
t.Cleanup(stunCleanup)
306+
// If the STUNAddresses setting is empty or the default, start a STUN
307+
// server. Otherwise, use the value as is.
308+
var (
309+
stunAddresses []string
310+
dvStunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
311+
)
312+
if len(dvStunAddresses) == 0 || (len(dvStunAddresses) == 1 && dvStunAddresses[0] == "stun.l.google.com:19302") {
313+
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
314+
stunAddr.IP = net.ParseIP("127.0.0.1")
315+
t.Cleanup(stunCleanup)
316+
stunAddresses = []string{stunAddr.String()}
317+
options.DeploymentValues.DERP.Server.STUNAddresses = stunAddresses
318+
} else if dvStunAddresses[0] != "disable" {
319+
stunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
320+
}
309321

310322
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(slogtest.Make(t, nil).Named("derp").Leveled(slog.LevelDebug)))
311323
derpServer.SetMeshKey("test-key")
@@ -346,7 +358,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
346358
if !options.DeploymentValues.DERP.Server.Enable.Value() {
347359
region = nil
348360
}
349-
derpMap, err := tailnet.NewDERPMap(ctx, region, []string{stunAddr.String()}, "", "", options.DeploymentValues.DERP.Config.BlockDirect.Value())
361+
derpMap, err := tailnet.NewDERPMap(ctx, region, stunAddresses, "", "", options.DeploymentValues.DERP.Config.BlockDirect.Value())
350362
require.NoError(t, err)
351363

352364
return func(h http.Handler) {

coderd/database/dbauthz/dbauthz.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -953,14 +953,9 @@ func (q *querier) GetLatestWorkspaceBuilds(ctx context.Context) ([]database.Work
953953
}
954954

955955
func (q *querier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceBuild, error) {
956-
// This is not ideal as not all builds will be returned if the workspace cannot be read.
957-
// This should probably be handled differently? Maybe join workspace builds with workspace
958-
// ownership properties and filter on that.
959-
for _, id := range ids {
960-
_, err := q.GetWorkspaceByID(ctx, id)
961-
if err != nil {
962-
return nil, err
963-
}
956+
// This function is a system function until we implement a join for workspace builds.
957+
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
958+
return nil, err
964959
}
965960

966961
return q.db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, ids)

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,11 +1024,6 @@ func (s *MethodTestSuite) TestWorkspace() {
10241024
b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID})
10251025
check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns(b)
10261026
}))
1027-
s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) {
1028-
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
1029-
b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID})
1030-
check.Args([]uuid.UUID{ws.ID}).Asserts(ws, rbac.ActionRead).Returns(slice.New(b))
1031-
}))
10321027
s.Run("GetWorkspaceAgentByID", s.Subtest(func(db database.Store, check *expects) {
10331028
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
10341029
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()})
@@ -1298,6 +1293,11 @@ func (s *MethodTestSuite) TestSystemFunctions() {
12981293
LoginType: database.LoginTypeGithub,
12991294
}).Asserts(rbac.ResourceSystem, rbac.ActionUpdate).Returns(l)
13001295
}))
1296+
s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) {
1297+
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
1298+
b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID})
1299+
check.Args([]uuid.UUID{ws.ID}).Asserts(rbac.ResourceSystem, rbac.ActionRead).Returns(slice.New(b))
1300+
}))
13011301
s.Run("UpsertDefaultProxy", s.Subtest(func(db database.Store, check *expects) {
13021302
check.Args(database.UpsertDefaultProxyParams{}).Asserts(rbac.ResourceSystem, rbac.ActionUpdate).Returns()
13031303
}))

coderd/healthcheck/derp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func (r *DERPNodeReport) derpURL() *url.URL {
172172
if r.Node.HostName == "" {
173173
derpURL.Host = r.Node.IPv4
174174
}
175-
if r.Node.DERPPort != 0 {
175+
if r.Node.DERPPort != 0 && !(r.Node.DERPPort == 443 && derpURL.Scheme == "https") && !(r.Node.DERPPort == 80 && derpURL.Scheme == "http") {
176176
derpURL.Host = fmt.Sprintf("%s:%d", derpURL.Host, r.Node.DERPPort)
177177
}
178178

0 commit comments

Comments
 (0)