From a5278c905a9904b21157aa96bcbebb12bed9afa2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 19 Jun 2025 10:56:53 +0000 Subject: [PATCH 1/4] feat(agent/agentcontainers): support agent name in customization This PR supports specifying a name that will be used for the devcontainer agent in the customizations section of the devcontainer.json configuration file. --- agent/agentcontainers/api.go | 13 ++++++++++++ agent/agentcontainers/api_test.go | 27 +++++++++++++++++++++++- agent/agentcontainers/devcontainercli.go | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index a6c2167ca8685..251396d130277 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -1146,6 +1146,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 +1174,14 @@ 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 very last 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 len(coderCustomization) > 0 { + possibleAgentName = coderCustomization[len(coderCustomization)-1].Name + } } displayApps := make([]codersdk.DisplayApp, 0, len(displayAppsMap)) @@ -1204,6 +1213,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..1b843aa20f2f8 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1739,6 +1739,32 @@ func TestAPI(t *testing.T) { assert.Equal(t, int32(2), subAgent.Apps[1].Order) }, }, + { + name: "Name", + customization: []agentcontainers.CoderCustomization{ + { + Name: "not-this-name", + }, + { + Name: "custom-name", + }, + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.Equal(t, "custom-name", subAgent.Name) + }, + }, + { + name: "NameIsOnlyUsedWhenInLastLayer", + customization: []agentcontainers.CoderCustomization{ + { + Name: "custom-name", + }, + {}, + }, + afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { + require.NotEqual(t, "custom-name", subAgent.Name) + }, + }, } for _, tt := range tests { @@ -1825,7 +1851,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..cd97918226029 100644 --- a/agent/agentcontainers/devcontainercli.go +++ b/agent/agentcontainers/devcontainercli.go @@ -34,6 +34,7 @@ type DevcontainerCustomizations struct { 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. From db59693f91ce55390665e30d8eff85447a05f238 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 19 Jun 2025 11:11:09 +0000 Subject: [PATCH 2/4] chore: add extra test case Technically this doesn't really test anything new but it is good to have an explicit test for anyways. --- agent/agentcontainers/api_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 1b843aa20f2f8..fef464efe2e6d 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1765,6 +1765,17 @@ func TestAPI(t *testing.T) { 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) + }, + }, } for _, tt := range tests { From d094205443540023b2123492505e89970dd672e4 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 19 Jun 2025 11:36:56 +0000 Subject: [PATCH 3/4] chore: ignore invalid names --- agent/agentcontainers/api.go | 10 +++++++++- agent/agentcontainers/api_test.go | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 251396d130277..175af251f2c7f 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" ) @@ -1180,7 +1181,14 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c // 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 len(coderCustomization) > 0 { - possibleAgentName = coderCustomization[len(coderCustomization)-1].Name + name := coderCustomization[len(coderCustomization)-1].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)) + } } } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index fef464efe2e6d..c32d12b090904 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -1776,6 +1776,17 @@ func TestAPI(t *testing.T) { 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 { From b738124e4760c4b48e285c54a20ecb6a12c127d4 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 19 Jun 2025 11:54:40 +0000 Subject: [PATCH 4/4] refactor: include configuration and merged configuration We want to be able to pick customizations that are only specified in the top-level customizations stanza. That is, we do not want to accidentally pick something specified in a feature (such as the agent name). This refactor picks the agent name from configuration instead of the merged configuration to ensure a feature cannot influence it. --- agent/agentcontainers/api.go | 6 +- agent/agentcontainers/api_test.go | 56 ++++++++++--------- agent/agentcontainers/devcontainercli.go | 13 ++++- agent/agentcontainers/devcontainercli_test.go | 8 +-- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 175af251f2c7f..4e8773792b7e5 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -1177,12 +1177,10 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c } // NOTE(DanielleMaywood): - // We only want to take an agent name specified in the very last customization layer. + // 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 len(coderCustomization) > 0 { - name := coderCustomization[len(coderCustomization)-1].Name - + 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 diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index c32d12b090904..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{ { @@ -1741,25 +1742,27 @@ func TestAPI(t *testing.T) { }, { name: "Name", - customization: []agentcontainers.CoderCustomization{ + customization: agentcontainers.CoderCustomization{ + Name: "this-name", + }, + mergedCustomizations: []agentcontainers.CoderCustomization{ { Name: "not-this-name", }, { - Name: "custom-name", + Name: "or-this-name", }, }, afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { - require.Equal(t, "custom-name", subAgent.Name) + require.Equal(t, "this-name", subAgent.Name) }, }, { - name: "NameIsOnlyUsedWhenInLastLayer", - customization: []agentcontainers.CoderCustomization{ + name: "NameIsOnlyUsedFromRoot", + mergedCustomizations: []agentcontainers.CoderCustomization{ { Name: "custom-name", }, - {}, }, afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { require.NotEqual(t, "custom-name", subAgent.Name) @@ -1767,10 +1770,8 @@ func TestAPI(t *testing.T) { }, { name: "EmptyNameIsIgnored", - customization: []agentcontainers.CoderCustomization{ - { - Name: "", - }, + customization: agentcontainers.CoderCustomization{ + Name: "", }, afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) { require.NotEmpty(t, subAgent.Name) @@ -1778,10 +1779,8 @@ func TestAPI(t *testing.T) { }, { name: "InvalidNameIsIgnored", - customization: []agentcontainers.CoderCustomization{ - { - Name: "This--Is_An_Invalid--Name", - }, + 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) @@ -1804,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), } diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go index cd97918226029..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,7 +37,7 @@ type DevcontainerConfiguration struct { } type DevcontainerCustomizations struct { - Coder []CoderCustomization `json:"coder,omitempty"` + Coder CoderCustomization `json:"coder,omitempty"` } type CoderCustomization struct { 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, }, }, 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