From 1ad998ee3af7db5fdaf3eef3936cc74fb2b37d76 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 18 Oct 2023 15:08:02 -0500 Subject: [PATCH] fix: add requester IP to workspace build audit logs (#10242) --- cli/server.go | 9 + cli/templates.go | 1 + cli/templateversionarchive.go | 1 + cli/templateversions.go | 1 + cli/user_delete_test.go | 1 - coderd/audit/request.go | 67 +++++- coderd/audit/request_test.go | 33 +++ coderd/autobuild/lifecycle_executor.go | 3 +- coderd/database/dbfake/dbfake.go | 1 + coderd/externalauth.go | 3 +- .../provisionerdserver/provisionerdserver.go | 6 + coderd/workspacebuilds.go | 2 + coderd/workspacebuilds_test.go | 199 +++++++++++++++++- coderd/workspaces.go | 4 +- coderd/workspaces_test.go | 149 ------------- coderd/wsbuilder/wsbuilder.go | 9 +- coderd/wsbuilder/wsbuilder_test.go | 66 +++++- 17 files changed, 381 insertions(+), 174 deletions(-) create mode 100644 coderd/audit/request_test.go diff --git a/cli/server.go b/cli/server.go index 9f33ced438..6b6b218fb7 100644 --- a/cli/server.go +++ b/cli/server.go @@ -41,6 +41,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "golang.org/x/mod/semver" "golang.org/x/oauth2" @@ -2020,6 +2022,13 @@ func ConfigureTraceProvider( sqlDriver = "postgres" ) + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ), + ) + if cfg.Trace.Enable.Value() || cfg.Trace.DataDog.Value() || cfg.Trace.HoneycombAPIKey != "" { sdkTracerProvider, _closeTracing, err := tracing.TracerProvider(ctx, "coderd", tracing.TracerOpts{ Default: cfg.Trace.Enable.Value(), diff --git a/cli/templates.go b/cli/templates.go index 391ac77008..2dec46d226 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -11,6 +11,7 @@ import ( "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" ) func (r *RootCmd) templates() *clibase.Cmd { diff --git a/cli/templateversionarchive.go b/cli/templateversionarchive.go index d334bdb83f..740de869ff 100644 --- a/cli/templateversionarchive.go +++ b/cli/templateversionarchive.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" ) func (r *RootCmd) unarchiveTemplateVersion() *clibase.Cmd { diff --git a/cli/templateversions.go b/cli/templateversions.go index 4ccba09b63..5b99a46310 100644 --- a/cli/templateversions.go +++ b/cli/templateversions.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" ) func (r *RootCmd) templateVersions() *clibase.Cmd { diff --git a/cli/user_delete_test.go b/cli/user_delete_test.go index d8a6956577..9ee546ca7a 100644 --- a/cli/user_delete_test.go +++ b/cli/user_delete_test.go @@ -121,7 +121,6 @@ func TestUserDelete(t *testing.T) { // pw, err := cryptorand.String(16) // require.NoError(t, err) - // fmt.Println(aUser.OrganizationID) // toDelete, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ // Email: "colin5@coder.com", // Username: "coolin", diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 812dc1e5c5..cc1f60779a 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -11,6 +11,8 @@ import ( "github.com/google/uuid" "github.com/sqlc-dev/pqtype" + "go.opentelemetry.io/otel/baggage" + "golang.org/x/xerrors" "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" @@ -54,6 +56,7 @@ type BuildAuditParams[T Auditable] struct { Status int Action database.AuditAction OrganizationID uuid.UUID + IP string AdditionalFields json.RawMessage New T @@ -248,9 +251,7 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request // WorkspaceBuildAudit creates an audit log for a workspace build. // The audit log is committed upon invocation. func WorkspaceBuildAudit[T Auditable](ctx context.Context, p *BuildAuditParams[T]) { - // As the audit request has not been initiated directly by a user, we omit - // certain user details. - ip := parseIP("") + ip := parseIP(p.IP) diff := Diff(p.Audit, p.Old, p.New) var err error @@ -280,16 +281,70 @@ func WorkspaceBuildAudit[T Auditable](ctx context.Context, p *BuildAuditParams[T RequestID: p.JobID, AdditionalFields: p.AdditionalFields, } - exportErr := p.Audit.Export(ctx, auditLog) - if exportErr != nil { + err = p.Audit.Export(ctx, auditLog) + if err != nil { p.Log.Error(ctx, "export audit log", slog.F("audit_log", auditLog), slog.Error(err), ) - return } } +type WorkspaceBuildBaggage struct { + IP string +} + +func (b WorkspaceBuildBaggage) Props() ([]baggage.Property, error) { + ipProp, err := baggage.NewKeyValueProperty("ip", b.IP) + if err != nil { + return nil, xerrors.Errorf("create ip kv property: %w", err) + } + + return []baggage.Property{ipProp}, nil +} + +func WorkspaceBuildBaggageFromRequest(r *http.Request) WorkspaceBuildBaggage { + return WorkspaceBuildBaggage{IP: r.RemoteAddr} +} + +type Baggage interface { + Props() ([]baggage.Property, error) +} + +func BaggageToContext(ctx context.Context, d Baggage) (context.Context, error) { + props, err := d.Props() + if err != nil { + return ctx, xerrors.Errorf("create baggage properties: %w", err) + } + + m, err := baggage.NewMember("audit", "baggage", props...) + if err != nil { + return ctx, xerrors.Errorf("create new baggage member: %w", err) + } + + b, err := baggage.New(m) + if err != nil { + return ctx, xerrors.Errorf("create new baggage carrier: %w", err) + } + + return baggage.ContextWithBaggage(ctx, b), nil +} + +func BaggageFromContext(ctx context.Context) WorkspaceBuildBaggage { + d := WorkspaceBuildBaggage{} + b := baggage.FromContext(ctx) + props := b.Member("audit").Properties() + for _, prop := range props { + switch prop.Key() { + case "ip": + d.IP, _ = prop.Value() + default: + } + } + + return d +} + func either[T Auditable, R any](old, new T, fn func(T) R, auditAction database.AuditAction) R { if ResourceID(new) != uuid.Nil { return fn(new) diff --git a/coderd/audit/request_test.go b/coderd/audit/request_test.go new file mode 100644 index 0000000000..e0040425d4 --- /dev/null +++ b/coderd/audit/request_test.go @@ -0,0 +1,33 @@ +package audit_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/propagation" + + "github.com/coder/coder/v2/coderd/audit" +) + +func TestBaggage(t *testing.T) { + t.Parallel() + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + + expected := audit.WorkspaceBuildBaggage{ + IP: "127.0.0.1", + } + + ctx, err := audit.BaggageToContext(context.Background(), expected) + require.NoError(t, err) + + carrier := propagation.MapCarrier{} + prop.Inject(ctx, carrier) + bCtx := prop.Extract(ctx, carrier) + got := audit.BaggageFromContext(bCtx) + + require.Equal(t, expected, got) +} diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index 07c586b233..f3b3ff5846 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -184,7 +184,8 @@ func (e *Executor) runOnce(t time.Time) Stats { builder = builder.ActiveVersion() } - build, job, err = builder.Build(e.ctx, tx, nil) + build, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"}) + if err != nil { log.Error(e.ctx, "unable to transition workspace", slog.F("transition", nextTransition), diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index bffd855da6..e138584498 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -4589,6 +4589,7 @@ func (q *FakeQuerier) InsertProvisionerJob(_ context.Context, arg database.Inser Type: arg.Type, Input: arg.Input, Tags: arg.Tags, + TraceMetadata: arg.TraceMetadata, } job.JobStatus = provisonerJobStatus(job) q.provisionerJobs = append(q.provisionerJobs, job) diff --git a/coderd/externalauth.go b/coderd/externalauth.go index 31dff667c2..774a5f8603 100644 --- a/coderd/externalauth.go +++ b/coderd/externalauth.go @@ -6,9 +6,8 @@ import ( "fmt" "net/http" - "golang.org/x/sync/errgroup" - "github.com/sqlc-dev/pqtype" + "golang.org/x/sync/errgroup" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 5afb85565c..38038df49d 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -912,12 +912,15 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto. s.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err)) } + bag := audit.BaggageFromContext(ctx) + audit.WorkspaceBuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ Audit: *auditor, Log: s.Logger, UserID: job.InitiatorID, OrganizationID: workspace.OrganizationID, JobID: job.ID, + IP: bag.IP, Action: auditAction, Old: previousBuild, New: build, @@ -1259,12 +1262,15 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) s.Logger.Error(ctx, "marshal resource info for successful job", slog.Error(err)) } + bag := audit.BaggageFromContext(ctx) + audit.WorkspaceBuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{ Audit: *auditor, Log: s.Logger, UserID: job.InitiatorID, OrganizationID: workspace.OrganizationID, JobID: job.ID, + IP: bag.IP, Action: auditAction, Old: previousBuild, New: workspaceBuild, diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 16326f9945..5025d778d8 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -17,6 +17,7 @@ import ( "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -372,6 +373,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { func(action rbac.Action, object rbac.Objecter) bool { return api.Authorize(r, action, object) }, + audit.WorkspaceBuildBaggageFromRequest(r), ) var buildErr wsbuilder.BuildError if xerrors.As(err, &buildErr) { diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index c5c1d353d2..1f487e6915 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -12,6 +12,8 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" "golang.org/x/xerrors" "cdr.dev/slog" @@ -29,11 +31,22 @@ import ( func TestWorkspaceBuild(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ), + ) + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Auditor: auditor, + }) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + auditor.ResetLogs() workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) @@ -41,6 +54,8 @@ func TestWorkspaceBuild(t *testing.T) { _, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID) require.NoError(t, err) + require.Len(t, auditor.AuditLogs(), 1) + require.Equal(t, auditor.AuditLogs()[0].Ip.IPNet.IP.String(), "127.0.0.1") } func TestWorkspaceBuildByBuildNumber(t *testing.T) { @@ -854,3 +869,185 @@ func TestWorkspaceBuildDebugMode(t *testing.T) { require.Equal(t, 2, logsProcessed) }) } + +func TestPostWorkspaceBuild(t *testing.T) { + t.Parallel() + t.Run("NoTemplateVersion", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: uuid.New(), + Transition: codersdk.WorkspaceTransitionStart, + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("TemplateVersionFailedImport", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + ProvisionApply: []*proto.Response{{}}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "workspace", + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("AlreadyActive", func(t *testing.T) { + t.Parallel() + client, closer := coderdtest.NewWithProvisionerCloser(t, nil) + defer closer.Close() + + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + closer.Close() + // Close here so workspace build doesn't process! + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: template.ActiveVersionID, + Transition: codersdk.WorkspaceTransitionStart, + }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("Audit", func(t *testing.T) { + t.Parallel() + + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ), + ) + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + auditor.ResetLogs() + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: template.ActiveVersionID, + Transition: codersdk.WorkspaceTransitionStart, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID) + + require.Len(t, auditor.AuditLogs(), 1) + require.Equal(t, auditor.AuditLogs()[0].Ip.IPNet.IP.String(), "127.0.0.1") + }) + + t.Run("IncrementBuildNumber", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: template.ActiveVersionID, + Transition: codersdk.WorkspaceTransitionStart, + }) + require.NoError(t, err) + require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) + }) + + t.Run("WithState", func(t *testing.T) { + t.Parallel() + client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + wantState := []byte("something") + _ = closeDaemon.Close() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: template.ActiveVersionID, + Transition: codersdk.WorkspaceTransitionStart, + ProvisionerState: wantState, + }) + require.NoError(t, err) + gotState, err := client.WorkspaceBuildState(ctx, build.ID) + require.NoError(t, err) + require.Equal(t, wantState, gotState) + }) + + t.Run("Delete", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + }) + require.NoError(t, err) + require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID) + + res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Owner: user.UserID.String(), + }) + require.NoError(t, err) + require.Len(t, res.Workspaces, 0) + }) +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 56cfc8e243..c495d25028 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -512,7 +512,9 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req db, func(action rbac.Action, object rbac.Objecter) bool { return api.Authorize(r, action, object) - }) + }, + audit.WorkspaceBuildBaggageFromRequest(r), + ) return err }, nil) var bldErr wsbuilder.BuildError diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 74a91c658e..a12c262cb1 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -1563,155 +1563,6 @@ func TestOffsetLimit(t *testing.T) { require.Len(t, ws.Workspaces, 0) } -func TestPostWorkspaceBuild(t *testing.T) { - t.Parallel() - t.Run("NoTemplateVersion", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - TemplateVersionID: uuid.New(), - Transition: codersdk.WorkspaceTransitionStart, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("TemplateVersionFailedImport", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - ProvisionApply: []*proto.Response{{}}, - }) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{ - TemplateID: template.ID, - Name: "workspace", - }) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) - }) - - t.Run("AlreadyActive", func(t *testing.T) { - t.Parallel() - client, closer := coderdtest.NewWithProvisionerCloser(t, nil) - defer closer.Close() - - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - closer.Close() - // Close here so workspace build doesn't process! - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - _, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - TemplateVersionID: template.ActiveVersionID, - Transition: codersdk.WorkspaceTransitionStart, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusConflict, apiErr.StatusCode()) - }) - - t.Run("IncrementBuildNumber", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - TemplateVersionID: template.ActiveVersionID, - Transition: codersdk.WorkspaceTransitionStart, - }) - require.NoError(t, err) - require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) - }) - - t.Run("WithState", func(t *testing.T) { - t.Parallel() - client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - wantState := []byte("something") - _ = closeDaemon.Close() - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - TemplateVersionID: template.ActiveVersionID, - Transition: codersdk.WorkspaceTransitionStart, - ProvisionerState: wantState, - }) - require.NoError(t, err) - gotState, err := client.WorkspaceBuildState(ctx, build.ID) - require.NoError(t, err) - require.Equal(t, wantState, gotState) - }) - - t.Run("Delete", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - Transition: codersdk.WorkspaceTransitionDelete, - }) - require.NoError(t, err) - require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID) - - res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ - Owner: user.UserID.String(), - }) - require.NoError(t, err) - require.Len(t, res.Workspaces, 0) - }) -} - func TestWorkspaceUpdateAutostart(t *testing.T) { t.Parallel() dublinLoc := mustLocation(t, "Europe/Dublin") diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 008bc88ab7..66e784d846 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -15,6 +15,7 @@ import ( "github.com/sqlc-dev/pqtype" "golang.org/x/xerrors" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -201,16 +202,20 @@ func (b *Builder) Build( ctx context.Context, store database.Store, authFunc func(action rbac.Action, object rbac.Objecter) bool, + auditBaggage audit.WorkspaceBuildBaggage, ) ( *database.WorkspaceBuild, *database.ProvisionerJob, error, ) { - b.ctx = ctx + var err error + b.ctx, err = audit.BaggageToContext(ctx, auditBaggage) + if err != nil { + return nil, nil, xerrors.Errorf("create audit baggage: %w", err) + } // Run the build in a transaction with RepeatableRead isolation, and retries. // RepeatableRead isolation ensures that we get a consistent view of the database while // computing the new build. This simplifies the logic so that we do not need to worry if // later reads are consistent with earlier ones. - var err error for retries := 0; retries < 5; retries++ { var workspaceBuild *database.WorkspaceBuild var provisionerJob *database.ProvisionerJob diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index 224a40cfb8..c55d440ac2 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -12,7 +12,10 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -88,7 +91,7 @@ func TestBuilder_NoOptions(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) } @@ -123,7 +126,48 @@ func TestBuilder_Initiator(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) + req.NoError(err) +} + +func TestBuilder_Baggage(t *testing.T) { + t.Parallel() + req := require.New(t) + asrt := assert.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ), + ) + + mDB := expectDB(t, + // Inputs + withTemplate, + withInactiveVersion(nil), + withLastBuildFound, + withRichParameters(nil), + withParameterSchemas(inactiveJobID, nil), + + // Outputs + expectProvisionerJob(func(job database.InsertProvisionerJobParams) { + asrt.Contains(string(job.TraceMetadata.RawMessage), "ip=127.0.0.1") + }), + withInTx, + expectBuild(func(bld database.InsertWorkspaceBuildParams) { + }), + expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) { + }), + withBuild, + ) + + ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} + uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"}) req.NoError(err) } @@ -157,7 +201,7 @@ func TestBuilder_Reason(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Reason(database.BuildReasonAutostart) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) } @@ -196,7 +240,7 @@ func TestBuilder_ActiveVersion(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).ActiveVersion() - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) } @@ -274,7 +318,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) }) t.Run("UsePreviousParameterValues", func(t *testing.T) { @@ -317,7 +361,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) }) @@ -357,7 +401,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) bldErr := wsbuilder.BuildError{} req.ErrorAs(err, &bldErr) asrt.Equal(http.StatusBadRequest, bldErr.Status) @@ -394,7 +438,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) bldErr := wsbuilder.BuildError{} req.ErrorAs(err, &bldErr) asrt.Equal(http.StatusBadRequest, bldErr.Status) @@ -456,7 +500,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { uut := wsbuilder.New(ws, database.WorkspaceTransitionStart). RichParameterValues(nextBuildParameters). VersionID(activeVersionID) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) }) @@ -516,7 +560,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { uut := wsbuilder.New(ws, database.WorkspaceTransitionStart). RichParameterValues(nextBuildParameters). VersionID(activeVersionID) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) }) @@ -574,7 +618,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { uut := wsbuilder.New(ws, database.WorkspaceTransitionStart). RichParameterValues(nextBuildParameters). VersionID(activeVersionID) - _, _, err := uut.Build(ctx, mDB, nil) + _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) req.NoError(err) }) }