feat(coderd/agentapi): support adding display apps to a sub agent (#18272)

Completely missed this in the original PR for adding support for
creating sub agents. This now allows specifying a list of display apps
to be added to the agent.
This commit is contained in:
Danielle Maywood
2025-06-06 17:30:52 +01:00
committed by GitHub
parent f569d9c33d
commit 8daa0aacc6
4 changed files with 669 additions and 359 deletions
+26 -1
View File
@@ -55,6 +55,31 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
createdAt := a.Clock.Now()
displayApps := make([]database.DisplayApp, 0, len(req.DisplayApps))
for idx, displayApp := range req.DisplayApps {
var app database.DisplayApp
switch displayApp {
case agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER:
app = database.DisplayAppPortForwardingHelper
case agentproto.CreateSubAgentRequest_SSH_HELPER:
app = database.DisplayAppSSHHelper
case agentproto.CreateSubAgentRequest_VSCODE:
app = database.DisplayAppVscode
case agentproto.CreateSubAgentRequest_VSCODE_INSIDERS:
app = database.DisplayAppVscodeInsiders
case agentproto.CreateSubAgentRequest_WEB_TERMINAL:
app = database.DisplayAppWebTerminal
default:
return nil, codersdk.ValidationError{
Field: fmt.Sprintf("display_apps[%d]", idx),
Detail: fmt.Sprintf("%q is not a valid display app", displayApp),
}
}
displayApps = append(displayApps, app)
}
subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
ID: uuid.New(),
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
@@ -73,7 +98,7 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
ConnectionTimeoutSeconds: parentAgent.ConnectionTimeoutSeconds,
TroubleshootingURL: parentAgent.TroubleshootingURL,
MOTDFile: "",
DisplayApps: []database.DisplayApp{},
DisplayApps: displayApps,
DisplayOrder: 0,
APIKeyScope: parentAgent.APIKeyScope,
})
+198
View File
@@ -939,6 +939,204 @@ func TestSubAgentAPI(t *testing.T) {
})
})
t.Run("CreateSubAgentWithDisplayApps", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
displayApps []proto.CreateSubAgentRequest_DisplayApp
expectedApps []database.DisplayApp
expectedError *codersdk.ValidationError
}{
{
name: "NoDisplayApps",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{},
expectedApps: []database.DisplayApp{},
},
{
name: "SingleDisplayApp_VSCode",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_VSCODE,
},
expectedApps: []database.DisplayApp{
database.DisplayAppVscode,
},
},
{
name: "SingleDisplayApp_VSCodeInsiders",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_VSCODE_INSIDERS,
},
expectedApps: []database.DisplayApp{
database.DisplayAppVscodeInsiders,
},
},
{
name: "SingleDisplayApp_WebTerminal",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_WEB_TERMINAL,
},
expectedApps: []database.DisplayApp{
database.DisplayAppWebTerminal,
},
},
{
name: "SingleDisplayApp_SSHHelper",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_SSH_HELPER,
},
expectedApps: []database.DisplayApp{
database.DisplayAppSSHHelper,
},
},
{
name: "SingleDisplayApp_PortForwardingHelper",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
},
expectedApps: []database.DisplayApp{
database.DisplayAppPortForwardingHelper,
},
},
{
name: "MultipleDisplayApps",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_VSCODE,
proto.CreateSubAgentRequest_WEB_TERMINAL,
proto.CreateSubAgentRequest_SSH_HELPER,
},
expectedApps: []database.DisplayApp{
database.DisplayAppVscode,
database.DisplayAppWebTerminal,
database.DisplayAppSSHHelper,
},
},
{
name: "AllDisplayApps",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_VSCODE,
proto.CreateSubAgentRequest_VSCODE_INSIDERS,
proto.CreateSubAgentRequest_WEB_TERMINAL,
proto.CreateSubAgentRequest_SSH_HELPER,
proto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
},
expectedApps: []database.DisplayApp{
database.DisplayAppVscode,
database.DisplayAppVscodeInsiders,
database.DisplayAppWebTerminal,
database.DisplayAppSSHHelper,
database.DisplayAppPortForwardingHelper,
},
},
{
name: "InvalidDisplayApp",
displayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_DisplayApp(9999), // Invalid enum value
},
expectedError: &codersdk.ValidationError{
Field: "display_apps[0]",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
log := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitLong)
clock := quartz.NewMock(t)
db, org := newDatabaseWithOrg(t)
user, agent := newUserWithWorkspaceAgent(t, db, org)
api := newAgentAPI(t, log, db, clock, user, org, agent)
createResp, err := api.CreateSubAgent(ctx, &proto.CreateSubAgentRequest{
Name: "test-agent",
Directory: "/workspaces/test",
Architecture: "amd64",
OperatingSystem: "linux",
DisplayApps: tt.displayApps,
})
if tt.expectedError != nil {
require.Error(t, err)
require.Nil(t, createResp)
var validationErr codersdk.ValidationError
require.ErrorAs(t, err, &validationErr)
require.Equal(t, tt.expectedError.Field, validationErr.Field)
require.Contains(t, validationErr.Detail, "is not a valid display app")
} else {
require.NoError(t, err)
require.NotNil(t, createResp.Agent)
agentID, err := uuid.FromBytes(createResp.Agent.Id)
require.NoError(t, err)
subAgent, err := api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test.
require.NoError(t, err)
require.Equal(t, len(tt.expectedApps), len(subAgent.DisplayApps), "display apps count mismatch")
for i, expectedApp := range tt.expectedApps {
require.Equal(t, expectedApp, subAgent.DisplayApps[i], "display app at index %d doesn't match", i)
}
}
})
}
})
t.Run("CreateSubAgentWithDisplayAppsAndApps", func(t *testing.T) {
t.Parallel()
log := testutil.Logger(t)
ctx := testutil.Context(t, testutil.WaitLong)
clock := quartz.NewMock(t)
db, org := newDatabaseWithOrg(t)
user, agent := newUserWithWorkspaceAgent(t, db, org)
api := newAgentAPI(t, log, db, clock, user, org, agent)
// Test that display apps and regular apps can coexist
createResp, err := api.CreateSubAgent(ctx, &proto.CreateSubAgentRequest{
Name: "test-agent",
Directory: "/workspaces/test",
Architecture: "amd64",
OperatingSystem: "linux",
DisplayApps: []proto.CreateSubAgentRequest_DisplayApp{
proto.CreateSubAgentRequest_VSCODE,
proto.CreateSubAgentRequest_WEB_TERMINAL,
},
Apps: []*proto.CreateSubAgentRequest_App{
{
Slug: "custom-app",
DisplayName: ptr.Ref("Custom App"),
Url: ptr.Ref("http://localhost:8080"),
},
},
})
require.NoError(t, err)
require.NotNil(t, createResp.Agent)
require.Empty(t, createResp.AppCreationErrors)
agentID, err := uuid.FromBytes(createResp.Agent.Id)
require.NoError(t, err)
// Verify display apps
subAgent, err := api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test.
require.NoError(t, err)
require.Len(t, subAgent.DisplayApps, 2)
require.Equal(t, database.DisplayAppVscode, subAgent.DisplayApps[0])
require.Equal(t, database.DisplayAppWebTerminal, subAgent.DisplayApps[1])
// Verify regular apps
apps, err := api.Database.GetWorkspaceAppsByAgentID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test.
require.NoError(t, err)
require.Len(t, apps, 1)
require.Equal(t, "custom-app", apps[0].Slug)
require.Equal(t, "Custom App", apps[0].DisplayName)
})
t.Run("ListSubAgents", func(t *testing.T) {
t.Parallel()