@@ -23,6 +23,12 @@ import (
2323
2424const statusUpdatePrefix = "scaletest status update:"
2525
26+ // createExternalWorkspaceResult contains the results from creating an external workspace.
27+ type createExternalWorkspaceResult struct {
28+ workspaceID uuid.UUID
29+ agentToken string
30+ }
31+
2632type Runner struct {
2733 client client
2834 patcher appStatusPatcher
@@ -65,6 +71,10 @@ func (r *Runner) Run(ctx context.Context, name string, logs io.Writer) error {
6571 }
6672 }()
6773
74+ // ensure these labels are initialized, so we see the time series right away in prometheus.
75+ r .cfg .Metrics .MissingStatusUpdatesTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Add (0 )
76+ r .cfg .Metrics .ReportTaskStatusErrorsTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Add (0 )
77+
6878 logs = loadtestutil .NewSyncWriter (logs )
6979 r .logger = slog .Make (sloghuman .Sink (logs )).Leveled (slog .LevelDebug ).Named (name )
7080 r .client .initialize (r .logger )
@@ -74,26 +84,23 @@ func (r *Runner) Run(ctx context.Context, name string, logs io.Writer) error {
7484 slog .F ("template_id" , r .cfg .TemplateID ),
7585 slog .F ("workspace_name" , r .cfg .WorkspaceName ))
7686
77- result , err := r .client . createExternalWorkspace (ctx , codersdk.CreateWorkspaceRequest {
87+ result , err := r .createExternalWorkspace (ctx , codersdk.CreateWorkspaceRequest {
7888 TemplateID : r .cfg .TemplateID ,
7989 Name : r .cfg .WorkspaceName ,
8090 })
8191 if err != nil {
92+ r .cfg .Metrics .ReportTaskStatusErrorsTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Inc ()
8293 return xerrors .Errorf ("create external workspace: %w" , err )
8394 }
8495
8596 // Set the workspace ID
86- r .workspaceID = result .WorkspaceID
97+ r .workspaceID = result .workspaceID
8798 r .logger .Info (ctx , "created external workspace" , slog .F ("workspace_id" , r .workspaceID ))
8899
89100 // Initialize the patcher with the agent token
90- r .patcher .initialize (r .logger , result .AgentToken )
101+ r .patcher .initialize (r .logger , result .agentToken )
91102 r .logger .Info (ctx , "initialized app status patcher with agent token" )
92103
93- // ensure these labels are initialized, so we see the time series right away in prometheus.
94- r .cfg .Metrics .MissingStatusUpdatesTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Add (0 )
95- r .cfg .Metrics .ReportTaskStatusErrorsTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Add (0 )
96-
97104 workspaceUpdatesCtx , cancelWorkspaceUpdates := context .WithCancel (ctx )
98105 defer cancelWorkspaceUpdates ()
99106 workspaceUpdatesResult := make (chan error , 1 )
@@ -257,3 +264,77 @@ func parseStatusMessage(message string) (int, bool) {
257264 }
258265 return msgNo , true
259266}
267+
268+ // createExternalWorkspace creates an external workspace and returns the workspace ID
269+ // and agent token for the first external agent found in the workspace resources.
270+ func (r * Runner ) createExternalWorkspace (ctx context.Context , req codersdk.CreateWorkspaceRequest ) (createExternalWorkspaceResult , error ) {
271+ // Create the workspace
272+ workspace , err := r .client .CreateUserWorkspace (ctx , codersdk .Me , req )
273+ if err != nil {
274+ return createExternalWorkspaceResult {}, err
275+ }
276+
277+ r .logger .Info (ctx , "waiting for workspace build to complete" ,
278+ slog .F ("workspace_name" , workspace .Name ),
279+ slog .F ("workspace_id" , workspace .ID ))
280+
281+ // Poll the workspace until the build is complete
282+ var finalWorkspace codersdk.Workspace
283+ buildComplete := xerrors .New ("build complete" ) // sentinel error
284+ waiter := r .clock .TickerFunc (ctx , 30 * time .Second , func () error {
285+ // Get the workspace with latest build details
286+ workspace , err := r .client .WorkspaceByOwnerAndName (ctx , codersdk .Me , workspace .Name , codersdk.WorkspaceOptions {})
287+ if err != nil {
288+ r .logger .Error (ctx , "failed to poll workspace while waiting for build to complete" , slog .Error (err ))
289+ return nil
290+ }
291+
292+ jobStatus := workspace .LatestBuild .Job .Status
293+ r .logger .Debug (ctx , "checking workspace build status" ,
294+ slog .F ("status" , jobStatus ),
295+ slog .F ("build_id" , workspace .LatestBuild .ID ))
296+
297+ switch jobStatus {
298+ case codersdk .ProvisionerJobSucceeded :
299+ // Build succeeded
300+ r .logger .Info (ctx , "workspace build succeeded" )
301+ finalWorkspace = workspace
302+ return buildComplete
303+ case codersdk .ProvisionerJobFailed :
304+ return xerrors .Errorf ("workspace build failed: %s" , workspace .LatestBuild .Job .Error )
305+ case codersdk .ProvisionerJobCanceled :
306+ return xerrors .Errorf ("workspace build was canceled" )
307+ case codersdk .ProvisionerJobPending , codersdk .ProvisionerJobRunning , codersdk .ProvisionerJobCanceling :
308+ // Still in progress, continue polling
309+ return nil
310+ default :
311+ return xerrors .Errorf ("unexpected job status: %s" , jobStatus )
312+ }
313+ }, "createExternalWorkspace" )
314+
315+ err = waiter .Wait ()
316+ if err != nil && ! xerrors .Is (err , buildComplete ) {
317+ return createExternalWorkspaceResult {}, xerrors .Errorf ("wait for build completion: %w" , err )
318+ }
319+
320+ // Find external agents in resources
321+ for _ , resource := range finalWorkspace .LatestBuild .Resources {
322+ if resource .Type != "coder_external_agent" || len (resource .Agents ) == 0 {
323+ continue
324+ }
325+
326+ // Get credentials for the first agent
327+ agent := resource .Agents [0 ]
328+ credentials , err := r .client .WorkspaceExternalAgentCredentials (ctx , finalWorkspace .ID , agent .Name )
329+ if err != nil {
330+ return createExternalWorkspaceResult {}, err
331+ }
332+
333+ return createExternalWorkspaceResult {
334+ workspaceID : finalWorkspace .ID ,
335+ agentToken : credentials .AgentToken ,
336+ }, nil
337+ }
338+
339+ return createExternalWorkspaceResult {}, xerrors .Errorf ("no external agent found in workspace" )
340+ }
0 commit comments