Skip to content

Commit dd6064c

Browse files
committed
Merge remote-tracking branch 'origin/main' into dk/prebuilds
2 parents 8cdd768 + ca414b0 commit dd6064c

File tree

83 files changed

+3279
-131
lines changed

Some content is hidden

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

83 files changed

+3279
-131
lines changed

.github/.linkspector.yml

+1
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ ignorePatterns:
2222
- pattern: "www.gnu.org"
2323
- pattern: "wiki.ubuntu.com"
2424
- pattern: "mutagen.io"
25+
- pattern: "docs.github.com"
2526
aliveStatusCodes:
2627
- 200

.golangci.yaml

+4-27
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,19 @@ linters-settings:
2424
enabled-checks:
2525
# - appendAssign
2626
# - appendCombine
27-
- argOrder
2827
# - assignOp
2928
# - badCall
30-
- badCond
3129
- badLock
3230
- badRegexp
3331
- boolExprSimplify
3432
# - builtinShadow
3533
- builtinShadowDecl
36-
- captLocal
37-
- caseOrder
38-
- codegenComment
3934
# - commentedOutCode
4035
- commentedOutImport
41-
- commentFormatting
42-
- defaultCaseOrder
4336
- deferUnlambda
4437
# - deprecatedComment
4538
# - docStub
46-
- dupArg
47-
- dupBranchBody
48-
- dupCase
4939
- dupImport
50-
- dupSubExpr
5140
# - elseif
5241
- emptyFallthrough
5342
# - emptyStringTest
@@ -56,56 +45,43 @@ linters-settings:
5645
# - exitAfterDefer
5746
# - exposedSyncMutex
5847
# - filepathJoin
59-
- flagDeref
60-
- flagName
6148
- hexLiteral
6249
# - httpNoBody
6350
# - hugeParam
6451
# - ifElseChain
6552
# - importShadow
6653
- indexAlloc
6754
- initClause
68-
- mapKey
6955
- methodExprCall
7056
# - nestingReduce
71-
- newDeref
7257
- nilValReturn
7358
# - octalLiteral
74-
- offBy1
7559
# - paramTypeCombine
7660
# - preferStringWriter
7761
# - preferWriteByte
7862
# - ptrToRefParam
7963
# - rangeExprCopy
8064
# - rangeValCopy
81-
- regexpMust
8265
- regexpPattern
8366
# - regexpSimplify
8467
- ruleguard
85-
- singleCaseSwitch
86-
- sloppyLen
8768
# - sloppyReassign
88-
- sloppyTypeAssert
8969
- sortSlice
9070
- sprintfQuotedString
9171
- sqlQuery
9272
# - stringConcatSimplify
9373
# - stringXbytes
9474
# - suspiciousSorting
95-
- switchTrue
9675
- truncateCmp
9776
- typeAssertChain
9877
# - typeDefFirst
99-
- typeSwitchVar
10078
# - typeUnparen
101-
- underef
10279
# - unlabelStmt
10380
# - unlambda
10481
# - unnamedResult
10582
# - unnecessaryBlock
10683
# - unnecessaryDefer
10784
# - unslice
108-
- valSwap
10985
- weakCond
11086
# - whyNoLint
11187
# - wrapperFunc
@@ -208,7 +184,7 @@ issues:
208184
- node_modules
209185
- .git
210186

211-
skip-files:
187+
exclude-files:
212188
- scripts/rules.go
213189

214190
# Rules listed here: https://github.com/securego/gosec#available-rules
@@ -224,15 +200,16 @@ issues:
224200
- path: scripts/*
225201
linters:
226202
- exhaustruct
203+
- path: scripts/rules.go
204+
linters:
205+
- ALL
227206

228207
fix: true
229208
max-issues-per-linter: 0
230209
max-same-issues: 0
231210

232211
run:
233212
timeout: 10m
234-
skip-files:
235-
- scripts/rules.go
236213

237214
# Over time, add more and more linters from
238215
# https://golangci-lint.run/usage/linters/ as the code improves.

agent/agent.go

+34-16
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
10831083
//
10841084
// An example is VS Code Remote, which must know the directory
10851085
// before initializing a connection.
1086-
manifest.Directory, err = expandDirectory(manifest.Directory)
1086+
manifest.Directory, err = expandPathToAbs(manifest.Directory)
10871087
if err != nil {
10881088
return xerrors.Errorf("expand directory: %w", err)
10891089
}
@@ -1123,16 +1123,35 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
11231123
}
11241124
}
11251125

1126-
err = a.scriptRunner.Init(manifest.Scripts, aAPI.ScriptCompleted)
1126+
var (
1127+
scripts = manifest.Scripts
1128+
scriptRunnerOpts []agentscripts.InitOption
1129+
)
1130+
if a.experimentalDevcontainersEnabled {
1131+
var dcScripts []codersdk.WorkspaceAgentScript
1132+
scripts, dcScripts = agentcontainers.ExtractAndInitializeDevcontainerScripts(a.logger, expandPathToAbs, manifest.Devcontainers, scripts)
1133+
// See ExtractAndInitializeDevcontainerScripts for motivation
1134+
// behind running dcScripts as post start scripts.
1135+
scriptRunnerOpts = append(scriptRunnerOpts, agentscripts.WithPostStartScripts(dcScripts...))
1136+
}
1137+
err = a.scriptRunner.Init(scripts, aAPI.ScriptCompleted, scriptRunnerOpts...)
11271138
if err != nil {
11281139
return xerrors.Errorf("init script runner: %w", err)
11291140
}
11301141
err = a.trackGoroutine(func() {
11311142
start := time.Now()
1132-
// here we use the graceful context because the script runner is not directly tied
1133-
// to the agent API.
1143+
// Here we use the graceful context because the script runner is
1144+
// not directly tied to the agent API.
1145+
//
1146+
// First we run the start scripts to ensure the workspace has
1147+
// been initialized and then the post start scripts which may
1148+
// depend on the workspace start scripts.
1149+
//
1150+
// Measure the time immediately after the start scripts have
1151+
// finished (both start and post start). For instance, an
1152+
// autostarted devcontainer will be included in this time.
11341153
err := a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecuteStartScripts)
1135-
// Measure the time immediately after the script has finished
1154+
err = errors.Join(err, a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecutePostStartScripts))
11361155
dur := time.Since(start).Seconds()
11371156
if err != nil {
11381157
a.logger.Warn(ctx, "startup script(s) failed", slog.Error(err))
@@ -1859,30 +1878,29 @@ func userHomeDir() (string, error) {
18591878
return u.HomeDir, nil
18601879
}
18611880

1862-
// expandDirectory converts a directory path to an absolute path.
1863-
// It primarily resolves the home directory and any environment
1864-
// variables that may be set
1865-
func expandDirectory(dir string) (string, error) {
1866-
if dir == "" {
1881+
// expandPathToAbs converts a path to an absolute path. It primarily resolves
1882+
// the home directory and any environment variables that may be set.
1883+
func expandPathToAbs(path string) (string, error) {
1884+
if path == "" {
18671885
return "", nil
18681886
}
1869-
if dir[0] == '~' {
1887+
if path[0] == '~' {
18701888
home, err := userHomeDir()
18711889
if err != nil {
18721890
return "", err
18731891
}
1874-
dir = filepath.Join(home, dir[1:])
1892+
path = filepath.Join(home, path[1:])
18751893
}
1876-
dir = os.ExpandEnv(dir)
1894+
path = os.ExpandEnv(path)
18771895

1878-
if !filepath.IsAbs(dir) {
1896+
if !filepath.IsAbs(path) {
18791897
home, err := userHomeDir()
18801898
if err != nil {
18811899
return "", err
18821900
}
1883-
dir = filepath.Join(home, dir)
1901+
path = filepath.Join(home, path)
18841902
}
1885-
return dir, nil
1903+
return path, nil
18861904
}
18871905

18881906
// EnvAgentSubsystem is the environment variable used to denote the

agent/agent_test.go

+128
Original file line numberDiff line numberDiff line change
@@ -1937,6 +1937,134 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
19371937
require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
19381938
}
19391939

1940+
// This tests end-to-end functionality of auto-starting a devcontainer.
1941+
// It runs "devcontainer up" which creates a real Docker container. As
1942+
// such, it does not run by default in CI.
1943+
//
1944+
// You can run it manually as follows:
1945+
//
1946+
// CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_DevcontainerAutostart
1947+
func TestAgent_DevcontainerAutostart(t *testing.T) {
1948+
t.Parallel()
1949+
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
1950+
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
1951+
}
1952+
1953+
ctx := testutil.Context(t, testutil.WaitLong)
1954+
1955+
// Connect to Docker
1956+
pool, err := dockertest.NewPool("")
1957+
require.NoError(t, err, "Could not connect to docker")
1958+
1959+
// Prepare temporary devcontainer for test (mywork).
1960+
devcontainerID := uuid.New()
1961+
tempWorkspaceFolder := t.TempDir()
1962+
tempWorkspaceFolder = filepath.Join(tempWorkspaceFolder, "mywork")
1963+
t.Logf("Workspace folder: %s", tempWorkspaceFolder)
1964+
devcontainerPath := filepath.Join(tempWorkspaceFolder, ".devcontainer")
1965+
err = os.MkdirAll(devcontainerPath, 0o755)
1966+
require.NoError(t, err, "create devcontainer directory")
1967+
devcontainerFile := filepath.Join(devcontainerPath, "devcontainer.json")
1968+
err = os.WriteFile(devcontainerFile, []byte(`{
1969+
"name": "mywork",
1970+
"image": "busybox:latest",
1971+
"cmd": ["sleep", "infinity"]
1972+
}`), 0o600)
1973+
require.NoError(t, err, "write devcontainer.json")
1974+
1975+
manifest := agentsdk.Manifest{
1976+
// Set up pre-conditions for auto-starting a devcontainer, the script
1977+
// is expected to be prepared by the provisioner normally.
1978+
Devcontainers: []codersdk.WorkspaceAgentDevcontainer{
1979+
{
1980+
ID: devcontainerID,
1981+
Name: "test",
1982+
WorkspaceFolder: tempWorkspaceFolder,
1983+
},
1984+
},
1985+
Scripts: []codersdk.WorkspaceAgentScript{
1986+
{
1987+
ID: devcontainerID,
1988+
LogSourceID: agentsdk.ExternalLogSourceID,
1989+
RunOnStart: true,
1990+
Script: "echo this-will-be-replaced",
1991+
DisplayName: "Dev Container (test)",
1992+
},
1993+
},
1994+
}
1995+
// nolint: dogsled
1996+
conn, _, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) {
1997+
o.ExperimentalDevcontainersEnabled = true
1998+
})
1999+
2000+
t.Logf("Waiting for container with label: devcontainer.local_folder=%s", tempWorkspaceFolder)
2001+
2002+
var container docker.APIContainers
2003+
require.Eventually(t, func() bool {
2004+
containers, err := pool.Client.ListContainers(docker.ListContainersOptions{All: true})
2005+
if err != nil {
2006+
t.Logf("Error listing containers: %v", err)
2007+
return false
2008+
}
2009+
2010+
for _, c := range containers {
2011+
t.Logf("Found container: %s with labels: %v", c.ID[:12], c.Labels)
2012+
if labelValue, ok := c.Labels["devcontainer.local_folder"]; ok {
2013+
if labelValue == tempWorkspaceFolder {
2014+
t.Logf("Found matching container: %s", c.ID[:12])
2015+
container = c
2016+
return true
2017+
}
2018+
}
2019+
}
2020+
2021+
return false
2022+
}, testutil.WaitSuperLong, testutil.IntervalMedium, "no container with workspace folder label found")
2023+
2024+
t.Cleanup(func() {
2025+
// We can't rely on pool here because the container is not
2026+
// managed by it (it is managed by @devcontainer/cli).
2027+
err := pool.Client.RemoveContainer(docker.RemoveContainerOptions{
2028+
ID: container.ID,
2029+
RemoveVolumes: true,
2030+
Force: true,
2031+
})
2032+
assert.NoError(t, err, "remove container")
2033+
})
2034+
2035+
containerInfo, err := pool.Client.InspectContainer(container.ID)
2036+
require.NoError(t, err, "inspect container")
2037+
t.Logf("Container state: status: %v", containerInfo.State.Status)
2038+
require.True(t, containerInfo.State.Running, "container should be running")
2039+
2040+
ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "", func(opts *workspacesdk.AgentReconnectingPTYInit) {
2041+
opts.Container = container.ID
2042+
})
2043+
require.NoError(t, err, "failed to create ReconnectingPTY")
2044+
defer ac.Close()
2045+
2046+
// Use terminal reader so we can see output in case somethin goes wrong.
2047+
tr := testutil.NewTerminalReader(t, ac)
2048+
2049+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
2050+
return strings.Contains(line, "#") || strings.Contains(line, "$")
2051+
}), "find prompt")
2052+
2053+
wantFileName := "file-from-devcontainer"
2054+
wantFile := filepath.Join(tempWorkspaceFolder, wantFileName)
2055+
2056+
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
2057+
// NOTE(mafredri): We must use absolute path here for some reason.
2058+
Data: fmt.Sprintf("touch /workspaces/mywork/%s; exit\r", wantFileName),
2059+
}), "create file inside devcontainer")
2060+
2061+
// Wait for the connection to close to ensure the touch was executed.
2062+
require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
2063+
2064+
_, err = os.Stat(wantFile)
2065+
require.NoError(t, err, "file should exist outside devcontainer")
2066+
}
2067+
19402068
func TestAgent_Dial(t *testing.T) {
19412069
t.Parallel()
19422070

agent/agentcontainers/containers_dockercli.go

+4-10
Original file line numberDiff line numberDiff line change
@@ -491,21 +491,15 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentContainer, []str
491491
// "8080" -> 8080, "tcp"
492492
func convertDockerPort(in string) (uint16, string, error) {
493493
parts := strings.Split(in, "/")
494+
p, err := strconv.ParseUint(parts[0], 10, 16)
495+
if err != nil {
496+
return 0, "", xerrors.Errorf("invalid port format: %s", in)
497+
}
494498
switch len(parts) {
495499
case 1:
496500
// assume it's a TCP port
497-
p, err := strconv.Atoi(parts[0])
498-
if err != nil {
499-
return 0, "", xerrors.Errorf("invalid port format: %s", in)
500-
}
501-
// #nosec G115 - Safe conversion since Docker TCP ports are limited to uint16 range
502501
return uint16(p), "tcp", nil
503502
case 2:
504-
p, err := strconv.Atoi(parts[0])
505-
if err != nil {
506-
return 0, "", xerrors.Errorf("invalid port format: %s", in)
507-
}
508-
// #nosec G115 - Safe conversion since Docker ports are limited to uint16 range
509503
return uint16(p), parts[1], nil
510504
default:
511505
return 0, "", xerrors.Errorf("invalid port format: %s", in)

0 commit comments

Comments
 (0)