feat(agent/agentcontainers): support agent name in customization (#18451)

Relates to https://github.com/coder/internal/issues/732

This PR supports specifying a name that will be used for the
devcontainer agent in the customizations section of the
devcontainer.json configuration file.
This commit is contained in:
Danielle Maywood
2025-06-19 13:21:59 +01:00
committed by GitHub
parent 884ad39bd0
commit a04268a188
4 changed files with 99 additions and 19 deletions
+19
View File
@@ -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)
+64 -13
View File
@@ -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])
+12 -2
View File
@@ -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.
@@ -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,
},
},