mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat(agent/agentcontainers): support displayApps from devcontainer config (#18342)
Updates the agent injection routine to read the dev container's configuration so we can add display apps to the sub agent.
This commit is contained in:
20
agent/agentcontainers/acmock/acmock.go
generated
20
agent/agentcontainers/acmock/acmock.go
generated
@ -149,6 +149,26 @@ func (mr *MockDevcontainerCLIMockRecorder) Exec(ctx, workspaceFolder, configPath
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDevcontainerCLI)(nil).Exec), varargs...)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDevcontainerCLI)(nil).Exec), varargs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadConfig mocks base method.
|
||||||
|
func (m *MockDevcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []any{ctx, workspaceFolder, configPath}
|
||||||
|
for _, a := range opts {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "ReadConfig", varargs...)
|
||||||
|
ret0, _ := ret[0].(agentcontainers.DevcontainerConfig)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig indicates an expected call of ReadConfig.
|
||||||
|
func (mr *MockDevcontainerCLIMockRecorder) ReadConfig(ctx, workspaceFolder, configPath any, opts ...any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]any{ctx, workspaceFolder, configPath}, opts...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockDevcontainerCLI)(nil).ReadConfig), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
// Up mocks base method.
|
// Up mocks base method.
|
||||||
func (m *MockDevcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
|
func (m *MockDevcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -1099,6 +1099,17 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
|
|||||||
directory = DevcontainerDefaultContainerWorkspaceFolder
|
directory = DevcontainerDefaultContainerWorkspaceFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var displayApps []codersdk.DisplayApp
|
||||||
|
|
||||||
|
if config, err := api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath); err != nil {
|
||||||
|
api.logger.Error(ctx, "unable to read devcontainer config", slog.Error(err))
|
||||||
|
} else {
|
||||||
|
coderCustomization := config.MergedConfiguration.Customizations.Coder
|
||||||
|
if coderCustomization != nil {
|
||||||
|
displayApps = coderCustomization.DisplayApps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The preparation of the subagent is done, now we can create the
|
// The preparation of the subagent is done, now we can create the
|
||||||
// subagent record in the database to receive the auth token.
|
// subagent record in the database to receive the auth token.
|
||||||
createdAgent, err := api.subAgentClient.Create(ctx, SubAgent{
|
createdAgent, err := api.subAgentClient.Create(ctx, SubAgent{
|
||||||
@ -1106,6 +1117,7 @@ func (api *API) injectSubAgentIntoContainerLocked(ctx context.Context, dc coders
|
|||||||
Directory: directory,
|
Directory: directory,
|
||||||
OperatingSystem: "linux", // Assuming Linux for dev containers.
|
OperatingSystem: "linux", // Assuming Linux for dev containers.
|
||||||
Architecture: arch,
|
Architecture: arch,
|
||||||
|
DisplayApps: displayApps,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create agent: %w", err)
|
return xerrors.Errorf("create agent: %w", err)
|
||||||
|
@ -60,11 +60,14 @@ func (f *fakeContainerCLI) ExecAs(ctx context.Context, name, user string, args .
|
|||||||
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
|
// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI
|
||||||
// interface for testing.
|
// interface for testing.
|
||||||
type fakeDevcontainerCLI struct {
|
type fakeDevcontainerCLI struct {
|
||||||
upID string
|
upID string
|
||||||
upErr error
|
upErr error
|
||||||
upErrC chan error // If set, send to return err, close to return upErr.
|
upErrC chan error // If set, send to return err, close to return upErr.
|
||||||
execErr error
|
execErr error
|
||||||
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
|
execErrC chan func(cmd string, args ...string) error // If set, send fn to return err, nil or close to return execErr.
|
||||||
|
readConfig agentcontainers.DevcontainerConfig
|
||||||
|
readConfigErr error
|
||||||
|
readConfigErrC chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeDevcontainerCLI) Up(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
|
func (f *fakeDevcontainerCLI) Up(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
|
||||||
@ -95,6 +98,20 @@ func (f *fakeDevcontainerCLI) Exec(ctx context.Context, _, _ string, cmd string,
|
|||||||
return f.execErr
|
return f.execErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeDevcontainerCLI) ReadConfig(ctx context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
|
||||||
|
if f.readConfigErrC != nil {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return agentcontainers.DevcontainerConfig{}, ctx.Err()
|
||||||
|
case err, ok := <-f.readConfigErrC:
|
||||||
|
if ok {
|
||||||
|
return f.readConfig, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.readConfig, f.readConfigErr
|
||||||
|
}
|
||||||
|
|
||||||
// fakeWatcher implements the watcher.Watcher interface for testing.
|
// fakeWatcher implements the watcher.Watcher interface for testing.
|
||||||
// It allows controlling what events are sent and when.
|
// It allows controlling what events are sent and when.
|
||||||
type fakeWatcher struct {
|
type fakeWatcher struct {
|
||||||
@ -1132,10 +1149,12 @@ func TestAPI(t *testing.T) {
|
|||||||
Containers: []codersdk.WorkspaceAgentContainer{container},
|
Containers: []codersdk.WorkspaceAgentContainer{container},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
fDCCLI := &fakeDevcontainerCLI{}
|
||||||
|
|
||||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
|
||||||
api := agentcontainers.NewAPI(
|
api := agentcontainers.NewAPI(
|
||||||
logger,
|
logger,
|
||||||
|
agentcontainers.WithDevcontainerCLI(fDCCLI),
|
||||||
agentcontainers.WithContainerCLI(fLister),
|
agentcontainers.WithContainerCLI(fLister),
|
||||||
agentcontainers.WithWatcher(fWatcher),
|
agentcontainers.WithWatcher(fWatcher),
|
||||||
agentcontainers.WithClock(mClock),
|
agentcontainers.WithClock(mClock),
|
||||||
@ -1421,6 +1440,130 @@ func TestAPI(t *testing.T) {
|
|||||||
assert.Contains(t, fakeSAC.deleted, existingAgentID)
|
assert.Contains(t, fakeSAC.deleted, existingAgentID)
|
||||||
assert.Empty(t, fakeSAC.agents)
|
assert.Empty(t, fakeSAC.agents)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Create", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
customization *agentcontainers.CoderCustomization
|
||||||
|
afterCreate func(t *testing.T, subAgent agentcontainers.SubAgent)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "WithoutCustomization",
|
||||||
|
customization: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithDisplayApps",
|
||||||
|
customization: &agentcontainers.CoderCustomization{
|
||||||
|
DisplayApps: []codersdk.DisplayApp{
|
||||||
|
codersdk.DisplayAppSSH,
|
||||||
|
codersdk.DisplayAppWebTerminal,
|
||||||
|
codersdk.DisplayAppVSCodeInsiders,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterCreate: func(t *testing.T, subAgent agentcontainers.SubAgent) {
|
||||||
|
require.Len(t, subAgent.DisplayApps, 3)
|
||||||
|
assert.Equal(t, codersdk.DisplayAppSSH, subAgent.DisplayApps[0])
|
||||||
|
assert.Equal(t, codersdk.DisplayAppWebTerminal, subAgent.DisplayApps[1])
|
||||||
|
assert.Equal(t, codersdk.DisplayAppVSCodeInsiders, subAgent.DisplayApps[2])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = testutil.Context(t, testutil.WaitMedium)
|
||||||
|
logger = testutil.Logger(t)
|
||||||
|
mClock = quartz.NewMock(t)
|
||||||
|
mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t))
|
||||||
|
fSAC = &fakeSubAgentClient{createErrC: make(chan error, 1)}
|
||||||
|
fDCCLI = &fakeDevcontainerCLI{
|
||||||
|
readConfig: agentcontainers.DevcontainerConfig{
|
||||||
|
MergedConfiguration: agentcontainers.DevcontainerConfiguration{
|
||||||
|
Customizations: agentcontainers.DevcontainerCustomizations{
|
||||||
|
Coder: tt.customization,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execErrC: make(chan func(cmd string, args ...string) error, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
testContainer = codersdk.WorkspaceAgentContainer{
|
||||||
|
ID: "test-container-id",
|
||||||
|
FriendlyName: "test-container",
|
||||||
|
Image: "test-image",
|
||||||
|
Running: true,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Labels: map[string]string{
|
||||||
|
agentcontainers.DevcontainerLocalFolderLabel: "/workspaces",
|
||||||
|
agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
coderBin, err := os.Executable()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Mock the `List` function to always return out test container.
|
||||||
|
mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{
|
||||||
|
Containers: []codersdk.WorkspaceAgentContainer{testContainer},
|
||||||
|
}, nil).AnyTimes()
|
||||||
|
|
||||||
|
// Mock the steps used for injecting the coder agent.
|
||||||
|
gomock.InOrder(
|
||||||
|
mCCLI.EXPECT().DetectArchitecture(gomock.Any(), testContainer.ID).Return(runtime.GOARCH, nil),
|
||||||
|
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil),
|
||||||
|
mCCLI.EXPECT().Copy(gomock.Any(), testContainer.ID, coderBin, "/.coder-agent/coder").Return(nil),
|
||||||
|
mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil),
|
||||||
|
)
|
||||||
|
|
||||||
|
mClock.Set(time.Now()).MustWait(ctx)
|
||||||
|
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
|
||||||
|
|
||||||
|
api := agentcontainers.NewAPI(logger,
|
||||||
|
agentcontainers.WithClock(mClock),
|
||||||
|
agentcontainers.WithContainerCLI(mCCLI),
|
||||||
|
agentcontainers.WithDevcontainerCLI(fDCCLI),
|
||||||
|
agentcontainers.WithSubAgentClient(fSAC),
|
||||||
|
agentcontainers.WithSubAgentURL("test-subagent-url"),
|
||||||
|
agentcontainers.WithWatcher(watcher.NewNoop()),
|
||||||
|
)
|
||||||
|
defer api.Close()
|
||||||
|
|
||||||
|
// Close before api.Close() defer to avoid deadlock after test.
|
||||||
|
defer close(fSAC.createErrC)
|
||||||
|
defer close(fDCCLI.execErrC)
|
||||||
|
|
||||||
|
// Given: We allow agent creation and injection to succeed.
|
||||||
|
testutil.RequireSend(ctx, t, fSAC.createErrC, nil)
|
||||||
|
testutil.RequireSend(ctx, t, fDCCLI.execErrC, func(cmd string, args ...string) error {
|
||||||
|
assert.Equal(t, "pwd", cmd)
|
||||||
|
assert.Empty(t, args)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait until the ticker has been registered.
|
||||||
|
tickerTrap.MustWait(ctx).MustRelease(ctx)
|
||||||
|
tickerTrap.Close()
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustFindDevcontainerByPath returns the devcontainer with the given workspace
|
// mustFindDevcontainerByPath returns the devcontainer with the given workspace
|
||||||
|
@ -12,12 +12,33 @@ import (
|
|||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"github.com/coder/coder/v2/agent/agentexec"
|
"github.com/coder/coder/v2/agent/agentexec"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DevcontainerConfig is a wrapper around the output from `read-configuration`.
|
||||||
|
// Unfortunately we cannot make use of `dcspec` as the output doesn't appear to
|
||||||
|
// match.
|
||||||
|
type DevcontainerConfig struct {
|
||||||
|
MergedConfiguration DevcontainerConfiguration `json:"mergedConfiguration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DevcontainerConfiguration struct {
|
||||||
|
Customizations DevcontainerCustomizations `json:"customizations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DevcontainerCustomizations struct {
|
||||||
|
Coder *CoderCustomization `json:"coder,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoderCustomization struct {
|
||||||
|
DisplayApps []codersdk.DisplayApp `json:"displayApps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// DevcontainerCLI is an interface for the devcontainer CLI.
|
// DevcontainerCLI is an interface for the devcontainer CLI.
|
||||||
type DevcontainerCLI interface {
|
type DevcontainerCLI interface {
|
||||||
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
|
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
|
||||||
Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error
|
Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error
|
||||||
|
ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
|
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
|
||||||
@ -83,6 +104,24 @@ func WithRemoteEnv(env ...string) DevcontainerCLIExecOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevcontainerCLIExecOptions are options for the devcontainer CLI ReadConfig
|
||||||
|
// command.
|
||||||
|
type DevcontainerCLIReadConfigOptions func(*devcontainerCLIReadConfigConfig)
|
||||||
|
|
||||||
|
type devcontainerCLIReadConfigConfig struct {
|
||||||
|
stdout io.Writer
|
||||||
|
stderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExecOutput sets additional stdout and stderr writers for logs
|
||||||
|
// during Exec operations.
|
||||||
|
func WithReadConfigOutput(stdout, stderr io.Writer) DevcontainerCLIReadConfigOptions {
|
||||||
|
return func(o *devcontainerCLIReadConfigConfig) {
|
||||||
|
o.stdout = stdout
|
||||||
|
o.stderr = stderr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig {
|
func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig {
|
||||||
conf := devcontainerCLIUpConfig{}
|
conf := devcontainerCLIUpConfig{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@ -103,6 +142,16 @@ func applyDevcontainerCLIExecOptions(opts []DevcontainerCLIExecOptions) devconta
|
|||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyDevcontainerCLIReadConfigOptions(opts []DevcontainerCLIReadConfigOptions) devcontainerCLIReadConfigConfig {
|
||||||
|
conf := devcontainerCLIReadConfigConfig{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(&conf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
type devcontainerCLI struct {
|
type devcontainerCLI struct {
|
||||||
logger slog.Logger
|
logger slog.Logger
|
||||||
execer agentexec.Execer
|
execer agentexec.Execer
|
||||||
@ -147,13 +196,14 @@ func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath st
|
|||||||
cmd.Stderr = io.MultiWriter(stderrWriters...)
|
cmd.Stderr = io.MultiWriter(stderrWriters...)
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
if _, err2 := parseDevcontainerCLILastLine(ctx, logger, stdoutBuf.Bytes()); err2 != nil {
|
_, err2 := parseDevcontainerCLILastLine[devcontainerCLIResult](ctx, logger, stdoutBuf.Bytes())
|
||||||
|
if err2 != nil {
|
||||||
err = errors.Join(err, err2)
|
err = errors.Join(err, err2)
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := parseDevcontainerCLILastLine(ctx, logger, stdoutBuf.Bytes())
|
result, err := parseDevcontainerCLILastLine[devcontainerCLIResult](ctx, logger, stdoutBuf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -200,9 +250,49 @@ func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error) {
|
||||||
|
conf := applyDevcontainerCLIReadConfigOptions(opts)
|
||||||
|
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
|
||||||
|
|
||||||
|
args := []string{"read-configuration", "--include-merged-configuration"}
|
||||||
|
if workspaceFolder != "" {
|
||||||
|
args = append(args, "--workspace-folder", workspaceFolder)
|
||||||
|
}
|
||||||
|
if configPath != "" {
|
||||||
|
args = append(args, "--config", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := d.execer.CommandContext(ctx, "devcontainer", args...)
|
||||||
|
|
||||||
|
var stdoutBuf bytes.Buffer
|
||||||
|
stdoutWriters := []io.Writer{&stdoutBuf, &devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stdout", true))}}
|
||||||
|
if conf.stdout != nil {
|
||||||
|
stdoutWriters = append(stdoutWriters, conf.stdout)
|
||||||
|
}
|
||||||
|
c.Stdout = io.MultiWriter(stdoutWriters...)
|
||||||
|
stderrWriters := []io.Writer{&devcontainerCLILogWriter{ctx: ctx, logger: logger.With(slog.F("stderr", true))}}
|
||||||
|
if conf.stderr != nil {
|
||||||
|
stderrWriters = append(stderrWriters, conf.stderr)
|
||||||
|
}
|
||||||
|
c.Stderr = io.MultiWriter(stderrWriters...)
|
||||||
|
|
||||||
|
if err := c.Run(); err != nil {
|
||||||
|
return DevcontainerConfig{}, xerrors.Errorf("devcontainer read-configuration failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := parseDevcontainerCLILastLine[DevcontainerConfig](ctx, logger, stdoutBuf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return DevcontainerConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
|
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
|
||||||
// which is a JSON object.
|
// which is a JSON object.
|
||||||
func parseDevcontainerCLILastLine(ctx context.Context, logger slog.Logger, p []byte) (result devcontainerCLIResult, err error) {
|
func parseDevcontainerCLILastLine[T any](ctx context.Context, logger slog.Logger, p []byte) (T, error) {
|
||||||
|
var result T
|
||||||
|
|
||||||
s := bufio.NewScanner(bytes.NewReader(p))
|
s := bufio.NewScanner(bytes.NewReader(p))
|
||||||
var lastLine []byte
|
var lastLine []byte
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
@ -212,19 +302,19 @@ func parseDevcontainerCLILastLine(ctx context.Context, logger slog.Logger, p []b
|
|||||||
}
|
}
|
||||||
lastLine = b
|
lastLine = b
|
||||||
}
|
}
|
||||||
if err = s.Err(); err != nil {
|
if err := s.Err(); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if len(lastLine) == 0 || lastLine[0] != '{' {
|
if len(lastLine) == 0 || lastLine[0] != '{' {
|
||||||
logger.Error(ctx, "devcontainer result is not json", slog.F("result", string(lastLine)))
|
logger.Error(ctx, "devcontainer result is not json", slog.F("result", string(lastLine)))
|
||||||
return result, xerrors.Errorf("devcontainer result is not json: %q", string(lastLine))
|
return result, xerrors.Errorf("devcontainer result is not json: %q", string(lastLine))
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(lastLine, &result); err != nil {
|
if err := json.Unmarshal(lastLine, &result); err != nil {
|
||||||
logger.Error(ctx, "parse devcontainer result failed", slog.Error(err), slog.F("result", string(lastLine)))
|
logger.Error(ctx, "parse devcontainer result failed", slog.Error(err), slog.F("result", string(lastLine)))
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, result.Err()
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// devcontainerCLIResult is the result of the devcontainer CLI command.
|
// devcontainerCLIResult is the result of the devcontainer CLI command.
|
||||||
@ -243,6 +333,18 @@ type devcontainerCLIResult struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *devcontainerCLIResult) UnmarshalJSON(data []byte) error {
|
||||||
|
type wrapperResult devcontainerCLIResult
|
||||||
|
|
||||||
|
var wrappedResult wrapperResult
|
||||||
|
if err := json.Unmarshal(data, &wrappedResult); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*r = devcontainerCLIResult(wrappedResult)
|
||||||
|
return r.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (r devcontainerCLIResult) Err() error {
|
func (r devcontainerCLIResult) Err() error {
|
||||||
if r.Outcome == "success" {
|
if r.Outcome == "success" {
|
||||||
return nil
|
return nil
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"cdr.dev/slog/sloggers/slogtest"
|
"cdr.dev/slog/sloggers/slogtest"
|
||||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||||
"github.com/coder/coder/v2/agent/agentexec"
|
"github.com/coder/coder/v2/agent/agentexec"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/pty"
|
"github.com/coder/coder/v2/pty"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
@ -233,6 +234,91 @@ func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ReadConfig", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
logFile string
|
||||||
|
workspaceFolder string
|
||||||
|
configPath string
|
||||||
|
opts []agentcontainers.DevcontainerCLIReadConfigOptions
|
||||||
|
wantArgs string
|
||||||
|
wantError bool
|
||||||
|
wantConfig agentcontainers.DevcontainerConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "WithCoderCustomization",
|
||||||
|
logFile: "read-config-with-coder-customization.log",
|
||||||
|
workspaceFolder: "/test/workspace",
|
||||||
|
configPath: "",
|
||||||
|
wantArgs: "read-configuration --include-merged-configuration --workspace-folder /test/workspace",
|
||||||
|
wantError: false,
|
||||||
|
wantConfig: agentcontainers.DevcontainerConfig{
|
||||||
|
MergedConfiguration: agentcontainers.DevcontainerConfiguration{
|
||||||
|
Customizations: agentcontainers.DevcontainerCustomizations{
|
||||||
|
Coder: &agentcontainers.CoderCustomization{
|
||||||
|
DisplayApps: []codersdk.DisplayApp{
|
||||||
|
codersdk.DisplayAppVSCodeDesktop,
|
||||||
|
codersdk.DisplayAppWebTerminal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WithoutCoderCustomization",
|
||||||
|
logFile: "read-config-without-coder-customization.log",
|
||||||
|
workspaceFolder: "/test/workspace",
|
||||||
|
configPath: "/test/config.json",
|
||||||
|
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{
|
||||||
|
Coder: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FileNotFound",
|
||||||
|
logFile: "read-config-error-not-found.log",
|
||||||
|
workspaceFolder: "/nonexistent/workspace",
|
||||||
|
configPath: "",
|
||||||
|
wantArgs: "read-configuration --include-merged-configuration --workspace-folder /nonexistent/workspace",
|
||||||
|
wantError: true,
|
||||||
|
wantConfig: agentcontainers.DevcontainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
|
testExecer := &testDevcontainerExecer{
|
||||||
|
testExePath: testExePath,
|
||||||
|
wantArgs: tt.wantArgs,
|
||||||
|
wantError: tt.wantError,
|
||||||
|
logFile: filepath.Join("testdata", "devcontainercli", "readconfig", tt.logFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||||
|
config, err := dccli.ReadConfig(ctx, tt.workspaceFolder, tt.configPath, tt.opts...)
|
||||||
|
if tt.wantError {
|
||||||
|
assert.Error(t, err, "want error")
|
||||||
|
assert.Equal(t, agentcontainers.DevcontainerConfig{}, config, "expected empty config on error")
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, "want no error")
|
||||||
|
assert.Equal(t, tt.wantConfig, config, "expected config to match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDevcontainerCLI_WithOutput tests that WithUpOutput and WithExecOutput capture CLI
|
// TestDevcontainerCLI_WithOutput tests that WithUpOutput and WithExecOutput capture CLI
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
|
||||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SubAgent represents an agent running in a dev container.
|
// SubAgent represents an agent running in a dev container.
|
||||||
@ -19,6 +20,7 @@ type SubAgent struct {
|
|||||||
Directory string
|
Directory string
|
||||||
Architecture string
|
Architecture string
|
||||||
OperatingSystem string
|
OperatingSystem string
|
||||||
|
DisplayApps []codersdk.DisplayApp
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubAgentClient is an interface for managing sub agents and allows
|
// SubAgentClient is an interface for managing sub agents and allows
|
||||||
@ -80,11 +82,34 @@ func (a *subAgentAPIClient) List(ctx context.Context) ([]SubAgent, error) {
|
|||||||
|
|
||||||
func (a *subAgentAPIClient) Create(ctx context.Context, agent SubAgent) (SubAgent, error) {
|
func (a *subAgentAPIClient) Create(ctx context.Context, agent SubAgent) (SubAgent, error) {
|
||||||
a.logger.Debug(ctx, "creating sub agent", slog.F("name", agent.Name), slog.F("directory", agent.Directory))
|
a.logger.Debug(ctx, "creating sub agent", slog.F("name", agent.Name), slog.F("directory", agent.Directory))
|
||||||
|
|
||||||
|
displayApps := make([]agentproto.CreateSubAgentRequest_DisplayApp, 0, len(agent.DisplayApps))
|
||||||
|
for _, displayApp := range agent.DisplayApps {
|
||||||
|
var app agentproto.CreateSubAgentRequest_DisplayApp
|
||||||
|
switch displayApp {
|
||||||
|
case codersdk.DisplayAppPortForward:
|
||||||
|
app = agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER
|
||||||
|
case codersdk.DisplayAppSSH:
|
||||||
|
app = agentproto.CreateSubAgentRequest_SSH_HELPER
|
||||||
|
case codersdk.DisplayAppVSCodeDesktop:
|
||||||
|
app = agentproto.CreateSubAgentRequest_VSCODE
|
||||||
|
case codersdk.DisplayAppVSCodeInsiders:
|
||||||
|
app = agentproto.CreateSubAgentRequest_VSCODE_INSIDERS
|
||||||
|
case codersdk.DisplayAppWebTerminal:
|
||||||
|
app = agentproto.CreateSubAgentRequest_WEB_TERMINAL
|
||||||
|
default:
|
||||||
|
return SubAgent{}, xerrors.Errorf("unexpected codersdk.DisplayApp: %#v", displayApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
displayApps = append(displayApps, app)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := a.api.CreateSubAgent(ctx, &agentproto.CreateSubAgentRequest{
|
resp, err := a.api.CreateSubAgent(ctx, &agentproto.CreateSubAgentRequest{
|
||||||
Name: agent.Name,
|
Name: agent.Name,
|
||||||
Directory: agent.Directory,
|
Directory: agent.Directory,
|
||||||
Architecture: agent.Architecture,
|
Architecture: agent.Architecture,
|
||||||
OperatingSystem: agent.OperatingSystem,
|
OperatingSystem: agent.OperatingSystem,
|
||||||
|
DisplayApps: displayApps,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SubAgent{}, err
|
return SubAgent{}, err
|
||||||
|
105
agent/agentcontainers/subagent_test.go
Normal file
105
agent/agentcontainers/subagent_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package agentcontainers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||||
|
"github.com/coder/coder/v2/agent/agenttest"
|
||||||
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
|
"github.com/coder/coder/v2/tailnet"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubAgentClient_CreateWithDisplayApps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("CreateWithDisplayApps", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
displayApps []codersdk.DisplayApp
|
||||||
|
expectedApps []agentproto.CreateSubAgentRequest_DisplayApp
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single display app",
|
||||||
|
displayApps: []codersdk.DisplayApp{codersdk.DisplayAppVSCodeDesktop},
|
||||||
|
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||||
|
agentproto.CreateSubAgentRequest_VSCODE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple display apps",
|
||||||
|
displayApps: []codersdk.DisplayApp{
|
||||||
|
codersdk.DisplayAppVSCodeDesktop,
|
||||||
|
codersdk.DisplayAppSSH,
|
||||||
|
codersdk.DisplayAppPortForward,
|
||||||
|
},
|
||||||
|
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||||
|
agentproto.CreateSubAgentRequest_VSCODE,
|
||||||
|
agentproto.CreateSubAgentRequest_SSH_HELPER,
|
||||||
|
agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all display apps",
|
||||||
|
displayApps: []codersdk.DisplayApp{
|
||||||
|
codersdk.DisplayAppPortForward,
|
||||||
|
codersdk.DisplayAppSSH,
|
||||||
|
codersdk.DisplayAppVSCodeDesktop,
|
||||||
|
codersdk.DisplayAppVSCodeInsiders,
|
||||||
|
codersdk.DisplayAppWebTerminal,
|
||||||
|
},
|
||||||
|
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||||
|
agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
|
||||||
|
agentproto.CreateSubAgentRequest_SSH_HELPER,
|
||||||
|
agentproto.CreateSubAgentRequest_VSCODE,
|
||||||
|
agentproto.CreateSubAgentRequest_VSCODE_INSIDERS,
|
||||||
|
agentproto.CreateSubAgentRequest_WEB_TERMINAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no display apps",
|
||||||
|
displayApps: []codersdk.DisplayApp{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
logger := testutil.Logger(t)
|
||||||
|
statsCh := make(chan *agentproto.Stats)
|
||||||
|
|
||||||
|
agentAPI := agenttest.NewClient(t, logger, uuid.New(), agentsdk.Manifest{}, statsCh, tailnet.NewCoordinator(logger))
|
||||||
|
|
||||||
|
agentClient, _, err := agentAPI.ConnectRPC26(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
subAgentClient := agentcontainers.NewSubAgentClientFromAPI(logger, agentClient)
|
||||||
|
|
||||||
|
// When: We create a sub agent with display apps.
|
||||||
|
subAgent, err := subAgentClient.Create(ctx, agentcontainers.SubAgent{
|
||||||
|
Name: "sub-agent-" + tt.name,
|
||||||
|
Directory: "/workspaces/coder",
|
||||||
|
Architecture: "amd64",
|
||||||
|
OperatingSystem: "linux",
|
||||||
|
DisplayApps: tt.displayApps,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
displayApps, err := agentAPI.GetSubAgentDisplayApps(subAgent.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Then: We expect the apps to be created.
|
||||||
|
require.Equal(t, tt.expectedApps, displayApps)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
2
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-error-not-found.log
generated
vendored
Normal file
2
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-error-not-found.log
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{"type":"text","level":3,"timestamp":1749557935646,"text":"@devcontainers/cli 0.75.0. Node.js v20.16.0. linux 6.8.0-60-generic x64."}
|
||||||
|
{"type":"text","level":2,"timestamp":1749557935646,"text":"Error: Dev container config (/home/coder/.devcontainer/devcontainer.json) not found.\n at v7 (/usr/local/nvm/versions/node/v20.16.0/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:668:6918)\n at async /usr/local/nvm/versions/node/v20.16.0/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:484:1188"}
|
8
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-with-coder-customization.log
generated
vendored
Normal file
8
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-with-coder-customization.log
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{"type":"text","level":3,"timestamp":1749557820014,"text":"@devcontainers/cli 0.75.0. Node.js v20.16.0. linux 6.8.0-60-generic x64."}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820014,"text":"Run: git rev-parse --show-cdup"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820023,"text":"Run: git rev-parse --show-cdup","startTimestamp":1749557820014}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820023,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder --filter label=devcontainer.config_file=/home/coder/coder/.devcontainer/devcontainer.json"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820039,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder --filter label=devcontainer.config_file=/home/coder/coder/.devcontainer/devcontainer.json","startTimestamp":1749557820023}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820039,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820054,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder","startTimestamp":1749557820039}
|
||||||
|
{"mergedConfiguration":{"customizations":{"coder":{"displayApps":["vscode", "web_terminal"]}}}}
|
8
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-without-coder-customization.log
generated
vendored
Normal file
8
agent/agentcontainers/testdata/devcontainercli/readconfig/read-config-without-coder-customization.log
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{"type":"text","level":3,"timestamp":1749557820014,"text":"@devcontainers/cli 0.75.0. Node.js v20.16.0. linux 6.8.0-60-generic x64."}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820014,"text":"Run: git rev-parse --show-cdup"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820023,"text":"Run: git rev-parse --show-cdup","startTimestamp":1749557820014}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820023,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder --filter label=devcontainer.config_file=/home/coder/coder/.devcontainer/devcontainer.json"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820039,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder --filter label=devcontainer.config_file=/home/coder/coder/.devcontainer/devcontainer.json","startTimestamp":1749557820023}
|
||||||
|
{"type":"start","level":2,"timestamp":1749557820039,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder"}
|
||||||
|
{"type":"stop","level":2,"timestamp":1749557820054,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/home/coder/coder","startTimestamp":1749557820039}
|
||||||
|
{"mergedConfiguration":{"customizations":{}}}
|
@ -171,22 +171,27 @@ func (c *Client) GetSubAgentDirectory(id uuid.UUID) (string, error) {
|
|||||||
return c.fakeAgentAPI.GetSubAgentDirectory(id)
|
return c.fakeAgentAPI.GetSubAgentDirectory(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetSubAgentDisplayApps(id uuid.UUID) ([]agentproto.CreateSubAgentRequest_DisplayApp, error) {
|
||||||
|
return c.fakeAgentAPI.GetSubAgentDisplayApps(id)
|
||||||
|
}
|
||||||
|
|
||||||
type FakeAgentAPI struct {
|
type FakeAgentAPI struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
t testing.TB
|
t testing.TB
|
||||||
logger slog.Logger
|
logger slog.Logger
|
||||||
|
|
||||||
manifest *agentproto.Manifest
|
manifest *agentproto.Manifest
|
||||||
startupCh chan *agentproto.Startup
|
startupCh chan *agentproto.Startup
|
||||||
statsCh chan *agentproto.Stats
|
statsCh chan *agentproto.Stats
|
||||||
appHealthCh chan *agentproto.BatchUpdateAppHealthRequest
|
appHealthCh chan *agentproto.BatchUpdateAppHealthRequest
|
||||||
logsCh chan<- *agentproto.BatchCreateLogsRequest
|
logsCh chan<- *agentproto.BatchCreateLogsRequest
|
||||||
lifecycleStates []codersdk.WorkspaceAgentLifecycle
|
lifecycleStates []codersdk.WorkspaceAgentLifecycle
|
||||||
metadata map[string]agentsdk.Metadata
|
metadata map[string]agentsdk.Metadata
|
||||||
timings []*agentproto.Timing
|
timings []*agentproto.Timing
|
||||||
connectionReports []*agentproto.ReportConnectionRequest
|
connectionReports []*agentproto.ReportConnectionRequest
|
||||||
subAgents map[uuid.UUID]*agentproto.SubAgent
|
subAgents map[uuid.UUID]*agentproto.SubAgent
|
||||||
subAgentDirs map[uuid.UUID]string
|
subAgentDirs map[uuid.UUID]string
|
||||||
|
subAgentDisplayApps map[uuid.UUID][]agentproto.CreateSubAgentRequest_DisplayApp
|
||||||
|
|
||||||
getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error)
|
getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error)
|
||||||
getResourcesMonitoringConfigurationFunc func() (*agentproto.GetResourcesMonitoringConfigurationResponse, error)
|
getResourcesMonitoringConfigurationFunc func() (*agentproto.GetResourcesMonitoringConfigurationResponse, error)
|
||||||
@ -401,6 +406,10 @@ func (f *FakeAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Creat
|
|||||||
f.subAgentDirs = make(map[uuid.UUID]string)
|
f.subAgentDirs = make(map[uuid.UUID]string)
|
||||||
}
|
}
|
||||||
f.subAgentDirs[subAgentID] = req.GetDirectory()
|
f.subAgentDirs[subAgentID] = req.GetDirectory()
|
||||||
|
if f.subAgentDisplayApps == nil {
|
||||||
|
f.subAgentDisplayApps = make(map[uuid.UUID][]agentproto.CreateSubAgentRequest_DisplayApp)
|
||||||
|
}
|
||||||
|
f.subAgentDisplayApps[subAgentID] = req.GetDisplayApps()
|
||||||
|
|
||||||
// For a fake implementation, we don't create workspace apps.
|
// For a fake implementation, we don't create workspace apps.
|
||||||
// Real implementations would handle req.Apps here.
|
// Real implementations would handle req.Apps here.
|
||||||
@ -477,6 +486,22 @@ func (f *FakeAgentAPI) GetSubAgentDirectory(id uuid.UUID) (string, error) {
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeAgentAPI) GetSubAgentDisplayApps(id uuid.UUID) ([]agentproto.CreateSubAgentRequest_DisplayApp, error) {
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
if f.subAgentDisplayApps == nil {
|
||||||
|
return nil, xerrors.New("no sub-agent display apps available")
|
||||||
|
}
|
||||||
|
|
||||||
|
displayApps, ok := f.subAgentDisplayApps[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, xerrors.New("sub-agent display apps not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayApps, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI {
|
func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI {
|
||||||
return &FakeAgentAPI{
|
return &FakeAgentAPI{
|
||||||
t: t,
|
t: t,
|
||||||
|
Reference in New Issue
Block a user