diff --git a/agent/agent.go b/agent/agent.go index 8d3766dae89ad..018b4e0ac29d0 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1166,7 +1166,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, ) if a.devcontainers { a.containerAPI.Init( - agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName), + agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName), agentcontainers.WithDevcontainers(manifest.Devcontainers, scripts), agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)), ) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index fba5babce5003..0f8fd3e380932 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -71,6 +71,7 @@ type API struct { ownerName string workspaceName string + parentAgent string mu sync.RWMutex closed bool @@ -188,10 +189,11 @@ func WithSubAgentEnv(env ...string) Option { // WithManifestInfo sets the owner name, and workspace name // for the sub-agent. -func WithManifestInfo(owner, workspace string) Option { +func WithManifestInfo(owner, workspace, parentAgent string) Option { return func(api *API) { api.ownerName = owner api.workspaceName = workspace + api.parentAgent = parentAgent } } @@ -494,8 +496,8 @@ func (api *API) Routes() http.Handler { r.Get("/", api.handleList) // TODO(mafredri): Simplify this route as the previous /devcontainers // /-route was dropped. We can drop the /devcontainers prefix here too. - r.Route("/devcontainers", func(r chi.Router) { - r.Post("/container/{container}/recreate", api.handleDevcontainerRecreate) + r.Route("/devcontainers/{devcontainer}", func(r chi.Router) { + r.Post("/recreate", api.handleDevcontainerRecreate) }) return r @@ -859,68 +861,40 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse, // devcontainer by referencing the container. func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - containerID := chi.URLParam(r, "container") + devcontainerID := chi.URLParam(r, "devcontainer") - if containerID == "" { + if devcontainerID == "" { httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Missing container ID or name", - Detail: "Container ID or name is required to recreate a devcontainer.", - }) - return - } - - containers, err := api.getContainers() - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Could not list containers", - Detail: err.Error(), - }) - return - } - - containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool { return c.Match(containerID) }) - if containerIdx == -1 { - httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ - Message: "Container not found", - Detail: "Container ID or name not found in the list of containers.", - }) - return - } - - container := containers.Containers[containerIdx] - workspaceFolder := container.Labels[DevcontainerLocalFolderLabel] - configPath := container.Labels[DevcontainerConfigFileLabel] - - // Workspace folder is required to recreate a container, we don't verify - // the config path here because it's optional. - if workspaceFolder == "" { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Missing workspace folder label", - Detail: "The container is not a devcontainer, the container must have the workspace folder label to support recreation.", + Message: "Missing devcontainer ID", + Detail: "Devcontainer ID is required to recreate a devcontainer.", }) return } api.mu.Lock() - dc, ok := api.knownDevcontainers[workspaceFolder] - switch { - case !ok: + var dc codersdk.WorkspaceAgentDevcontainer + for _, knownDC := range api.knownDevcontainers { + if knownDC.ID.String() == devcontainerID { + dc = knownDC + break + } + } + if dc.ID == uuid.Nil { api.mu.Unlock() - // This case should not happen if the container is a valid devcontainer. - api.logger.Error(ctx, "devcontainer not found for workspace folder", slog.F("workspace_folder", workspaceFolder)) - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ Message: "Devcontainer not found.", - Detail: fmt.Sprintf("Could not find devcontainer for workspace folder: %q", workspaceFolder), + Detail: fmt.Sprintf("Could not find devcontainer with ID: %q", devcontainerID), }) return - case dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting: + } + if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting { api.mu.Unlock() httpapi.Write(ctx, w, http.StatusConflict, codersdk.Response{ Message: "Devcontainer recreation already in progress", - Detail: fmt.Sprintf("Recreation for workspace folder %q is already underway.", dc.WorkspaceFolder), + Detail: fmt.Sprintf("Recreation for devcontainer %q is already underway.", dc.Name), }) return } @@ -931,14 +905,14 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques dc.Container = nil api.knownDevcontainers[dc.WorkspaceFolder] = dc go func() { - _ = api.CreateDevcontainer(dc.WorkspaceFolder, configPath, WithRemoveExistingContainer()) + _ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath, WithRemoveExistingContainer()) }() api.mu.Unlock() httpapi.Write(ctx, w, http.StatusAccepted, codersdk.Response{ Message: "Devcontainer recreation initiated", - Detail: fmt.Sprintf("Recreation process for workspace folder %q has started.", dc.WorkspaceFolder), + Detail: fmt.Sprintf("Recreation process for devcontainer %q has started.", dc.Name), }) } @@ -1319,7 +1293,9 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", subAgentConfig.Name), fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName), fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName), + fmt.Sprintf("CODER_WORKSPACE_PARENT_AGENT_NAME=%s", api.parentAgent), fmt.Sprintf("CODER_URL=%s", api.subAgentURL), + fmt.Sprintf("CONTAINER_ID=%s", container.ID), }...), ) } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index f61f4c24bfc2b..33f35e2b21602 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -493,78 +493,77 @@ func TestAPI(t *testing.T) { t.Run("Recreate", func(t *testing.T) { t.Parallel() - validContainer := codersdk.WorkspaceAgentContainer{ - ID: "container-id", - FriendlyName: "container-name", + devcontainerID1 := uuid.New() + devcontainerID2 := uuid.New() + workspaceFolder1 := "/workspace/test1" + workspaceFolder2 := "/workspace/test2" + configPath1 := "/workspace/test1/.devcontainer/devcontainer.json" + configPath2 := "/workspace/test2/.devcontainer/devcontainer.json" + + // Create a container that represents an existing devcontainer + devContainer1 := codersdk.WorkspaceAgentContainer{ + ID: "container-1", + FriendlyName: "test-container-1", Running: true, Labels: map[string]string{ - agentcontainers.DevcontainerLocalFolderLabel: "/workspaces", - agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json", + agentcontainers.DevcontainerLocalFolderLabel: workspaceFolder1, + agentcontainers.DevcontainerConfigFileLabel: configPath1, }, } - missingFolderContainer := codersdk.WorkspaceAgentContainer{ - ID: "missing-folder-container", - FriendlyName: "missing-folder-container", - Labels: map[string]string{}, + devContainer2 := codersdk.WorkspaceAgentContainer{ + ID: "container-2", + FriendlyName: "test-container-2", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: workspaceFolder2, + agentcontainers.DevcontainerConfigFileLabel: configPath2, + }, } tests := []struct { - name string - containerID string - lister *fakeContainerCLI - devcontainerCLI *fakeDevcontainerCLI - wantStatus []int - wantBody []string + name string + devcontainerID string + setupDevcontainers []codersdk.WorkspaceAgentDevcontainer + lister *fakeContainerCLI + devcontainerCLI *fakeDevcontainerCLI + wantStatus []int + wantBody []string }{ { - name: "Missing container ID", - containerID: "", + name: "Missing devcontainer ID", + devcontainerID: "", lister: &fakeContainerCLI{}, devcontainerCLI: &fakeDevcontainerCLI{}, wantStatus: []int{http.StatusBadRequest}, - wantBody: []string{"Missing container ID or name"}, + wantBody: []string{"Missing devcontainer ID"}, }, { - name: "List error", - containerID: "container-id", + name: "Devcontainer not found", + devcontainerID: uuid.NewString(), lister: &fakeContainerCLI{ - listErr: xerrors.New("list error"), - }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: []int{http.StatusInternalServerError}, - wantBody: []string{"Could not list containers"}, - }, - { - name: "Container not found", - containerID: "nonexistent-container", - lister: &fakeContainerCLI{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, - }, + arch: "", // Unsupported architecture, don't inject subagent. }, devcontainerCLI: &fakeDevcontainerCLI{}, wantStatus: []int{http.StatusNotFound}, - wantBody: []string{"Container not found"}, + wantBody: []string{"Devcontainer not found"}, }, { - name: "Missing workspace folder label", - containerID: "missing-folder-container", - lister: &fakeContainerCLI{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{missingFolderContainer}, + name: "Devcontainer CLI error", + devcontainerID: devcontainerID1.String(), + setupDevcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: devcontainerID1, + Name: "test-devcontainer-1", + WorkspaceFolder: workspaceFolder1, + ConfigPath: configPath1, + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Container: &devContainer1, }, }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: []int{http.StatusBadRequest}, - wantBody: []string{"Missing workspace folder label"}, - }, - { - name: "Devcontainer CLI error", - containerID: "container-id", lister: &fakeContainerCLI{ containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + Containers: []codersdk.WorkspaceAgentContainer{devContainer1}, }, arch: "", // Unsupported architecture, don't inject subagent. }, @@ -575,11 +574,21 @@ func TestAPI(t *testing.T) { wantBody: []string{"Devcontainer recreation initiated", "Devcontainer recreation already in progress"}, }, { - name: "OK", - containerID: "container-id", + name: "OK", + devcontainerID: devcontainerID2.String(), + setupDevcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: devcontainerID2, + Name: "test-devcontainer-2", + WorkspaceFolder: workspaceFolder2, + ConfigPath: configPath2, + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Container: &devContainer2, + }, + }, lister: &fakeContainerCLI{ containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + Containers: []codersdk.WorkspaceAgentContainer{devContainer2}, }, arch: "", // Unsupported architecture, don't inject subagent. }, @@ -608,13 +617,16 @@ func TestAPI(t *testing.T) { // Setup router with the handler under test. r := chi.NewRouter() + api := agentcontainers.NewAPI( logger, agentcontainers.WithClock(mClock), agentcontainers.WithContainerCLI(tt.lister), agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI), agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithDevcontainers(tt.setupDevcontainers, nil), ) + api.Init() defer api.Close() r.Mount("/", api.Routes()) @@ -626,7 +638,7 @@ func TestAPI(t *testing.T) { for i := range tt.wantStatus { // Simulate HTTP request to the recreate endpoint. - req := httptest.NewRequest(http.MethodPost, "/devcontainers/container/"+tt.containerID+"/recreate", nil). + req := httptest.NewRequest(http.MethodPost, "/devcontainers/"+tt.devcontainerID+"/recreate", nil). WithContext(ctx) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) @@ -1393,7 +1405,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithSubAgentClient(fakeSAC), agentcontainers.WithSubAgentURL("test-subagent-url"), agentcontainers.WithDevcontainerCLI(fakeDCCLI), - agentcontainers.WithManifestInfo("test-user", "test-workspace"), + agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"), ) api.Init() apiClose := func() { @@ -1415,7 +1427,9 @@ func TestAPI(t *testing.T) { assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_WORKSPACE_PARENT_AGENT_NAME=test-parent-agent") assert.Contains(t, envs, "CODER_URL=test-subagent-url") + assert.Contains(t, envs, "CONTAINER_ID=test-container-id") return nil }) @@ -1557,7 +1571,9 @@ func TestAPI(t *testing.T) { assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_WORKSPACE_PARENT_AGENT_NAME=test-parent-agent") assert.Contains(t, envs, "CODER_URL=test-subagent-url") + assert.NotContains(t, envs, "CONTAINER_ID=test-container-id") return nil }) @@ -2134,7 +2150,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithSubAgentClient(fSAC), agentcontainers.WithSubAgentURL("test-subagent-url"), agentcontainers.WithWatcher(watcher.NewNoop()), - agentcontainers.WithManifestInfo("test-user", "test-workspace"), + agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"), ) api.Init() defer api.Close() @@ -2150,7 +2166,9 @@ func TestAPI(t *testing.T) { assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_WORKSPACE_PARENT_AGENT_NAME=test-parent-agent") assert.Contains(t, envs, "CODER_URL=test-subagent-url") + assert.Contains(t, envs, "CONTAINER_ID=test-container-id") // First call should not have feature envs. assert.NotContains(t, envs, "FEATURE_CODE_SERVER_OPTION_PORT=9090") assert.NotContains(t, envs, "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false") @@ -2161,7 +2179,9 @@ func TestAPI(t *testing.T) { assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_WORKSPACE_PARENT_AGENT_NAME=test-parent-agent") assert.Contains(t, envs, "CODER_URL=test-subagent-url") + assert.Contains(t, envs, "CONTAINER_ID=test-container-id") // Second call should have feature envs from the first config read. assert.Contains(t, envs, "FEATURE_CODE_SERVER_OPTION_PORT=9090") assert.Contains(t, envs, "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false") diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index f08b7dfd7fc4d..cbcfe1d72b380 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8453,7 +8453,7 @@ const docTemplate = `{ } } }, - "/workspaceagents/{workspaceagent}/containers/devcontainers/container/{container}/recreate": { + "/workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate": { "post": { "security": [ { @@ -8479,8 +8479,8 @@ const docTemplate = `{ }, { "type": "string", - "description": "Container ID or name", - "name": "container", + "description": "Devcontainer ID", + "name": "devcontainer", "in": "path", "required": true } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8317f9ed90503..ef723652a0941 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7472,7 +7472,7 @@ } } }, - "/workspaceagents/{workspaceagent}/containers/devcontainers/container/{container}/recreate": { + "/workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate": { "post": { "security": [ { @@ -7494,8 +7494,8 @@ }, { "type": "string", - "description": "Container ID or name", - "name": "container", + "description": "Devcontainer ID", + "name": "devcontainer", "in": "path", "required": true } diff --git a/coderd/coderd.go b/coderd/coderd.go index 9618e5e751931..b6a4bcbfa801b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1314,7 +1314,7 @@ func New(options *Options) *API { r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) r.Get("/containers", api.workspaceAgentListContainers) - r.Post("/containers/devcontainers/container/{container}/recreate", api.workspaceAgentRecreateDevcontainer) + r.Post("/containers/devcontainers/{devcontainer}/recreate", api.workspaceAgentRecreateDevcontainer) r.Get("/coordinate", api.workspaceAgentClientCoordinate) // PTY is part of workspaceAppServer. diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 8282eb9e7d01f..0ab28b340a1d1 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -905,19 +905,19 @@ func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Req // @Tags Agents // @Produce json // @Param workspaceagent path string true "Workspace agent ID" format(uuid) -// @Param container path string true "Container ID or name" +// @Param devcontainer path string true "Devcontainer ID" // @Success 202 {object} codersdk.Response -// @Router /workspaceagents/{workspaceagent}/containers/devcontainers/container/{container}/recreate [post] +// @Router /workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate [post] func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() workspaceAgent := httpmw.WorkspaceAgentParam(r) - container := chi.URLParam(r, "container") - if container == "" { + devcontainer := chi.URLParam(r, "devcontainer") + if devcontainer == "" { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Container ID or name is required.", + Message: "Devcontainer ID is required.", Validations: []codersdk.ValidationError{ - {Field: "container", Detail: "Container ID or name is required."}, + {Field: "devcontainer", Detail: "Devcontainer ID is required."}, }, }) return @@ -961,7 +961,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht } defer release() - m, err := agentConn.RecreateDevcontainer(ctx, container) + m, err := agentConn.RecreateDevcontainer(ctx, devcontainer) if err != nil { if errors.Is(err, context.Canceled) { httpapi.Write(ctx, rw, http.StatusRequestTimeout, codersdk.Response{ diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 4559131e98d0a..4a37a1bf7bc52 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1396,36 +1396,42 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { var ( workspaceFolder = t.TempDir() configFile = filepath.Join(workspaceFolder, ".devcontainer", "devcontainer.json") - dcLabels = map[string]string{ - agentcontainers.DevcontainerLocalFolderLabel: workspaceFolder, - agentcontainers.DevcontainerConfigFileLabel: configFile, - } + devcontainerID = uuid.New() + + // Create a container that would be associated with the devcontainer devContainer = codersdk.WorkspaceAgentContainer{ ID: uuid.NewString(), CreatedAt: dbtime.Now(), FriendlyName: testutil.GetRandomName(t), Image: "busybox:latest", - Labels: dcLabels, - Running: true, - Status: "running", + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: workspaceFolder, + agentcontainers.DevcontainerConfigFileLabel: configFile, + }, + Running: true, + Status: "running", } - plainContainer = codersdk.WorkspaceAgentContainer{ - ID: uuid.NewString(), - CreatedAt: dbtime.Now(), - FriendlyName: testutil.GetRandomName(t), - Image: "busybox:latest", - Labels: map[string]string{}, - Running: true, - Status: "running", + + devcontainer = codersdk.WorkspaceAgentDevcontainer{ + ID: devcontainerID, + Name: "test-devcontainer", + WorkspaceFolder: workspaceFolder, + ConfigPath: configFile, + Status: codersdk.WorkspaceAgentDevcontainerStatusRunning, + Container: &devContainer, } ) for _, tc := range []struct { - name string - setupMock func(mccli *acmock.MockContainerCLI, mdccli *acmock.MockDevcontainerCLI) (status int) + name string + devcontainerID string + setupDevcontainers []codersdk.WorkspaceAgentDevcontainer + setupMock func(mccli *acmock.MockContainerCLI, mdccli *acmock.MockDevcontainerCLI) (status int) }{ { - name: "Recreate", + name: "Recreate", + devcontainerID: devcontainerID.String(), + setupDevcontainers: []codersdk.WorkspaceAgentDevcontainer{devcontainer}, setupMock: func(mccli *acmock.MockContainerCLI, mdccli *acmock.MockDevcontainerCLI) int { mccli.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ Containers: []codersdk.WorkspaceAgentContainer{devContainer}, @@ -1438,21 +1444,14 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { }, }, { - name: "Container does not exist", + name: "Devcontainer does not exist", + devcontainerID: uuid.NewString(), + setupDevcontainers: nil, setupMock: func(mccli *acmock.MockContainerCLI, mdccli *acmock.MockDevcontainerCLI) int { mccli.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{}, nil).AnyTimes() return http.StatusNotFound }, }, - { - name: "Not a devcontainer", - setupMock: func(mccli *acmock.MockContainerCLI, mdccli *acmock.MockDevcontainerCLI) int { - mccli.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{plainContainer}, - }, nil).AnyTimes() - return http.StatusNotFound - }, - }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -1472,16 +1471,21 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { return agents }).Do() + + devcontainerAPIOptions := []agentcontainers.Option{ + agentcontainers.WithContainerCLI(mccli), + agentcontainers.WithDevcontainerCLI(mdccli), + agentcontainers.WithWatcher(watcher.NewNoop()), + } + if tc.setupDevcontainers != nil { + devcontainerAPIOptions = append(devcontainerAPIOptions, + agentcontainers.WithDevcontainers(tc.setupDevcontainers, nil)) + } + _ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { o.Logger = logger.Named("agent") o.Devcontainers = true - o.DevcontainerAPIOptions = append( - o.DevcontainerAPIOptions, - agentcontainers.WithContainerCLI(mccli), - agentcontainers.WithDevcontainerCLI(mdccli), - agentcontainers.WithWatcher(watcher.NewNoop()), - agentcontainers.WithContainerLabelIncludeFilter(agentcontainers.DevcontainerLocalFolderLabel, workspaceFolder), - ) + o.DevcontainerAPIOptions = devcontainerAPIOptions }) resources := coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() require.Len(t, resources, 1, "expected one resource") @@ -1490,7 +1494,7 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - _, err := client.WorkspaceAgentRecreateDevcontainer(ctx, agentID, devContainer.ID) + _, err := client.WorkspaceAgentRecreateDevcontainer(ctx, agentID, tc.devcontainerID) 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 5fe648ce15045..77cc0aff9a6be 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -519,8 +519,8 @@ 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) (Response, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaceagents/%s/containers/devcontainers/container/%s/recreate", agentID, containerIDOrName), nil) +func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, devcontainerID string) (Response, error) { + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaceagents/%s/containers/devcontainers/%s/recreate", agentID, devcontainerID), nil) if err != nil { return Response{}, err } diff --git a/codersdk/workspacesdk/agentconn.go b/codersdk/workspacesdk/agentconn.go index 3477ec98328ac..ee0b36e5a0c23 100644 --- a/codersdk/workspacesdk/agentconn.go +++ b/codersdk/workspacesdk/agentconn.go @@ -389,10 +389,10 @@ 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) (codersdk.Response, error) { +func (c *AgentConn) RecreateDevcontainer(ctx context.Context, devcontainerID 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) + res, err := c.apiRequest(ctx, http.MethodPost, "/api/v0/containers/devcontainers/"+devcontainerID+"/recreate", nil) if err != nil { return codersdk.Response{}, xerrors.Errorf("do request: %w", err) } diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 1c0534ad4c2bf..c32e8202fa945 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -859,19 +859,19 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl -curl -X POST http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/containers/devcontainers/container/{container}/recreate \ +curl -X POST http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`POST /workspaceagents/{workspaceagent}/containers/devcontainers/container/{container}/recreate` +`POST /workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}/recreate` ### Parameters -| Name | In | Type | Required | Description | -|------------------|------|--------------|----------|----------------------| -| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | -| `container` | path | string | true | Container ID or name | +| Name | In | Type | Required | Description | +|------------------|------|--------------|----------|--------------------| +| `workspaceagent` | path | string(uuid) | true | Workspace agent ID | +| `devcontainer` | path | string | true | Devcontainer ID | ### Example responses diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 9985b03f2718d..47ef6388a3209 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -80,7 +80,7 @@ export const AgentDevcontainerCard: FC = ({ const rebuildDevcontainerMutation = useMutation({ mutationFn: async () => { const response = await fetch( - `/api/v2/workspaceagents/${parentAgent.id}/containers/devcontainers/container/${devcontainer.container?.id}/recreate`, + `/api/v2/workspaceagents/${parentAgent.id}/containers/devcontainers/${devcontainer.id}/recreate`, { method: "POST" }, ); if (!response.ok) { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 2d38e1f3a7dc1..f085c74c57073 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -277,6 +277,8 @@ export const PresetsWithDefault: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); + // Wait for the switch to be available since preset parameters are populated asynchronously + await canvas.findByLabelText("Show preset parameters"); // Toggle off the show preset parameters switch await userEvent.click(canvas.getByLabelText("Show preset parameters")); }, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index addace484e84e..75c382f807b1b 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -385,25 +385,28 @@ export const CreateWorkspacePageView: FC = ({ selectedOption={presetOptions[selectedPresetIndex]} /> -
- -
+ )} )} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 59277d5f07aa9..8cb6c4acb6e49 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -562,7 +562,6 @@ export const CreateWorkspacePageViewExperimental: FC<
-
@@ -594,16 +593,19 @@ export const CreateWorkspacePageViewExperimental: FC<
- - - - + {/* Only show the preset parameter visibility toggle if preset parameters are actually being modified, otherwise it is ineffectual */} + {presetParameterNames.length > 0 && ( + + + + + )}
)}