diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index a6c2167ca8685..4e8773792b7e5 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -28,6 +28,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/provisioner" "github.com/coder/quartz" ) @@ -1146,6 +1147,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c } var appsWithPossibleDuplicates []SubAgentApp + var possibleAgentName string if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath, []string{ @@ -1173,6 +1175,19 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c appsWithPossibleDuplicates = append(appsWithPossibleDuplicates, customization.Apps...) } + + // NOTE(DanielleMaywood): + // We only want to take an agent name specified in the root customization layer. + // This restricts the ability for a feature to specify the agent name. We may revisit + // this in the future, but for now we want to restrict this behavior. + if name := config.Configuration.Customizations.Coder.Name; name != "" { + // We only want to pick this name if it is a valid name. + if provisioner.AgentNameRegex.Match([]byte(name)) { + possibleAgentName = name + } else { + logger.Warn(ctx, "invalid agent name in devcontainer customization, ignoring", slog.F("name", name)) + } + } } displayApps := make([]codersdk.DisplayApp, 0, len(displayAppsMap)) @@ -1204,6 +1219,10 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c subAgentConfig.DisplayApps = displayApps subAgentConfig.Apps = apps + + if possibleAgentName != "" { + subAgentConfig.Name = possibleAgentName + } } deleteSubAgent := proc.agent.ID != uuid.Nil && maybeRecreateSubAgent && !proc.agent.EqualConfig(subAgentConfig) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 3bf6206e2adce..bcd76c658a717 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1556,17 +1556,18 @@ func TestAPI(t *testing.T) { } tests := []struct { - name string - customization []agentcontainers.CoderCustomization - afterCreate func(t *testing.T, subAgent agentcontainers.SubAgent) + name string + customization agentcontainers.CoderCustomization + mergedCustomizations []agentcontainers.CoderCustomization + afterCreate func(t *testing.T, subAgent agentcontainers.SubAgent) }{ { - name: "WithoutCustomization", - customization: nil, + name: "WithoutCustomization", + mergedCustomizations: nil, }, { - name: "WithDefaultDisplayApps", - customization: []agentcontainers.CoderCustomization{}, + name: "WithDefaultDisplayApps", + mergedCustomizations: []agentcontainers.CoderCustomization{}, afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { require.Len(t, subAgent.DisplayApps, 4) assert.Contains(t, subAgent.DisplayApps, codersdk.DisplayAppVSCodeDesktop) @@ -1577,7 +1578,7 @@ func TestAPI(t *testing.T) { }, { name: "WithAllDisplayApps", - customization: []agentcontainers.CoderCustomization{ + mergedCustomizations: []agentcontainers.CoderCustomization{ { DisplayApps: map[codersdk.DisplayApp]bool{ codersdk.DisplayAppSSH: true, @@ -1599,7 +1600,7 @@ func TestAPI(t *testing.T) { }, { name: "WithSomeDisplayAppsDisabled", - customization: []agentcontainers.CoderCustomization{ + mergedCustomizations: []agentcontainers.CoderCustomization{ { DisplayApps: map[codersdk.DisplayApp]bool{ codersdk.DisplayAppSSH: false, @@ -1631,7 +1632,7 @@ func TestAPI(t *testing.T) { }, { name: "WithApps", - customization: []agentcontainers.CoderCustomization{ + mergedCustomizations: []agentcontainers.CoderCustomization{ { Apps: []agentcontainers.SubAgentApp{ { @@ -1699,7 +1700,7 @@ func TestAPI(t *testing.T) { }, { name: "AppDeduplication", - customization: []agentcontainers.CoderCustomization{ + mergedCustomizations: []agentcontainers.CoderCustomization{ { Apps: []agentcontainers.SubAgentApp{ { @@ -1739,6 +1740,52 @@ func TestAPI(t *testing.T) { assert.Equal(t, int32(2), subAgent.Apps[1].Order) }, }, + { + name: "Name", + customization: agentcontainers.CoderCustomization{ + Name: "this-name", + }, + mergedCustomizations: []agentcontainers.CoderCustomization{ + { + Name: "not-this-name", + }, + { + Name: "or-this-name", + }, + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.Equal(t, "this-name", subAgent.Name) + }, + }, + { + name: "NameIsOnlyUsedFromRoot", + mergedCustomizations: []agentcontainers.CoderCustomization{ + { + Name: "custom-name", + }, + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.NotEqual(t, "custom-name", subAgent.Name) + }, + }, + { + name: "EmptyNameIsIgnored", + customization: agentcontainers.CoderCustomization{ + Name: "", + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.NotEmpty(t, subAgent.Name) + }, + }, + { + name: "InvalidNameIsIgnored", + customization: agentcontainers.CoderCustomization{ + Name: "This--Is_An_Invalid--Name", + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.NotEqual(t, "This--Is_An_Invalid--Name", subAgent.Name) + }, + }, } for _, tt := range tests { @@ -1756,11 +1803,16 @@ func TestAPI(t *testing.T) { } fDCCLI = &fakeDevcontainerCLI{ readConfig: agentcontainers.DevcontainerConfig{ - MergedConfiguration: agentcontainers.DevcontainerConfiguration{ + Configuration: agentcontainers.DevcontainerConfiguration{ Customizations: agentcontainers.DevcontainerCustomizations{ Coder: tt.customization, }, }, + MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{ + Customizations: agentcontainers.DevcontainerMergedCustomizations{ + Coder: tt.mergedCustomizations, + }, + }, }, execErrC: make(chan func(cmd string, args ...string) error, 1), } @@ -1825,7 +1877,6 @@ func TestAPI(t *testing.T) { // Then: We expected it to succeed require.Len(t, fSAC.created, 1) - assert.Equal(t, testContainer.FriendlyName, fSAC.created[0].Name) if tt.afterCreate != nil { tt.afterCreate(t, fSAC.created[0]) diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go index 335be53648c2d..e302ff07d6dd9 100644 --- a/agent/agentcontainers/devcontainercli.go +++ b/agent/agentcontainers/devcontainercli.go @@ -20,7 +20,16 @@ import ( // Unfortunately we cannot make use of `dcspec` as the output doesn't appear to // match. type DevcontainerConfig struct { - MergedConfiguration DevcontainerConfiguration `json:"mergedConfiguration"` + MergedConfiguration DevcontainerMergedConfiguration `json:"mergedConfiguration"` + Configuration DevcontainerConfiguration `json:"configuration"` +} + +type DevcontainerMergedConfiguration struct { + Customizations DevcontainerMergedCustomizations `json:"customizations,omitempty"` +} + +type DevcontainerMergedCustomizations struct { + Coder []CoderCustomization `json:"coder,omitempty"` } type DevcontainerConfiguration struct { @@ -28,12 +37,13 @@ type DevcontainerConfiguration struct { } type DevcontainerCustomizations struct { - Coder []CoderCustomization `json:"coder,omitempty"` + Coder CoderCustomization `json:"coder,omitempty"` } type CoderCustomization struct { DisplayApps map[codersdk.DisplayApp]bool `json:"displayApps,omitempty"` Apps []SubAgentApp `json:"apps,omitempty"` + Name string `json:"name,omitempty"` } // DevcontainerCLI is an interface for the devcontainer CLI. diff --git a/agent/agentcontainers/devcontainercli_test.go b/agent/agentcontainers/devcontainercli_test.go index 311ec440e357a..821e6e8f95e76 100644 --- a/agent/agentcontainers/devcontainercli_test.go +++ b/agent/agentcontainers/devcontainercli_test.go @@ -256,8 +256,8 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) { wantArgs: "read-configuration --include-merged-configuration --workspace-folder /test/workspace", wantError: false, wantConfig: agentcontainers.DevcontainerConfig{ - MergedConfiguration: agentcontainers.DevcontainerConfiguration{ - Customizations: agentcontainers.DevcontainerCustomizations{ + MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{ + Customizations: agentcontainers.DevcontainerMergedCustomizations{ Coder: []agentcontainers.CoderCustomization{ { DisplayApps: map[codersdk.DisplayApp]bool{ @@ -284,8 +284,8 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) { wantArgs: "read-configuration --include-merged-configuration --workspace-folder /test/workspace --config /test/config.json", wantError: false, wantConfig: agentcontainers.DevcontainerConfig{ - MergedConfiguration: agentcontainers.DevcontainerConfiguration{ - Customizations: agentcontainers.DevcontainerCustomizations{ + MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{ + Customizations: agentcontainers.DevcontainerMergedCustomizations{ Coder: nil, }, },