mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): allow recreating devcontainers and showing dirty status (#18049)
This change allows showing the devcontainer dirty status in the UI as well as a recreate button to update the devcontainer. Closes #16424
This commit is contained in:
+1
-1
@@ -2226,7 +2226,7 @@ func TestAgent_DevcontainerRecreate(t *testing.T) {
|
||||
// devcontainer, we do it in a goroutine so we can process logs
|
||||
// concurrently.
|
||||
go func(container codersdk.WorkspaceAgentContainer) {
|
||||
err := conn.RecreateDevcontainer(ctx, container.ID)
|
||||
_, err := conn.RecreateDevcontainer(ctx, container.ID)
|
||||
assert.NoError(t, err, "recreate devcontainer should succeed")
|
||||
}(container)
|
||||
|
||||
|
||||
@@ -403,6 +403,7 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
|
||||
// Check if the container is running and update the known devcontainers.
|
||||
for i := range updated.Containers {
|
||||
container := &updated.Containers[i] // Grab a reference to the container to allow mutating it.
|
||||
container.DevcontainerStatus = "" // Reset the status for the container (updated later).
|
||||
container.DevcontainerDirty = false // Reset dirty state for the container (updated later).
|
||||
|
||||
workspaceFolder := container.Labels[DevcontainerLocalFolderLabel]
|
||||
@@ -465,9 +466,17 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
|
||||
for _, dc := range api.knownDevcontainers {
|
||||
switch {
|
||||
case dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting:
|
||||
if dc.Container != nil {
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
dc.Container.DevcontainerDirty = dc.Dirty
|
||||
}
|
||||
continue // This state is handled by the recreation routine.
|
||||
|
||||
case dc.Status == codersdk.WorkspaceAgentDevcontainerStatusError && (dc.Container == nil || dc.Container.CreatedAt.Before(api.recreateErrorTimes[dc.WorkspaceFolder])):
|
||||
if dc.Container != nil {
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
dc.Container.DevcontainerDirty = dc.Dirty
|
||||
}
|
||||
continue // The devcontainer needs to be recreated.
|
||||
|
||||
case dc.Container != nil:
|
||||
@@ -475,6 +484,7 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
|
||||
if dc.Container.Running {
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusRunning
|
||||
}
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
|
||||
dc.Dirty = false
|
||||
if lastModified, hasModTime := api.configFileModifiedTimes[dc.ConfigPath]; hasModTime && dc.Container.CreatedAt.Before(lastModified) {
|
||||
@@ -608,6 +618,9 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
|
||||
// Update the status so that we don't try to recreate the
|
||||
// devcontainer multiple times in parallel.
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting
|
||||
if dc.Container != nil {
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
}
|
||||
api.knownDevcontainers[dc.WorkspaceFolder] = dc
|
||||
api.recreateWg.Add(1)
|
||||
go api.recreateDevcontainer(dc, configPath)
|
||||
@@ -680,6 +693,9 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
|
||||
api.mu.Lock()
|
||||
dc = api.knownDevcontainers[dc.WorkspaceFolder]
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusError
|
||||
if dc.Container != nil {
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
}
|
||||
api.knownDevcontainers[dc.WorkspaceFolder] = dc
|
||||
api.recreateErrorTimes[dc.WorkspaceFolder] = api.clock.Now("recreate", "errorTimes")
|
||||
api.mu.Unlock()
|
||||
@@ -695,10 +711,12 @@ func (api *API) recreateDevcontainer(dc codersdk.WorkspaceAgentDevcontainer, con
|
||||
// allows the update routine to update the devcontainer status, but
|
||||
// to minimize the time between API consistency, we guess the status
|
||||
// based on the container state.
|
||||
if dc.Container != nil && dc.Container.Running {
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusRunning
|
||||
} else {
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStopped
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStopped
|
||||
if dc.Container != nil {
|
||||
if dc.Container.Running {
|
||||
dc.Status = codersdk.WorkspaceAgentDevcontainerStatusRunning
|
||||
}
|
||||
dc.Container.DevcontainerStatus = dc.Status
|
||||
}
|
||||
dc.Dirty = false
|
||||
api.recreateSuccessTimes[dc.WorkspaceFolder] = api.clock.Now("recreate", "successTimes")
|
||||
|
||||
@@ -477,6 +477,8 @@ func TestAPI(t *testing.T) {
|
||||
require.NoError(t, err, "unmarshal response failed")
|
||||
require.Len(t, resp.Devcontainers, 1, "expected one devcontainer in response")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusStarting, resp.Devcontainers[0].Status, "devcontainer is not starting")
|
||||
require.NotNil(t, resp.Devcontainers[0].Container, "devcontainer should have container reference")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusStarting, resp.Devcontainers[0].Container.DevcontainerStatus, "container dc status is not starting")
|
||||
|
||||
// Allow the devcontainer CLI to continue the up process.
|
||||
close(tt.devcontainerCLI.continueUp)
|
||||
@@ -503,6 +505,8 @@ func TestAPI(t *testing.T) {
|
||||
require.NoError(t, err, "unmarshal response failed after error")
|
||||
require.Len(t, resp.Devcontainers, 1, "expected one devcontainer in response after error")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusError, resp.Devcontainers[0].Status, "devcontainer is not in an error state after up failure")
|
||||
require.NotNil(t, resp.Devcontainers[0].Container, "devcontainer should have container reference after up failure")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusError, resp.Devcontainers[0].Container.DevcontainerStatus, "container dc status is not error after up failure")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -525,7 +529,9 @@ func TestAPI(t *testing.T) {
|
||||
err = json.NewDecoder(rec.Body).Decode(&resp)
|
||||
require.NoError(t, err, "unmarshal response failed after recreation")
|
||||
require.Len(t, resp.Devcontainers, 1, "expected one devcontainer in response after recreation")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, resp.Devcontainers[0].Status, "devcontainer is not stopped after recreation")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, resp.Devcontainers[0].Status, "devcontainer is not running after recreation")
|
||||
require.NotNil(t, resp.Devcontainers[0].Container, "devcontainer should have container reference after recreation")
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, resp.Devcontainers[0].Container.DevcontainerStatus, "container dc status is not running after recreation")
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -620,6 +626,7 @@ func TestAPI(t *testing.T) {
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, dc.Status)
|
||||
require.NotNil(t, dc.Container)
|
||||
assert.Equal(t, "runtime-container-1", dc.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, dc.Container.DevcontainerStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -660,12 +667,14 @@ func TestAPI(t *testing.T) {
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusStopped, known2.Status)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, runtime1.Status)
|
||||
|
||||
require.NotNil(t, known1.Container)
|
||||
assert.Nil(t, known2.Container)
|
||||
require.NotNil(t, runtime1.Container)
|
||||
|
||||
require.NotNil(t, known1.Container)
|
||||
assert.Equal(t, "known-container-1", known1.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, known1.Container.DevcontainerStatus)
|
||||
require.NotNil(t, runtime1.Container)
|
||||
assert.Equal(t, "runtime-container-1", runtime1.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, runtime1.Container.DevcontainerStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -704,10 +713,12 @@ func TestAPI(t *testing.T) {
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusStopped, nonRunning.Status)
|
||||
|
||||
require.NotNil(t, running.Container, "running container should have container reference")
|
||||
require.NotNil(t, nonRunning.Container, "non-running container should have container reference")
|
||||
|
||||
assert.Equal(t, "running-container", running.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, running.Container.DevcontainerStatus)
|
||||
|
||||
require.NotNil(t, nonRunning.Container, "non-running container should have container reference")
|
||||
assert.Equal(t, "non-running-container", nonRunning.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusStopped, nonRunning.Container.DevcontainerStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -743,6 +754,7 @@ func TestAPI(t *testing.T) {
|
||||
assert.NotEmpty(t, dc2.ConfigPath)
|
||||
require.NotNil(t, dc2.Container)
|
||||
assert.Equal(t, "known-container-2", dc2.Container.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceAgentDevcontainerStatusRunning, dc2.Container.DevcontainerStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -811,9 +823,14 @@ func TestAPI(t *testing.T) {
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
mClock := quartz.NewMock(t)
|
||||
mClock.Set(time.Now()).MustWait(testutil.Context(t, testutil.WaitShort))
|
||||
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
|
||||
|
||||
// Setup router with the handler under test.
|
||||
r := chi.NewRouter()
|
||||
apiOptions := []agentcontainers.Option{
|
||||
agentcontainers.WithClock(mClock),
|
||||
agentcontainers.WithLister(tt.lister),
|
||||
agentcontainers.WithWatcher(watcher.NewNoop()),
|
||||
}
|
||||
@@ -838,6 +855,15 @@ func TestAPI(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// Make sure the ticker function has been registered
|
||||
// before advancing the clock.
|
||||
tickerTrap.MustWait(ctx).MustRelease(ctx)
|
||||
tickerTrap.Close()
|
||||
|
||||
// Advance the clock to run the updater loop.
|
||||
_, aw := mClock.AdvanceNext()
|
||||
aw.MustWait(ctx)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/devcontainers", nil).
|
||||
WithContext(ctx)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user