mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+10
-6
@@ -89,9 +89,9 @@ type Options struct {
|
||||
ServiceBannerRefreshInterval time.Duration
|
||||
BlockFileTransfer bool
|
||||
Execer agentexec.Execer
|
||||
ContainerLister agentcontainers.Lister
|
||||
|
||||
ExperimentalDevcontainersEnabled bool
|
||||
ContainerAPIOptions []agentcontainers.Option // Enable ExperimentalDevcontainersEnabled for these to be effective.
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
@@ -154,9 +154,6 @@ func New(options Options) Agent {
|
||||
if options.Execer == nil {
|
||||
options.Execer = agentexec.DefaultExecer
|
||||
}
|
||||
if options.ContainerLister == nil {
|
||||
options.ContainerLister = agentcontainers.NoopLister{}
|
||||
}
|
||||
|
||||
hardCtx, hardCancel := context.WithCancel(context.Background())
|
||||
gracefulCtx, gracefulCancel := context.WithCancel(hardCtx)
|
||||
@@ -192,9 +189,9 @@ func New(options Options) Agent {
|
||||
prometheusRegistry: prometheusRegistry,
|
||||
metrics: newAgentMetrics(prometheusRegistry),
|
||||
execer: options.Execer,
|
||||
lister: options.ContainerLister,
|
||||
|
||||
experimentalDevcontainersEnabled: options.ExperimentalDevcontainersEnabled,
|
||||
containerAPIOptions: options.ContainerAPIOptions,
|
||||
}
|
||||
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
|
||||
// Each time we connect we replace the channel (while holding the closeMutex) with a new one
|
||||
@@ -274,9 +271,10 @@ type agent struct {
|
||||
// labeled in Coder with the agent + workspace.
|
||||
metrics *agentMetrics
|
||||
execer agentexec.Execer
|
||||
lister agentcontainers.Lister
|
||||
|
||||
experimentalDevcontainersEnabled bool
|
||||
containerAPIOptions []agentcontainers.Option
|
||||
containerAPI atomic.Pointer[agentcontainers.API] // Set by apiHandler.
|
||||
}
|
||||
|
||||
func (a *agent) TailnetConn() *tailnet.Conn {
|
||||
@@ -1170,6 +1168,12 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
|
||||
}
|
||||
a.metrics.startupScriptSeconds.WithLabelValues(label).Set(dur)
|
||||
a.scriptRunner.StartCron()
|
||||
if containerAPI := a.containerAPI.Load(); containerAPI != nil {
|
||||
// Inform the container API that the agent is ready.
|
||||
// This allows us to start watching for changes to
|
||||
// the devcontainer configuration files.
|
||||
containerAPI.SignalReady()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("track conn goroutine: %w", err)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
@@ -253,6 +254,7 @@ func TestAPI(t *testing.T) {
|
||||
logger,
|
||||
agentcontainers.WithLister(tt.lister),
|
||||
agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI),
|
||||
agentcontainers.WithWatcher(watcher.NewNoop()),
|
||||
)
|
||||
defer api.Close()
|
||||
r.Mount("/", api.Routes())
|
||||
@@ -558,6 +560,7 @@ func TestAPI(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
apiOptions := []agentcontainers.Option{
|
||||
agentcontainers.WithLister(tt.lister),
|
||||
agentcontainers.WithWatcher(watcher.NewNoop()),
|
||||
}
|
||||
|
||||
if len(tt.knownDevcontainers) > 0 {
|
||||
@@ -631,6 +634,8 @@ func TestAPI(t *testing.T) {
|
||||
)
|
||||
defer api.Close()
|
||||
|
||||
api.SignalReady()
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Mount("/", api.Routes())
|
||||
|
||||
|
||||
+21
-6
@@ -37,10 +37,10 @@ func (a *agent) apiHandler() (http.Handler, func() error) {
|
||||
cacheDuration: cacheDuration,
|
||||
}
|
||||
|
||||
containerAPIOpts := []agentcontainers.Option{
|
||||
agentcontainers.WithLister(a.lister),
|
||||
}
|
||||
if a.experimentalDevcontainersEnabled {
|
||||
containerAPIOpts := []agentcontainers.Option{
|
||||
agentcontainers.WithExecer(a.execer),
|
||||
}
|
||||
manifest := a.manifest.Load()
|
||||
if manifest != nil && len(manifest.Devcontainers) > 0 {
|
||||
containerAPIOpts = append(
|
||||
@@ -48,12 +48,24 @@ func (a *agent) apiHandler() (http.Handler, func() error) {
|
||||
agentcontainers.WithDevcontainers(manifest.Devcontainers),
|
||||
)
|
||||
}
|
||||
|
||||
// Append after to allow the agent options to override the default options.
|
||||
containerAPIOpts = append(containerAPIOpts, a.containerAPIOptions...)
|
||||
|
||||
containerAPI := agentcontainers.NewAPI(a.logger.Named("containers"), containerAPIOpts...)
|
||||
r.Mount("/api/v0/containers", containerAPI.Routes())
|
||||
a.containerAPI.Store(containerAPI)
|
||||
} else {
|
||||
r.HandleFunc("/api/v0/containers", func(w http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
|
||||
Message: "The agent dev containers feature is experimental and not enabled by default.",
|
||||
Detail: "To enable this feature, set CODER_AGENT_DEVCONTAINERS_ENABLE=true in your template.",
|
||||
})
|
||||
})
|
||||
}
|
||||
containerAPI := agentcontainers.NewAPI(a.logger.Named("containers"), containerAPIOpts...)
|
||||
|
||||
promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger)
|
||||
|
||||
r.Mount("/api/v0/containers", containerAPI.Routes())
|
||||
r.Get("/api/v0/listening-ports", lp.handler)
|
||||
r.Get("/api/v0/netcheck", a.HandleNetcheck)
|
||||
r.Post("/api/v0/list-directory", a.HandleLS)
|
||||
@@ -64,7 +76,10 @@ func (a *agent) apiHandler() (http.Handler, func() error) {
|
||||
r.Get("/debug/prometheus", promHandler.ServeHTTP)
|
||||
|
||||
return r, func() error {
|
||||
return containerAPI.Close()
|
||||
if containerAPI := a.containerAPI.Load(); containerAPI != nil {
|
||||
return containerAPI.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user