@@ -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 {
2733client client
2834patcher 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+
6878logs = loadtestutil .NewSyncWriter (logs )
6979r .logger = slog .Make (sloghuman .Sink (logs )).Leveled (slog .LevelDebug ).Named (name )
7080r .client .initialize (r .logger )
@@ -74,26 +84,23 @@ func (r *Runner) Run(ctx context.Context, name string, logs io.Writer) error {
7484slog .F ("template_id" ,r .cfg .TemplateID ),
7585slog .F ("workspace_name" ,r .cfg .WorkspaceName ))
7686
77- result ,err := r .client . createExternalWorkspace (ctx , codersdk.CreateWorkspaceRequest {
87+ result ,err := r .createExternalWorkspace (ctx , codersdk.CreateWorkspaceRequest {
7888TemplateID :r .cfg .TemplateID ,
7989Name :r .cfg .WorkspaceName ,
8090})
8191if err != nil {
92+ r .cfg .Metrics .ReportTaskStatusErrorsTotal .WithLabelValues (r .cfg .MetricLabelValues ... ).Inc ()
8293return xerrors .Errorf ("create external workspace: %w" ,err )
8394}
8495
8596// Set the workspace ID
86- r .workspaceID = result .WorkspaceID
97+ r .workspaceID = result .workspaceID
8798r .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 )
91102r .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-
97104workspaceUpdatesCtx ,cancelWorkspaceUpdates := context .WithCancel (ctx )
98105defer cancelWorkspaceUpdates ()
99106workspaceUpdatesResult := make (chan error ,1 )
@@ -257,3 +264,77 @@ func parseStatusMessage(message string) (int, bool) {
257264}
258265return 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+ }