Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 213ba4a

Browse files
committed
Merge branch 'main' of github.com:/coder/coder into dk/coder-ai-task-res
2 parents 84caed7 + bacdc28 commit 213ba4a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3303
-1020
lines changed

agent/agentcontainers/api.go

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,18 +1147,49 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11471147
}
11481148

11491149
var appsWithPossibleDuplicates []SubAgentApp
1150-
var possibleAgentName string
1151-
1152-
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath,
1153-
[]string{
1154-
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", dc.Name),
1155-
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName),
1156-
fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName),
1157-
fmt.Sprintf("CODER_URL=%s", api.subAgentURL),
1158-
},
1159-
); err != nil {
1160-
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
1161-
} else {
1150+
1151+
if err := func() error {
1152+
var (
1153+
config DevcontainerConfig
1154+
configOutdated bool
1155+
)
1156+
1157+
readConfig := func() (DevcontainerConfig, error) {
1158+
return api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath, []string{
1159+
fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", subAgentConfig.Name),
1160+
fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName),
1161+
fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName),
1162+
fmt.Sprintf("CODER_URL=%s", api.subAgentURL),
1163+
})
1164+
}
1165+
1166+
if config, err = readConfig(); err != nil {
1167+
return err
1168+
}
1169+
1170+
// NOTE(DanielleMaywood):
1171+
// We only want to take an agent name specified in the root customization layer.
1172+
// This restricts the ability for a feature to specify the agent name. We may revisit
1173+
// this in the future, but for now we want to restrict this behavior.
1174+
if name := config.Configuration.Customizations.Coder.Name; name != "" {
1175+
// We only want to pick this name if it is a valid name.
1176+
if provisioner.AgentNameRegex.Match([]byte(name)) {
1177+
subAgentConfig.Name = name
1178+
configOutdated = true
1179+
} else {
1180+
logger.Warn(ctx, "invalid name in devcontainer customization, ignoring",
1181+
slog.F("name", name),
1182+
slog.F("regex", provisioner.AgentNameRegex.String()),
1183+
)
1184+
}
1185+
}
1186+
1187+
if configOutdated {
1188+
if config, err = readConfig(); err != nil {
1189+
return err
1190+
}
1191+
}
1192+
11621193
coderCustomization := config.MergedConfiguration.Customizations.Coder
11631194

11641195
for _, customization := range coderCustomization {
@@ -1176,18 +1207,9 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
11761207
appsWithPossibleDuplicates = append(appsWithPossibleDuplicates, customization.Apps...)
11771208
}
11781209

1179-
// NOTE(DanielleMaywood):
1180-
// We only want to take an agent name specified in the root customization layer.
1181-
// This restricts the ability for a feature to specify the agent name. We may revisit
1182-
// this in the future, but for now we want to restrict this behavior.
1183-
if name := config.Configuration.Customizations.Coder.Name; name != "" {
1184-
// We only want to pick this name if it is a valid name.
1185-
if provisioner.AgentNameRegex.Match([]byte(name)) {
1186-
possibleAgentName = name
1187-
} else {
1188-
logger.Warn(ctx, "invalid agent name in devcontainer customization, ignoring", slog.F("name", name))
1189-
}
1190-
}
1210+
return nil
1211+
}(); err != nil {
1212+
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
11911213
}
11921214

11931215
displayApps := make([]codersdk.DisplayApp, 0, len(displayAppsMap))
@@ -1219,10 +1241,6 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c
12191241

12201242
subAgentConfig.DisplayApps = displayApps
12211243
subAgentConfig.Apps = apps
1222-
1223-
if possibleAgentName != "" {
1224-
subAgentConfig.Name = possibleAgentName
1225-
}
12261244
}
12271245

12281246
deleteSubAgent := proc.agent.ID != uuid.Nil && maybeRecreateSubAgent && !proc.agent.EqualConfig(subAgentConfig)

agent/agentcontainers/api_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,6 +1884,111 @@ func TestAPI(t *testing.T) {
18841884
})
18851885
}
18861886
})
1887+
1888+
t.Run("CreateReadsConfigTwice", func(t *testing.T) {
1889+
t.Parallel()
1890+
1891+
if runtime.GOOS == "windows" {
1892+
t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)")
1893+
}
1894+
1895+
var (
1896+
ctx = testutil.Context(t, testutil.WaitMedium)
1897+
logger = testutil.Logger(t)
1898+
mClock = quartz.NewMock(t)
1899+
mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t))
1900+
fSAC = &fakeSubAgentClient{
1901+
logger: logger.Named("fakeSubAgentClient"),
1902+
createErrC: make(chan error, 1),
1903+
}
1904+
fDCCLI = &fakeDevcontainerCLI{
1905+
readConfig: agentcontainers.DevcontainerConfig{
1906+
Configuration: agentcontainers.DevcontainerConfiguration{
1907+
Customizations: agentcontainers.DevcontainerCustomizations{
1908+
Coder: agentcontainers.CoderCustomization{
1909+
// We want to specify a custom name for this agent.
1910+
Name: "custom-name",
1911+
},
1912+
},
1913+
},
1914+
},
1915+
readConfigErrC: make(chan func(envs []string) error, 2),
1916+
execErrC: make(chan func(cmd string, args ...string) error, 1),
1917+
}
1918+
1919+
testContainer = codersdk.WorkspaceAgentContainer{
1920+
ID: "test-container-id",
1921+
FriendlyName: "test-container",
1922+
Image: "test-image",
1923+
Running: true,
1924+
CreatedAt: time.Now(),
1925+
Labels: map[string]string{
1926+
agentcontainers.DevcontainerLocalFolderLabel: "/workspaces",
1927+
agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json",
1928+
},
1929+
}
1930+
)
1931+
1932+
coderBin, err := os.Executable()
1933+
require.NoError(t, err)
1934+
1935+
// Mock the `List` function to always return out test container.
1936+
mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{
1937+
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
1938+
}, nil).AnyTimes()
1939+
1940+
// Mock the steps used for injecting the coder agent.
1941+
gomock.InOrder(
1942+
mCCLI.EXPECT().DetectArchitecture(gomock.Any(), testContainer.ID).Return(runtime.GOARCH, nil),
1943+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil),
1944+
mCCLI.EXPECT().Copy(gomock.Any(), testContainer.ID, coderBin, "/.coder-agent/coder").Return(nil),
1945+
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil),
1946+
)
1947+
1948+
mClock.Set(time.Now()).MustWait(ctx)
1949+
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
1950+
1951+
api := agentcontainers.NewAPI(logger,
1952+
agentcontainers.WithClock(mClock),
1953+
agentcontainers.WithContainerCLI(mCCLI),
1954+
agentcontainers.WithDevcontainerCLI(fDCCLI),
1955+
agentcontainers.WithSubAgentClient(fSAC),
1956+
agentcontainers.WithSubAgentURL("test-subagent-url"),
1957+
agentcontainers.WithWatcher(watcher.NewNoop()),
1958+
)
1959+
defer api.Close()
1960+
1961+
// Close before api.Close() defer to avoid deadlock after test.
1962+
defer close(fSAC.createErrC)
1963+
defer close(fDCCLI.execErrC)
1964+
defer close(fDCCLI.readConfigErrC)
1965+
1966+
// Given: We allow agent creation and injection to succeed.
1967+
testutil.RequireSend(ctx, t, fSAC.createErrC, nil)
1968+
testutil.RequireSend(ctx, t, fDCCLI.execErrC, func(cmd string, args ...string) error {
1969+
assert.Equal(t, "pwd", cmd)
1970+
assert.Empty(t, args)
1971+
return nil
1972+
})
1973+
testutil.RequireSend(ctx, t, fDCCLI.readConfigErrC, func(env []string) error {
1974+
// We expect the wrong workspace agent name passed in first.
1975+
assert.Contains(t, env, "CODER_WORKSPACE_AGENT_NAME=test-container")
1976+
return nil
1977+
})
1978+
testutil.RequireSend(ctx, t, fDCCLI.readConfigErrC, func(env []string) error {
1979+
// We then expect the agent name passed here to have been read from the config.
1980+
assert.Contains(t, env, "CODER_WORKSPACE_AGENT_NAME=custom-name")
1981+
assert.NotContains(t, env, "CODER_WORKSPACE_AGENT_NAME=test-container")
1982+
return nil
1983+
})
1984+
1985+
// Wait until the ticker has been registered.
1986+
tickerTrap.MustWait(ctx).MustRelease(ctx)
1987+
tickerTrap.Close()
1988+
1989+
// Then: We expected it to succeed
1990+
require.Len(t, fSAC.created, 1)
1991+
})
18871992
}
18881993

18891994
// mustFindDevcontainerByPath returns the devcontainer with the given workspace

cli/ssh.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -925,36 +925,33 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
925925
func getWorkspaceAgent(workspace codersdk.Workspace, agentName string) (workspaceAgent codersdk.WorkspaceAgent, err error) {
926926
resources := workspace.LatestBuild.Resources
927927

928-
agents := make([]codersdk.WorkspaceAgent, 0)
928+
var (
929+
availableNames []string
930+
agents []codersdk.WorkspaceAgent
931+
)
929932
for _, resource := range resources {
930-
agents = append(agents, resource.Agents...)
933+
for _, agent := range resource.Agents {
934+
availableNames = append(availableNames, agent.Name)
935+
agents = append(agents, agent)
936+
}
931937
}
932938
if len(agents) == 0 {
933939
return codersdk.WorkspaceAgent{}, xerrors.Errorf("workspace %q has no agents", workspace.Name)
934940
}
941+
slices.Sort(availableNames)
935942
if agentName != "" {
936943
for _, otherAgent := range agents {
937944
if otherAgent.Name != agentName {
938945
continue
939946
}
940-
workspaceAgent = otherAgent
941-
break
942-
}
943-
if workspaceAgent.ID == uuid.Nil {
944-
return codersdk.WorkspaceAgent{}, xerrors.Errorf("agent not found by name %q", agentName)
947+
return otherAgent, nil
945948
}
949+
return codersdk.WorkspaceAgent{}, xerrors.Errorf("agent not found by name %q, available agents: %v", agentName, availableNames)
946950
}
947-
if workspaceAgent.ID == uuid.Nil {
948-
if len(agents) > 1 {
949-
workspaceAgent, err = cryptorand.Element(agents)
950-
if err != nil {
951-
return codersdk.WorkspaceAgent{}, err
952-
}
953-
} else {
954-
workspaceAgent = agents[0]
955-
}
951+
if len(agents) == 1 {
952+
return agents[0], nil
956953
}
957-
return workspaceAgent, nil
954+
return codersdk.WorkspaceAgent{}, xerrors.Errorf("multiple agents found, please specify the agent name, available agents: %v", availableNames)
958955
}
959956

960957
// Attempt to poll workspace autostop. We write a per-workspace lockfile to

cli/ssh_internal_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
gliderssh "github.com/gliderlabs/ssh"
14+
"github.com/google/uuid"
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
1617
"golang.org/x/crypto/ssh"
@@ -346,3 +347,97 @@ func newAsyncCloser(ctx context.Context, t *testing.T) *asyncCloser {
346347
started: make(chan struct{}),
347348
}
348349
}
350+
351+
func Test_getWorkspaceAgent(t *testing.T) {
352+
t.Parallel()
353+
354+
createWorkspaceWithAgents := func(agents []codersdk.WorkspaceAgent) codersdk.Workspace {
355+
return codersdk.Workspace{
356+
Name: "test-workspace",
357+
LatestBuild: codersdk.WorkspaceBuild{
358+
Resources: []codersdk.WorkspaceResource{
359+
{
360+
Agents: agents,
361+
},
362+
},
363+
},
364+
}
365+
}
366+
367+
createAgent := func(name string) codersdk.WorkspaceAgent {
368+
return codersdk.WorkspaceAgent{
369+
ID: uuid.New(),
370+
Name: name,
371+
}
372+
}
373+
374+
t.Run("SingleAgent_NoNameSpecified", func(t *testing.T) {
375+
t.Parallel()
376+
agent := createAgent("main")
377+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{agent})
378+
379+
result, err := getWorkspaceAgent(workspace, "")
380+
require.NoError(t, err)
381+
assert.Equal(t, agent.ID, result.ID)
382+
assert.Equal(t, "main", result.Name)
383+
})
384+
385+
t.Run("MultipleAgents_NoNameSpecified", func(t *testing.T) {
386+
t.Parallel()
387+
agent1 := createAgent("main1")
388+
agent2 := createAgent("main2")
389+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{agent1, agent2})
390+
391+
_, err := getWorkspaceAgent(workspace, "")
392+
require.Error(t, err)
393+
assert.Contains(t, err.Error(), "multiple agents found")
394+
assert.Contains(t, err.Error(), "available agents: [main1 main2]")
395+
})
396+
397+
t.Run("AgentNameSpecified_Found", func(t *testing.T) {
398+
t.Parallel()
399+
agent1 := createAgent("main1")
400+
agent2 := createAgent("main2")
401+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{agent1, agent2})
402+
403+
result, err := getWorkspaceAgent(workspace, "main1")
404+
require.NoError(t, err)
405+
assert.Equal(t, agent1.ID, result.ID)
406+
assert.Equal(t, "main1", result.Name)
407+
})
408+
409+
t.Run("AgentNameSpecified_NotFound", func(t *testing.T) {
410+
t.Parallel()
411+
agent1 := createAgent("main1")
412+
agent2 := createAgent("main2")
413+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{agent1, agent2})
414+
415+
_, err := getWorkspaceAgent(workspace, "nonexistent")
416+
require.Error(t, err)
417+
assert.Contains(t, err.Error(), `agent not found by name "nonexistent"`)
418+
assert.Contains(t, err.Error(), "available agents: [main1 main2]")
419+
})
420+
421+
t.Run("NoAgents", func(t *testing.T) {
422+
t.Parallel()
423+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{})
424+
425+
_, err := getWorkspaceAgent(workspace, "")
426+
require.Error(t, err)
427+
assert.Contains(t, err.Error(), `workspace "test-workspace" has no agents`)
428+
})
429+
430+
t.Run("AvailableAgentNames_SortedCorrectly", func(t *testing.T) {
431+
t.Parallel()
432+
// Define agents in non-alphabetical order.
433+
agent2 := createAgent("zod")
434+
agent1 := createAgent("clark")
435+
agent3 := createAgent("krypton")
436+
workspace := createWorkspaceWithAgents([]codersdk.WorkspaceAgent{agent2, agent1, agent3})
437+
438+
_, err := getWorkspaceAgent(workspace, "nonexistent")
439+
require.Error(t, err)
440+
// Available agents should be sorted alphabetically.
441+
assert.Contains(t, err.Error(), "available agents: [clark krypton zod]")
442+
})
443+
}

coderd/agentapi/subagent_test.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -875,14 +875,9 @@ func TestSubAgentAPI(t *testing.T) {
875875
require.NoError(t, err)
876876
})
877877

878-
t.Run("DeletesWorkspaceApps", func(t *testing.T) {
878+
t.Run("DeleteRetainsWorkspaceApps", func(t *testing.T) {
879879
t.Parallel()
880880

881-
// Skip test on in-memory database since CASCADE DELETE is not implemented
882-
if !dbtestutil.WillUsePostgres() {
883-
t.Skip("CASCADE DELETE behavior requires PostgreSQL")
884-
}
885-
886881
log := testutil.Logger(t)
887882
ctx := testutil.Context(t, testutil.WaitShort)
888883
clock := quartz.NewMock(t)
@@ -931,11 +926,11 @@ func TestSubAgentAPI(t *testing.T) {
931926
_, err = api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), subAgentID) //nolint:gocritic // this is a test.
932927
require.ErrorIs(t, err, sql.ErrNoRows)
933928

934-
// And: The apps are also deleted (due to CASCADE DELETE)
935-
// Use raw database since authorization layer requires agent to exist
929+
// And: The apps are *retained* to avoid causing issues
930+
// where the resources are expected to be present.
936931
appsAfterDeletion, err := db.GetWorkspaceAppsByAgentID(ctx, subAgentID)
937932
require.NoError(t, err)
938-
require.Empty(t, appsAfterDeletion)
933+
require.NotEmpty(t, appsAfterDeletion)
939934
})
940935
})
941936

0 commit comments

Comments
 (0)