mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
Audit build outcomes/kira pilot (#5143)
* auditing failed builds * logging workspace build successes * remove duplicate workspace build entry * fixed workspacebuilds_test * PR feedback * lint and migrations * fix nil auditors * workspace_build test * fixed workspaces_teest Co-authored-by: Colin Adler <colin1adler@gmail.com>
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
@ -21,6 +22,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
@ -46,6 +48,7 @@ type Server struct {
|
||||
Pubsub database.Pubsub
|
||||
Telemetry telemetry.Reporter
|
||||
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
|
||||
Auditor *atomic.Pointer[audit.Auditor]
|
||||
|
||||
AcquireJobDebounce time.Duration
|
||||
}
|
||||
@ -522,6 +525,43 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
|
||||
case *proto.FailedJob_TemplateImport_:
|
||||
}
|
||||
|
||||
// if failed job is a workspace build, audit the outcome
|
||||
if job.Type == database.ProvisionerJobTypeWorkspaceBuild {
|
||||
auditor := server.Auditor.Load()
|
||||
build, getBuildErr := server.Database.GetWorkspaceBuildByJobID(ctx, job.ID)
|
||||
if getBuildErr != nil {
|
||||
server.Logger.Error(ctx, "failed to create audit log - get build err", slog.Error(err))
|
||||
} else {
|
||||
auditAction := auditActionFromTransition(build.Transition)
|
||||
workspace, getWorkspaceErr := server.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
if getWorkspaceErr != nil {
|
||||
server.Logger.Error(ctx, "failed to create audit log - get workspace err", slog.Error(err))
|
||||
} else {
|
||||
// We pass the workspace name to the Auditor so that it
|
||||
// can form a friendly string for the user.
|
||||
workspaceResourceInfo := map[string]string{
|
||||
"workspaceName": workspace.Name,
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(workspaceResourceInfo)
|
||||
if err != nil {
|
||||
server.Logger.Error(ctx, "could not marshal workspace name", slog.Error(err))
|
||||
}
|
||||
|
||||
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
|
||||
Audit: *auditor,
|
||||
Log: server.Logger,
|
||||
UserID: job.InitiatorID,
|
||||
JobID: job.ID,
|
||||
Action: auditAction,
|
||||
New: build,
|
||||
Status: http.StatusInternalServerError,
|
||||
AdditionalFields: wriBytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(ProvisionerJobLogsNotifyMessage{EndOfLogs: true})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("marshal job log: %w", err)
|
||||
@ -600,11 +640,14 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
|
||||
return nil, xerrors.Errorf("get workspace build: %w", err)
|
||||
}
|
||||
|
||||
var workspace database.Workspace
|
||||
var getWorkspaceError error
|
||||
|
||||
err = server.Database.InTx(func(db database.Store) error {
|
||||
now := database.Now()
|
||||
var workspaceDeadline time.Time
|
||||
workspace, err := db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
|
||||
if err == nil {
|
||||
workspace, getWorkspaceError = db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
|
||||
if getWorkspaceError == nil {
|
||||
if workspace.Ttl.Valid {
|
||||
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
|
||||
}
|
||||
@ -704,6 +747,34 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
|
||||
return nil, xerrors.Errorf("complete job: %w", err)
|
||||
}
|
||||
|
||||
// audit the outcome of the workspace build
|
||||
if getWorkspaceError == nil {
|
||||
auditor := server.Auditor.Load()
|
||||
auditAction := auditActionFromTransition(workspaceBuild.Transition)
|
||||
|
||||
// We pass the workspace name to the Auditor so that it
|
||||
// can form a friendly string for the user.
|
||||
workspaceResourceInfo := map[string]string{
|
||||
"workspaceName": workspace.Name,
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(workspaceResourceInfo)
|
||||
if err != nil {
|
||||
server.Logger.Error(ctx, "marshal resource info", slog.Error(err))
|
||||
}
|
||||
|
||||
audit.BuildAudit(ctx, &audit.BuildAuditParams[database.WorkspaceBuild]{
|
||||
Audit: *auditor,
|
||||
Log: server.Logger,
|
||||
UserID: job.InitiatorID,
|
||||
JobID: job.ID,
|
||||
Action: auditAction,
|
||||
New: workspaceBuild,
|
||||
Status: http.StatusOK,
|
||||
AdditionalFields: wriBytes,
|
||||
})
|
||||
}
|
||||
|
||||
err = server.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceBuild.WorkspaceID), []byte{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update workspace: %w", err)
|
||||
@ -1015,6 +1086,19 @@ func convertWorkspaceTransition(transition database.WorkspaceTransition) (sdkpro
|
||||
}
|
||||
}
|
||||
|
||||
func auditActionFromTransition(transition database.WorkspaceTransition) database.AuditAction {
|
||||
switch transition {
|
||||
case database.WorkspaceTransitionStart:
|
||||
return database.AuditActionStart
|
||||
case database.WorkspaceTransitionStop:
|
||||
return database.AuditActionStop
|
||||
case database.WorkspaceTransitionDelete:
|
||||
return database.AuditActionDelete
|
||||
default:
|
||||
return database.AuditActionWrite
|
||||
}
|
||||
}
|
||||
|
||||
// WorkspaceProvisionJob is the payload for the "workspace_provision" job type.
|
||||
type WorkspaceProvisionJob struct {
|
||||
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/provisionerdserver"
|
||||
@ -21,6 +23,13 @@ import (
|
||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func mockAuditor() *atomic.Pointer[audit.Auditor] {
|
||||
ptr := &atomic.Pointer[audit.Auditor]{}
|
||||
mock := audit.Auditor(audit.NewMock())
|
||||
ptr.Store(&mock)
|
||||
return ptr
|
||||
}
|
||||
|
||||
func TestAcquireJob(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Debounce", func(t *testing.T) {
|
||||
@ -36,6 +45,7 @@ func TestAcquireJob(t *testing.T) {
|
||||
Pubsub: pubsub,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
AcquireJobDebounce: time.Hour,
|
||||
Auditor: mockAuditor(),
|
||||
}
|
||||
job, err := srv.AcquireJob(context.Background(), nil)
|
||||
require.NoError(t, err)
|
||||
@ -799,5 +809,6 @@ func setup(t *testing.T) *provisionerdserver.Server {
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
Auditor: mockAuditor(),
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user