feat: add agent timings (#14713)

* feat: begin impl of agent script timings

* feat: add job_id and display_name to script timings

* fix: increment migration number

* fix: rename migrations from 251 to 254

* test: get tests compiling

* fix: appease the linter

* fix: get tests passing again

* fix: drop column from correct table

* test: add fixture for agent script timings

* fix: typo

* fix: use job id used in provisioner job timings

* fix: increment migration number

* test: behaviour of script runner

* test: rewrite test

* test: does exit 1 script break things?

* test: rewrite test again

* fix: revert change

Not sure how this came to be, I do not recall manually changing
these files.

* fix: let code breathe

* fix: wrap errors

* fix: justify nolint

* fix: swap require.Equal argument order

* fix: add mutex operations

* feat: add 'ran_on_start' and 'blocked_login' fields

* fix: update testdata fixture

* fix: refer to agent_id instead of job_id in timings

* fix: JobID -> AgentID in dbauthz_test

* fix: add 'id' to scripts, make timing refer to script id

* fix: fix broken tests and convert bug

* fix: update testdata fixtures

* fix: update testdata fixtures again

* feat: capture stage and if script timed out

* fix: update migration number

* test: add test for script api

* fix: fake db query

* fix: use UTC time

* fix: ensure r.scriptComplete is not nil

* fix: move err check to right after call

* fix: uppercase sql

* fix: use dbtime.Now()

* fix: debug log on r.scriptCompleted being nil

* fix: ensure correct rbac permissions

* chore: remove DisplayName

* fix: get tests passing

* fix: remove space in sql up

* docs: document ExecuteOption

* fix: drop 'RETURNING' from sql

* chore: remove 'display_name' from timing table

* fix: testdata fixture

* fix: put r.scriptCompleted call in goroutine

* fix: track goroutine for test + use separate context for reporting

* fix: appease linter, handle trackCommandGoroutine error

* fix: resolve race condition

* feat: replace timed_out column with status column

* test: update testdata fixture

* fix: apply suggestions from review

* revert: linter changes
This commit is contained in:
Danielle Maywood
2024-09-24 10:51:49 +01:00
committed by GitHub
parent b8944074c4
commit ae522c558d
43 changed files with 1367 additions and 232 deletions

View File

@ -42,6 +42,7 @@ type API struct {
*AppsAPI
*MetadataAPI
*LogsAPI
*ScriptsAPI
*tailnet.DRPCService
mu sync.Mutex
@ -152,6 +153,10 @@ func New(opts Options) *API {
PublishWorkspaceAgentLogsUpdateFn: opts.PublishWorkspaceAgentLogsUpdateFn,
}
api.ScriptsAPI = &ScriptsAPI{
Database: opts.Database,
}
api.DRPCService = &tailnet.DRPCService{
CoordPtr: opts.TailnetCoordinator,
Logger: opts.Log,

View File

@ -178,6 +178,7 @@ func dbAgentScriptsToProto(scripts []database.WorkspaceAgentScript) []*agentprot
func dbAgentScriptToProto(script database.WorkspaceAgentScript) *agentproto.WorkspaceAgentScript {
return &agentproto.WorkspaceAgentScript{
Id: script.ID[:],
LogSourceId: script.LogSourceID[:],
LogPath: script.LogPath,
Script: script.Script,

View File

@ -108,6 +108,7 @@ func TestGetManifest(t *testing.T) {
}
scripts = []database.WorkspaceAgentScript{
{
ID: uuid.New(),
WorkspaceAgentID: agent.ID,
LogSourceID: uuid.New(),
LogPath: "/cool/log/path/1",
@ -119,6 +120,7 @@ func TestGetManifest(t *testing.T) {
TimeoutSeconds: 60,
},
{
ID: uuid.New(),
WorkspaceAgentID: agent.ID,
LogSourceID: uuid.New(),
LogPath: "/cool/log/path/2",
@ -227,6 +229,7 @@ func TestGetManifest(t *testing.T) {
}
protoScripts = []*agentproto.WorkspaceAgentScript{
{
Id: scripts[0].ID[:],
LogSourceId: scripts[0].LogSourceID[:],
LogPath: scripts[0].LogPath,
Script: scripts[0].Script,
@ -237,6 +240,7 @@ func TestGetManifest(t *testing.T) {
Timeout: durationpb.New(time.Duration(scripts[0].TimeoutSeconds) * time.Second),
},
{
Id: scripts[1].ID[:],
LogSourceId: scripts[1].LogSourceID[:],
LogPath: scripts[1].LogPath,
Script: scripts[1].Script,

View File

@ -0,0 +1,63 @@
package agentapi
import (
"context"
"github.com/google/uuid"
"golang.org/x/xerrors"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
)
type ScriptsAPI struct {
Database database.Store
}
func (s *ScriptsAPI) ScriptCompleted(ctx context.Context, req *agentproto.WorkspaceAgentScriptCompletedRequest) (*agentproto.WorkspaceAgentScriptCompletedResponse, error) {
res := &agentproto.WorkspaceAgentScriptCompletedResponse{}
scriptID, err := uuid.FromBytes(req.Timing.ScriptId)
if err != nil {
return nil, xerrors.Errorf("script id from bytes: %w", err)
}
var stage database.WorkspaceAgentScriptTimingStage
switch req.Timing.Stage {
case agentproto.Timing_START:
stage = database.WorkspaceAgentScriptTimingStageStart
case agentproto.Timing_STOP:
stage = database.WorkspaceAgentScriptTimingStageStop
case agentproto.Timing_CRON:
stage = database.WorkspaceAgentScriptTimingStageCron
}
var status database.WorkspaceAgentScriptTimingStatus
switch req.Timing.Status {
case agentproto.Timing_OK:
status = database.WorkspaceAgentScriptTimingStatusOk
case agentproto.Timing_EXIT_FAILURE:
status = database.WorkspaceAgentScriptTimingStatusExitFailure
case agentproto.Timing_TIMED_OUT:
status = database.WorkspaceAgentScriptTimingStatusTimedOut
case agentproto.Timing_PIPES_LEFT_OPEN:
status = database.WorkspaceAgentScriptTimingStatusPipesLeftOpen
}
//nolint:gocritic // We need permissions to write to the DB here and we are in the context of the agent.
ctx = dbauthz.AsProvisionerd(ctx)
err = s.Database.InsertWorkspaceAgentScriptTimings(ctx, database.InsertWorkspaceAgentScriptTimingsParams{
ScriptID: scriptID,
Stage: stage,
Status: status,
StartedAt: req.Timing.Start.AsTime(),
EndedAt: req.Timing.End.AsTime(),
ExitCode: req.Timing.ExitCode,
})
if err != nil {
return nil, xerrors.Errorf("insert workspace agent script timings into database: %w", err)
}
return res, nil
}

View File

@ -0,0 +1,125 @@
package agentapi_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/timestamppb"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/agentapi"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtime"
)
func TestScriptCompleted(t *testing.T) {
t.Parallel()
tests := []struct {
scriptID uuid.UUID
timing *agentproto.Timing
}{
{
scriptID: uuid.New(),
timing: &agentproto.Timing{
Stage: agentproto.Timing_START,
Start: timestamppb.New(dbtime.Now()),
End: timestamppb.New(dbtime.Now().Add(time.Second)),
Status: agentproto.Timing_OK,
ExitCode: 0,
},
},
{
scriptID: uuid.New(),
timing: &agentproto.Timing{
Stage: agentproto.Timing_STOP,
Start: timestamppb.New(dbtime.Now()),
End: timestamppb.New(dbtime.Now().Add(time.Second)),
Status: agentproto.Timing_OK,
ExitCode: 0,
},
},
{
scriptID: uuid.New(),
timing: &agentproto.Timing{
Stage: agentproto.Timing_CRON,
Start: timestamppb.New(dbtime.Now()),
End: timestamppb.New(dbtime.Now().Add(time.Second)),
Status: agentproto.Timing_OK,
ExitCode: 0,
},
},
{
scriptID: uuid.New(),
timing: &agentproto.Timing{
Stage: agentproto.Timing_START,
Start: timestamppb.New(dbtime.Now()),
End: timestamppb.New(dbtime.Now().Add(time.Second)),
Status: agentproto.Timing_TIMED_OUT,
ExitCode: 255,
},
},
{
scriptID: uuid.New(),
timing: &agentproto.Timing{
Stage: agentproto.Timing_START,
Start: timestamppb.New(dbtime.Now()),
End: timestamppb.New(dbtime.Now().Add(time.Second)),
Status: agentproto.Timing_EXIT_FAILURE,
ExitCode: 1,
},
},
}
for _, tt := range tests {
// Setup the script ID
tt.timing.ScriptId = tt.scriptID[:]
mDB := dbmock.NewMockStore(gomock.NewController(t))
mDB.EXPECT().InsertWorkspaceAgentScriptTimings(gomock.Any(), database.InsertWorkspaceAgentScriptTimingsParams{
ScriptID: tt.scriptID,
Stage: protoScriptTimingStageToDatabase(tt.timing.Stage),
Status: protoScriptTimingStatusToDatabase(tt.timing.Status),
StartedAt: tt.timing.Start.AsTime(),
EndedAt: tt.timing.End.AsTime(),
ExitCode: tt.timing.ExitCode,
})
api := &agentapi.ScriptsAPI{Database: mDB}
api.ScriptCompleted(context.Background(), &agentproto.WorkspaceAgentScriptCompletedRequest{
Timing: tt.timing,
})
}
}
func protoScriptTimingStageToDatabase(stage agentproto.Timing_Stage) database.WorkspaceAgentScriptTimingStage {
var dbStage database.WorkspaceAgentScriptTimingStage
switch stage {
case agentproto.Timing_START:
dbStage = database.WorkspaceAgentScriptTimingStageStart
case agentproto.Timing_STOP:
dbStage = database.WorkspaceAgentScriptTimingStageStop
case agentproto.Timing_CRON:
dbStage = database.WorkspaceAgentScriptTimingStageCron
}
return dbStage
}
func protoScriptTimingStatusToDatabase(stage agentproto.Timing_Status) database.WorkspaceAgentScriptTimingStatus {
var dbStatus database.WorkspaceAgentScriptTimingStatus
switch stage {
case agentproto.Timing_OK:
dbStatus = database.WorkspaceAgentScriptTimingStatusOk
case agentproto.Timing_EXIT_FAILURE:
dbStatus = database.WorkspaceAgentScriptTimingStatusExitFailure
case agentproto.Timing_TIMED_OUT:
dbStatus = database.WorkspaceAgentScriptTimingStatusTimedOut
case agentproto.Timing_PIPES_LEFT_OPEN:
dbStatus = database.WorkspaceAgentScriptTimingStatusPipesLeftOpen
}
return dbStatus
}