mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd/agentapi): support terraform-defined subagent ids (#21837)
Update `coderd/agentapi` to handle pre-created sub agents
This commit is contained in:
@@ -249,11 +249,17 @@ func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ow
|
||||
func dbAgentDevcontainersToProto(devcontainers []database.WorkspaceAgentDevcontainer) []*agentproto.WorkspaceAgentDevcontainer {
|
||||
ret := make([]*agentproto.WorkspaceAgentDevcontainer, len(devcontainers))
|
||||
for i, dc := range devcontainers {
|
||||
var subagentID []byte
|
||||
if dc.SubagentID.Valid {
|
||||
subagentID = dc.SubagentID.UUID[:]
|
||||
}
|
||||
|
||||
ret[i] = &agentproto.WorkspaceAgentDevcontainer{
|
||||
Id: dc.ID[:],
|
||||
Name: dc.Name,
|
||||
WorkspaceFolder: dc.WorkspaceFolder,
|
||||
ConfigPath: dc.ConfigPath,
|
||||
SubagentId: subagentID,
|
||||
}
|
||||
}
|
||||
return ret
|
||||
|
||||
+56
-19
@@ -37,25 +37,6 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
//nolint:gocritic // This gives us only the permissions required to do the job.
|
||||
ctx = dbauthz.AsSubAgentAPI(ctx, a.OrganizationID, a.OwnerID)
|
||||
|
||||
parentAgent, err := a.AgentFn(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get parent agent: %w", err)
|
||||
}
|
||||
|
||||
agentName := req.Name
|
||||
if agentName == "" {
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: "agent name cannot be empty",
|
||||
}
|
||||
}
|
||||
if !provisioner.AgentNameRegex.MatchString(agentName) {
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),
|
||||
}
|
||||
}
|
||||
|
||||
createdAt := a.Clock.Now()
|
||||
|
||||
displayApps := make([]database.DisplayApp, 0, len(req.DisplayApps))
|
||||
@@ -83,6 +64,62 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
displayApps = append(displayApps, app)
|
||||
}
|
||||
|
||||
parentAgent, err := a.AgentFn(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get parent agent: %w", err)
|
||||
}
|
||||
|
||||
// An ID is only given in the request when it is a terraform-defined devcontainer
|
||||
// that has attached resources. These subagents are pre-provisioned by terraform
|
||||
// (the agent record already exists), so we update configurable fields like
|
||||
// display_apps rather than creating a new agent.
|
||||
if req.Id != nil {
|
||||
id, err := uuid.FromBytes(req.Id)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse agent id: %w", err)
|
||||
}
|
||||
|
||||
subAgent, err := a.Database.GetWorkspaceAgentByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get workspace agent by id: %w", err)
|
||||
}
|
||||
|
||||
// Validate that the subagent belongs to the current parent agent to
|
||||
// prevent updating subagents from other agents within the same workspace.
|
||||
if !subAgent.ParentID.Valid || subAgent.ParentID.UUID != parentAgent.ID {
|
||||
return nil, xerrors.Errorf("subagent does not belong to this parent agent")
|
||||
}
|
||||
|
||||
if err := a.Database.UpdateWorkspaceAgentDisplayAppsByID(ctx, database.UpdateWorkspaceAgentDisplayAppsByIDParams{
|
||||
ID: id,
|
||||
DisplayApps: displayApps,
|
||||
UpdatedAt: createdAt,
|
||||
}); err != nil {
|
||||
return nil, xerrors.Errorf("update workspace agent display apps: %w", err)
|
||||
}
|
||||
|
||||
return &agentproto.CreateSubAgentResponse{
|
||||
Agent: &agentproto.SubAgent{
|
||||
Name: subAgent.Name,
|
||||
Id: subAgent.ID[:],
|
||||
AuthToken: subAgent.AuthToken[:],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
agentName := req.Name
|
||||
if agentName == "" {
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: "agent name cannot be empty",
|
||||
}
|
||||
}
|
||||
if !provisioner.AgentNameRegex.MatchString(agentName) {
|
||||
return nil, codersdk.ValidationError{
|
||||
Field: "name",
|
||||
Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),
|
||||
}
|
||||
}
|
||||
subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
|
||||
ID: uuid.New(),
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
|
||||
|
||||
@@ -1132,6 +1132,225 @@ func TestSubAgentAPI(t *testing.T) {
|
||||
require.Equal(t, "Custom App", apps[0].DisplayName)
|
||||
})
|
||||
|
||||
t.Run("CreateSubAgentUpdatesExisting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseChildAgent := database.WorkspaceAgent{
|
||||
Name: "existing-child-agent",
|
||||
Directory: "/workspaces/test",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
DisplayApps: []database.DisplayApp{database.DisplayAppVscode},
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
setup func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest
|
||||
wantErr string
|
||||
check func(t *testing.T, ctx context.Context, db database.Store, resp *proto.CreateSubAgentResponse, agent database.WorkspaceAgent)
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "OK",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// Given: An existing child agent with some display apps.
|
||||
childAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: baseChildAgent.Name,
|
||||
Directory: baseChildAgent.Directory,
|
||||
Architecture: baseChildAgent.Architecture,
|
||||
OperatingSystem: baseChildAgent.OperatingSystem,
|
||||
DisplayApps: baseChildAgent.DisplayApps,
|
||||
})
|
||||
|
||||
// When: We call CreateSubAgent with the existing agent's ID and new display apps.
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: childAgent.ID[:],
|
||||
DisplayApps: []proto.CreateSubAgentRequest_DisplayApp{
|
||||
proto.CreateSubAgentRequest_WEB_TERMINAL,
|
||||
proto.CreateSubAgentRequest_SSH_HELPER,
|
||||
},
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, ctx context.Context, db database.Store, resp *proto.CreateSubAgentResponse, agent database.WorkspaceAgent) {
|
||||
// Then: The response contains the existing agent's details.
|
||||
require.NotNil(t, resp.Agent)
|
||||
require.Equal(t, baseChildAgent.Name, resp.Agent.Name)
|
||||
|
||||
agentID, err := uuid.FromBytes(resp.Agent.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// And: The database agent's display apps are updated.
|
||||
updatedAgent, err := db.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, updatedAgent.DisplayApps, 2)
|
||||
require.Contains(t, updatedAgent.DisplayApps, database.DisplayAppWebTerminal)
|
||||
require.Contains(t, updatedAgent.DisplayApps, database.DisplayAppSSHHelper)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OK_OtherFieldsNotModified",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// Given: An existing child agent with specific properties.
|
||||
childAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: agent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: baseChildAgent.Name,
|
||||
Directory: baseChildAgent.Directory,
|
||||
Architecture: baseChildAgent.Architecture,
|
||||
OperatingSystem: baseChildAgent.OperatingSystem,
|
||||
DisplayApps: baseChildAgent.DisplayApps,
|
||||
})
|
||||
|
||||
// When: We call CreateSubAgent with different values for name, directory, arch, and OS.
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: childAgent.ID[:],
|
||||
Name: "different-name",
|
||||
Directory: "/different/path",
|
||||
Architecture: "arm64",
|
||||
OperatingSystem: "darwin",
|
||||
DisplayApps: []proto.CreateSubAgentRequest_DisplayApp{
|
||||
proto.CreateSubAgentRequest_WEB_TERMINAL,
|
||||
},
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, ctx context.Context, db database.Store, resp *proto.CreateSubAgentResponse, agent database.WorkspaceAgent) {
|
||||
// Then: The response contains the original agent name, not the new one.
|
||||
require.NotNil(t, resp.Agent)
|
||||
require.Equal(t, baseChildAgent.Name, resp.Agent.Name)
|
||||
|
||||
agentID, err := uuid.FromBytes(resp.Agent.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// And: The database agent's other fields are unchanged.
|
||||
updatedAgent, err := db.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, baseChildAgent.Name, updatedAgent.Name)
|
||||
require.Equal(t, baseChildAgent.Directory, updatedAgent.Directory)
|
||||
require.Equal(t, baseChildAgent.Architecture, updatedAgent.Architecture)
|
||||
require.Equal(t, baseChildAgent.OperatingSystem, updatedAgent.OperatingSystem)
|
||||
|
||||
// But display apps should be updated.
|
||||
require.Len(t, updatedAgent.DisplayApps, 1)
|
||||
require.Equal(t, database.DisplayAppWebTerminal, updatedAgent.DisplayApps[0])
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Error/MalformedID",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// When: We call CreateSubAgent with malformed ID bytes (not 16 bytes).
|
||||
// uuid.FromBytes requires exactly 16 bytes, so we provide fewer.
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: []byte("short"),
|
||||
}
|
||||
},
|
||||
wantErr: "parse agent id",
|
||||
},
|
||||
{
|
||||
name: "Error/AgentNotFound",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// When: We call CreateSubAgent with a non-existent agent ID.
|
||||
nonExistentID := uuid.New()
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: nonExistentID[:],
|
||||
}
|
||||
},
|
||||
wantErr: "get workspace agent by id",
|
||||
},
|
||||
{
|
||||
name: "Error/ParentMismatch",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// Create a second agent (sibling) within the same workspace/resource.
|
||||
// This sibling has a different parent ID (or no parent).
|
||||
siblingAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: false}, // No parent - it's a top-level agent
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "sibling-agent",
|
||||
Directory: "/workspaces/sibling",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// Create a child of the sibling agent (not our agent).
|
||||
childOfSibling := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: siblingAgent.ID},
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "child-of-sibling",
|
||||
Directory: "/workspaces/test",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: Our API (which is for `agent`) tries to update the child of `siblingAgent`.
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: childOfSibling.ID[:],
|
||||
DisplayApps: []proto.CreateSubAgentRequest_DisplayApp{
|
||||
proto.CreateSubAgentRequest_VSCODE,
|
||||
},
|
||||
}
|
||||
},
|
||||
wantErr: "subagent does not belong to this parent agent",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Error/NoParentID",
|
||||
setup: func(t *testing.T, db database.Store, agent database.WorkspaceAgent) *proto.CreateSubAgentRequest {
|
||||
// Given: An agent without a parent (a top-level agent).
|
||||
topLevelAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: false}, // No parent
|
||||
ResourceID: agent.ResourceID,
|
||||
Name: "top-level-agent",
|
||||
Directory: "/workspaces/test",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
})
|
||||
|
||||
// When: We try to update this agent as if it were a subagent.
|
||||
return &proto.CreateSubAgentRequest{
|
||||
Id: topLevelAgent.ID[:],
|
||||
DisplayApps: []proto.CreateSubAgentRequest_DisplayApp{
|
||||
proto.CreateSubAgentRequest_VSCODE,
|
||||
},
|
||||
}
|
||||
},
|
||||
wantErr: "subagent does not belong to this parent agent",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
log = testutil.Logger(t)
|
||||
clock = quartz.NewMock(t)
|
||||
|
||||
db, org = newDatabaseWithOrg(t)
|
||||
user, agent = newUserWithWorkspaceAgent(t, db, org)
|
||||
api = newAgentAPI(t, log, db, clock, user, org, agent)
|
||||
)
|
||||
|
||||
req := tc.setup(t, db, agent)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
resp, err := api.CreateSubAgent(ctx, req)
|
||||
|
||||
if tc.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
if tc.check != nil {
|
||||
tc.check(t, ctx, db, resp, agent)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListSubAgents", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Generated
+6
@@ -12698,6 +12698,7 @@ const docTemplate = `{
|
||||
"workspace:start",
|
||||
"workspace:stop",
|
||||
"workspace:update",
|
||||
"workspace:update_agent",
|
||||
"workspace_agent_devcontainers:*",
|
||||
"workspace_agent_devcontainers:create",
|
||||
"workspace_agent_resource_monitor:*",
|
||||
@@ -12716,6 +12717,7 @@ const docTemplate = `{
|
||||
"workspace_dormant:start",
|
||||
"workspace_dormant:stop",
|
||||
"workspace_dormant:update",
|
||||
"workspace_dormant:update_agent",
|
||||
"workspace_proxy:*",
|
||||
"workspace_proxy:create",
|
||||
"workspace_proxy:delete",
|
||||
@@ -12900,6 +12902,7 @@ const docTemplate = `{
|
||||
"APIKeyScopeWorkspaceStart",
|
||||
"APIKeyScopeWorkspaceStop",
|
||||
"APIKeyScopeWorkspaceUpdate",
|
||||
"APIKeyScopeWorkspaceUpdateAgent",
|
||||
"APIKeyScopeWorkspaceAgentDevcontainersAll",
|
||||
"APIKeyScopeWorkspaceAgentDevcontainersCreate",
|
||||
"APIKeyScopeWorkspaceAgentResourceMonitorAll",
|
||||
@@ -12918,6 +12921,7 @@ const docTemplate = `{
|
||||
"APIKeyScopeWorkspaceDormantStart",
|
||||
"APIKeyScopeWorkspaceDormantStop",
|
||||
"APIKeyScopeWorkspaceDormantUpdate",
|
||||
"APIKeyScopeWorkspaceDormantUpdateAgent",
|
||||
"APIKeyScopeWorkspaceProxyAll",
|
||||
"APIKeyScopeWorkspaceProxyCreate",
|
||||
"APIKeyScopeWorkspaceProxyDelete",
|
||||
@@ -17792,6 +17796,7 @@ const docTemplate = `{
|
||||
"share",
|
||||
"unassign",
|
||||
"update",
|
||||
"update_agent",
|
||||
"update_personal",
|
||||
"use",
|
||||
"view_insights",
|
||||
@@ -17811,6 +17816,7 @@ const docTemplate = `{
|
||||
"ActionShare",
|
||||
"ActionUnassign",
|
||||
"ActionUpdate",
|
||||
"ActionUpdateAgent",
|
||||
"ActionUpdatePersonal",
|
||||
"ActionUse",
|
||||
"ActionViewInsights",
|
||||
|
||||
Generated
+6
@@ -11320,6 +11320,7 @@
|
||||
"workspace:start",
|
||||
"workspace:stop",
|
||||
"workspace:update",
|
||||
"workspace:update_agent",
|
||||
"workspace_agent_devcontainers:*",
|
||||
"workspace_agent_devcontainers:create",
|
||||
"workspace_agent_resource_monitor:*",
|
||||
@@ -11338,6 +11339,7 @@
|
||||
"workspace_dormant:start",
|
||||
"workspace_dormant:stop",
|
||||
"workspace_dormant:update",
|
||||
"workspace_dormant:update_agent",
|
||||
"workspace_proxy:*",
|
||||
"workspace_proxy:create",
|
||||
"workspace_proxy:delete",
|
||||
@@ -11522,6 +11524,7 @@
|
||||
"APIKeyScopeWorkspaceStart",
|
||||
"APIKeyScopeWorkspaceStop",
|
||||
"APIKeyScopeWorkspaceUpdate",
|
||||
"APIKeyScopeWorkspaceUpdateAgent",
|
||||
"APIKeyScopeWorkspaceAgentDevcontainersAll",
|
||||
"APIKeyScopeWorkspaceAgentDevcontainersCreate",
|
||||
"APIKeyScopeWorkspaceAgentResourceMonitorAll",
|
||||
@@ -11540,6 +11543,7 @@
|
||||
"APIKeyScopeWorkspaceDormantStart",
|
||||
"APIKeyScopeWorkspaceDormantStop",
|
||||
"APIKeyScopeWorkspaceDormantUpdate",
|
||||
"APIKeyScopeWorkspaceDormantUpdateAgent",
|
||||
"APIKeyScopeWorkspaceProxyAll",
|
||||
"APIKeyScopeWorkspaceProxyCreate",
|
||||
"APIKeyScopeWorkspaceProxyDelete",
|
||||
@@ -16218,6 +16222,7 @@
|
||||
"share",
|
||||
"unassign",
|
||||
"update",
|
||||
"update_agent",
|
||||
"update_personal",
|
||||
"use",
|
||||
"view_insights",
|
||||
@@ -16237,6 +16242,7 @@
|
||||
"ActionShare",
|
||||
"ActionUnassign",
|
||||
"ActionUpdate",
|
||||
"ActionUpdateAgent",
|
||||
"ActionUpdatePersonal",
|
||||
"ActionUse",
|
||||
"ActionViewInsights",
|
||||
|
||||
@@ -412,7 +412,7 @@ var (
|
||||
ByOrgID: map[string]rbac.OrgPermissions{
|
||||
orgID.String(): {
|
||||
Member: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -442,7 +442,7 @@ var (
|
||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
|
||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH, policy.ActionCreateAgent, policy.ActionDeleteAgent},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionSSH, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
rbac.ResourceWorkspaceProxy.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
@@ -5726,6 +5726,19 @@ func (q *querier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg da
|
||||
return q.db.UpdateWorkspaceAgentConnectionByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg database.UpdateWorkspaceAgentDisplayAppsByIDParams) error {
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdateAgent, workspace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return q.db.UpdateWorkspaceAgentDisplayAppsByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error {
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1929,6 +1929,17 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
dbm.EXPECT().UpdateWorkspaceAgentStartupByID(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(w, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpdateWorkspaceAgentDisplayAppsByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
arg := database.UpdateWorkspaceAgentDisplayAppsByIDParams{
|
||||
ID: agt.ID,
|
||||
DisplayApps: []database.DisplayApp{database.DisplayAppVscode},
|
||||
}
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateWorkspaceAgentDisplayAppsByID(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(w, policy.ActionUpdateAgent).Returns()
|
||||
}))
|
||||
s.Run("GetWorkspaceAgentLogsAfter", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
ws := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
|
||||
@@ -3909,6 +3909,14 @@ func (m queryMetricsStore) UpdateWorkspaceAgentConnectionByID(ctx context.Contex
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg database.UpdateWorkspaceAgentDisplayAppsByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateWorkspaceAgentDisplayAppsByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateWorkspaceAgentDisplayAppsByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateWorkspaceAgentDisplayAppsByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateWorkspaceAgentLifecycleStateByID(ctx, arg)
|
||||
|
||||
@@ -7321,6 +7321,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentConnectionByID(ctx, arg any
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentConnectionByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentConnectionByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceAgentDisplayAppsByID mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg database.UpdateWorkspaceAgentDisplayAppsByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateWorkspaceAgentDisplayAppsByID", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateWorkspaceAgentDisplayAppsByID indicates an expected call of UpdateWorkspaceAgentDisplayAppsByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentDisplayAppsByID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentDisplayAppsByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentDisplayAppsByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceAgentLifecycleStateByID mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg database.UpdateWorkspaceAgentLifecycleStateByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+3
-1
@@ -208,7 +208,9 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'boundary_usage:*',
|
||||
'boundary_usage:delete',
|
||||
'boundary_usage:read',
|
||||
'boundary_usage:update'
|
||||
'boundary_usage:update',
|
||||
'workspace:update_agent',
|
||||
'workspace_dormant:update_agent'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- No-op for update agent scopes: keep enum values to avoid dependency churn.
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace:update_agent';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_dormant:update_agent';
|
||||
@@ -217,6 +217,8 @@ const (
|
||||
ApiKeyScopeBoundaryUsageDelete APIKeyScope = "boundary_usage:delete"
|
||||
ApiKeyScopeBoundaryUsageRead APIKeyScope = "boundary_usage:read"
|
||||
ApiKeyScopeBoundaryUsageUpdate APIKeyScope = "boundary_usage:update"
|
||||
ApiKeyScopeWorkspaceUpdateAgent APIKeyScope = "workspace:update_agent"
|
||||
ApiKeyScopeWorkspaceDormantUpdateAgent APIKeyScope = "workspace_dormant:update_agent"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -453,7 +455,9 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeBoundaryUsage,
|
||||
ApiKeyScopeBoundaryUsageDelete,
|
||||
ApiKeyScopeBoundaryUsageRead,
|
||||
ApiKeyScopeBoundaryUsageUpdate:
|
||||
ApiKeyScopeBoundaryUsageUpdate,
|
||||
ApiKeyScopeWorkspaceUpdateAgent,
|
||||
ApiKeyScopeWorkspaceDormantUpdateAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -659,6 +663,8 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeBoundaryUsageDelete,
|
||||
ApiKeyScopeBoundaryUsageRead,
|
||||
ApiKeyScopeBoundaryUsageUpdate,
|
||||
ApiKeyScopeWorkspaceUpdateAgent,
|
||||
ApiKeyScopeWorkspaceDormantUpdateAgent,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -738,6 +738,7 @@ type sqlcQuerier interface {
|
||||
UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error)
|
||||
UpdateWorkspaceACLByID(ctx context.Context, arg UpdateWorkspaceACLByIDParams) error
|
||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||
UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg UpdateWorkspaceAgentDisplayAppsByIDParams) error
|
||||
UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error
|
||||
UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error
|
||||
UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error
|
||||
|
||||
@@ -19306,6 +19306,26 @@ func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceAgentDisplayAppsByID = `-- name: UpdateWorkspaceAgentDisplayAppsByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
display_apps = $2, updated_at = $3
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateWorkspaceAgentDisplayAppsByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg UpdateWorkspaceAgentDisplayAppsByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceAgentDisplayAppsByID, arg.ID, pq.Array(arg.DisplayApps), arg.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceAgentLifecycleStateByID = `-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
|
||||
@@ -180,6 +180,14 @@ SET
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateWorkspaceAgentDisplayAppsByID :exec
|
||||
UPDATE
|
||||
workspace_agents
|
||||
SET
|
||||
display_apps = $2, updated_at = $3
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: GetWorkspaceAgentLogsAfter :many
|
||||
SELECT
|
||||
*
|
||||
|
||||
@@ -370,6 +370,7 @@ var (
|
||||
// - "ActionWorkspaceStart" :: allows starting a workspace
|
||||
// - "ActionWorkspaceStop" :: allows stopping a workspace
|
||||
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
|
||||
// - "ActionUpdateAgent" :: update an existing workspace agent
|
||||
ResourceWorkspace = Object{
|
||||
Type: "workspace",
|
||||
}
|
||||
@@ -403,6 +404,7 @@ var (
|
||||
// - "ActionWorkspaceStart" :: allows starting a workspace
|
||||
// - "ActionWorkspaceStop" :: allows stopping a workspace
|
||||
// - "ActionUpdate" :: edit workspace settings (scheduling, permissions, parameters)
|
||||
// - "ActionUpdateAgent" :: update an existing workspace agent
|
||||
ResourceWorkspaceDormant = Object{
|
||||
Type: "workspace_dormant",
|
||||
}
|
||||
@@ -480,6 +482,7 @@ func AllActions() []policy.Action {
|
||||
policy.ActionShare,
|
||||
policy.ActionUnassign,
|
||||
policy.ActionUpdate,
|
||||
policy.ActionUpdateAgent,
|
||||
policy.ActionUpdatePersonal,
|
||||
policy.ActionUse,
|
||||
policy.ActionViewInsights,
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
|
||||
ActionCreateAgent Action = "create_agent"
|
||||
ActionDeleteAgent Action = "delete_agent"
|
||||
ActionUpdateAgent Action = "update_agent"
|
||||
|
||||
ActionShare Action = "share"
|
||||
)
|
||||
@@ -63,6 +64,7 @@ var workspaceActions = map[Action]ActionDefinition{
|
||||
|
||||
ActionCreateAgent: "create a new workspace agent",
|
||||
ActionDeleteAgent: "delete an existing workspace agent",
|
||||
ActionUpdateAgent: "update an existing workspace agent",
|
||||
|
||||
// Sharing a workspace
|
||||
ActionShare: "share a workspace with other users or groups",
|
||||
|
||||
@@ -290,7 +290,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
// This adds back in the Workspace permissions.
|
||||
Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspace.Type: ownerWorkspaceActions,
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
||||
@@ -434,7 +434,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
// Org admins should not have workspace exec perms.
|
||||
organizationID.String(): {
|
||||
Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage), Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
||||
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
@@ -972,6 +972,7 @@ func OrgMemberPermissions(workspaceSharingDisabled bool) (
|
||||
policy.ActionWorkspaceStop,
|
||||
policy.ActionCreateAgent,
|
||||
policy.ActionDeleteAgent,
|
||||
policy.ActionUpdateAgent,
|
||||
},
|
||||
// Can read their own organization member record.
|
||||
ResourceOrganizationMember.Type: {
|
||||
|
||||
@@ -294,6 +294,15 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateWorkspaceAgent",
|
||||
Actions: []policy.Action{policy.ActionUpdateAgent},
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ShareMyWorkspace",
|
||||
Actions: []policy.Action{policy.ActionShare},
|
||||
@@ -563,7 +572,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "WorkspaceDormant",
|
||||
Actions: append(crud, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent),
|
||||
Actions: append(crud, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent),
|
||||
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {orgAdmin, owner},
|
||||
|
||||
@@ -135,6 +135,7 @@ const (
|
||||
ScopeWorkspaceStart ScopeName = "workspace:start"
|
||||
ScopeWorkspaceStop ScopeName = "workspace:stop"
|
||||
ScopeWorkspaceUpdate ScopeName = "workspace:update"
|
||||
ScopeWorkspaceUpdateAgent ScopeName = "workspace:update_agent"
|
||||
ScopeWorkspaceAgentDevcontainersCreate ScopeName = "workspace_agent_devcontainers:create"
|
||||
ScopeWorkspaceAgentResourceMonitorCreate ScopeName = "workspace_agent_resource_monitor:create"
|
||||
ScopeWorkspaceAgentResourceMonitorRead ScopeName = "workspace_agent_resource_monitor:read"
|
||||
@@ -150,6 +151,7 @@ const (
|
||||
ScopeWorkspaceDormantStart ScopeName = "workspace_dormant:start"
|
||||
ScopeWorkspaceDormantStop ScopeName = "workspace_dormant:stop"
|
||||
ScopeWorkspaceDormantUpdate ScopeName = "workspace_dormant:update"
|
||||
ScopeWorkspaceDormantUpdateAgent ScopeName = "workspace_dormant:update_agent"
|
||||
ScopeWorkspaceProxyCreate ScopeName = "workspace_proxy:create"
|
||||
ScopeWorkspaceProxyDelete ScopeName = "workspace_proxy:delete"
|
||||
ScopeWorkspaceProxyRead ScopeName = "workspace_proxy:read"
|
||||
@@ -293,6 +295,7 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeWorkspaceStart,
|
||||
ScopeWorkspaceStop,
|
||||
ScopeWorkspaceUpdate,
|
||||
ScopeWorkspaceUpdateAgent,
|
||||
ScopeWorkspaceAgentDevcontainersCreate,
|
||||
ScopeWorkspaceAgentResourceMonitorCreate,
|
||||
ScopeWorkspaceAgentResourceMonitorRead,
|
||||
@@ -308,6 +311,7 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeWorkspaceDormantStart,
|
||||
ScopeWorkspaceDormantStop,
|
||||
ScopeWorkspaceDormantUpdate,
|
||||
ScopeWorkspaceDormantUpdateAgent,
|
||||
ScopeWorkspaceProxyCreate,
|
||||
ScopeWorkspaceProxyDelete,
|
||||
ScopeWorkspaceProxyRead,
|
||||
@@ -452,6 +456,7 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeWorkspaceStart,
|
||||
ScopeWorkspaceStop,
|
||||
ScopeWorkspaceUpdate,
|
||||
ScopeWorkspaceUpdateAgent,
|
||||
ScopeWorkspaceAgentDevcontainersCreate,
|
||||
ScopeWorkspaceAgentResourceMonitorCreate,
|
||||
ScopeWorkspaceAgentResourceMonitorRead,
|
||||
@@ -467,6 +472,7 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeWorkspaceDormantStart,
|
||||
ScopeWorkspaceDormantStop,
|
||||
ScopeWorkspaceDormantUpdate,
|
||||
ScopeWorkspaceDormantUpdateAgent,
|
||||
ScopeWorkspaceProxyCreate,
|
||||
ScopeWorkspaceProxyDelete,
|
||||
ScopeWorkspaceProxyRead,
|
||||
|
||||
Reference in New Issue
Block a user