From 437fe8c13ea748cb0e5ac6b5f09b44ce0dbe4110 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Mon, 4 Aug 2025 18:54:15 -0400 Subject: [PATCH 1/2] fix(docker): Workaround secure hardening feature in docker v28 In docker v28, a security hardening feature broke Windsor's ability to route traffic through the VM to the containers. The feature drops packets originating from a non-host. Since we intend for packets to originate from the host interface, we can add this as trusted using new Docker network options in the docker-compose config. The solution is to add: ``` driver_opts: com.docker.network.bridge.trusted_host_interfaces: "col0" ``` When the running docker engine is >= v28 and we're using Colima. Security hardening outlined in https://www.docker.com/blog/docker-engine-28-hardening-container-networking-by-default/ --- pkg/virt/docker_virt.go | 41 +++- pkg/virt/docker_virt_test.go | 358 +++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+), 5 deletions(-) diff --git a/pkg/virt/docker_virt.go b/pkg/virt/docker_virt.go index a21f03a73..148bd738a 100644 --- a/pkg/virt/docker_virt.go +++ b/pkg/virt/docker_virt.go @@ -329,11 +329,11 @@ func (v *DockerVirt) checkDockerDaemon() error { return err } -// getFullComposeConfig builds a complete Docker Compose configuration by combining -// settings from all services. It creates a network configuration with optional IPAM -// settings based on the network CIDR, collects service configurations with their -// network settings and IP addresses, and aggregates volumes and networks from all -// services into a single project configuration. +// getFullComposeConfig assembles a Docker Compose project configuration for the current Windsor context. +// Aggregates service, volume, and network definitions from all registered services. Applies Windsor-specific +// network settings, including optional IPAM configuration based on the context's network CIDR. Ensures +// compatibility with Docker Engine v28+ by setting the bridge gateway mode to nat-unprotected when supported. +// Returns the constructed types.Project or an error if service configuration retrieval fails. func (v *DockerVirt) getFullComposeConfig() (*types.Project, error) { contextName := v.configHandler.GetContext() @@ -355,6 +355,14 @@ func (v *DockerVirt) getFullComposeConfig() (*types.Project, error) { Driver: "bridge", } + if v.supportsDockerEngineV28Plus() { + // For Colima VMs, use trusted_host_interfaces to allow the Colima interface + // to bypass Docker Engine v28+ security hardening + networkConfig.DriverOpts = map[string]string{ + "com.docker.network.bridge.trusted_host_interfaces": "col0", + } + } + networkCIDR := v.configHandler.GetString("network.cidr_block") if networkCIDR != "" { networkConfig.Ipam = types.IPAMConfig{ @@ -417,3 +425,26 @@ func (v *DockerVirt) getFullComposeConfig() (*types.Project, error) { return project, nil } + +// supportsDockerEngineV28Plus determines if the Docker Engine version is 28 or higher, indicating support for the nat-unprotected gateway mode option. +// It executes 'docker version' to retrieve the server version, parses the major version, and returns true if the version is 28 or above. +// Returns false if the version cannot be determined or is below 28. +func (v *DockerVirt) supportsDockerEngineV28Plus() bool { + output, err := v.shell.ExecSilent("docker", "version", "--format", "{{.Server.Version}}") + if err != nil { + return false + } + versionStr := strings.TrimSpace(output) + if versionStr == "" { + return false + } + parts := strings.Split(versionStr, ".") + if len(parts) < 2 { + return false + } + majorVersion := parts[0] + if majorVersion >= "28" { + return true + } + return false +} diff --git a/pkg/virt/docker_virt_test.go b/pkg/virt/docker_virt_test.go index 195e887ed..cb0c59642 100644 --- a/pkg/virt/docker_virt_test.go +++ b/pkg/virt/docker_virt_test.go @@ -67,6 +67,10 @@ contexts: return "Docker Compose version 2.0.0", nil case "info": return "Docker info output", nil + case "version": + if len(args) >= 3 && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "28.0.3", nil // Mock Docker Engine v28+ for testing + } case "ps": var hasManagedBy, hasContext, hasFormat bool for i := 0; i < len(args); i++ { @@ -1343,6 +1347,157 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { if network.Ipam.Config[0].Subnet != "10.0.0.0/24" { t.Errorf("expected network CIDR to be 10.0.0.0/24, got %s", network.Ipam.Config[0].Subnet) } + + // And the network should have Docker Engine v28+ compatibility driver options + // (since we're mocking Docker Engine v28+ in the test) + if network.DriverOpts == nil { + t.Errorf("expected network to have driver options for Docker v28+ compatibility") + } + expectedDriverOpt := "com.docker.network.bridge.trusted_host_interfaces" + if _, exists := network.DriverOpts[expectedDriverOpt]; !exists { + t.Errorf("expected network to have driver option %s", expectedDriverOpt) + } + if network.DriverOpts[expectedDriverOpt] != "col0" { + t.Errorf("expected driver option %s to be 'col0', got %s", expectedDriverOpt, network.DriverOpts[expectedDriverOpt]) + } + }) + + t.Run("DockerEngineV28Compatibility", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, _ := setup(t) + + // When getting the full compose config + project, err := dockerVirt.getFullComposeConfig() + + // Then no error should occur + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + // And the project should not be nil + if project == nil { + t.Errorf("expected project to not be nil") + } + + // And the network should have Docker Engine v28+ compatibility configuration + networkName := fmt.Sprintf("windsor-%s", dockerVirt.configHandler.GetContext()) + network, exists := project.Networks[networkName] + if !exists { + t.Errorf("expected network %s to exist", networkName) + } + + // Verify the network has the required driver options for Docker v28+ compatibility + if network.DriverOpts == nil { + t.Errorf("expected network to have driver options for Docker v28+ compatibility") + } + + // Check for the specific driver option that bypasses Docker v28 security hardening + expectedDriverOpt := "com.docker.network.bridge.trusted_host_interfaces" + driverOptValue, exists := network.DriverOpts[expectedDriverOpt] + if !exists { + t.Errorf("expected network to have driver option %s for Docker v28+ compatibility", expectedDriverOpt) + } + + // Verify the driver option is set to col0 interface + expectedValue := "col0" + if driverOptValue != expectedValue { + t.Errorf("expected driver option %s to be '%s', got '%s'", expectedDriverOpt, expectedValue, driverOptValue) + } + + // Verify the network driver is bridge (required for this compatibility fix) + if network.Driver != "bridge" { + t.Errorf("expected network driver to be 'bridge', got '%s'", network.Driver) + } + + }) + + t.Run("DockerEnginePreV28", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine is pre-v28 (older version) + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) > 0 { + switch args[0] { + case "compose": + return "Docker Compose version 2.0.0", nil + case "info": + return "Docker info output", nil + case "version": + if len(args) >= 3 && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "27.0.3", nil // Mock Docker Engine pre-v28 for testing + } + case "ps": + var hasManagedBy, hasContext, hasFormat bool + for i := 0; i < len(args); i++ { + if args[i] == "--filter" && i+1 < len(args) { + switch args[i+1] { + case "label=managed_by=windsor": + hasManagedBy = true + case fmt.Sprintf("label=context=%s", mocks.ConfigHandler.GetContext()): + hasContext = true + } + } else if args[i] == "--format" && i+1 < len(args) && args[i+1] == "{{.ID}}" { + hasFormat = true + } + } + if hasManagedBy && hasContext && hasFormat { + return "container1\ncontainer2", nil + } + case "inspect": + if len(args) >= 4 && args[2] == "--format" { + switch args[3] { + case "{{json .Config.Labels}}": + switch args[1] { + case "container1": + return `{"managed_by":"windsor","context":"mock-context","com.docker.compose.service":"service1","role":"test"}`, nil + case "container2": + return `{"managed_by":"windsor","context":"mock-context","com.docker.compose.service":"service2","role":"test"}`, nil + } + case "{{json .NetworkSettings.Networks}}": + switch args[1] { + case "container1": + return fmt.Sprintf(`{"windsor-%s":{"IPAddress":"192.168.1.2"}}`, mocks.ConfigHandler.GetContext()), nil + case "container2": + return fmt.Sprintf(`{"windsor-%s":{"IPAddress":"192.168.1.3"}}`, mocks.ConfigHandler.GetContext()), nil + } + } + } + } + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When getting the full compose config + project, err := dockerVirt.getFullComposeConfig() + + // Then no error should occur + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + // And the project should not be nil + if project == nil { + t.Errorf("expected project to not be nil") + } + + // And the network should NOT have Docker Engine v28+ compatibility driver options + // (since we're mocking Docker Engine pre-v28 in the test) + networkName := fmt.Sprintf("windsor-%s", dockerVirt.configHandler.GetContext()) + network, exists := project.Networks[networkName] + if !exists { + t.Errorf("expected network %s to exist", networkName) + } + + // Verify the network does NOT have driver options for older Docker Engine + if network.DriverOpts != nil { + t.Errorf("expected network to NOT have driver options for pre-v28 Docker Engine, but got %v", network.DriverOpts) + } + + // Verify the network driver is still bridge + if network.Driver != "bridge" { + t.Errorf("expected network driver to be 'bridge', got '%s'", network.Driver) + } }) t.Run("DockerNotEnabled", func(t *testing.T) { @@ -1497,3 +1652,206 @@ contexts: } }) } + +// TestDockerVirt_SupportsDockerEngineV28Plus tests the Docker Engine v28+ version detection functionality. +func TestDockerVirt_SupportsDockerEngineV28Plus(t *testing.T) { + setup := func(t *testing.T) (*DockerVirt, *Mocks) { + t.Helper() + mocks := setupDockerMocks(t) + dockerVirt := NewDockerVirt(mocks.Injector) + dockerVirt.shims = mocks.Shims + if err := dockerVirt.Initialize(); err != nil { + t.Fatalf("Failed to initialize DockerVirt: %v", err) + } + return dockerVirt, mocks + } + + t.Run("DockerEngineV28Plus", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine v28+ is detected + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "28.0.3", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return true + if !supported { + t.Errorf("expected Docker Engine v28+ to be supported, got false") + } + }) + + t.Run("DockerEngineV28Exact", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine v28.0.0 is detected (exact version) + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "28.0.0", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return true + if !supported { + t.Errorf("expected Docker Engine v28.0.0 to be supported, got false") + } + }) + + t.Run("DockerEngineV29Plus", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine v29+ is detected + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "29.0.0", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return true + if !supported { + t.Errorf("expected Docker Engine v29+ to be supported, got false") + } + }) + + t.Run("DockerEnginePreV28", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine pre-v28 is detected + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "27.0.3", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false + if supported { + t.Errorf("expected Docker Engine pre-v28 to not be supported, got true") + } + }) + + t.Run("DockerEngineV27Exact", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker Engine v27.9.9 is detected (highest pre-v28) + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "27.9.9", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false + if supported { + t.Errorf("expected Docker Engine v27.9.9 to not be supported, got true") + } + }) + + t.Run("DockerCommandError", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker version command fails + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "", fmt.Errorf("docker command failed") + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false (graceful fallback) + if supported { + t.Errorf("expected Docker Engine detection to fail gracefully, got true") + } + }) + + t.Run("EmptyVersionString", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker version command returns empty string + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false (graceful fallback) + if supported { + t.Errorf("expected empty version string to not be supported, got true") + } + }) + + t.Run("InvalidVersionFormat", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker version command returns invalid format + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "invalid-version", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false (graceful fallback) + if supported { + t.Errorf("expected invalid version format to not be supported, got true") + } + }) + + t.Run("SingleVersionComponent", func(t *testing.T) { + // Given a docker virt instance with valid mocks + dockerVirt, mocks := setup(t) + + // And Docker version command returns single component + mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { + if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { + return "28", nil + } + return "", fmt.Errorf("unexpected command: %s %v", command, args) + } + + // When checking if Docker Engine v28+ is supported + supported := dockerVirt.supportsDockerEngineV28Plus() + + // Then it should return false (graceful fallback for malformed version) + if supported { + t.Errorf("expected single version component to not be supported, got true") + } + }) +} From 65652e8ab28d827358da744a2cfd5b7ad30fb485 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Mon, 4 Aug 2025 19:37:33 -0400 Subject: [PATCH 2/2] fix(docker): Work around docker v28 hardening scheme In docker v28, a security enhancement was introduced that drops packets coming from a non-host source. This breaks the mechanism through which traffic gets routed from the MacOS host when using Colima. This PR fixes by adding `"com.docker.network.bridge.gateway_mode_ipv4": "nat-unprotected"` to the docker network options, which is permissable in a local dev environment. --- pkg/virt/docker_virt.go | 32 +++-- pkg/virt/docker_virt_test.go | 272 ++++------------------------------- 2 files changed, 44 insertions(+), 260 deletions(-) diff --git a/pkg/virt/docker_virt.go b/pkg/virt/docker_virt.go index 148bd738a..bdfd61038 100644 --- a/pkg/virt/docker_virt.go +++ b/pkg/virt/docker_virt.go @@ -83,15 +83,23 @@ func (v *DockerVirt) Initialize() error { return nil } -// Up starts docker compose in detached mode with retry logic for reliability. It -// verifies Docker is enabled, checks the daemon is running, sets the compose file -// path, and attempts to start services with up to 3 retries if initial attempts fail. +// Up starts Docker Compose in detached mode with retry logic. It checks if Docker is enabled, +// verifies the Docker daemon is running, regenerates the Docker Compose configuration when running +// in a Colima VM to ensure network and driver options are compatible with Colima's requirements, +// sets the COMPOSE_FILE environment variable, and attempts to start services with up to 3 retries. +// Returns an error if all attempts fail or if prerequisites are not met. func (v *DockerVirt) Up() error { if v.configHandler.GetBool("docker.enabled") { if err := v.checkDockerDaemon(); err != nil { return fmt.Errorf("Docker daemon is not running: %w", err) } + if v.configHandler.GetString("vm.driver") == "colima" { + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("error regenerating docker compose config: %w", err) + } + } + projectRoot, err := v.shell.GetProjectRoot() if err != nil { return fmt.Errorf("error retrieving project root: %w", err) @@ -355,11 +363,9 @@ func (v *DockerVirt) getFullComposeConfig() (*types.Project, error) { Driver: "bridge", } - if v.supportsDockerEngineV28Plus() { - // For Colima VMs, use trusted_host_interfaces to allow the Colima interface - // to bypass Docker Engine v28+ security hardening + if v.configHandler.GetString("vm.driver") == "colima" && v.supportsDockerEngineV28Plus() { networkConfig.DriverOpts = map[string]string{ - "com.docker.network.bridge.trusted_host_interfaces": "col0", + "com.docker.network.bridge.gateway_mode_ipv4": "nat-unprotected", } } @@ -426,9 +432,10 @@ func (v *DockerVirt) getFullComposeConfig() (*types.Project, error) { return project, nil } -// supportsDockerEngineV28Plus determines if the Docker Engine version is 28 or higher, indicating support for the nat-unprotected gateway mode option. -// It executes 'docker version' to retrieve the server version, parses the major version, and returns true if the version is 28 or above. -// Returns false if the version cannot be determined or is below 28. +// supportsDockerEngineV28Plus returns true if the Docker Engine major version is 28 or higher. +// It executes 'docker version' to retrieve the server version, parses the major version component, +// and determines compatibility with features introduced in Docker Engine v28, such as nat-unprotected gateway mode. +// Returns false if the version cannot be determined or is less than 28. func (v *DockerVirt) supportsDockerEngineV28Plus() bool { output, err := v.shell.ExecSilent("docker", "version", "--format", "{{.Server.Version}}") if err != nil { @@ -443,8 +450,5 @@ func (v *DockerVirt) supportsDockerEngineV28Plus() bool { return false } majorVersion := parts[0] - if majorVersion >= "28" { - return true - } - return false + return majorVersion >= "28" } diff --git a/pkg/virt/docker_virt_test.go b/pkg/virt/docker_virt_test.go index cb0c59642..bd22794c7 100644 --- a/pkg/virt/docker_virt_test.go +++ b/pkg/virt/docker_virt_test.go @@ -51,7 +51,9 @@ contexts: remote: "remote-registry.example.com" local: "localhost:5000" hostname: "registry.local" - hostport: 5000` + hostport: 5000 + vm: + driver: colima` if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { t.Fatalf("Failed to load config string: %v", err) @@ -1349,73 +1351,24 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { } // And the network should have Docker Engine v28+ compatibility driver options - // (since we're mocking Docker Engine v28+ in the test) + // (since the test config has vm.driver: colima and Docker Engine v28+ is mocked) if network.DriverOpts == nil { t.Errorf("expected network to have driver options for Docker v28+ compatibility") } - expectedDriverOpt := "com.docker.network.bridge.trusted_host_interfaces" + expectedDriverOpt := "com.docker.network.bridge.gateway_mode_ipv4" if _, exists := network.DriverOpts[expectedDriverOpt]; !exists { t.Errorf("expected network to have driver option %s", expectedDriverOpt) } - if network.DriverOpts[expectedDriverOpt] != "col0" { - t.Errorf("expected driver option %s to be 'col0', got %s", expectedDriverOpt, network.DriverOpts[expectedDriverOpt]) + if network.DriverOpts[expectedDriverOpt] != "nat-unprotected" { + t.Errorf("expected driver option %s to be 'nat-unprotected', got %s", expectedDriverOpt, network.DriverOpts[expectedDriverOpt]) } }) t.Run("DockerEngineV28Compatibility", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, _ := setup(t) - - // When getting the full compose config - project, err := dockerVirt.getFullComposeConfig() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - // And the project should not be nil - if project == nil { - t.Errorf("expected project to not be nil") - } - - // And the network should have Docker Engine v28+ compatibility configuration - networkName := fmt.Sprintf("windsor-%s", dockerVirt.configHandler.GetContext()) - network, exists := project.Networks[networkName] - if !exists { - t.Errorf("expected network %s to exist", networkName) - } - - // Verify the network has the required driver options for Docker v28+ compatibility - if network.DriverOpts == nil { - t.Errorf("expected network to have driver options for Docker v28+ compatibility") - } - - // Check for the specific driver option that bypasses Docker v28 security hardening - expectedDriverOpt := "com.docker.network.bridge.trusted_host_interfaces" - driverOptValue, exists := network.DriverOpts[expectedDriverOpt] - if !exists { - t.Errorf("expected network to have driver option %s for Docker v28+ compatibility", expectedDriverOpt) - } - - // Verify the driver option is set to col0 interface - expectedValue := "col0" - if driverOptValue != expectedValue { - t.Errorf("expected driver option %s to be '%s', got '%s'", expectedDriverOpt, expectedValue, driverOptValue) - } - - // Verify the network driver is bridge (required for this compatibility fix) - if network.Driver != "bridge" { - t.Errorf("expected network driver to be 'bridge', got '%s'", network.Driver) - } - - }) - - t.Run("DockerEnginePreV28", func(t *testing.T) { // Given a docker virt instance with valid mocks dockerVirt, mocks := setup(t) - // And Docker Engine is pre-v28 (older version) + // And Docker Engine is v28+ and we're running in a Colima VM mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "docker" && len(args) > 0 { switch args[0] { @@ -1425,7 +1378,7 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { return "Docker info output", nil case "version": if len(args) >= 3 && args[1] == "--format" && args[2] == "{{.Server.Version}}" { - return "27.0.3", nil // Mock Docker Engine pre-v28 for testing + return "28.0.3", nil // Mock Docker Engine v28+ for testing } case "ps": var hasManagedBy, hasContext, hasFormat bool @@ -1481,210 +1434,37 @@ func TestDockerVirt_GetFullComposeConfig(t *testing.T) { t.Errorf("expected project to not be nil") } - // And the network should NOT have Docker Engine v28+ compatibility driver options - // (since we're mocking Docker Engine pre-v28 in the test) + // And the network should have Docker Engine v28+ compatibility driver options + // (since we're mocking both Colima VM and Docker Engine v28+ in the test) networkName := fmt.Sprintf("windsor-%s", dockerVirt.configHandler.GetContext()) network, exists := project.Networks[networkName] if !exists { t.Errorf("expected network %s to exist", networkName) } - // Verify the network does NOT have driver options for older Docker Engine - if network.DriverOpts != nil { - t.Errorf("expected network to NOT have driver options for pre-v28 Docker Engine, but got %v", network.DriverOpts) - } - - // Verify the network driver is still bridge - if network.Driver != "bridge" { - t.Errorf("expected network driver to be 'bridge', got '%s'", network.Driver) - } - }) - - t.Run("DockerNotEnabled", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, mocks := setup(t) - - // And Docker is not enabled - // Create a new config handler with Docker disabled - configStr := ` -contexts: - mock-context: - dns: - domain: mock.domain.com - enabled: true - address: 10.0.0.53 - network: - cidr_block: 10.0.0.0/24 - docker: - enabled: false - registry_url: "https://registry.example.com" - registries: - local: - remote: "remote-registry.example.com" - local: "localhost:5000" - hostname: "registry.local" - hostport: 5000` - - if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config string: %v", err) - } - - // When getting the full compose config - project, err := dockerVirt.getFullComposeConfig() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } - - // And the error should contain the expected message - expectedErrorSubstring := "Docker configuration is not defined" - if !strings.Contains(err.Error(), expectedErrorSubstring) { - t.Errorf("expected error message to contain %q, got %q", expectedErrorSubstring, err.Error()) - } - - // And the project should be nil - if project != nil { - t.Errorf("expected project to be nil") - } - }) - - t.Run("ServiceGetComposeConfigError", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, mocks := setup(t) - - // And a service returns an error when getting compose config - mocks.Service.GetComposeConfigFunc = func() (*types.Config, error) { - return nil, fmt.Errorf("mock error getting compose config") - } - - // When getting the full compose config - project, err := dockerVirt.getFullComposeConfig() - - // Then an error should occur - if err == nil { - t.Errorf("expected error, got none") - } - - // And the error should contain the expected message - expectedErrorSubstring := "error getting container config from service" - if !strings.Contains(err.Error(), expectedErrorSubstring) { - t.Errorf("expected error message to contain %q, got %q", expectedErrorSubstring, err.Error()) - } - - // And the project should be nil - if project != nil { - t.Errorf("expected project to be nil") - } - }) - - t.Run("ServiceReturnsNilConfig", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, mocks := setup(t) - - // And a service returns nil when getting compose config - mocks.Service.GetComposeConfigFunc = func() (*types.Config, error) { - return nil, nil - } - - // When getting the full compose config - project, err := dockerVirt.getFullComposeConfig() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - // And the project should not be nil - if project == nil { - t.Errorf("expected project to not be nil") - } - - // And the project should have no services - if len(project.Services) != 0 { - t.Errorf("expected 0 services, got %d", len(project.Services)) - } - }) - - t.Run("ServiceReturnsEmptyConfig", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, mocks := setup(t) - - // And a service returns a config with no services - mocks.Service.GetComposeConfigFunc = func() (*types.Config, error) { - return &types.Config{ - Services: nil, - Volumes: map[string]types.VolumeConfig{ - "test-volume": {}, - }, - Networks: map[string]types.NetworkConfig{ - "test-network": {}, - }, - }, nil - } - - // When getting the full compose config - project, err := dockerVirt.getFullComposeConfig() - - // Then no error should occur - if err != nil { - t.Errorf("expected no error, got %v", err) - } - - // And the project should not be nil - if project == nil { - t.Errorf("expected project to not be nil") - } - - // And the project should have no services - if len(project.Services) != 0 { - t.Errorf("expected 0 services, got %d", len(project.Services)) - } - - // And the project should have the volume - if _, exists := project.Volumes["test-volume"]; !exists { - t.Errorf("expected volume test-volume to exist") + // Verify the network has the required driver options for Docker v28+ compatibility + if network.DriverOpts == nil { + t.Errorf("expected network to have driver options for Docker v28+ compatibility") } - // And the project should have the network - if _, exists := project.Networks["test-network"]; !exists { - t.Errorf("expected network test-network to exist") + // Check for the specific driver option that bypasses Docker v28 security hardening + expectedDriverOpt := "com.docker.network.bridge.gateway_mode_ipv4" + driverOptValue, exists := network.DriverOpts[expectedDriverOpt] + if !exists { + t.Errorf("expected network to have driver option %s for Docker v28+ compatibility", expectedDriverOpt) } - }) -} -// TestDockerVirt_SupportsDockerEngineV28Plus tests the Docker Engine v28+ version detection functionality. -func TestDockerVirt_SupportsDockerEngineV28Plus(t *testing.T) { - setup := func(t *testing.T) (*DockerVirt, *Mocks) { - t.Helper() - mocks := setupDockerMocks(t) - dockerVirt := NewDockerVirt(mocks.Injector) - dockerVirt.shims = mocks.Shims - if err := dockerVirt.Initialize(); err != nil { - t.Fatalf("Failed to initialize DockerVirt: %v", err) + // Verify the driver option is set to nat-unprotected + expectedValue := "nat-unprotected" + if driverOptValue != expectedValue { + t.Errorf("expected driver option %s to be '%s', got '%s'", expectedDriverOpt, expectedValue, driverOptValue) } - return dockerVirt, mocks - } - - t.Run("DockerEngineV28Plus", func(t *testing.T) { - // Given a docker virt instance with valid mocks - dockerVirt, mocks := setup(t) - // And Docker Engine v28+ is detected - mocks.Shell.ExecSilentFunc = func(command string, args ...string) (string, error) { - if command == "docker" && len(args) >= 3 && args[0] == "version" && args[1] == "--format" && args[2] == "{{.Server.Version}}" { - return "28.0.3", nil - } - return "", fmt.Errorf("unexpected command: %s %v", command, args) + // Verify the network driver is bridge (required for this compatibility fix) + if network.Driver != "bridge" { + t.Errorf("expected network driver to be 'bridge', got '%s'", network.Driver) } - // When checking if Docker Engine v28+ is supported - supported := dockerVirt.supportsDockerEngineV28Plus() - - // Then it should return true - if !supported { - t.Errorf("expected Docker Engine v28+ to be supported, got false") - } }) t.Run("DockerEngineV28Exact", func(t *testing.T) {