mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
Updates the agent injection routine to read the dev container's configuration so we can add display apps to the sub agent.
632 lines
21 KiB
Go
632 lines
21 KiB
Go
package agentcontainers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/ory/dockertest/v3/docker"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/agent/agentcontainers"
|
|
"github.com/coder/coder/v2/agent/agentexec"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/pty"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testExePath, err := os.Executable()
|
|
require.NoError(t, err, "get test executable path")
|
|
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
|
|
t.Run("Up", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
logFile string
|
|
workspace string
|
|
config string
|
|
opts []agentcontainers.DevcontainerCLIUpOptions
|
|
wantArgs string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "success",
|
|
logFile: "up.log",
|
|
workspace: "/test/workspace",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "success with config",
|
|
logFile: "up.log",
|
|
workspace: "/test/workspace",
|
|
config: "/test/config.json",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace --config /test/config.json",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "already exists",
|
|
logFile: "up-already-exists.log",
|
|
workspace: "/test/workspace",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "docker error",
|
|
logFile: "up-error-docker.log",
|
|
workspace: "/test/workspace",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "bad outcome",
|
|
logFile: "up-error-bad-outcome.log",
|
|
workspace: "/test/workspace",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "does not exist",
|
|
logFile: "up-error-does-not-exist.log",
|
|
workspace: "/test/workspace",
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "with remove existing container",
|
|
logFile: "up.log",
|
|
workspace: "/test/workspace",
|
|
opts: []agentcontainers.DevcontainerCLIUpOptions{
|
|
agentcontainers.WithRemoveExistingContainer(),
|
|
},
|
|
wantArgs: "up --log-format json --workspace-folder /test/workspace --remove-existing-container",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
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", "parse", tt.logFile),
|
|
}
|
|
|
|
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
|
containerID, err := dccli.Up(ctx, tt.workspace, tt.config, tt.opts...)
|
|
if tt.wantError {
|
|
assert.Error(t, err, "want error")
|
|
assert.Empty(t, containerID, "expected empty container ID")
|
|
} else {
|
|
assert.NoError(t, err, "want no error")
|
|
assert.NotEmpty(t, containerID, "expected non-empty container ID")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Exec", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
workspaceFolder string
|
|
configPath string
|
|
cmd string
|
|
cmdArgs []string
|
|
opts []agentcontainers.DevcontainerCLIExecOptions
|
|
wantArgs string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "simple command",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "",
|
|
cmd: "echo",
|
|
cmdArgs: []string{"hello"},
|
|
wantArgs: "exec --workspace-folder /test/workspace echo hello",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "command with multiple args",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "/test/config.json",
|
|
cmd: "ls",
|
|
cmdArgs: []string{"-la", "/workspace"},
|
|
wantArgs: "exec --workspace-folder /test/workspace --config /test/config.json ls -la /workspace",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "empty command args",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "",
|
|
cmd: "bash",
|
|
cmdArgs: nil,
|
|
wantArgs: "exec --workspace-folder /test/workspace bash",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "workspace not found",
|
|
workspaceFolder: "/nonexistent/workspace",
|
|
configPath: "",
|
|
cmd: "echo",
|
|
cmdArgs: []string{"test"},
|
|
wantArgs: "exec --workspace-folder /nonexistent/workspace echo test",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "with container ID",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "",
|
|
cmd: "echo",
|
|
cmdArgs: []string{"hello"},
|
|
opts: []agentcontainers.DevcontainerCLIExecOptions{agentcontainers.WithExecContainerID("test-container-123")},
|
|
wantArgs: "exec --workspace-folder /test/workspace --container-id test-container-123 echo hello",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "with container ID and config",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "/test/config.json",
|
|
cmd: "bash",
|
|
cmdArgs: []string{"-c", "ls -la"},
|
|
opts: []agentcontainers.DevcontainerCLIExecOptions{agentcontainers.WithExecContainerID("my-container")},
|
|
wantArgs: "exec --workspace-folder /test/workspace --config /test/config.json --container-id my-container bash -c ls -la",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "with container ID and output capture",
|
|
workspaceFolder: "/test/workspace",
|
|
configPath: "",
|
|
cmd: "cat",
|
|
cmdArgs: []string{"/etc/hostname"},
|
|
opts: []agentcontainers.DevcontainerCLIExecOptions{
|
|
agentcontainers.WithExecContainerID("test-container-789"),
|
|
},
|
|
wantArgs: "exec --workspace-folder /test/workspace --container-id test-container-789 cat /etc/hostname",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
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: "", // Exec doesn't need log file parsing
|
|
}
|
|
|
|
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
|
err := dccli.Exec(ctx, tt.workspaceFolder, tt.configPath, tt.cmd, tt.cmdArgs, tt.opts...)
|
|
if tt.wantError {
|
|
assert.Error(t, err, "want error")
|
|
} else {
|
|
assert.NoError(t, err, "want no error")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
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
|
|
// logs to provided writers.
|
|
func TestDevcontainerCLI_WithOutput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Prepare test executable and logger.
|
|
testExePath, err := os.Executable()
|
|
require.NoError(t, err, "get test executable path")
|
|
|
|
t.Run("Up", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Buffers to capture stdout and stderr.
|
|
outBuf := &bytes.Buffer{}
|
|
errBuf := &bytes.Buffer{}
|
|
|
|
// Simulate CLI execution with a standard up.log file.
|
|
wantArgs := "up --log-format json --workspace-folder /test/workspace"
|
|
testExecer := &testDevcontainerExecer{
|
|
testExePath: testExePath,
|
|
wantArgs: wantArgs,
|
|
wantError: false,
|
|
logFile: filepath.Join("testdata", "devcontainercli", "parse", "up.log"),
|
|
}
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
|
|
|
// Call Up with WithUpOutput to capture CLI logs.
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
containerID, err := dccli.Up(ctx, "/test/workspace", "", agentcontainers.WithUpOutput(outBuf, errBuf))
|
|
require.NoError(t, err, "Up should succeed")
|
|
require.NotEmpty(t, containerID, "expected non-empty container ID")
|
|
|
|
// Read expected log content.
|
|
expLog, err := os.ReadFile(filepath.Join("testdata", "devcontainercli", "parse", "up.log"))
|
|
require.NoError(t, err, "reading expected log file")
|
|
|
|
// Verify stdout buffer contains the CLI logs and stderr is empty.
|
|
assert.Equal(t, string(expLog), outBuf.String(), "stdout buffer should match CLI logs")
|
|
assert.Empty(t, errBuf.String(), "stderr buffer should be empty on success")
|
|
})
|
|
|
|
t.Run("Exec", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logFile := filepath.Join(t.TempDir(), "exec.log")
|
|
f, err := os.Create(logFile)
|
|
require.NoError(t, err, "create exec log file")
|
|
_, err = f.WriteString("exec command log\n")
|
|
require.NoError(t, err, "write to exec log file")
|
|
err = f.Close()
|
|
require.NoError(t, err, "close exec log file")
|
|
|
|
// Buffers to capture stdout and stderr.
|
|
outBuf := &bytes.Buffer{}
|
|
errBuf := &bytes.Buffer{}
|
|
|
|
// Simulate CLI execution for exec command with container ID.
|
|
wantArgs := "exec --workspace-folder /test/workspace --container-id test-container-456 echo hello"
|
|
testExecer := &testDevcontainerExecer{
|
|
testExePath: testExePath,
|
|
wantArgs: wantArgs,
|
|
wantError: false,
|
|
logFile: logFile,
|
|
}
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
|
|
|
// Call Exec with WithExecOutput and WithContainerID to capture any command output.
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = dccli.Exec(ctx, "/test/workspace", "", "echo", []string{"hello"},
|
|
agentcontainers.WithExecContainerID("test-container-456"),
|
|
agentcontainers.WithExecOutput(outBuf, errBuf),
|
|
)
|
|
require.NoError(t, err, "Exec should succeed")
|
|
|
|
assert.NotEmpty(t, outBuf.String(), "stdout buffer should not be empty for exec with log file")
|
|
assert.Empty(t, errBuf.String(), "stderr buffer should be empty")
|
|
})
|
|
}
|
|
|
|
// testDevcontainerExecer implements the agentexec.Execer interface for testing.
|
|
type testDevcontainerExecer struct {
|
|
testExePath string
|
|
wantArgs string
|
|
wantError bool
|
|
logFile string
|
|
}
|
|
|
|
// CommandContext returns a test binary command that simulates devcontainer responses.
|
|
func (e *testDevcontainerExecer) CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
|
|
// Only handle "devcontainer" commands.
|
|
if name != "devcontainer" {
|
|
// For non-devcontainer commands, use a standard execer.
|
|
return agentexec.DefaultExecer.CommandContext(ctx, name, args...)
|
|
}
|
|
|
|
// Create a command that runs the test binary with special flags
|
|
// that tell it to simulate a devcontainer command.
|
|
testArgs := []string{
|
|
"-test.run=TestDevcontainerHelperProcess",
|
|
"--",
|
|
name,
|
|
}
|
|
testArgs = append(testArgs, args...)
|
|
|
|
//nolint:gosec // This is a test binary, so we don't need to worry about command injection.
|
|
cmd := exec.CommandContext(ctx, e.testExePath, testArgs...)
|
|
// Set this environment variable so the child process knows it's the helper.
|
|
cmd.Env = append(os.Environ(),
|
|
"TEST_DEVCONTAINER_WANT_HELPER_PROCESS=1",
|
|
"TEST_DEVCONTAINER_WANT_ARGS="+e.wantArgs,
|
|
"TEST_DEVCONTAINER_WANT_ERROR="+fmt.Sprintf("%v", e.wantError),
|
|
"TEST_DEVCONTAINER_LOG_FILE="+e.logFile,
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
// PTYCommandContext returns a PTY command.
|
|
func (*testDevcontainerExecer) PTYCommandContext(_ context.Context, name string, args ...string) *pty.Cmd {
|
|
// This method shouldn't be called for our devcontainer tests.
|
|
panic("PTYCommandContext not expected in devcontainer tests")
|
|
}
|
|
|
|
// This is a special test helper that is executed as a subprocess.
|
|
// It simulates the behavior of the devcontainer CLI.
|
|
//
|
|
//nolint:revive,paralleltest // This is a test helper function.
|
|
func TestDevcontainerHelperProcess(t *testing.T) {
|
|
// If not called by the test as a helper process, do nothing.
|
|
if os.Getenv("TEST_DEVCONTAINER_WANT_HELPER_PROCESS") != "1" {
|
|
return
|
|
}
|
|
|
|
helperArgs := flag.Args()
|
|
if len(helperArgs) < 1 {
|
|
fmt.Fprintf(os.Stderr, "No command\n")
|
|
os.Exit(2)
|
|
}
|
|
|
|
if helperArgs[0] != "devcontainer" {
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", helperArgs[0])
|
|
os.Exit(2)
|
|
}
|
|
|
|
// Verify arguments against expected arguments and skip
|
|
// "devcontainer", it's not included in the input args.
|
|
wantArgs := os.Getenv("TEST_DEVCONTAINER_WANT_ARGS")
|
|
gotArgs := strings.Join(helperArgs[1:], " ")
|
|
if gotArgs != wantArgs {
|
|
fmt.Fprintf(os.Stderr, "Arguments don't match.\nWant: %q\nGot: %q\n",
|
|
wantArgs, gotArgs)
|
|
os.Exit(2)
|
|
}
|
|
|
|
logFilePath := os.Getenv("TEST_DEVCONTAINER_LOG_FILE")
|
|
if logFilePath != "" {
|
|
// Read and output log file for commands that need it (like "up")
|
|
output, err := os.ReadFile(logFilePath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Reading log file %s failed: %v\n", logFilePath, err)
|
|
os.Exit(2)
|
|
}
|
|
_, _ = io.Copy(os.Stdout, bytes.NewReader(output))
|
|
}
|
|
|
|
if os.Getenv("TEST_DEVCONTAINER_WANT_ERROR") == "true" {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
// TestDockerDevcontainerCLI tests the DevcontainerCLI component with real Docker containers.
|
|
// This test verifies that containers can be created and recreated using the actual
|
|
// devcontainer CLI and Docker. It is skipped by default and can be run with:
|
|
//
|
|
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerDevcontainerCLI
|
|
//
|
|
// The test requires Docker to be installed and running.
|
|
func TestDockerDevcontainerCLI(t *testing.T) {
|
|
t.Parallel()
|
|
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
|
|
t.Skip("skipping Docker test; set CODER_TEST_USE_DOCKER=1 to run")
|
|
}
|
|
if _, err := exec.LookPath("devcontainer"); err != nil {
|
|
t.Fatal("this test requires the devcontainer CLI: npm install -g @devcontainers/cli")
|
|
}
|
|
|
|
// Connect to Docker.
|
|
pool, err := dockertest.NewPool("")
|
|
require.NoError(t, err, "connect to Docker")
|
|
|
|
t.Run("ContainerLifecycle", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Set up workspace directory with a devcontainer configuration.
|
|
workspaceFolder := t.TempDir()
|
|
configPath := setupDevcontainerWorkspace(t, workspaceFolder)
|
|
|
|
// Use a long timeout because container operations are slow.
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
|
|
// Create the devcontainer CLI under test.
|
|
dccli := agentcontainers.NewDevcontainerCLI(logger, agentexec.DefaultExecer)
|
|
|
|
// Create a container.
|
|
firstID, err := dccli.Up(ctx, workspaceFolder, configPath)
|
|
require.NoError(t, err, "create container")
|
|
require.NotEmpty(t, firstID, "container ID should not be empty")
|
|
defer removeDevcontainerByID(t, pool, firstID)
|
|
|
|
// Verify container exists.
|
|
firstContainer, found := findDevcontainerByID(t, pool, firstID)
|
|
require.True(t, found, "container should exist")
|
|
|
|
// Remember the container creation time.
|
|
firstCreated := firstContainer.Created
|
|
|
|
// Recreate the container.
|
|
secondID, err := dccli.Up(ctx, workspaceFolder, configPath, agentcontainers.WithRemoveExistingContainer())
|
|
require.NoError(t, err, "recreate container")
|
|
require.NotEmpty(t, secondID, "recreated container ID should not be empty")
|
|
defer removeDevcontainerByID(t, pool, secondID)
|
|
|
|
// Verify the new container exists and is different.
|
|
secondContainer, found := findDevcontainerByID(t, pool, secondID)
|
|
require.True(t, found, "recreated container should exist")
|
|
|
|
// Verify it's a different container by checking creation time.
|
|
secondCreated := secondContainer.Created
|
|
assert.NotEqual(t, firstCreated, secondCreated, "recreated container should have different creation time")
|
|
|
|
// Verify the first container is removed by the recreation.
|
|
_, found = findDevcontainerByID(t, pool, firstID)
|
|
assert.False(t, found, "first container should be removed")
|
|
})
|
|
}
|
|
|
|
// setupDevcontainerWorkspace prepares a test environment with a minimal
|
|
// devcontainer.json configuration and returns the path to the config file.
|
|
func setupDevcontainerWorkspace(t *testing.T, workspaceFolder string) string {
|
|
t.Helper()
|
|
|
|
// Create the devcontainer directory structure.
|
|
devcontainerDir := filepath.Join(workspaceFolder, ".devcontainer")
|
|
err := os.MkdirAll(devcontainerDir, 0o755)
|
|
require.NoError(t, err, "create .devcontainer directory")
|
|
|
|
// Write a minimal configuration with test labels for identification.
|
|
configPath := filepath.Join(devcontainerDir, "devcontainer.json")
|
|
content := `{
|
|
"image": "alpine:latest",
|
|
"containerEnv": {
|
|
"TEST_CONTAINER": "true"
|
|
},
|
|
"runArgs": ["--label", "com.coder.test=devcontainercli"]
|
|
}`
|
|
err = os.WriteFile(configPath, []byte(content), 0o600)
|
|
require.NoError(t, err, "create devcontainer.json file")
|
|
|
|
return configPath
|
|
}
|
|
|
|
// findDevcontainerByID locates a container by its ID and verifies it has our
|
|
// test label. Returns the container and whether it was found.
|
|
func findDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) (*docker.Container, bool) {
|
|
t.Helper()
|
|
|
|
container, err := pool.Client.InspectContainer(id)
|
|
if err != nil {
|
|
t.Logf("Inspect container failed: %v", err)
|
|
return nil, false
|
|
}
|
|
require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label")
|
|
|
|
return container, true
|
|
}
|
|
|
|
// removeDevcontainerByID safely cleans up a test container by ID, verifying
|
|
// it has our test label before removal to prevent accidental deletion.
|
|
func removeDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) {
|
|
t.Helper()
|
|
|
|
errNoSuchContainer := &docker.NoSuchContainer{}
|
|
|
|
// Check if the container has the expected label.
|
|
container, err := pool.Client.InspectContainer(id)
|
|
if err != nil {
|
|
if errors.As(err, &errNoSuchContainer) {
|
|
t.Logf("Container %s not found, skipping removal", id)
|
|
return
|
|
}
|
|
require.NoError(t, err, "inspect container")
|
|
}
|
|
require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label")
|
|
|
|
t.Logf("Removing container with ID: %s", id)
|
|
err = pool.Client.RemoveContainer(docker.RemoveContainerOptions{
|
|
ID: container.ID,
|
|
Force: true,
|
|
RemoveVolumes: true,
|
|
})
|
|
if err != nil && !errors.As(err, &errNoSuchContainer) {
|
|
assert.NoError(t, err, "remove container failed")
|
|
}
|
|
}
|