Skip to content

Commit e7c5811

Browse files
committed
Merge branch 'main' into 16055-column-sorting
2 parents b01c35f + 075e5f4 commit e7c5811

File tree

101 files changed

+1038
-586
lines changed

Some content is hidden

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

101 files changed

+1038
-586
lines changed

.github/ISSUE_TEMPLATE/1-bug.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "🐞 Bug"
22
description: "File a bug report."
3-
title: "<title>"
3+
title: "bug: "
44
labels: ["needs-triage"]
55
body:
66
- type: checkboxes

.github/workflows/scorecard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ jobs:
4747

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

.github/workflows/security.yaml

Lines changed: 3 additions & 3 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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
41+
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
51+
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
5252

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

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

agent/agentcontainers/containers_dockercli.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,18 @@ func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container str
182182
if !ok {
183183
return nil, nil
184184
}
185-
meta := struct {
186-
RemoteEnv map[string]string `json:"remoteEnv"`
187-
}{}
185+
186+
meta := make([]DevContainerMeta, 0)
188187
if err := json.Unmarshal([]byte(rawMeta), &meta); err != nil {
189188
return nil, xerrors.Errorf("unmarshal devcontainer.metadata: %w", err)
190189
}
191190

192191
// The environment variables are stored in the `remoteEnv` key.
193-
env := make([]string, 0, len(meta.RemoteEnv))
194-
for k, v := range meta.RemoteEnv {
195-
env = append(env, fmt.Sprintf("%s=%s", k, v))
192+
env := make([]string, 0)
193+
for _, m := range meta {
194+
for k, v := range m.RemoteEnv {
195+
env = append(env, fmt.Sprintf("%s=%s", k, v))
196+
}
196197
}
197198
slices.Sort(env)
198199
return env, nil

agent/agentcontainers/containers_internal_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestIntegrationDocker(t *testing.T) {
5353
Cmd: []string{"sleep", "infnity"},
5454
Labels: map[string]string{
5555
"com.coder.test": testLabelValue,
56-
"devcontainer.metadata": `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`,
56+
"devcontainer.metadata": `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`,
5757
},
5858
Mounts: []string{testTempDir + ":" + testTempDir},
5959
ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)},
@@ -437,38 +437,46 @@ func TestDockerEnvInfoer(t *testing.T) {
437437
}{
438438
{
439439
image: "busybox:latest",
440-
labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
440+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
441441

442442
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
443443
expectedUsername: "root",
444444
expectedUserShell: "/bin/sh",
445445
},
446446
{
447447
image: "busybox:latest",
448-
labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
448+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
449449
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
450450
containerUser: "root",
451451
expectedUsername: "root",
452452
expectedUserShell: "/bin/sh",
453453
},
454454
{
455455
image: "codercom/enterprise-minimal:ubuntu",
456-
labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
456+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
457457
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
458458
expectedUsername: "coder",
459459
expectedUserShell: "/bin/bash",
460460
},
461461
{
462462
image: "codercom/enterprise-minimal:ubuntu",
463-
labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
463+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
464464
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
465465
containerUser: "coder",
466466
expectedUsername: "coder",
467467
expectedUserShell: "/bin/bash",
468468
},
469469
{
470470
image: "codercom/enterprise-minimal:ubuntu",
471-
labels: map[string]string{`devcontainer.metadata`: `{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
471+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
472+
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
473+
containerUser: "root",
474+
expectedUsername: "root",
475+
expectedUserShell: "/bin/bash",
476+
},
477+
{
478+
image: "codercom/enterprise-minimal:ubuntu",
479+
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar"}},{"remoteEnv": {"MULTILINE": "foo\nbar\nbaz"}}]`},
472480
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
473481
containerUser: "root",
474482
expectedUsername: "root",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package agentcontainers
2+
3+
type DevContainerMeta struct {
4+
RemoteEnv map[string]string `json:"remoteEnv,omitempty"`
5+
}

agent/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func (a *agent) apiHandler() http.Handler {
4141
r.Get("/api/v0/containers", ch.ServeHTTP)
4242
r.Get("/api/v0/listening-ports", lp.handler)
4343
r.Get("/api/v0/netcheck", a.HandleNetcheck)
44+
r.Post("/api/v0/list-directory", a.HandleLS)
4445
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
4546
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
4647
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)

agent/ls.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package agent
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/shirou/gopsutil/v4/disk"
13+
"golang.org/x/xerrors"
14+
15+
"github.com/coder/coder/v2/coderd/httpapi"
16+
"github.com/coder/coder/v2/codersdk"
17+
)
18+
19+
var WindowsDriveRegex = regexp.MustCompile(`^[a-zA-Z]:\\$`)
20+
21+
func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
22+
ctx := r.Context()
23+
24+
var query LSRequest
25+
if !httpapi.Read(ctx, rw, r, &query) {
26+
return
27+
}
28+
29+
resp, err := listFiles(query)
30+
if err != nil {
31+
status := http.StatusInternalServerError
32+
switch {
33+
case errors.Is(err, os.ErrNotExist):
34+
status = http.StatusNotFound
35+
case errors.Is(err, os.ErrPermission):
36+
status = http.StatusForbidden
37+
default:
38+
}
39+
httpapi.Write(ctx, rw, status, codersdk.Response{
40+
Message: err.Error(),
41+
})
42+
return
43+
}
44+
45+
httpapi.Write(ctx, rw, http.StatusOK, resp)
46+
}
47+
48+
func listFiles(query LSRequest) (LSResponse, error) {
49+
var fullPath []string
50+
switch query.Relativity {
51+
case LSRelativityHome:
52+
home, err := os.UserHomeDir()
53+
if err != nil {
54+
return LSResponse{}, xerrors.Errorf("failed to get user home directory: %w", err)
55+
}
56+
fullPath = []string{home}
57+
case LSRelativityRoot:
58+
if runtime.GOOS == "windows" {
59+
if len(query.Path) == 0 {
60+
return listDrives()
61+
}
62+
if !WindowsDriveRegex.MatchString(query.Path[0]) {
63+
return LSResponse{}, xerrors.Errorf("invalid drive letter %q", query.Path[0])
64+
}
65+
} else {
66+
fullPath = []string{"/"}
67+
}
68+
default:
69+
return LSResponse{}, xerrors.Errorf("unsupported relativity type %q", query.Relativity)
70+
}
71+
72+
fullPath = append(fullPath, query.Path...)
73+
fullPathRelative := filepath.Join(fullPath...)
74+
absolutePathString, err := filepath.Abs(fullPathRelative)
75+
if err != nil {
76+
return LSResponse{}, xerrors.Errorf("failed to get absolute path of %q: %w", fullPathRelative, err)
77+
}
78+
79+
f, err := os.Open(absolutePathString)
80+
if err != nil {
81+
return LSResponse{}, xerrors.Errorf("failed to open directory %q: %w", absolutePathString, err)
82+
}
83+
defer f.Close()
84+
85+
stat, err := f.Stat()
86+
if err != nil {
87+
return LSResponse{}, xerrors.Errorf("failed to stat directory %q: %w", absolutePathString, err)
88+
}
89+
90+
if !stat.IsDir() {
91+
return LSResponse{}, xerrors.Errorf("path %q is not a directory", absolutePathString)
92+
}
93+
94+
// `contents` may be partially populated even if the operation fails midway.
95+
contents, _ := f.ReadDir(-1)
96+
respContents := make([]LSFile, 0, len(contents))
97+
for _, file := range contents {
98+
respContents = append(respContents, LSFile{
99+
Name: file.Name(),
100+
AbsolutePathString: filepath.Join(absolutePathString, file.Name()),
101+
IsDir: file.IsDir(),
102+
})
103+
}
104+
105+
absolutePath := pathToArray(absolutePathString)
106+
107+
return LSResponse{
108+
AbsolutePath: absolutePath,
109+
AbsolutePathString: absolutePathString,
110+
Contents: respContents,
111+
}, nil
112+
}
113+
114+
func listDrives() (LSResponse, error) {
115+
partitionStats, err := disk.Partitions(true)
116+
if err != nil {
117+
return LSResponse{}, xerrors.Errorf("failed to get partitions: %w", err)
118+
}
119+
contents := make([]LSFile, 0, len(partitionStats))
120+
for _, a := range partitionStats {
121+
// Drive letters on Windows have a trailing separator as part of their name.
122+
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
123+
name := a.Mountpoint + string(os.PathSeparator)
124+
contents = append(contents, LSFile{
125+
Name: name,
126+
AbsolutePathString: name,
127+
IsDir: true,
128+
})
129+
}
130+
131+
return LSResponse{
132+
AbsolutePath: []string{},
133+
AbsolutePathString: "",
134+
Contents: contents,
135+
}, nil
136+
}
137+
138+
func pathToArray(path string) []string {
139+
out := strings.FieldsFunc(path, func(r rune) bool {
140+
return r == os.PathSeparator
141+
})
142+
// Drive letters on Windows have a trailing separator as part of their name.
143+
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
144+
if runtime.GOOS == "windows" && len(out) > 0 {
145+
out[0] += string(os.PathSeparator)
146+
}
147+
return out
148+
}
149+
150+
type LSRequest struct {
151+
// e.g. [], ["repos", "coder"],
152+
Path []string `json:"path"`
153+
// Whether the supplied path is relative to the user's home directory,
154+
// or the root directory.
155+
Relativity LSRelativity `json:"relativity"`
156+
}
157+
158+
type LSResponse struct {
159+
AbsolutePath []string `json:"absolute_path"`
160+
// Returned so clients can display the full path to the user, and
161+
// copy it to configure file sync
162+
// e.g. Windows: "C:\\Users\\coder"
163+
// Linux: "/home/coder"
164+
AbsolutePathString string `json:"absolute_path_string"`
165+
Contents []LSFile `json:"contents"`
166+
}
167+
168+
type LSFile struct {
169+
Name string `json:"name"`
170+
// e.g. "C:\\Users\\coder\\hello.txt"
171+
// "/home/coder/hello.txt"
172+
AbsolutePathString string `json:"absolute_path_string"`
173+
IsDir bool `json:"is_dir"`
174+
}
175+
176+
type LSRelativity string
177+
178+
const (
179+
LSRelativityRoot LSRelativity = "root"
180+
LSRelativityHome LSRelativity = "home"
181+
)

0 commit comments

Comments
 (0)