Skip to content

Commit 19edb7b

Browse files
committed
fix(coderd): mark sub agent deletion via boolean instead of delete
Fixes coder/internal#685
1 parent 68f21fa commit 19edb7b

File tree

12 files changed

+260
-37
lines changed

12 files changed

+260
-37
lines changed

coderd/agentapi/subagent_test.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -875,14 +875,9 @@ func TestSubAgentAPI(t *testing.T) {
875875
require.NoError(t, err)
876876
})
877877

878-
t.Run("DeletesWorkspaceApps", func(t *testing.T) {
878+
t.Run("DeleteRetainsWorkspaceApps", func(t *testing.T) {
879879
t.Parallel()
880880

881-
// Skip test on in-memory database since CASCADE DELETE is not implemented
882-
if !dbtestutil.WillUsePostgres() {
883-
t.Skip("CASCADE DELETE behavior requires PostgreSQL")
884-
}
885-
886881
log := testutil.Logger(t)
887882
ctx := testutil.Context(t, testutil.WaitShort)
888883
clock := quartz.NewMock(t)
@@ -931,11 +926,11 @@ func TestSubAgentAPI(t *testing.T) {
931926
_, err = api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), subAgentID) //nolint:gocritic // this is a test.
932927
require.ErrorIs(t, err, sql.ErrNoRows)
933928

934-
// And: The apps are also deleted (due to CASCADE DELETE)
935-
// Use raw database since authorization layer requires agent to exist
929+
// And: The apps are *retained* to avoid causing issues
930+
// where the resources are expected to be present.
936931
appsAfterDeletion, err := db.GetWorkspaceAppsByAgentID(ctx, subAgentID)
937932
require.NoError(t, err)
938-
require.Empty(t, appsAfterDeletion)
933+
require.NotEmpty(t, appsAfterDeletion)
939934
})
940935
})
941936

coderd/database/dbgen/dbgen.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen
209209
},
210210
ConnectionTimeoutSeconds: takeFirst(orig.ConnectionTimeoutSeconds, 3600),
211211
TroubleshootingURL: takeFirst(orig.TroubleshootingURL, "https://example.com"),
212-
MOTDFile: takeFirst(orig.TroubleshootingURL, ""),
212+
MOTDFile: takeFirst(orig.MOTDFile, ""),
213213
DisplayApps: append([]database.DisplayApp{}, orig.DisplayApps...),
214214
DisplayOrder: takeFirst(orig.DisplayOrder, 1),
215215
APIKeyScope: takeFirst(orig.APIKeyScope, database.AgentKeyScopeEnumAll),
@@ -226,6 +226,35 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen
226226
})
227227
require.NoError(t, err, "update workspace agent first connected at")
228228
}
229+
230+
// Add a test antagonist. For every agent we add a deleted sub agent
231+
// to discover cases where deletion should be handled.
232+
subAgt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{
233+
ID: uuid.New(),
234+
ParentID: uuid.NullUUID{UUID: agt.ID, Valid: true},
235+
CreatedAt: dbtime.Now(),
236+
UpdatedAt: dbtime.Now(),
237+
Name: testutil.GetRandomName(t),
238+
ResourceID: agt.ResourceID,
239+
AuthToken: uuid.New(),
240+
AuthInstanceID: sql.NullString{},
241+
Architecture: agt.Architecture,
242+
EnvironmentVariables: pqtype.NullRawMessage{},
243+
OperatingSystem: agt.OperatingSystem,
244+
Directory: agt.Directory,
245+
InstanceMetadata: pqtype.NullRawMessage{},
246+
ResourceMetadata: pqtype.NullRawMessage{},
247+
ConnectionTimeoutSeconds: agt.ConnectionTimeoutSeconds,
248+
TroubleshootingURL: agt.TroubleshootingURL,
249+
MOTDFile: "",
250+
DisplayApps: nil,
251+
DisplayOrder: agt.DisplayOrder,
252+
APIKeyScope: agt.APIKeyScope,
253+
})
254+
require.NoError(t, err, "insert workspace agent subagent antagonist")
255+
err = db.DeleteWorkspaceSubAgentByID(genCtx, subAgt.ID)
256+
require.NoError(t, err, "delete workspace agent subagent antagonist")
257+
229258
return agt
230259
}
231260

coderd/database/dbmem/dbmem.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ func (q *FakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUI
792792
// The schema sorts this by created at, so we iterate the array backwards.
793793
for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
794794
agent := q.workspaceAgents[i]
795-
if agent.ID == id {
795+
if !agent.Deleted && agent.ID == id {
796796
return agent, nil
797797
}
798798
}
@@ -802,6 +802,9 @@ func (q *FakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUI
802802
func (q *FakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) {
803803
workspaceAgents := make([]database.WorkspaceAgent, 0)
804804
for _, agent := range q.workspaceAgents {
805+
if agent.Deleted {
806+
continue
807+
}
805808
for _, resourceID := range resourceIDs {
806809
if agent.ResourceID != resourceID {
807810
continue
@@ -2554,13 +2557,13 @@ func (q *FakeQuerier) DeleteWorkspaceAgentPortSharesByTemplate(_ context.Context
25542557
return nil
25552558
}
25562559

2557-
func (q *FakeQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
2560+
func (q *FakeQuerier) DeleteWorkspaceSubAgentByID(_ context.Context, id uuid.UUID) error {
25582561
q.mutex.Lock()
25592562
defer q.mutex.Unlock()
25602563

25612564
for i, agent := range q.workspaceAgents {
25622565
if agent.ID == id && agent.ParentID.Valid {
2563-
q.workspaceAgents = slices.Delete(q.workspaceAgents, i, i+1)
2566+
q.workspaceAgents[i].Deleted = true
25642567
return nil
25652568
}
25662569
}
@@ -7077,6 +7080,10 @@ func (q *FakeQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(_ context.Conte
70777080
latestBuildNumber := make(map[uuid.UUID]int32)
70787081

70797082
for _, agt := range q.workspaceAgents {
7083+
if agt.Deleted {
7084+
continue
7085+
}
7086+
70807087
// get the related workspace and user
70817088
for _, res := range q.workspaceResources {
70827089
if agt.ResourceID != res.ID {
@@ -7146,7 +7153,7 @@ func (q *FakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI
71467153
// The schema sorts this by created at, so we iterate the array backwards.
71477154
for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
71487155
agent := q.workspaceAgents[i]
7149-
if agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID {
7156+
if !agent.Deleted && agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID {
71507157
return agent, nil
71517158
}
71527159
}
@@ -7706,13 +7713,13 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr
77067713
return stats, nil
77077714
}
77087715

7709-
func (q *FakeQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
7716+
func (q *FakeQuerier) GetWorkspaceAgentsByParentID(_ context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
77107717
q.mutex.RLock()
77117718
defer q.mutex.RUnlock()
77127719

77137720
workspaceAgents := make([]database.WorkspaceAgent, 0)
77147721
for _, agent := range q.workspaceAgents {
7715-
if !agent.ParentID.Valid || agent.ParentID.UUID != parentID {
7722+
if !agent.ParentID.Valid || agent.ParentID.UUID != parentID || agent.Deleted {
77167723
continue
77177724
}
77187725

@@ -7759,6 +7766,9 @@ func (q *FakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after ti
77597766

77607767
workspaceAgents := make([]database.WorkspaceAgent, 0)
77617768
for _, agent := range q.workspaceAgents {
7769+
if agent.Deleted {
7770+
continue
7771+
}
77627772
if agent.CreatedAt.After(after) {
77637773
workspaceAgents = append(workspaceAgents, agent)
77647774
}

coderd/database/dump.sql

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
ALTER TABLE workspace_agents
2+
DROP COLUMN deleted;
3+
4+
-- Restore trigger without deleted check.
5+
DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents;
6+
DROP FUNCTION IF EXISTS check_workspace_agent_name_unique();
7+
8+
CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique()
9+
RETURNS TRIGGER AS $$
10+
DECLARE
11+
workspace_build_id uuid;
12+
agents_with_name int;
13+
BEGIN
14+
-- Find the workspace build the workspace agent is being inserted into.
15+
SELECT workspace_builds.id INTO workspace_build_id
16+
FROM workspace_resources
17+
JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
18+
WHERE workspace_resources.id = NEW.resource_id;
19+
20+
-- If the agent doesn't have a workspace build, we'll allow the insert.
21+
IF workspace_build_id IS NULL THEN
22+
RETURN NEW;
23+
END IF;
24+
25+
-- Count how many agents in this workspace build already have the given agent name.
26+
SELECT COUNT(*) INTO agents_with_name
27+
FROM workspace_agents
28+
JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
29+
JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
30+
WHERE workspace_builds.id = workspace_build_id
31+
AND workspace_agents.name = NEW.name
32+
AND workspace_agents.id != NEW.id;
33+
34+
-- If there's already an agent with this name, raise an error
35+
IF agents_with_name > 0 THEN
36+
RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace build', NEW.name
37+
USING ERRCODE = 'unique_violation';
38+
END IF;
39+
40+
RETURN NEW;
41+
END;
42+
$$ LANGUAGE plpgsql;
43+
44+
CREATE TRIGGER workspace_agent_name_unique_trigger
45+
BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents
46+
FOR EACH ROW
47+
EXECUTE FUNCTION check_workspace_agent_name_unique();
48+
49+
COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS
50+
'Use a trigger instead of a unique constraint because existing data may violate
51+
the uniqueness requirement. A trigger allows us to enforce uniqueness going
52+
forward without requiring a migration to clean up historical data.';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
ALTER TABLE workspace_agents
2+
ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE;
3+
4+
COMMENT ON COLUMN workspace_agents.deleted IS 'Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents.';
5+
6+
-- Recreate the trigger with deleted check.
7+
DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents;
8+
DROP FUNCTION IF EXISTS check_workspace_agent_name_unique();
9+
10+
CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique()
11+
RETURNS TRIGGER AS $$
12+
DECLARE
13+
workspace_build_id uuid;
14+
agents_with_name int;
15+
BEGIN
16+
-- Find the workspace build the workspace agent is being inserted into.
17+
SELECT workspace_builds.id INTO workspace_build_id
18+
FROM workspace_resources
19+
JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
20+
WHERE workspace_resources.id = NEW.resource_id;
21+
22+
-- If the agent doesn't have a workspace build, we'll allow the insert.
23+
IF workspace_build_id IS NULL THEN
24+
RETURN NEW;
25+
END IF;
26+
27+
-- Count how many agents in this workspace build already have the given agent name.
28+
SELECT COUNT(*) INTO agents_with_name
29+
FROM workspace_agents
30+
JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
31+
JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
32+
WHERE workspace_builds.id = workspace_build_id
33+
AND workspace_agents.name = NEW.name
34+
AND workspace_agents.id != NEW.id
35+
AND workspace_agents.deleted = FALSE; -- Ensure we only count non-deleted agents.
36+
37+
-- If there's already an agent with this name, raise an error
38+
IF agents_with_name > 0 THEN
39+
RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace build', NEW.name
40+
USING ERRCODE = 'unique_violation';
41+
END IF;
42+
43+
RETURN NEW;
44+
END;
45+
$$ LANGUAGE plpgsql;
46+
47+
CREATE TRIGGER workspace_agent_name_unique_trigger
48+
BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents
49+
FOR EACH ROW
50+
EXECUTE FUNCTION check_workspace_agent_name_unique();
51+
52+
COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS
53+
'Use a trigger instead of a unique constraint because existing data may violate
54+
the uniqueness requirement. A trigger allows us to enforce uniqueness going
55+
forward without requiring a migration to clean up historical data.';

coderd/database/models.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy