mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user