mirror of https://github.com/coder/coder.git
fix: add requester IP to workspace build audit logs (#10242)
This commit is contained in:
parent
504cedf15a
commit
1ad998ee3a
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue