Thanks to visit codestin.com
Credit goes to github.com

Skip to content

refactor(agent/agentcontainers): update routes and locking in container api #17768

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 81 additions & 55 deletions agent/agentcontainers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,10 @@ func (api *API) Routes() http.Handler {
r := chi.NewRouter()

r.Get("/", api.handleList)
r.Get("/devcontainers", api.handleListDevcontainers)
r.Post("/{id}/recreate", api.handleRecreate)
r.Route("/devcontainers", func(r chi.Router) {
r.Get("/", api.handleDevcontainersList)
r.Post("/container/{container}/recreate", api.handleDevcontainerRecreate)
})

return r
}
Expand Down Expand Up @@ -376,12 +378,13 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
return copyListContainersResponse(api.containers), nil
}

// handleRecreate handles the HTTP request to recreate a container.
func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
// handleDevcontainerRecreate handles the HTTP request to recreate a
// devcontainer by referencing the container.
func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id")
containerID := chi.URLParam(r, "container")

if id == "" {
if containerID == "" {
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.",
Expand All @@ -399,7 +402,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
}

containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool {
return c.Match(id)
return c.Match(containerID)
})
if containerIdx == -1 {
httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{
Expand All @@ -418,7 +421,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
if workspaceFolder == "" {
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{
Message: "Missing workspace folder label",
Detail: "The workspace folder label is required to recreate a devcontainer.",
Detail: "The container is not a devcontainer, the container must have the workspace folder label to support recreation.",
})
return
}
Expand All @@ -434,32 +437,28 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {

// TODO(mafredri): Temporarily handle clearing the dirty state after
// recreation, later on this should be handled by a "container watcher".
select {
case <-api.ctx.Done():
return
case <-ctx.Done():
return
case api.lockCh <- struct{}{}:
defer func() { <-api.lockCh }()
}
for i := range api.knownDevcontainers {
if api.knownDevcontainers[i].WorkspaceFolder == workspaceFolder {
if api.knownDevcontainers[i].Dirty {
api.logger.Info(ctx, "clearing dirty flag after recreation",
slog.F("workspace_folder", workspaceFolder),
slog.F("name", api.knownDevcontainers[i].Name),
)
api.knownDevcontainers[i].Dirty = false
if !api.doLockedHandler(w, r, func() {
for i := range api.knownDevcontainers {
if api.knownDevcontainers[i].WorkspaceFolder == workspaceFolder {
if api.knownDevcontainers[i].Dirty {
api.logger.Info(ctx, "clearing dirty flag after recreation",
slog.F("workspace_folder", workspaceFolder),
slog.F("name", api.knownDevcontainers[i].Name),
)
api.knownDevcontainers[i].Dirty = false
}
return
}
break
}
}) {
return
}

w.WriteHeader(http.StatusNoContent)
}

// handleListDevcontainers handles the HTTP request to list known devcontainers.
func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) {
// handleDevcontainersList handles the HTTP request to list known devcontainers.
func (api *API) handleDevcontainersList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

// Run getContainers to detect the latest devcontainers and their state.
Expand All @@ -472,15 +471,12 @@ func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request)
return
}

select {
case <-api.ctx.Done():
var devcontainers []codersdk.WorkspaceAgentDevcontainer
if !api.doLockedHandler(w, r, func() {
devcontainers = slices.Clone(api.knownDevcontainers)
}) {
return
case <-ctx.Done():
return
case api.lockCh <- struct{}{}:
}
devcontainers := slices.Clone(api.knownDevcontainers)
<-api.lockCh

slices.SortFunc(devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int {
if cmp := strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder); cmp != 0 {
Expand All @@ -499,34 +495,64 @@ func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request)
// markDevcontainerDirty finds the devcontainer with the given config file path
// and marks it as dirty. It acquires the lock before modifying the state.
func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) {
ok := api.doLocked(func() {
// Record the timestamp of when this configuration file was modified.
api.configFileModifiedTimes[configPath] = modifiedAt

for i := range api.knownDevcontainers {
if api.knownDevcontainers[i].ConfigPath != configPath {
continue
}

// TODO(mafredri): Simplistic mark for now, we should check if the
// container is running and if the config file was modified after
// the container was created.
if !api.knownDevcontainers[i].Dirty {
api.logger.Info(api.ctx, "marking devcontainer as dirty",
slog.F("file", configPath),
slog.F("name", api.knownDevcontainers[i].Name),
slog.F("workspace_folder", api.knownDevcontainers[i].WorkspaceFolder),
slog.F("modified_at", modifiedAt),
)
api.knownDevcontainers[i].Dirty = true
}
}
})
if !ok {
api.logger.Debug(api.ctx, "mark devcontainer dirty failed", slog.F("file", configPath))
}
}

func (api *API) doLockedHandler(w http.ResponseWriter, r *http.Request, f func()) bool {
select {
case <-r.Context().Done():
httpapi.Write(r.Context(), w, http.StatusRequestTimeout, codersdk.Response{
Message: "Request canceled",
Detail: "Request was canceled before we could process it.",
})
return false
case <-api.ctx.Done():
return
httpapi.Write(r.Context(), w, http.StatusServiceUnavailable, codersdk.Response{
Message: "API closed",
Detail: "The API is closed and cannot process requests.",
})
return false
case api.lockCh <- struct{}{}:
defer func() { <-api.lockCh }()
}
f()
return true
}

// Record the timestamp of when this configuration file was modified.
api.configFileModifiedTimes[configPath] = modifiedAt

for i := range api.knownDevcontainers {
if api.knownDevcontainers[i].ConfigPath != configPath {
continue
}

// TODO(mafredri): Simplistic mark for now, we should check if the
// container is running and if the config file was modified after
// the container was created.
if !api.knownDevcontainers[i].Dirty {
api.logger.Info(api.ctx, "marking devcontainer as dirty",
slog.F("file", configPath),
slog.F("name", api.knownDevcontainers[i].Name),
slog.F("workspace_folder", api.knownDevcontainers[i].WorkspaceFolder),
slog.F("modified_at", modifiedAt),
)
api.knownDevcontainers[i].Dirty = true
}
func (api *API) doLocked(f func()) bool {
select {
case <-api.ctx.Done():
return false
case api.lockCh <- struct{}{}:
defer func() { <-api.lockCh }()
}
f()
return true
}

func (api *API) Close() error {
Expand Down
4 changes: 2 additions & 2 deletions agent/agentcontainers/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func TestAPI(t *testing.T) {
wantBody string
}{
{
name: "Missing ID",
name: "Missing container ID",
containerID: "",
lister: &fakeLister{},
devcontainerCLI: &fakeDevcontainerCLI{},
Expand Down Expand Up @@ -260,7 +260,7 @@ func TestAPI(t *testing.T) {
r.Mount("/", api.Routes())

// Simulate HTTP request to the recreate endpoint.
req := httptest.NewRequest(http.MethodPost, "/"+tt.containerID+"/recreate", nil)
req := httptest.NewRequest(http.MethodPost, "/devcontainers/container/"+tt.containerID+"/recreate", nil)
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)

Expand Down
Loading