Skip to content

Commit 4166c29

Browse files
committed
Merge branch 'main' into mafredri/feat-add-agent-connection-reporting
2 parents 82b6fab + 6dd51f9 commit 4166c29

File tree

227 files changed

+5713
-1627
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

227 files changed

+5713
-1627
lines changed

.github/workflows/ci.yaml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ jobs:
178178
echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV
179179
180180
- name: golangci-lint cache
181-
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
181+
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
182182
with:
183183
path: |
184184
${{ env.LINT_CACHE_DIR }}
@@ -188,7 +188,7 @@ jobs:
188188
189189
# Check for any typos
190190
- name: Check for typos
191-
uses: crate-ci/typos@51f257b946f503b768e522781f56e9b7b5570d48 # v1.29.7
191+
uses: crate-ci/typos@212923e4ff05b7fc2294a204405eec047b807138 # v1.29.9
192192
with:
193193
config: .github/workflows/typos.toml
194194

@@ -201,7 +201,7 @@ jobs:
201201
202202
# Needed for helm chart linting
203203
- name: Install helm
204-
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0
204+
uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4.3.0
205205
with:
206206
version: v3.9.2
207207

@@ -733,15 +733,15 @@ jobs:
733733

734734
- name: Upload Playwright Failed Tests
735735
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
736-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
736+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
737737
with:
738738
name: failed-test-videos${{ matrix.variant.premium && '-premium' || '' }}
739739
path: ./site/test-results/**/*.webm
740740
retention-days: 7
741741

742742
- name: Upload pprof dumps
743743
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
744-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
744+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
745745
with:
746746
name: debug-pprof-dumps${{ matrix.variant.premium && '-premium' || '' }}
747747
path: ./site/test-results/**/debug-pprof-*.txt
@@ -1000,7 +1000,7 @@ jobs:
10001000

10011001
- name: Upload build artifacts
10021002
if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }}
1003-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
1003+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
10041004
with:
10051005
name: dylibs
10061006
path: |
@@ -1140,7 +1140,7 @@ jobs:
11401140

11411141
- name: Upload build artifacts
11421142
if: github.ref == 'refs/heads/main'
1143-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
1143+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
11441144
with:
11451145
name: coder
11461146
path: |
@@ -1183,7 +1183,7 @@ jobs:
11831183
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
11841184

11851185
- name: Set up Flux CLI
1186-
uses: fluxcd/flux2/action@5350425cdcd5fa015337e09fa502153c0275bd4b # v2.4.0
1186+
uses: fluxcd/flux2/action@af67405ee43a6cd66e0b73f4b3802e8583f9d961 # v2.5.0
11871187
with:
11881188
# Keep this and the github action up to date with the version of flux installed in dogfood cluster
11891189
version: "2.2.1"
@@ -1219,6 +1219,8 @@ jobs:
12191219
kubectl --namespace coder rollout status deployment/coder
12201220
kubectl --namespace coder rollout restart deployment/coder-provisioner
12211221
kubectl --namespace coder rollout status deployment/coder-provisioner
1222+
kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged
1223+
kubectl --namespace coder rollout status deployment/coder-provisioner-tagged
12221224
12231225
deploy-wsproxies:
12241226
runs-on: ubuntu-latest

.github/workflows/release.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ jobs:
101101
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
102102

103103
- name: Upload build artifacts
104-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
104+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
105105
with:
106106
name: dylibs
107107
path: |
@@ -485,7 +485,7 @@ jobs:
485485
486486
- name: Upload artifacts to actions (if dry-run)
487487
if: ${{ inputs.dry_run }}
488-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
488+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
489489
with:
490490
name: release-artifacts
491491
path: |

.github/workflows/scorecard.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
persist-credentials: false
3131

3232
- name: "Run analysis"
33-
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
33+
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
3434
with:
3535
results_file: results.sarif
3636
results_format: sarif
@@ -39,14 +39,14 @@ jobs:
3939

4040
# Upload the results as artifacts.
4141
- name: "Upload artifact"
42-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
42+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
4343
with:
4444
name: SARIF file
4545
path: results.sarif
4646
retention-days: 5
4747

4848
# Upload the results to GitHub's code scanning dashboard.
4949
- name: "Upload to code-scanning"
50-
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
50+
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
5151
with:
5252
sarif_file: results.sarif

.github/workflows/security.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
uses: ./.github/actions/setup-go
3939

4040
- name: Initialize CodeQL
41-
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
41+
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
4242
with:
4343
languages: go, javascript
4444

@@ -48,7 +48,7 @@ jobs:
4848
rm Makefile
4949
5050
- name: Perform CodeQL Analysis
51-
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
51+
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
5252

5353
- name: Send Slack notification on failure
5454
if: ${{ failure() }}
@@ -144,13 +144,13 @@ jobs:
144144
severity: "CRITICAL,HIGH"
145145

146146
- name: Upload Trivy scan results to GitHub Security tab
147-
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
147+
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
148148
with:
149149
sarif_file: trivy-results.sarif
150150
category: "Trivy"
151151

152152
- name: Upload Trivy scan results as an artifact
153-
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
153+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
154154
with:
155155
name: trivy
156156
path: trivy-results.sarif

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ endif
116116

117117
clean:
118118
rm -rf build/ site/build/ site/out/
119-
mkdir -p build/ site/out/bin/
119+
mkdir -p build/
120120
git restore site/out/
121121
.PHONY: clean
122122

agent/agent.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"hash/fnv"
910
"io"
1011
"net"
1112
"net/http"
@@ -90,6 +91,7 @@ type Options struct {
9091
Execer agentexec.Execer
9192
ContainerLister agentcontainers.Lister
9293

94+
ExperimentalContainersEnabled bool
9395
ExperimentalConnectionReports bool
9496
}
9597

@@ -193,7 +195,8 @@ func New(options Options) Agent {
193195
execer: options.Execer,
194196
lister: options.ContainerLister,
195197

196-
experimentalConnectionReports: options.ExperimentalConnectionReports,
198+
experimentalDevcontainersEnabled: options.ExperimentalContainersEnabled,
199+
experimentalConnectionReports: options.ExperimentalConnectionReports,
197200
}
198201
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
199202
// Each time we connect we replace the channel (while holding the closeMutex) with a new one
@@ -269,7 +272,8 @@ type agent struct {
269272
execer agentexec.Execer
270273
lister agentcontainers.Lister
271274

272-
experimentalConnectionReports bool
275+
experimentalDevcontainersEnabled bool
276+
experimentalConnectionReports bool
273277
}
274278

275279
func (a *agent) TailnetConn() *tailnet.Conn {
@@ -330,6 +334,9 @@ func (a *agent) init() {
330334
},
331335
a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors,
332336
a.reconnectingPTYTimeout,
337+
func(s *reconnectingpty.Server) {
338+
s.ExperimentalContainersEnabled = a.experimentalDevcontainersEnabled
339+
},
333340
)
334341
go a.runLoop()
335342
}
@@ -1122,7 +1129,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
11221129
if err := manifestOK.wait(ctx); err != nil {
11231130
return xerrors.Errorf("no manifest: %w", err)
11241131
}
1125-
var err error
11261132
defer func() {
11271133
networkOK.complete(retErr)
11281134
}()
@@ -1131,9 +1137,20 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
11311137
network := a.network
11321138
a.closeMutex.Unlock()
11331139
if network == nil {
1140+
keySeed, err := WorkspaceKeySeed(manifest.WorkspaceID, manifest.AgentName)
1141+
if err != nil {
1142+
return xerrors.Errorf("generate seed from workspace id: %w", err)
1143+
}
11341144
// use the graceful context here, because creating the tailnet is not itself tied to the
11351145
// agent API.
1136-
network, err = a.createTailnet(a.gracefulCtx, manifest.AgentID, manifest.DERPMap, manifest.DERPForceWebSockets, manifest.DisableDirectConnections)
1146+
network, err = a.createTailnet(
1147+
a.gracefulCtx,
1148+
manifest.AgentID,
1149+
manifest.DERPMap,
1150+
manifest.DERPForceWebSockets,
1151+
manifest.DisableDirectConnections,
1152+
keySeed,
1153+
)
11371154
if err != nil {
11381155
return xerrors.Errorf("create tailnet: %w", err)
11391156
}
@@ -1273,7 +1290,13 @@ func (a *agent) trackGoroutine(fn func()) error {
12731290
return nil
12741291
}
12751292

1276-
func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *tailcfg.DERPMap, derpForceWebSockets, disableDirectConnections bool) (_ *tailnet.Conn, err error) {
1293+
func (a *agent) createTailnet(
1294+
ctx context.Context,
1295+
agentID uuid.UUID,
1296+
derpMap *tailcfg.DERPMap,
1297+
derpForceWebSockets, disableDirectConnections bool,
1298+
keySeed int64,
1299+
) (_ *tailnet.Conn, err error) {
12771300
// Inject `CODER_AGENT_HEADER` into the DERP header.
12781301
var header http.Header
12791302
if client, ok := a.client.(*agentsdk.Client); ok {
@@ -1300,6 +1323,10 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
13001323
}
13011324
}()
13021325

1326+
if err := a.sshServer.UpdateHostSigner(keySeed); err != nil {
1327+
return nil, xerrors.Errorf("update host signer: %w", err)
1328+
}
1329+
13031330
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentSSHPort))
13041331
if err != nil {
13051332
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
@@ -1977,3 +2004,20 @@ func PrometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger sl
19772004
}
19782005
})
19792006
}
2007+
2008+
// WorkspaceKeySeed converts a WorkspaceID UUID and agent name to an int64 hash.
2009+
// This uses the FNV-1a hash algorithm which provides decent distribution and collision
2010+
// resistance for string inputs.
2011+
func WorkspaceKeySeed(workspaceID uuid.UUID, agentName string) (int64, error) {
2012+
h := fnv.New64a()
2013+
_, err := h.Write(workspaceID[:])
2014+
if err != nil {
2015+
return 42, err
2016+
}
2017+
_, err = h.Write([]byte(agentName))
2018+
if err != nil {
2019+
return 42, err
2020+
}
2021+
2022+
return int64(h.Sum64()), nil
2023+
}

agent/agent_test.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,28 @@ import (
2525
"testing"
2626
"time"
2727

28+
"go.uber.org/goleak"
29+
"tailscale.com/net/speedtest"
30+
"tailscale.com/tailcfg"
31+
2832
"github.com/bramvdbogaerde/go-scp"
2933
"github.com/google/uuid"
34+
"github.com/ory/dockertest/v3"
35+
"github.com/ory/dockertest/v3/docker"
3036
"github.com/pion/udp"
3137
"github.com/pkg/sftp"
3238
"github.com/prometheus/client_golang/prometheus"
3339
promgo "github.com/prometheus/client_model/go"
3440
"github.com/spf13/afero"
3541
"github.com/stretchr/testify/assert"
3642
"github.com/stretchr/testify/require"
37-
"go.uber.org/goleak"
3843
"golang.org/x/crypto/ssh"
3944
"golang.org/x/exp/slices"
4045
"golang.org/x/xerrors"
41-
"tailscale.com/net/speedtest"
42-
"tailscale.com/tailcfg"
4346

4447
"cdr.dev/slog"
4548
"cdr.dev/slog/sloggers/slogtest"
49+
4650
"github.com/coder/coder/v2/agent"
4751
"github.com/coder/coder/v2/agent/agentssh"
4852
"github.com/coder/coder/v2/agent/agenttest"
@@ -1800,6 +1804,74 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
18001804
}
18011805
}
18021806

1807+
// This tests end-to-end functionality of connecting to a running container
1808+
// and executing a command. It creates a real Docker container and runs a
1809+
// command. As such, it does not run by default in CI.
1810+
// You can run it manually as follows:
1811+
//
1812+
// CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_ReconnectingPTYContainer
1813+
func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1814+
t.Parallel()
1815+
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
1816+
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
1817+
}
1818+
1819+
ctx := testutil.Context(t, testutil.WaitLong)
1820+
1821+
pool, err := dockertest.NewPool("")
1822+
require.NoError(t, err, "Could not connect to docker")
1823+
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
1824+
Repository: "busybox",
1825+
Tag: "latest",
1826+
Cmd: []string{"sleep", "infnity"},
1827+
}, func(config *docker.HostConfig) {
1828+
config.AutoRemove = true
1829+
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
1830+
})
1831+
require.NoError(t, err, "Could not start container")
1832+
t.Cleanup(func() {
1833+
err := pool.Purge(ct)
1834+
require.NoError(t, err, "Could not stop container")
1835+
})
1836+
// Wait for container to start
1837+
require.Eventually(t, func() bool {
1838+
ct, ok := pool.ContainerByName(ct.Container.Name)
1839+
return ok && ct.Container.State.Running
1840+
}, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time")
1841+
1842+
// nolint: dogsled
1843+
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
1844+
o.ExperimentalContainersEnabled = true
1845+
})
1846+
ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "/bin/sh", func(arp *workspacesdk.AgentReconnectingPTYInit) {
1847+
arp.Container = ct.Container.ID
1848+
})
1849+
require.NoError(t, err, "failed to create ReconnectingPTY")
1850+
defer ac.Close()
1851+
tr := testutil.NewTerminalReader(t, ac)
1852+
1853+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1854+
return strings.Contains(line, "#") || strings.Contains(line, "$")
1855+
}), "find prompt")
1856+
1857+
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
1858+
Data: "hostname\r",
1859+
}), "write hostname")
1860+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1861+
return strings.Contains(line, "hostname")
1862+
}), "find hostname command")
1863+
1864+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1865+
return strings.Contains(line, ct.Container.Config.Hostname)
1866+
}), "find hostname output")
1867+
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
1868+
Data: "exit\r",
1869+
}), "write exit command")
1870+
1871+
// Wait for the connection to close.
1872+
require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
1873+
}
1874+
18031875
func TestAgent_Dial(t *testing.T) {
18041876
t.Parallel()
18051877

0 commit comments

Comments
 (0)