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

Commitf87d23f

Browse files
committed
fix(agent/agentcontainer): allow lifecycle script error on devcontainer up
When devcontainer up fails due to a lifecycle script error likepostCreateCommand, the CLI still returns a container ID. Previouslythis was discarded and the devcontainer marked as failed. Now wecontinue with agent injection if a container ID is available,allowing users to debug the issue in the running container.Fixescoder/internal#1137
1 parentffc3e81 commitf87d23f

File tree

5 files changed

+235
-75
lines changed

5 files changed

+235
-75
lines changed

‎agent/agentcontainers/api.go‎

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,26 +1347,40 @@ func (api *API) CreateDevcontainer(workspaceFolder, configPath string, opts ...D
13471347
upOptions:= []DevcontainerCLIUpOptions{WithUpOutput(infoW,errW)}
13481348
upOptions=append(upOptions,opts...)
13491349

1350-
_,err:=api.dccli.Up(ctx,dc.WorkspaceFolder,configPath,upOptions...)
1351-
iferr!=nil {
1350+
containerID,upErr:=api.dccli.Up(ctx,dc.WorkspaceFolder,configPath,upOptions...)
1351+
ifupErr!=nil {
13521352
// No need to log if the API is closing (context canceled), as this
13531353
// is expected behavior when the API is shutting down.
1354-
if!errors.Is(err,context.Canceled) {
1355-
logger.Error(ctx,"devcontainer creation failed",slog.Error(err))
1354+
if!errors.Is(upErr,context.Canceled) {
1355+
logger.Error(ctx,"devcontainer creation failed",slog.Error(upErr))
13561356
}
13571357

1358-
api.mu.Lock()
1359-
dc=api.knownDevcontainers[dc.WorkspaceFolder]
1360-
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusError
1361-
dc.Error=err.Error()
1362-
api.knownDevcontainers[dc.WorkspaceFolder]=dc
1363-
api.recreateErrorTimes[dc.WorkspaceFolder]=api.clock.Now("agentcontainers","recreate","errorTimes")
1364-
api.mu.Unlock()
1358+
// If we don't have a container ID, the error is fatal, so we
1359+
// should mark the devcontainer as errored and return.
1360+
ifcontainerID=="" {
1361+
api.mu.Lock()
1362+
dc=api.knownDevcontainers[dc.WorkspaceFolder]
1363+
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusError
1364+
dc.Error=upErr.Error()
1365+
api.knownDevcontainers[dc.WorkspaceFolder]=dc
1366+
api.recreateErrorTimes[dc.WorkspaceFolder]=api.clock.Now("agentcontainers","recreate","errorTimes")
1367+
api.broadcastUpdatesLocked()
1368+
api.mu.Unlock()
13651369

1366-
returnxerrors.Errorf("start devcontainer: %w",err)
1367-
}
1370+
returnxerrors.Errorf("start devcontainer: %w",upErr)
1371+
}
13681372

1369-
logger.Info(ctx,"devcontainer created successfully")
1373+
// If we have a container ID, it means the container was created
1374+
// but a lifecycle script (e.g. postCreateCommand) failed. In this
1375+
// case, we still want to refresh containers to pick up the new
1376+
// container, inject the agent, and allow the user to debug the
1377+
// issue. We store the error to surface it to the user.
1378+
logger.Warn(ctx,"devcontainer created with errors (e.g. lifecycle script failure), container is available",
1379+
slog.F("container_id",containerID),
1380+
)
1381+
}else {
1382+
logger.Info(ctx,"devcontainer created successfully")
1383+
}
13701384

13711385
api.mu.Lock()
13721386
dc=api.knownDevcontainers[dc.WorkspaceFolder]
@@ -1376,13 +1390,18 @@ func (api *API) CreateDevcontainer(workspaceFolder, configPath string, opts ...D
13761390
// to minimize the time between API consistency, we guess the status
13771391
// based on the container state.
13781392
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusStopped
1379-
ifdc.Container!=nil {
1380-
ifdc.Container.Running {
1381-
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusRunning
1382-
}
1393+
ifdc.Container!=nil&&dc.Container.Running {
1394+
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusRunning
13831395
}
13841396
dc.Dirty=false
1385-
dc.Error=""
1397+
ifupErr!=nil {
1398+
// If there was a lifecycle script error but we have a container ID,
1399+
// the container is running so we should set the status to Running.
1400+
dc.Status=codersdk.WorkspaceAgentDevcontainerStatusRunning
1401+
dc.Error=upErr.Error()
1402+
}else {
1403+
dc.Error=""
1404+
}
13861405
api.recreateSuccessTimes[dc.WorkspaceFolder]=api.clock.Now("agentcontainers","recreate","successTimes")
13871406
api.knownDevcontainers[dc.WorkspaceFolder]=dc
13881407
api.broadcastUpdatesLocked()

‎agent/agentcontainers/api_test.go‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,118 @@ func TestAPI(t *testing.T) {
20702070
require.Equal(t,"",response.Devcontainers[0].Error)
20712071
})
20722072

2073+
// This test verifies that when devcontainer up fails due to a
2074+
// lifecycle script error (such as postCreateCommand failing) but the
2075+
// container was successfully created, we still proceed with the
2076+
// devcontainer. The container should be available for use and the
2077+
// agent should be injected.
2078+
t.Run("DuringUpWithContainerID",func(t*testing.T) {
2079+
t.Parallel()
2080+
2081+
var (
2082+
ctx=testutil.Context(t,testutil.WaitMedium)
2083+
logger=slogtest.Make(t,&slogtest.Options{IgnoreErrors:true}).Leveled(slog.LevelDebug)
2084+
mClock=quartz.NewMock(t)
2085+
2086+
testContainer= codersdk.WorkspaceAgentContainer{
2087+
ID:"test-container-id",
2088+
FriendlyName:"test-container",
2089+
Image:"test-image",
2090+
Running:true,
2091+
CreatedAt:time.Now(),
2092+
Labels:map[string]string{
2093+
agentcontainers.DevcontainerLocalFolderLabel:"/workspaces/project",
2094+
agentcontainers.DevcontainerConfigFileLabel:"/workspaces/project/.devcontainer/devcontainer.json",
2095+
},
2096+
}
2097+
fCCLI=&fakeContainerCLI{
2098+
containers: codersdk.WorkspaceAgentListContainersResponse{
2099+
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
2100+
},
2101+
arch:"amd64",
2102+
}
2103+
fDCCLI=&fakeDevcontainerCLI{
2104+
upID:testContainer.ID,
2105+
upErrC:make(chanfunc()error,1),
2106+
}
2107+
fSAC=&fakeSubAgentClient{
2108+
logger:logger.Named("fakeSubAgentClient"),
2109+
}
2110+
2111+
testDevcontainer= codersdk.WorkspaceAgentDevcontainer{
2112+
ID:uuid.New(),
2113+
Name:"test-devcontainer",
2114+
WorkspaceFolder:"/workspaces/project",
2115+
ConfigPath:"/workspaces/project/.devcontainer/devcontainer.json",
2116+
Status:codersdk.WorkspaceAgentDevcontainerStatusStopped,
2117+
}
2118+
)
2119+
2120+
mClock.Set(time.Now()).MustWait(ctx)
2121+
tickerTrap:=mClock.Trap().TickerFunc("updaterLoop")
2122+
nowRecreateSuccessTrap:=mClock.Trap().Now("recreate","successTimes")
2123+
2124+
api:=agentcontainers.NewAPI(logger,
2125+
agentcontainers.WithClock(mClock),
2126+
agentcontainers.WithContainerCLI(fCCLI),
2127+
agentcontainers.WithDevcontainerCLI(fDCCLI),
2128+
agentcontainers.WithDevcontainers(
2129+
[]codersdk.WorkspaceAgentDevcontainer{testDevcontainer},
2130+
[]codersdk.WorkspaceAgentScript{{ID:testDevcontainer.ID,LogSourceID:uuid.New()}},
2131+
),
2132+
agentcontainers.WithSubAgentClient(fSAC),
2133+
agentcontainers.WithSubAgentURL("test-subagent-url"),
2134+
agentcontainers.WithWatcher(watcher.NewNoop()),
2135+
)
2136+
api.Start()
2137+
deferfunc() {
2138+
close(fDCCLI.upErrC)
2139+
api.Close()
2140+
}()
2141+
2142+
r:=chi.NewRouter()
2143+
r.Mount("/",api.Routes())
2144+
2145+
tickerTrap.MustWait(ctx).MustRelease(ctx)
2146+
tickerTrap.Close()
2147+
2148+
// Send a recreate request to trigger devcontainer up.
2149+
req:=httptest.NewRequest(http.MethodPost,"/devcontainers/"+testDevcontainer.ID.String()+"/recreate",nil)
2150+
rec:=httptest.NewRecorder()
2151+
r.ServeHTTP(rec,req)
2152+
require.Equal(t,http.StatusAccepted,rec.Code)
2153+
2154+
// Simulate a lifecycle script failure. The devcontainer CLI
2155+
// will return an error but also provide a container ID since
2156+
// the container was created before the script failed.
2157+
simulatedError:=xerrors.New("postCreateCommand failed with exit code 1")
2158+
testutil.RequireSend(ctx,t,fDCCLI.upErrC,func()error {returnsimulatedError })
2159+
2160+
// Wait for the recreate operation to complete. We expect it to
2161+
// record a success time because the container was created.
2162+
nowRecreateSuccessTrap.MustWait(ctx).MustRelease(ctx)
2163+
nowRecreateSuccessTrap.Close()
2164+
2165+
req=httptest.NewRequest(http.MethodGet,"/",nil)
2166+
rec=httptest.NewRecorder()
2167+
r.ServeHTTP(rec,req)
2168+
require.Equal(t,http.StatusOK,rec.Code)
2169+
2170+
varresponse codersdk.WorkspaceAgentListContainersResponse
2171+
err:=json.NewDecoder(rec.Body).Decode(&response)
2172+
require.NoError(t,err)
2173+
2174+
// Verify that the devcontainer is running and has the container
2175+
// associated with it despite the lifecycle script error. The
2176+
// error may be cleared during refresh if agent injection
2177+
// succeeds, but the important thing is that the container is
2178+
// available for use.
2179+
require.Len(t,response.Devcontainers,1)
2180+
assert.Equal(t,codersdk.WorkspaceAgentDevcontainerStatusRunning,response.Devcontainers[0].Status)
2181+
assert.NotNil(t,response.Devcontainers[0].Container)
2182+
assert.Equal(t,testContainer.ID,response.Devcontainers[0].Container.ID)
2183+
})
2184+
20732185
t.Run("DuringInjection",func(t*testing.T) {
20742186
t.Parallel()
20752187

‎agent/agentcontainers/devcontainercli.go‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -263,18 +263,28 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
263263
}
264264

265265
iferr:=cmd.Run();err!=nil {
266-
_,err2:=parseDevcontainerCLILastLine[devcontainerCLIResult](ctx,logger,stdoutBuf.Bytes())
266+
result,err2:=parseDevcontainerCLILastLine[devcontainerCLIResult](ctx,logger,stdoutBuf.Bytes())
267267
iferr2!=nil {
268268
err=errors.Join(err,err2)
269269
}
270-
return"",err
270+
// Return the container ID if available, even if there was an error.
271+
// This can happen if the container was created successfully but a
272+
// lifecycle script (e.g. postCreateCommand) failed.
273+
returnresult.ContainerID,err
271274
}
272275

273276
result,err:=parseDevcontainerCLILastLine[devcontainerCLIResult](ctx,logger,stdoutBuf.Bytes())
274277
iferr!=nil {
275278
return"",err
276279
}
277280

281+
// Check if the result indicates an error (e.g. lifecycle script failure)
282+
// but still has a container ID, allowing the caller to potentially
283+
// continue with the container that was created.
284+
iferr:=result.Err();err!=nil {
285+
returnresult.ContainerID,err
286+
}
287+
278288
returnresult.ContainerID,nil
279289
}
280290

@@ -394,7 +404,10 @@ func parseDevcontainerCLILastLine[T any](ctx context.Context, logger slog.Logger
394404
typedevcontainerCLIResultstruct {
395405
Outcomestring`json:"outcome"`// "error", "success".
396406

397-
// The following fields are set if outcome is success.
407+
// The following fields are typically set if outcome is success, but
408+
// ContainerID may also be present when outcome is error if the
409+
// container was created but a lifecycle script (e.g. postCreateCommand)
410+
// failed.
398411
ContainerIDstring`json:"containerId"`
399412
RemoteUserstring`json:"remoteUser"`
400413
RemoteWorkspaceFolderstring`json:"remoteWorkspaceFolder"`
@@ -404,18 +417,6 @@ type devcontainerCLIResult struct {
404417
Descriptionstring`json:"description"`
405418
}
406419

407-
func (r*devcontainerCLIResult)UnmarshalJSON(data []byte)error {
408-
typewrapperResultdevcontainerCLIResult
409-
410-
varwrappedResultwrapperResult
411-
iferr:=json.Unmarshal(data,&wrappedResult);err!=nil {
412-
returnerr
413-
}
414-
415-
*r=devcontainerCLIResult(wrappedResult)
416-
returnr.Err()
417-
}
418-
419420
func (rdevcontainerCLIResult)Err()error {
420421
ifr.Outcome=="success" {
421422
returnnil

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp