diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9fdd028225..de5700bd2a 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1154,8 +1154,29 @@ func (q *FakeQuerier) DeleteOldProvisionerDaemons(_ context.Context) error { return nil } -func (*FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context) error { - // no-op +func (q *FakeQuerier) DeleteOldWorkspaceAgentLogs(_ context.Context) error { + q.mutex.Lock() + defer q.mutex.Unlock() + + now := dbtime.Now() + weekInterval := 7 * 24 * time.Hour + weekAgo := now.Add(-weekInterval) + + var validLogs []database.WorkspaceAgentLog + for _, log := range q.workspaceAgentLogs { + var toBeDeleted bool + for _, agent := range q.workspaceAgents { + if agent.ID == log.AgentID && agent.LastConnectedAt.Valid && agent.LastConnectedAt.Time.Before(weekAgo) { + toBeDeleted = true + break + } + } + + if !toBeDeleted { + validLogs = append(validLogs, log) + } + } + q.workspaceAgentLogs = validLogs return nil } diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index a7926a7079..8f5eb0f68e 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -14,6 +14,7 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/dbpurge" "github.com/coder/coder/v2/coderd/database/dbtestutil" @@ -33,6 +34,118 @@ func TestPurge(t *testing.T) { require.NoError(t, err) } +func TestDeleteOldWorkspaceAgentLogs(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + user := dbgen.User(t, db, database.User{}) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID}) + tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{OrganizationID: org.ID, CreatedBy: user.ID}) + tmpl := dbgen.Template(t, db, database.Template{OrganizationID: org.ID, ActiveVersionID: tv.ID, CreatedBy: user.ID}) + + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) + now := dbtime.Now() + + t.Run("AgentHasNotConnectedSinceWeek_LogsExpired", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // given + agent := mustCreateAgentWithLogs(ctx, t, db, user, org, tmpl, tv, now.Add(-8*24*time.Hour), t.Name()) + + // when + closer := dbpurge.New(ctx, logger, db) + defer closer.Close() + + // then + require.Eventually(t, func() bool { + agentLogs, err := db.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{ + AgentID: agent, + }) + if err != nil { + return false + } + return !containsAgentLog(agentLogs, t.Name()) + }, testutil.WaitShort, testutil.IntervalFast) + }) + + t.Run("AgentConnectedSixDaysAgo_LogsValid", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // given + agent := mustCreateAgentWithLogs(ctx, t, db, user, org, tmpl, tv, now.Add(-6*24*time.Hour), t.Name()) + + // when + closer := dbpurge.New(ctx, logger, db) + defer closer.Close() + + // then + require.Eventually(t, func() bool { + agentLogs, err := db.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{ + AgentID: agent, + }) + if err != nil { + return false + } + return containsAgentLog(agentLogs, t.Name()) + }, testutil.WaitShort, testutil.IntervalFast) + }) +} + +func mustCreateAgentWithLogs(ctx context.Context, t *testing.T, db database.Store, user database.User, org database.Organization, tmpl database.Template, tv database.TemplateVersion, agentLastConnectedAt time.Time, output string) uuid.UUID { + agent := mustCreateAgent(t, db, user, org, tmpl, tv) + + err := db.UpdateWorkspaceAgentConnectionByID(ctx, database.UpdateWorkspaceAgentConnectionByIDParams{ + ID: agent.ID, + LastConnectedAt: sql.NullTime{Time: agentLastConnectedAt, Valid: true}, + }) + require.NoError(t, err) + _, err = db.InsertWorkspaceAgentLogs(ctx, database.InsertWorkspaceAgentLogsParams{ + AgentID: agent.ID, + CreatedAt: agentLastConnectedAt, + Output: []string{output}, + Level: []database.LogLevel{database.LogLevelDebug}, + }) + require.NoError(t, err) + return agent.ID +} + +func mustCreateAgent(t *testing.T, db database.Store, user database.User, org database.Organization, tmpl database.Template, tv database.TemplateVersion) database.WorkspaceAgent { + workspace := dbgen.Workspace(t, db, database.Workspace{OwnerID: user.ID, OrganizationID: org.ID, TemplateID: tmpl.ID}) + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + }) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + JobID: job.ID, + TemplateVersionID: tv.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + }) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: job.ID, + Transition: database.WorkspaceTransitionStart, + }) + return dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) +} + +func containsAgentLog(daemons []database.WorkspaceAgentLog, output string) bool { + return slices.ContainsFunc(daemons, func(d database.WorkspaceAgentLog) bool { + return d.Output == output + }) +} + func TestDeleteOldProvisionerDaemons(t *testing.T) { t.Parallel() @@ -91,12 +204,14 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { if err != nil { return false } - return contains(daemons, "external-0") && - contains(daemons, "external-3") + return containsProvisionerDaemon(daemons, "external-0") && + !containsProvisionerDaemon(daemons, "external-1") && + !containsProvisionerDaemon(daemons, "external-2") && + containsProvisionerDaemon(daemons, "external-3") }, testutil.WaitShort, testutil.IntervalFast) } -func contains(daemons []database.ProvisionerDaemon, name string) bool { +func containsProvisionerDaemon(daemons []database.ProvisionerDaemon, name string) bool { return slices.ContainsFunc(daemons, func(d database.ProvisionerDaemon) bool { return d.Name == name })