refactor(agent): update agentcontainers api initialization (#17600)

There were too many ways to configure the agentcontainers API resulting
in inconsistent behavior or features not being enabled. This refactor
introduces a control flag for enabling or disabling the containers API.
When disabled, all implementations are no-op and explicit endpoint
behaviors are defined. When enabled, concrete implementations are used
by default but can be overridden by passing options.
This commit is contained in:
Mathias Fredriksson
2025-04-29 17:53:10 +03:00
committed by GitHub
parent 22b932a8e0
commit 1fc74f629e
12 changed files with 119 additions and 68 deletions

View File

@ -39,6 +39,7 @@ type API struct {
watcher watcher.Watcher
cacheDuration time.Duration
execer agentexec.Execer
cl Lister
dccli DevcontainerCLI
clock quartz.Clock
@ -56,14 +57,6 @@ type API struct {
// Option is a functional option for API.
type Option func(*API)
// WithLister sets the agentcontainers.Lister implementation to use.
// The default implementation uses the Docker CLI to list containers.
func WithLister(cl Lister) Option {
return func(api *API) {
api.cl = cl
}
}
// WithClock sets the quartz.Clock implementation to use.
// This is primarily used for testing to control time.
func WithClock(clock quartz.Clock) Option {
@ -72,6 +65,21 @@ func WithClock(clock quartz.Clock) Option {
}
}
// WithExecer sets the agentexec.Execer implementation to use.
func WithExecer(execer agentexec.Execer) Option {
return func(api *API) {
api.execer = execer
}
}
// WithLister sets the agentcontainers.Lister implementation to use.
// The default implementation uses the Docker CLI to list containers.
func WithLister(cl Lister) Option {
return func(api *API) {
api.cl = cl
}
}
// WithDevcontainerCLI sets the DevcontainerCLI implementation to use.
// This can be used in tests to modify @devcontainer/cli behavior.
func WithDevcontainerCLI(dccli DevcontainerCLI) Option {
@ -113,6 +121,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
done: make(chan struct{}),
logger: logger,
clock: quartz.NewReal(),
execer: agentexec.DefaultExecer,
cacheDuration: defaultGetContainersCacheDuration,
lockCh: make(chan struct{}, 1),
devcontainerNames: make(map[string]struct{}),
@ -123,30 +132,46 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
opt(api)
}
if api.cl == nil {
api.cl = &DockerCLILister{}
api.cl = NewDocker(api.execer)
}
if api.dccli == nil {
api.dccli = NewDevcontainerCLI(logger.Named("devcontainer-cli"), agentexec.DefaultExecer)
api.dccli = NewDevcontainerCLI(logger.Named("devcontainer-cli"), api.execer)
}
if api.watcher == nil {
api.watcher = watcher.NewNoop()
}
// Make sure we watch the devcontainer config files for changes.
for _, devcontainer := range api.knownDevcontainers {
if devcontainer.ConfigPath != "" {
if err := api.watcher.Add(devcontainer.ConfigPath); err != nil {
api.logger.Error(ctx, "watch devcontainer config file failed", slog.Error(err), slog.F("file", devcontainer.ConfigPath))
}
var err error
api.watcher, err = watcher.NewFSNotify()
if err != nil {
logger.Error(ctx, "create file watcher service failed", slog.Error(err))
api.watcher = watcher.NewNoop()
}
}
go api.start()
go api.loop()
return api
}
func (api *API) start() {
// SignalReady signals the API that we are ready to begin watching for
// file changes. This is used to prime the cache with the current list
// of containers and to start watching the devcontainer config files for
// changes. It should be called after the agent ready.
func (api *API) SignalReady() {
// Prime the cache with the current list of containers.
_, _ = api.cl.List(api.ctx)
// Make sure we watch the devcontainer config files for changes.
for _, devcontainer := range api.knownDevcontainers {
if devcontainer.ConfigPath == "" {
continue
}
if err := api.watcher.Add(devcontainer.ConfigPath); err != nil {
api.logger.Error(api.ctx, "watch devcontainer config file failed", slog.Error(err), slog.F("file", devcontainer.ConfigPath))
}
}
}
func (api *API) loop() {
defer close(api.done)
for {
@ -187,9 +212,11 @@ func (api *API) start() {
// Routes returns the HTTP handler for container-related routes.
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
}