Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit91dfec3

Browse files
committed
feat(agent): implement recreate for devcontainers
Fixescoder/internal#481Fixescoder/internal#482
1 parent3f3e201 commit91dfec3

16 files changed

+1375
-23
lines changed

‎.github/workflows/typos.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ extend-exclude = [
4242
"site/src/pages/SetupPage/countries.tsx",
4343
"provisioner/terraform/testdata/**",
4444
# notifications' golden files confuse the detector because of quoted-printable encoding
45-
"coderd/notifications/testdata/**"
45+
"coderd/notifications/testdata/**",
46+
"agent/agentcontainers/testdata/devcontainercli/**"
4647
]

‎agent/agentcontainers/containers.go

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"golang.org/x/xerrors"
1111

12+
"github.com/go-chi/chi/v5"
13+
1214
"github.com/coder/coder/v2/coderd/httpapi"
1315
"github.com/coder/coder/v2/codersdk"
1416
"github.com/coder/quartz"
@@ -20,9 +22,10 @@ const (
2022
getContainersTimeout=5*time.Second
2123
)
2224

23-
typedevcontainersHandlerstruct {
25+
typeHandlerstruct {
2426
cacheDuration time.Duration
2527
clLister
28+
dccliDevcontainerCLI
2629
clock quartz.Clock
2730

2831
// lockCh protects the below fields. We use a channel instead of a mutex so we
@@ -32,20 +35,26 @@ type devcontainersHandler struct {
3235
mtime time.Time
3336
}
3437

35-
// Option is a functional option fordevcontainersHandler.
36-
typeOptionfunc(*devcontainersHandler)
38+
// Option is a functional option forHandler.
39+
typeOptionfunc(*Handler)
3740

3841
// WithLister sets the agentcontainers.Lister implementation to use.
3942
// The default implementation uses the Docker CLI to list containers.
4043
funcWithLister(clLister)Option {
41-
returnfunc(ch*devcontainersHandler) {
44+
returnfunc(ch*Handler) {
4245
ch.cl=cl
4346
}
4447
}
4548

46-
// New returns a new devcontainersHandler with the given options applied.
47-
funcNew(options...Option) http.Handler {
48-
ch:=&devcontainersHandler{
49+
funcWithDevcontainerCLI(dccliDevcontainerCLI)Option {
50+
returnfunc(ch*Handler) {
51+
ch.dccli=dccli
52+
}
53+
}
54+
55+
// New returns a new Handler with the given options applied.
56+
funcNew(options...Option)*Handler {
57+
ch:=&Handler{
4958
lockCh:make(chanstruct{},1),
5059
}
5160
for_,opt:=rangeoptions {
@@ -54,7 +63,7 @@ func New(options ...Option) http.Handler {
5463
returnch
5564
}
5665

57-
func (ch*devcontainersHandler)ServeHTTP(rw http.ResponseWriter,r*http.Request) {
66+
func (ch*Handler)List(rw http.ResponseWriter,r*http.Request) {
5867
select {
5968
case<-r.Context().Done():
6069
// Client went away.
@@ -80,7 +89,7 @@ func (ch *devcontainersHandler) ServeHTTP(rw http.ResponseWriter, r *http.Reques
8089
}
8190
}
8291

83-
func (ch*devcontainersHandler)getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
92+
func (ch*Handler)getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
8493
select {
8594
case<-ctx.Done():
8695
return codersdk.WorkspaceAgentListContainersResponse{},ctx.Err()
@@ -149,3 +158,61 @@ var _ Lister = NoopLister{}
149158
func (NoopLister)List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
150159
return codersdk.WorkspaceAgentListContainersResponse{},nil
151160
}
161+
162+
func (ch*Handler)Recreate(w http.ResponseWriter,r*http.Request) {
163+
ctx:=r.Context()
164+
id:=chi.URLParam(r,"id")
165+
166+
ifid=="" {
167+
httpapi.Write(ctx,w,http.StatusBadRequest, codersdk.Response{
168+
Message:"Missing container ID or name",
169+
Detail:"Container ID or name is required to recreate a container.",
170+
})
171+
return
172+
}
173+
174+
containers,err:=ch.cl.List(ctx)
175+
iferr!=nil {
176+
httpapi.Write(ctx,w,http.StatusInternalServerError, codersdk.Response{
177+
Message:"Could not list containers",
178+
Detail:err.Error(),
179+
})
180+
return
181+
}
182+
183+
containerIdx:=slices.IndexFunc(containers.Containers,func(c codersdk.WorkspaceAgentContainer)bool {
184+
returnc.Match(id)
185+
})
186+
ifcontainerIdx==-1 {
187+
httpapi.Write(ctx,w,http.StatusNotFound, codersdk.Response{
188+
Message:"Container not found",
189+
Detail:"Container ID or name not found in the list of containers.",
190+
})
191+
return
192+
}
193+
194+
container:=containers.Containers[containerIdx]
195+
workspaceFolder:=container.Labels[DevcontainerLocalFolderLabel]
196+
configPath:=container.Labels[DevcontainerConfigFileLabel]
197+
198+
// Workspace folder is required to recreate a container, we don't verify
199+
// the config path here because it's optional.
200+
ifworkspaceFolder=="" {
201+
httpapi.Write(ctx,w,http.StatusBadRequest, codersdk.Response{
202+
Message:"Missing workspace folder",
203+
Detail:"Workspace folder is required to recreate a container.",
204+
})
205+
return
206+
}
207+
208+
_,err=ch.dccli.Up(ctx,workspaceFolder,configPath,WithRemoveExistingContainer())
209+
iferr!=nil {
210+
httpapi.Write(ctx,w,http.StatusInternalServerError, codersdk.Response{
211+
Message:"Could not recreate container",
212+
Detail:err.Error(),
213+
})
214+
return
215+
}
216+
217+
w.WriteHeader(http.StatusNoContent)
218+
}

‎agent/agentcontainers/containers_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ func TestContainersHandler(t *testing.T) {
277277
ctrl=gomock.NewController(t)
278278
mockLister=acmock.NewMockLister(ctrl)
279279
now=time.Now().UTC()
280-
ch=devcontainersHandler{
280+
ch=Handler{
281281
cacheDuration:tc.cacheDur,
282282
cl:mockLister,
283283
clock:clk,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package agentcontainers_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/go-chi/chi/v5"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/v2/agent/agentcontainers"
15+
"github.com/coder/coder/v2/codersdk"
16+
)
17+
18+
// fakeLister implements the agentcontainers.Lister interface for
19+
// testing.
20+
typefakeListerstruct {
21+
containers codersdk.WorkspaceAgentListContainersResponse
22+
errerror
23+
}
24+
25+
func (f*fakeLister)List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
26+
returnf.containers,f.err
27+
}
28+
29+
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
30+
// interface for testing.
31+
typefakeDevcontainerCLIstruct {
32+
idstring
33+
errerror
34+
}
35+
36+
func (f*fakeDevcontainerCLI)Up(_ context.Context,_,_string,_...agentcontainers.DevcontainerCLIUpOptions) (string,error) {
37+
returnf.id,f.err
38+
}
39+
40+
funcTestHandler(t*testing.T) {
41+
t.Parallel()
42+
43+
t.Run("Recreate",func(t*testing.T) {
44+
t.Parallel()
45+
46+
validContainer:= codersdk.WorkspaceAgentContainer{
47+
ID:"container-id",
48+
FriendlyName:"container-name",
49+
Labels:map[string]string{
50+
agentcontainers.DevcontainerLocalFolderLabel:"/workspace",
51+
agentcontainers.DevcontainerConfigFileLabel:"/workspace/.devcontainer/devcontainer.json",
52+
},
53+
}
54+
55+
missingFolderContainer:= codersdk.WorkspaceAgentContainer{
56+
ID:"missing-folder-container",
57+
FriendlyName:"missing-folder-container",
58+
Labels:map[string]string{},
59+
}
60+
61+
tests:= []struct {
62+
namestring
63+
containerIDstring
64+
lister*fakeLister
65+
devcontainerCLI*fakeDevcontainerCLI
66+
wantStatusint
67+
wantBodystring
68+
}{
69+
{
70+
name:"Missing ID",
71+
containerID:"",
72+
lister:&fakeLister{},
73+
devcontainerCLI:&fakeDevcontainerCLI{},
74+
wantStatus:http.StatusBadRequest,
75+
wantBody:"Missing container ID or name",
76+
},
77+
{
78+
name:"List error",
79+
containerID:"container-id",
80+
lister:&fakeLister{
81+
err:xerrors.New("list error"),
82+
},
83+
devcontainerCLI:&fakeDevcontainerCLI{},
84+
wantStatus:http.StatusInternalServerError,
85+
wantBody:"Could not list containers",
86+
},
87+
{
88+
name:"Container not found",
89+
containerID:"nonexistent-container",
90+
lister:&fakeLister{
91+
containers: codersdk.WorkspaceAgentListContainersResponse{
92+
Containers: []codersdk.WorkspaceAgentContainer{validContainer},
93+
},
94+
},
95+
devcontainerCLI:&fakeDevcontainerCLI{},
96+
wantStatus:http.StatusNotFound,
97+
wantBody:"Container not found",
98+
},
99+
{
100+
name:"Missing workspace folder",
101+
containerID:"missing-folder-container",
102+
lister:&fakeLister{
103+
containers: codersdk.WorkspaceAgentListContainersResponse{
104+
Containers: []codersdk.WorkspaceAgentContainer{missingFolderContainer},
105+
},
106+
},
107+
devcontainerCLI:&fakeDevcontainerCLI{},
108+
wantStatus:http.StatusBadRequest,
109+
wantBody:"Missing workspace folder",
110+
},
111+
{
112+
name:"Devcontainer CLI error",
113+
containerID:"container-id",
114+
lister:&fakeLister{
115+
containers: codersdk.WorkspaceAgentListContainersResponse{
116+
Containers: []codersdk.WorkspaceAgentContainer{validContainer},
117+
},
118+
},
119+
devcontainerCLI:&fakeDevcontainerCLI{
120+
err:xerrors.New("devcontainer CLI error"),
121+
},
122+
wantStatus:http.StatusInternalServerError,
123+
wantBody:"Could not recreate container",
124+
},
125+
{
126+
name:"OK",
127+
containerID:"container-id",
128+
lister:&fakeLister{
129+
containers: codersdk.WorkspaceAgentListContainersResponse{
130+
Containers: []codersdk.WorkspaceAgentContainer{validContainer},
131+
},
132+
},
133+
devcontainerCLI:&fakeDevcontainerCLI{},
134+
wantStatus:http.StatusNoContent,
135+
wantBody:"",
136+
},
137+
}
138+
139+
for_,tt:=rangetests {
140+
tt:=tt
141+
t.Run(tt.name,func(t*testing.T) {
142+
t.Parallel()
143+
144+
// Create router with URL parameter handling
145+
r:=chi.NewRouter()
146+
handler:=agentcontainers.New(
147+
agentcontainers.WithLister(tt.lister),
148+
agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI),
149+
)
150+
151+
r.Post("/containers/{id}/recreate",handler.Recreate)
152+
153+
// Create test request
154+
req:=httptest.NewRequest(http.MethodPost,"/containers/"+tt.containerID+"/recreate",nil)
155+
rec:=httptest.NewRecorder()
156+
157+
// Run the handler
158+
r.ServeHTTP(rec,req)
159+
160+
// Check status code - use require to fail fast if this doesn't match
161+
require.Equal(t,tt.wantStatus,rec.Code,"status code mismatch")
162+
163+
// For non-empty expected responses, check the body contains the expected message
164+
iftt.wantBody!="" {
165+
// For error responses, contains is appropriate as we don't need to test the entire response
166+
assert.Contains(t,rec.Body.String(),tt.wantBody,"response body mismatch")
167+
}elseiftt.wantStatus==http.StatusNoContent {
168+
// For success cases, verify the body is actually empty
169+
assert.Empty(t,rec.Body.String(),"expected empty response body")
170+
}
171+
})
172+
}
173+
})
174+
}

‎agent/agentcontainers/devcontainer.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import (
1212
"github.com/coder/coder/v2/codersdk"
1313
)
1414

15+
const (
16+
// DevcontainerLocalFolderLabel is the label that contains the path to
17+
// the local workspace folder for a devcontainer.
18+
DevcontainerLocalFolderLabel="devcontainer.local_folder"
19+
// DevcontainerConfigFileLabel is the label that contains the path to
20+
// the devcontainer.json configuration file.
21+
DevcontainerConfigFileLabel="devcontainer.config_file"
22+
)
23+
1524
constdevcontainerUpScriptTemplate=`
1625
if ! which devcontainer > /dev/null 2>&1; then
1726
echo "ERROR: Unable to start devcontainer, @devcontainers/cli is not installed."
@@ -52,8 +61,10 @@ ScriptLoop:
5261
}
5362

5463
funcdevcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer,script codersdk.WorkspaceAgentScript) codersdk.WorkspaceAgentScript {
55-
varargs []string
56-
args=append(args,fmt.Sprintf("--workspace-folder %q",dc.WorkspaceFolder))
64+
args:= []string{
65+
"--log-format json",
66+
fmt.Sprintf("--workspace-folder %q",dc.WorkspaceFolder),
67+
}
5768
ifdc.ConfigPath!="" {
5869
args=append(args,fmt.Sprintf("--config %q",dc.ConfigPath))
5970
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp