diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5eac1a4..8c85bc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,6 @@ jobs: run: go test ./internal/... - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: version: latest diff --git a/internal/config.go b/internal/config.go index e180fe4..9f142d0 100644 --- a/internal/config.go +++ b/internal/config.go @@ -36,6 +36,7 @@ type Config struct { Env Environment Volumes []string DockerfilePath string + Network string } type GitUserConfig struct { @@ -67,14 +68,18 @@ func ParseConfig(args []string, environment []string) Config { } } - var additionalEnv stringSlice - var volumes stringSlice - var dockerfilePath string + var ( + additionalEnv stringSlice + volumes stringSlice + dockerfilePath string + network string + ) fs := flag.NewFlagSet("contagent", flag.ContinueOnError) fs.Var(&additionalEnv, "env", "environment variable") fs.Var(&volumes, "volume", "volume mount") fs.StringVar(&dockerfilePath, "dockerfile", "", "Dockerfile path") + fs.StringVar(&network, "network", "default", " Connect to a container network") // Ignore errors since we want to capture remaining args _ = fs.Parse(args) @@ -123,5 +128,6 @@ func ParseConfig(args []string, environment []string) Config { Args: Command(programArgs), Env: Environment(env), Volumes: allVolumes, + Network: network, } } diff --git a/internal/config_test.go b/internal/config_test.go index 0fbfa9f..ee945f5 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -27,6 +27,7 @@ func TestConfig(t *testing.T) { "ANTHROPIC_API_KEY=some-api-key", "SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock", }), config.Env) + require.Equal(t, "default", config.Network) }) t.Run("with --env flags", func(t *testing.T) { @@ -125,5 +126,18 @@ func TestConfig(t *testing.T) { config := internal.ParseConfig(args, env) require.Equal(t, "/some/path/to/a/Dockerfile", config.DockerfilePath) }) + + t.Run("when given a --network flag", func(t *testing.T) { + args := []string{ + "--network", "some-network", + "some-program", + } + env := []string{ + "TERM=some-term", + } + + config := internal.ParseConfig(args, env) + require.Equal(t, "some-network", config.Network) + }) }) } diff --git a/internal/docker/client.go b/internal/docker/client.go index 48c9a16..128584a 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -141,7 +141,7 @@ func (c Client) BuildImage(ctx context.Context, dockerfilePath string, imageName // It configures the container with TTY support, stdin attachment, environment variables, // working directory, volume mounts, and network settings to allow communication with the host // via host.docker.internal. Returns a Container handle or an error if creation fails. -func (c Client) CreateContainer(ctx context.Context, sessionID internal.SessionID, image Image, args internal.Command, env internal.Environment, volumes []string, workingDir string, stopTimeout, ttyRetries int, retryDelay time.Duration) (Container, error) { +func (c Client) CreateContainer(ctx context.Context, sessionID internal.SessionID, image Image, args internal.Command, env internal.Environment, volumes []string, workingDir, network string, stopTimeout, ttyRetries int, retryDelay time.Duration) (Container, error) { response, err := c.client.ContainerCreate(ctx, client.ContainerCreateOptions{ Config: &container.Config{ Image: image.Name, @@ -158,7 +158,8 @@ func (c Client) CreateContainer(ctx context.Context, sessionID internal.SessionI ExtraHosts: []string{ "host.docker.internal:host-gateway", }, - Binds: volumes, + Binds: volumes, + NetworkMode: container.NetworkMode(network), }, Name: string(sessionID), NetworkingConfig: nil, diff --git a/internal/docker/client_unit_test.go b/internal/docker/client_unit_test.go index 8eaafef..d7e53c4 100644 --- a/internal/docker/client_unit_test.go +++ b/internal/docker/client_unit_test.go @@ -155,7 +155,7 @@ func TestCreateContainerWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test-container", image, []string{"echo", "test"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test-container", image, []string{"echo", "test"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) assert.Equal(t, "container123", container.ID) assert.Equal(t, "test-container", container.Name) @@ -172,7 +172,7 @@ func TestCreateContainerWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "nonexistent:latest"} - _, err := c.CreateContainer(ctx, "test-container", image, []string{"echo", "test"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + _, err := c.CreateContainer(ctx, "test-container", image, []string{"echo", "test"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.Error(t, err) assert.Contains(t, err.Error(), "failed to create container") }) @@ -195,7 +195,7 @@ func TestCreateContainerWithMock(t *testing.T) { volumes := []string{"/host:/container"} workingDir := "/custom" - _, err := c.CreateContainer(ctx, "test-name", image, args, env, volumes, workingDir, 10, 10, 100*time.Millisecond) + _, err := c.CreateContainer(ctx, "test-name", image, args, env, volumes, workingDir, "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) assert.Equal(t, "alpine:latest", capturedOptions.Config.Image) diff --git a/internal/docker/container_unit_test.go b/internal/docker/container_unit_test.go index d1f0aa5..d86461c 100644 --- a/internal/docker/container_unit_test.go +++ b/internal/docker/container_unit_test.go @@ -33,7 +33,7 @@ func TestContainerStartWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.Start(ctx) @@ -55,7 +55,7 @@ func TestContainerStartWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.Start(ctx) @@ -84,7 +84,7 @@ func TestContainerRemoveWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.Remove(ctx) @@ -106,7 +106,7 @@ func TestContainerRemoveWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.Remove(ctx) @@ -135,7 +135,7 @@ func TestContainerForceRemoveWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.ForceRemove(ctx) @@ -157,7 +157,7 @@ func TestContainerForceRemoveWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.ForceRemove(ctx) @@ -186,7 +186,7 @@ func TestContainerCopyToWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.CopyTo(ctx, io.NopCloser(nil), "/tmp") @@ -208,7 +208,7 @@ func TestContainerCopyToWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) err = container.CopyTo(ctx, io.NopCloser(nil), "/tmp") @@ -239,7 +239,7 @@ func TestContainerWaitWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) writer := newMockWriter() @@ -265,7 +265,7 @@ func TestContainerWaitWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"sh", "-c", "exit 42"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"sh", "-c", "exit 42"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) writer := newMockWriter() @@ -291,7 +291,7 @@ func TestContainerWaitWithMock(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) writer := newMockWriter() diff --git a/internal/docker/tty_unit_test.go b/internal/docker/tty_unit_test.go index 43956c4..690c1e2 100644 --- a/internal/docker/tty_unit_test.go +++ b/internal/docker/tty_unit_test.go @@ -170,7 +170,7 @@ func TestContainerResizeIntegration(t *testing.T) { ctx := context.Background() image := docker.Image{Name: "alpine:latest"} - container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", 10, 10, 100*time.Millisecond) + container, err := c.CreateContainer(ctx, "test", image, []string{"echo"}, []string{}, []string{}, "/app", "some-network", 10, 10, 100*time.Millisecond) require.NoError(t, err) writer := newMockWriter() diff --git a/internal/git/errors_test.go b/internal/git/errors_test.go index b31059e..2c9625a 100644 --- a/internal/git/errors_test.go +++ b/internal/git/errors_test.go @@ -32,31 +32,6 @@ func TestGitServerErrorCases(t *testing.T) { require.Error(t, err) }) - t.Run("directory with permission denied", func(t *testing.T) { - if os.Getuid() == 0 { - t.Skip("Running as root, cannot test permission denied") - } - - dir, err := os.MkdirTemp("", "git-perm-test") - require.NoError(t, err) - defer os.RemoveAll(dir) - - // Initialize git repo - cmd := exec.Command("git", "init") - cmd.Dir = dir - require.NoError(t, cmd.Run()) - - // Remove permissions - err = os.Chmod(dir, 0000) - require.NoError(t, err) - - // Restore permissions for cleanup - defer os.Chmod(dir, 0755) - - _, err = git.NewServer(dir, internal.NewStandardWriter()) - require.Error(t, err) - }) - t.Run("relative path resolution", func(t *testing.T) { dir, err := os.MkdirTemp("", "git-rel-test") require.NoError(t, err) diff --git a/main.go b/main.go index 0cce703..7e254c9 100644 --- a/main.go +++ b/main.go @@ -81,6 +81,7 @@ func run(args, env []string) error { config.Env, config.Volumes, config.WorkingDir, + config.Network, config.StopTimeout, config.TTYRetries, config.RetryDelay,