diff --git a/agent/agent_test.go b/agent/agent_test.go index 6c0feca812e8b..3a2562237b603 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -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) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index b28e39ad8c57b..349b85e3d269f 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -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") diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 37d613f0ac954..fb55825097190 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -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() diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b6a00051bba77..fde3a48cd4462 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17198,6 +17198,14 @@ const docTemplate = `{ "description": "DevcontainerDirty is true if the devcontainer configuration has changed\nsince the container was created. This is used to determine if the\ncontainer needs to be rebuilt.", "type": "boolean" }, + "devcontainer_status": { + "description": "DevcontainerStatus is the status of the devcontainer, if this\ncontainer is a devcontainer. This is used to determine if the\ndevcontainer is running, stopped, starting, or in an error state.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainerStatus" + } + ] + }, "id": { "description": "ID is the unique identifier of the container.", "type": "string" @@ -17262,6 +17270,21 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentDevcontainerStatus": { + "type": "string", + "enum": [ + "running", + "stopped", + "starting", + "error" + ], + "x-enum-varnames": [ + "WorkspaceAgentDevcontainerStatusRunning", + "WorkspaceAgentDevcontainerStatusStopped", + "WorkspaceAgentDevcontainerStatusStarting", + "WorkspaceAgentDevcontainerStatusError" + ] + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e5fdca7025089..6023ea23ec481 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15703,6 +15703,14 @@ "description": "DevcontainerDirty is true if the devcontainer configuration has changed\nsince the container was created. This is used to determine if the\ncontainer needs to be rebuilt.", "type": "boolean" }, + "devcontainer_status": { + "description": "DevcontainerStatus is the status of the devcontainer, if this\ncontainer is a devcontainer. This is used to determine if the\ndevcontainer is running, stopped, starting, or in an error state.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainerStatus" + } + ] + }, "id": { "description": "ID is the unique identifier of the container.", "type": "string" @@ -15767,6 +15775,16 @@ } } }, + "codersdk.WorkspaceAgentDevcontainerStatus": { + "type": "string", + "enum": ["running", "stopped", "starting", "error"], + "x-enum-varnames": [ + "WorkspaceAgentDevcontainerStatusRunning", + "WorkspaceAgentDevcontainerStatusStopped", + "WorkspaceAgentDevcontainerStatusStarting", + "WorkspaceAgentDevcontainerStatusError" + ] + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 5a8adab6132c5..6b25fcbcfeaf6 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -956,7 +956,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht } defer release() - err = agentConn.RecreateDevcontainer(ctx, container) + m, err := agentConn.RecreateDevcontainer(ctx, container) if err != nil { if errors.Is(err, context.Canceled) { httpapi.Write(ctx, rw, http.StatusRequestTimeout, codersdk.Response{ @@ -977,7 +977,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht return } - httpapi.Write(ctx, rw, http.StatusNoContent, nil) + httpapi.Write(ctx, rw, http.StatusAccepted, m) } // @Summary Get connection info for workspace agent diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 1d17560c38816..5635296d1a47b 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1483,7 +1483,7 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - err := client.WorkspaceAgentRecreateDevcontainer(ctx, agentID, devContainer.ID) + _, err := client.WorkspaceAgentRecreateDevcontainer(ctx, agentID, devContainer.ID) if wantStatus > 0 { cerr, ok := codersdk.AsError(err) require.True(t, ok, "expected error to be a coder error") diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 0c5aaddf913da..6a4380fed47ac 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -450,6 +450,10 @@ type WorkspaceAgentContainer struct { // Volumes is a map of "things" mounted into the container. Again, this // is somewhat implementation-dependent. Volumes map[string]string `json:"volumes"` + // DevcontainerStatus is the status of the devcontainer, if this + // container is a devcontainer. This is used to determine if the + // devcontainer is running, stopped, starting, or in an error state. + DevcontainerStatus WorkspaceAgentDevcontainerStatus `json:"devcontainer_status,omitempty"` // DevcontainerDirty is true if the devcontainer configuration has changed // since the container was created. This is used to determine if the // container needs to be rebuilt. @@ -518,16 +522,20 @@ func (c *Client) WorkspaceAgentListContainers(ctx context.Context, agentID uuid. } // WorkspaceAgentRecreateDevcontainer recreates the devcontainer with the given ID. -func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, containerIDOrName string) error { +func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, containerIDOrName string) (Response, error) { res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaceagents/%s/containers/devcontainers/container/%s/recreate", agentID, containerIDOrName), nil) if err != nil { - return err + return Response{}, err } defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) + if res.StatusCode != http.StatusAccepted { + return Response{}, ReadBodyAsError(res) } - return nil + var m Response + if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + return Response{}, xerrors.Errorf("decode response body: %w", err) + } + return m, nil } //nolint:revive // Follow is a control flag on the server as well. diff --git a/codersdk/workspacesdk/agentconn.go b/codersdk/workspacesdk/agentconn.go index c9e9824e2950f..3477ec98328ac 100644 --- a/codersdk/workspacesdk/agentconn.go +++ b/codersdk/workspacesdk/agentconn.go @@ -389,18 +389,22 @@ func (c *AgentConn) ListContainers(ctx context.Context) (codersdk.WorkspaceAgent // RecreateDevcontainer recreates a devcontainer with the given container. // This is a blocking call and will wait for the container to be recreated. -func (c *AgentConn) RecreateDevcontainer(ctx context.Context, containerIDOrName string) error { +func (c *AgentConn) RecreateDevcontainer(ctx context.Context, containerIDOrName string) (codersdk.Response, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() res, err := c.apiRequest(ctx, http.MethodPost, "/api/v0/containers/devcontainers/container/"+containerIDOrName+"/recreate", nil) if err != nil { - return xerrors.Errorf("do request: %w", err) + return codersdk.Response{}, xerrors.Errorf("do request: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusAccepted { - return codersdk.ReadBodyAsError(res) + return codersdk.Response{}, codersdk.ReadBodyAsError(res) } - return nil + var m codersdk.Response + if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + return codersdk.Response{}, xerrors.Errorf("decode response body: %w", err) + } + return m, nil } // apiRequest makes a request to the workspace agent's HTTP API server. diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index fd0cd38d355e0..d0169416239d7 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -777,6 +777,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con { "created_at": "2019-08-24T14:15:22Z", "devcontainer_dirty": true, + "devcontainer_status": "running", "id": "string", "image": "string", "labels": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 2374c6af8800f..7d3f94ff6f12f 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -8636,6 +8636,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "created_at": "2019-08-24T14:15:22Z", "devcontainer_dirty": true, + "devcontainer_status": "running", "id": "string", "image": "string", "labels": { @@ -8662,20 +8663,21 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|----------------------|---------------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `created_at` | string | false | | Created at is the time the container was created. | -| `devcontainer_dirty` | boolean | false | | Devcontainer dirty is true if the devcontainer configuration has changed since the container was created. This is used to determine if the container needs to be rebuilt. | -| `id` | string | false | | ID is the unique identifier of the container. | -| `image` | string | false | | Image is the name of the container image. | -| `labels` | object | false | | Labels is a map of key-value pairs of container labels. | -| » `[any property]` | string | false | | | -| `name` | string | false | | Name is the human-readable name of the container. | -| `ports` | array of [codersdk.WorkspaceAgentContainerPort](#codersdkworkspaceagentcontainerport) | false | | Ports includes ports exposed by the container. | -| `running` | boolean | false | | Running is true if the container is currently running. | -| `status` | string | false | | Status is the current status of the container. This is somewhat implementation-dependent, but should generally be a human-readable string. | -| `volumes` | object | false | | Volumes is a map of "things" mounted into the container. Again, this is somewhat implementation-dependent. | -| » `[any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------|----------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `created_at` | string | false | | Created at is the time the container was created. | +| `devcontainer_dirty` | boolean | false | | Devcontainer dirty is true if the devcontainer configuration has changed since the container was created. This is used to determine if the container needs to be rebuilt. | +| `devcontainer_status` | [codersdk.WorkspaceAgentDevcontainerStatus](#codersdkworkspaceagentdevcontainerstatus) | false | | Devcontainer status is the status of the devcontainer, if this container is a devcontainer. This is used to determine if the devcontainer is running, stopped, starting, or in an error state. | +| `id` | string | false | | ID is the unique identifier of the container. | +| `image` | string | false | | Image is the name of the container image. | +| `labels` | object | false | | Labels is a map of key-value pairs of container labels. | +| » `[any property]` | string | false | | | +| `name` | string | false | | Name is the human-readable name of the container. | +| `ports` | array of [codersdk.WorkspaceAgentContainerPort](#codersdkworkspaceagentcontainerport) | false | | Ports includes ports exposed by the container. | +| `running` | boolean | false | | Running is true if the container is currently running. | +| `status` | string | false | | Status is the current status of the container. This is somewhat implementation-dependent, but should generally be a human-readable string. | +| `volumes` | object | false | | Volumes is a map of "things" mounted into the container. Again, this is somewhat implementation-dependent. | +| » `[any property]` | string | false | | | ## codersdk.WorkspaceAgentContainerPort @@ -8697,6 +8699,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `network` | string | false | | Network is the network protocol used by the port (tcp, udp, etc). | | `port` | integer | false | | Port is the port number *inside* the container. | +## codersdk.WorkspaceAgentDevcontainerStatus + +```json +"running" +``` + +### Properties + +#### Enumerated Values + +| Value | +|------------| +| `running` | +| `stopped` | +| `starting` | +| `error` | + ## codersdk.WorkspaceAgentHealth ```json @@ -8743,6 +8762,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| { "created_at": "2019-08-24T14:15:22Z", "devcontainer_dirty": true, + "devcontainer_status": "running", "id": "string", "image": "string", "labels": { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5125a554cacc1..9fe982fe40d12 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3351,6 +3351,7 @@ export interface WorkspaceAgentContainer { readonly ports: readonly WorkspaceAgentContainerPort[]; readonly status: string; readonly volumes: Record; + readonly devcontainer_status?: WorkspaceAgentDevcontainerStatus; readonly devcontainer_dirty: boolean; } diff --git a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx index e965efea75b6d..fdd85d95c4849 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx @@ -31,3 +31,24 @@ export const WithPorts: Story = { }, }, }; + +export const Dirty: Story = { + args: { + container: { + ...MockWorkspaceAgentContainer, + devcontainer_dirty: true, + ports: MockWorkspaceAgentContainerPorts, + }, + }, +}; + +export const Recreating: Story = { + args: { + container: { + ...MockWorkspaceAgentContainer, + devcontainer_dirty: true, + devcontainer_status: "starting", + ports: MockWorkspaceAgentContainerPorts, + }, + }, +}; diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 543004de5c1e2..4891c632bbc2a 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -3,14 +3,24 @@ import type { WorkspaceAgent, WorkspaceAgentContainer, } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { + HelpTooltip, + HelpTooltipContent, + HelpTooltipText, + HelpTooltipTitle, + HelpTooltipTrigger, +} from "components/HelpTooltip/HelpTooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; -import { ExternalLinkIcon } from "lucide-react"; +import { ExternalLinkIcon, Loader2Icon } from "lucide-react"; import type { FC } from "react"; +import { useEffect, useState } from "react"; import { portForwardURL } from "utils/portForward"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; @@ -32,24 +42,103 @@ export const AgentDevcontainerCard: FC = ({ }) => { const folderPath = container.labels["devcontainer.local_folder"]; const containerFolder = container.volumes[folderPath]; + const [isRecreating, setIsRecreating] = useState(false); + + const handleRecreateDevcontainer = async () => { + setIsRecreating(true); + let recreateSucceeded = false; + try { + const response = await fetch( + `/api/v2/workspaceagents/${agent.id}/containers/devcontainers/container/${container.id}/recreate`, + { + method: "POST", + }, + ); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `Failed to recreate: ${response.statusText}`, + ); + } + // If the request was accepted (e.g. 202), we mark it as succeeded. + // Once complete, the component will unmount, so the spinner will + // disappear with it. + if (response.status === 202) { + recreateSucceeded = true; + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred."; + displayError(`Failed to recreate devcontainer: ${errorMessage}`); + console.error("Failed to recreate devcontainer:", error); + } finally { + if (!recreateSucceeded) { + setIsRecreating(false); + } + } + }; + + // If the container is starting, reflect this in the recreate button. + useEffect(() => { + if (container.devcontainer_status === "starting") { + setIsRecreating(true); + } else { + setIsRecreating(false); + } + }, [container.devcontainer_status]); return (
-
-

- {container.name} -

+
+
+

+ {container.name} +

+ {container.devcontainer_dirty && ( + + + Outdated + + + Devcontainer Outdated + + Devcontainer configuration has been modified and is outdated. + Recreate to get an up-to-date container. + + + + )} +
- +
+ + + +
-

Forwarded ports

+

Forwarded ports

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy