Files
coder/scaletest/workspacebuild/run.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

291 lines
8.1 KiB
Go

package workspacebuild
import (
"context"
"fmt"
"io"
"net/http"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/sloghuman"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/scaletest/harness"
"github.com/coder/coder/v2/scaletest/loadtestutil"
)
type Runner struct {
client *codersdk.Client
cfg Config
workspaceID uuid.UUID
}
func NewRunner(client *codersdk.Client, cfg Config) *Runner {
return &Runner{
client: client,
cfg: cfg,
}
}
type SlimWorkspace struct {
ID uuid.UUID
Name string
}
// Run implements Runnable.
func (r *Runner) RunReturningWorkspace(ctx context.Context, id string, logs io.Writer) (SlimWorkspace, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
logs = loadtestutil.NewSyncWriter(logs)
logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
r.client.SetLogger(logger)
r.client.SetLogBodies(true)
req := r.cfg.Request
if req.Name == "" {
randName, err := loadtestutil.GenerateWorkspaceName(id)
if err != nil {
return SlimWorkspace{}, xerrors.Errorf("generate random name for workspace: %w", err)
}
req.Name = randName
}
workspace, err := r.client.CreateWorkspace(ctx, r.cfg.OrganizationID, r.cfg.UserID, req)
if err != nil {
return SlimWorkspace{}, xerrors.Errorf("create workspace: %w", err)
}
r.workspaceID = workspace.ID
if r.cfg.NoWaitForBuild {
_, _ = fmt.Fprintln(logs, "Skipping waiting for build")
} else {
err = waitForBuild(ctx, logs, r.client, workspace.LatestBuild.ID)
if err != nil {
for i := 0; i < r.cfg.Retry; i++ {
_, _ = fmt.Fprintf(logs, "Retrying build %d/%d...\n", i+1, r.cfg.Retry)
workspace.LatestBuild, err = r.client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: req.RichParameterValues,
TemplateVersionID: req.TemplateVersionID,
})
if err != nil {
return SlimWorkspace{}, xerrors.Errorf("create workspace build: %w", err)
}
err = waitForBuild(ctx, logs, r.client, workspace.LatestBuild.ID)
if err == nil {
break
}
}
if err != nil {
return SlimWorkspace{}, xerrors.Errorf("wait for build: %w", err)
}
}
}
if r.cfg.NoWaitForAgents {
_, _ = fmt.Fprintln(logs, "Skipping agent connectivity check.")
} else {
_, _ = fmt.Fprintln(logs, "")
err = waitForAgents(ctx, logs, r.client, workspace.ID)
if err != nil {
return SlimWorkspace{}, xerrors.Errorf("wait for agent: %w", err)
}
}
// Some users of this runner might not need the full workspace, and
// want to avoid querying the workspace.
return SlimWorkspace{ID: workspace.ID, Name: workspace.Name}, nil
}
// CleanupRunner is a runner that deletes a workspace in the Run phase.
type CleanupRunner struct {
client *codersdk.Client
workspaceID uuid.UUID
}
var _ harness.Runnable = &CleanupRunner{}
func NewCleanupRunner(client *codersdk.Client, workspaceID uuid.UUID) *CleanupRunner {
return &CleanupRunner{
client: client,
workspaceID: workspaceID,
}
}
// Run implements Runnable.
func (r *CleanupRunner) Run(ctx context.Context, _ string, logs io.Writer) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
logs = loadtestutil.NewSyncWriter(logs)
logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
if r.workspaceID == uuid.Nil {
return nil
}
logger.Info(ctx, "deleting workspace", slog.F("workspace_id", r.workspaceID))
r.client.SetLogger(logger)
r.client.SetLogBodies(true)
ws, err := r.client.Workspace(ctx, r.workspaceID)
if err != nil {
var sdkErr *codersdk.Error
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
logger.Info(ctx, "workspace not found, skipping delete", slog.F("workspace_id", r.workspaceID))
return nil
}
return err
}
build, err := r.client.WorkspaceBuild(ctx, ws.LatestBuild.ID)
if err == nil && build.Job.Status.Active() {
// mark the build as canceled
logger.Info(ctx, "canceling workspace build", slog.F("build_id", build.ID), slog.F("workspace_id", r.workspaceID))
if err = r.client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{}); err != nil {
logger.Warn(ctx, "failed to cancel workspace build", slog.Error(err))
}
// Wait for either the build or the cancellation to finish
// either is necessary or we'll fail at the delete step.
_ = waitForBuild(ctx, logs, r.client, build.ID) // it will return a "build canceled" error
} else {
logger.Warn(ctx, "unable to lookup latest workspace build, attempting to delete anyway", slog.Error(err))
}
build, err = r.client.CreateWorkspaceBuild(ctx, r.workspaceID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
if err != nil {
return xerrors.Errorf("delete workspace: %w", err)
}
err = waitForBuild(ctx, logs, r.client, build.ID)
if err != nil {
return xerrors.Errorf("wait for build: %w", err)
}
return nil
}
// Cleanup implements Cleanable by wrapping CleanupRunner.
func (r *Runner) Cleanup(ctx context.Context, id string, w io.Writer) error {
return (&CleanupRunner{
client: r.client,
workspaceID: r.workspaceID,
}).Run(ctx, id, w)
}
func waitForBuild(ctx context.Context, w io.Writer, client *codersdk.Client, buildID uuid.UUID) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
_, _ = fmt.Fprint(w, "Build is currently queued...")
// Wait for build to start.
for {
build, err := client.WorkspaceBuild(ctx, buildID)
if err != nil {
return xerrors.Errorf("fetch build: %w", err)
}
if build.Job.Status != codersdk.ProvisionerJobPending {
break
}
_, _ = fmt.Fprint(w, ".")
time.Sleep(500 * time.Millisecond)
}
_, _ = fmt.Fprintln(w, "\nBuild started! Streaming logs below:")
logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, buildID, 0)
if err != nil {
return xerrors.Errorf("start streaming build logs: %w", err)
}
defer closer.Close()
currentStage := ""
for {
select {
case <-ctx.Done():
return ctx.Err()
case log, ok := <-logs:
if !ok {
build, err := client.WorkspaceBuild(ctx, buildID)
if err != nil {
return xerrors.Errorf("fetch build: %w", err)
}
_, _ = fmt.Fprintln(w, "")
switch build.Job.Status {
case codersdk.ProvisionerJobSucceeded:
_, _ = fmt.Fprintln(w, "\nBuild succeeded!")
return nil
case codersdk.ProvisionerJobFailed:
_, _ = fmt.Fprintf(w, "\nBuild failed with error %q.\nSee logs above for more details.\n", build.Job.Error)
return xerrors.Errorf("build failed with status %q: %s", build.Job.Status, build.Job.Error)
case codersdk.ProvisionerJobCanceled:
_, _ = fmt.Fprintln(w, "\nBuild canceled.")
return xerrors.New("build canceled")
default:
_, _ = fmt.Fprintf(w, "\nLogs disconnected with unexpected job status %q and error %q.\n", build.Job.Status, build.Job.Error)
return xerrors.Errorf("logs disconnected with unexpected job status %q and error %q", build.Job.Status, build.Job.Error)
}
}
if log.Stage != currentStage {
currentStage = log.Stage
_, _ = fmt.Fprintf(w, "\n%s\n", currentStage)
}
level := "unknown"
if log.Level != "" {
level = string(log.Level)
}
_, _ = fmt.Fprintf(w, "\t%s:\t%s\n", level, log.Output)
}
}
}
func waitForAgents(ctx context.Context, w io.Writer, client *codersdk.Client, workspaceID uuid.UUID) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
_, _ = fmt.Fprint(w, "Waiting for agents to connect...\n\n")
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
workspace, err := client.Workspace(ctx, workspaceID)
if err != nil {
return xerrors.Errorf("fetch workspace: %w", err)
}
ok := true
for _, res := range workspace.LatestBuild.Resources {
for _, agent := range res.Agents {
if agent.Status != codersdk.WorkspaceAgentConnected {
ok = false
}
_, _ = fmt.Fprintf(w, "\tAgent %q is %s\n", agent.Name, agent.Status)
}
}
if ok {
break
}
_, _ = fmt.Fprintln(w, "")
time.Sleep(1 * time.Second)
}
_, _ = fmt.Fprint(w, "\nAgents connected!\n\n")
return nil
}