diff --git a/agent/agent.go b/agent/agent.go index b05a4d4a90ed8..3c02b5f2790f0 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1158,11 +1158,26 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, } } - scripts := manifest.Scripts + var ( + scripts = manifest.Scripts + devcontainerScripts map[uuid.UUID]codersdk.WorkspaceAgentScript + ) if a.containerAPI != nil { + // Init the container API with the manifest and client so that + // we can start accepting requests. The final start of the API + // happens after the startup scripts have been executed to + // ensure the presence of required tools. This means we can + // return existing devcontainers but actual container detection + // and creation will be deferred. + a.containerAPI.Init( + agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName), + agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts), + agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)), + ) + // Since devcontainer are enabled, remove devcontainer scripts // from the main scripts list to avoid showing an error. - scripts, _ = agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, manifest.Scripts) + scripts, devcontainerScripts = agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, scripts) } err = a.scriptRunner.Init(scripts, aAPI.ScriptCompleted) if err != nil { @@ -1183,13 +1198,10 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, err := a.scriptRunner.Execute(a.gracefulCtx, agentscripts.ExecuteStartScripts) if a.containerAPI != nil { - a.containerAPI.Init( - agentcontainers.WithManifestInfo(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName), - agentcontainers.WithDevcontainers(manifest.Devcontainers, manifest.Scripts), - agentcontainers.WithSubAgentClient(agentcontainers.NewSubAgentClientFromAPI(a.logger, aAPI)), - ) - - _, devcontainerScripts := agentcontainers.ExtractDevcontainerScripts(manifest.Devcontainers, manifest.Scripts) + // Start the container API after the startup scripts have + // been executed to ensure that the required tools can be + // installed. + a.containerAPI.Start() for _, dc := range manifest.Devcontainers { cErr := a.createDevcontainer(ctx, aAPI, dc, devcontainerScripts[dc.ID]) err = errors.Join(err, cErr) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 336ab72ccf806..3faa97c3e0511 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -53,7 +53,6 @@ type API struct { cancel context.CancelFunc watcherDone chan struct{} updaterDone chan struct{} - initialUpdateDone chan struct{} // Closed after first update in updaterLoop. updateTrigger chan chan error // Channel to trigger manual refresh. updateInterval time.Duration // Interval for periodic container updates. logger slog.Logger @@ -73,7 +72,8 @@ type API struct { workspaceName string parentAgent string - mu sync.RWMutex + mu sync.RWMutex // Protects the following fields. + initDone chan struct{} // Closed by Init. closed bool containers codersdk.WorkspaceAgentListContainersResponse // Output from the last list operation. containersErr error // Error from the last list operation. @@ -270,7 +270,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API { api := &API{ ctx: ctx, cancel: cancel, - initialUpdateDone: make(chan struct{}), + initDone: make(chan struct{}), updateTrigger: make(chan chan error), updateInterval: defaultUpdateInterval, logger: logger, @@ -322,18 +322,37 @@ func NewAPI(logger slog.Logger, options ...Option) *API { } // Init applies a final set of options to the API and then -// begins the watcherLoop and updaterLoop. This function -// must only be called once. +// closes initDone. This method can only be called once. func (api *API) Init(opts ...Option) { api.mu.Lock() defer api.mu.Unlock() if api.closed { return } + select { + case <-api.initDone: + return + default: + } + defer close(api.initDone) for _, opt := range opts { opt(api) } +} + +// Start starts the API by initializing the watcher and updater loops. +// This method calls Init, if it is desired to apply options after +// the API has been created, it should be done by calling Init before +// Start. This method must only be called once. +func (api *API) Start() { + api.Init() + + api.mu.Lock() + defer api.mu.Unlock() + if api.closed { + return + } api.watcherDone = make(chan struct{}) api.updaterDone = make(chan struct{}) @@ -412,9 +431,6 @@ func (api *API) updaterLoop() { } else { api.logger.Debug(api.ctx, "initial containers update complete") } - // Signal that the initial update attempt (successful or not) is done. - // Other services can wait on this if they need the first data to be available. - close(api.initialUpdateDone) // We utilize a TickerFunc here instead of a regular Ticker so that // we can guarantee execution of the updateContainers method after @@ -474,7 +490,7 @@ func (api *API) UpdateSubAgentClient(client SubAgentClient) { func (api *API) Routes() http.Handler { r := chi.NewRouter() - ensureInitialUpdateDoneMW := func(next http.Handler) http.Handler { + ensureInitDoneMW := func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { select { case <-api.ctx.Done(): @@ -485,9 +501,8 @@ func (api *API) Routes() http.Handler { return case <-r.Context().Done(): return - case <-api.initialUpdateDone: - // Initial update is done, we can start processing - // requests. + case <-api.initDone: + // API init is done, we can start processing requests. } next.ServeHTTP(rw, r) }) @@ -496,7 +511,7 @@ func (api *API) Routes() http.Handler { // For now, all endpoints require the initial update to be done. // If we want to allow some endpoints to be available before // the initial update, we can enable this per-route. - r.Use(ensureInitialUpdateDoneMW) + r.Use(ensureInitDoneMW) r.Get("/", api.handleList) // TODO(mafredri): Simplify this route as the previous /devcontainers diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 7c9b7ce0f632d..ab857ee59cb0b 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -437,7 +437,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithContainerCLI(mLister), agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) - api.Init() + api.Start() defer api.Close() r.Mount("/", api.Routes()) @@ -627,7 +627,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithDevcontainers(tt.setupDevcontainers, nil), ) - api.Init() + api.Start() defer api.Close() r.Mount("/", api.Routes()) @@ -1068,7 +1068,7 @@ func TestAPI(t *testing.T) { } api := agentcontainers.NewAPI(logger, apiOptions...) - api.Init() + api.Start() defer api.Close() r.Mount("/", api.Routes()) @@ -1158,7 +1158,7 @@ func TestAPI(t *testing.T) { []codersdk.WorkspaceAgentScript{{LogSourceID: uuid.New(), ID: dc.ID}}, ), ) - api.Init() + api.Start() defer api.Close() // Make sure the ticker function has been registered @@ -1254,7 +1254,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithWatcher(fWatcher), agentcontainers.WithClock(mClock), ) - api.Init() + api.Start() defer api.Close() r := chi.NewRouter() @@ -1408,7 +1408,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithDevcontainerCLI(fakeDCCLI), agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"), ) - api.Init() + api.Start() apiClose := func() { closeOnce.Do(func() { // Close before api.Close() defer to avoid deadlock after test. @@ -1635,7 +1635,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithSubAgentClient(fakeSAC), agentcontainers.WithDevcontainerCLI(&fakeDevcontainerCLI{}), ) - api.Init() + api.Start() defer api.Close() tickerTrap.MustWait(ctx).MustRelease(ctx) @@ -1958,7 +1958,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithSubAgentURL("test-subagent-url"), agentcontainers.WithWatcher(watcher.NewNoop()), ) - api.Init() + api.Start() defer api.Close() // Close before api.Close() defer to avoid deadlock after test. @@ -2052,7 +2052,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithSubAgentURL("test-subagent-url"), agentcontainers.WithWatcher(watcher.NewNoop()), ) - api.Init() + api.Start() defer api.Close() // Close before api.Close() defer to avoid deadlock after test. @@ -2158,7 +2158,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithWatcher(watcher.NewNoop()), agentcontainers.WithManifestInfo("test-user", "test-workspace", "test-parent-agent"), ) - api.Init() + api.Start() defer api.Close() // Close before api.Close() defer to avoid deadlock after test. @@ -2228,7 +2228,7 @@ func TestAPI(t *testing.T) { agentcontainers.WithExecer(fakeExec), agentcontainers.WithCommandEnv(commandEnv), ) - api.Init() + api.Start() defer api.Close() // Call RefreshContainers directly to trigger CommandEnv usage. @@ -2318,13 +2318,16 @@ func TestAPI(t *testing.T) { agentcontainers.WithWatcher(fWatcher), agentcontainers.WithClock(mClock), ) - api.Init() + api.Start() defer func() { close(fakeSAC.createErrC) close(fakeSAC.deleteErrC) api.Close() }() + err := api.RefreshContainers(ctx) + require.NoError(t, err, "RefreshContainers should not error") + r := chi.NewRouter() r.Mount("/", api.Routes()) @@ -2335,7 +2338,7 @@ func TestAPI(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) var response codersdk.WorkspaceAgentListContainersResponse - err := json.NewDecoder(rec.Body).Decode(&response) + err = json.NewDecoder(rec.Body).Decode(&response) require.NoError(t, err) assert.Empty(t, response.Devcontainers, "ignored devcontainer should not be in response when ignore=true") @@ -2519,7 +2522,7 @@ func TestSubAgentCreationWithNameRetry(t *testing.T) { agentcontainers.WithSubAgentClient(fSAC), agentcontainers.WithWatcher(watcher.NewNoop()), ) - api.Init() + api.Start() defer api.Close() tickerTrap.MustWait(ctx).MustRelease(ctx) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy