mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat(agent/agentcontainers): add devcontainers list endpoint (#17389)
This change allows listing both predefined and runtime-detected devcontainers, as well as showing whether or not the devcontainer is running and which container represents it. Fixes coder/internal#478
This commit is contained in:
committed by
GitHub
parent
c8c4de5f7a
commit
00b5f56734
@ -3,11 +3,15 @@ package agentcontainers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@ -31,11 +35,13 @@ type API struct {
|
||||
dccli DevcontainerCLI
|
||||
clock quartz.Clock
|
||||
|
||||
// lockCh protects the below fields. We use a channel instead of a mutex so we
|
||||
// can handle cancellation properly.
|
||||
lockCh chan struct{}
|
||||
containers codersdk.WorkspaceAgentListContainersResponse
|
||||
mtime time.Time
|
||||
// lockCh protects the below fields. We use a channel instead of a
|
||||
// mutex so we can handle cancellation properly.
|
||||
lockCh chan struct{}
|
||||
containers codersdk.WorkspaceAgentListContainersResponse
|
||||
mtime time.Time
|
||||
devcontainerNames map[string]struct{} // Track devcontainer names to avoid duplicates.
|
||||
knownDevcontainers []codersdk.WorkspaceAgentDevcontainer // Track predefined and runtime-detected devcontainers.
|
||||
}
|
||||
|
||||
// Option is a functional option for API.
|
||||
@ -55,12 +61,29 @@ func WithDevcontainerCLI(dccli DevcontainerCLI) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDevcontainers sets the known devcontainers for the API. This
|
||||
// allows the API to be aware of devcontainers defined in the workspace
|
||||
// agent manifest.
|
||||
func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer) Option {
|
||||
return func(api *API) {
|
||||
if len(devcontainers) > 0 {
|
||||
api.knownDevcontainers = slices.Clone(devcontainers)
|
||||
api.devcontainerNames = make(map[string]struct{}, len(devcontainers))
|
||||
for _, devcontainer := range devcontainers {
|
||||
api.devcontainerNames[devcontainer.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewAPI returns a new API with the given options applied.
|
||||
func NewAPI(logger slog.Logger, options ...Option) *API {
|
||||
api := &API{
|
||||
clock: quartz.NewReal(),
|
||||
cacheDuration: defaultGetContainersCacheDuration,
|
||||
lockCh: make(chan struct{}, 1),
|
||||
clock: quartz.NewReal(),
|
||||
cacheDuration: defaultGetContainersCacheDuration,
|
||||
lockCh: make(chan struct{}, 1),
|
||||
devcontainerNames: make(map[string]struct{}),
|
||||
knownDevcontainers: []codersdk.WorkspaceAgentDevcontainer{},
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(api)
|
||||
@ -79,6 +102,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
|
||||
func (api *API) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", api.handleList)
|
||||
r.Get("/devcontainers", api.handleListDevcontainers)
|
||||
r.Post("/{id}/recreate", api.handleRecreate)
|
||||
return r
|
||||
}
|
||||
@ -121,12 +145,11 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err()
|
||||
default:
|
||||
api.lockCh <- struct{}{}
|
||||
case api.lockCh <- struct{}{}:
|
||||
defer func() {
|
||||
<-api.lockCh
|
||||
}()
|
||||
}
|
||||
defer func() {
|
||||
<-api.lockCh
|
||||
}()
|
||||
|
||||
now := api.clock.Now()
|
||||
if now.Sub(api.mtime) < api.cacheDuration {
|
||||
@ -142,6 +165,53 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
|
||||
api.containers = updated
|
||||
api.mtime = now
|
||||
|
||||
// Reset all known devcontainers to not running.
|
||||
for i := range api.knownDevcontainers {
|
||||
api.knownDevcontainers[i].Running = false
|
||||
api.knownDevcontainers[i].Container = nil
|
||||
}
|
||||
|
||||
// Check if the container is running and update the known devcontainers.
|
||||
for _, container := range updated.Containers {
|
||||
workspaceFolder := container.Labels[DevcontainerLocalFolderLabel]
|
||||
if workspaceFolder != "" {
|
||||
// Check if this is already in our known list.
|
||||
if knownIndex := slices.IndexFunc(api.knownDevcontainers, func(dc codersdk.WorkspaceAgentDevcontainer) bool {
|
||||
return dc.WorkspaceFolder == workspaceFolder
|
||||
}); knownIndex != -1 {
|
||||
// Update existing entry with runtime information.
|
||||
if api.knownDevcontainers[knownIndex].ConfigPath == "" {
|
||||
api.knownDevcontainers[knownIndex].ConfigPath = container.Labels[DevcontainerConfigFileLabel]
|
||||
}
|
||||
api.knownDevcontainers[knownIndex].Running = container.Running
|
||||
api.knownDevcontainers[knownIndex].Container = &container
|
||||
continue
|
||||
}
|
||||
|
||||
// If not in our known list, add as a runtime detected entry.
|
||||
name := path.Base(workspaceFolder)
|
||||
if _, ok := api.devcontainerNames[name]; ok {
|
||||
// Try to find a unique name by appending a number.
|
||||
for i := 2; ; i++ {
|
||||
newName := fmt.Sprintf("%s-%d", name, i)
|
||||
if _, ok := api.devcontainerNames[newName]; !ok {
|
||||
name = newName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
api.devcontainerNames[name] = struct{}{}
|
||||
api.knownDevcontainers = append(api.knownDevcontainers, codersdk.WorkspaceAgentDevcontainer{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
WorkspaceFolder: workspaceFolder,
|
||||
ConfigPath: container.Labels[DevcontainerConfigFileLabel],
|
||||
Running: container.Running,
|
||||
Container: &container,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return copyListContainersResponse(api.containers), nil
|
||||
}
|
||||
|
||||
@ -158,7 +228,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
containers, err := api.cl.List(ctx)
|
||||
containers, err := api.getContainers(ctx)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Could not list containers",
|
||||
@ -203,3 +273,39 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// handleListDevcontainers handles the HTTP request to list known devcontainers.
|
||||
func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Run getContainers to detect the latest devcontainers and their state.
|
||||
_, err := api.getContainers(ctx)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Could not list containers",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
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 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(a.ConfigPath, b.ConfigPath)
|
||||
})
|
||||
|
||||
response := codersdk.WorkspaceAgentDevcontainersResponse{
|
||||
Devcontainers: devcontainers,
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, w, http.StatusOK, response)
|
||||
}
|
||||
|
Reference in New Issue
Block a user