From aac1cc87bedd6f0e92aaa763ebb49ee92e623478 Mon Sep 17 00:00:00 2001 From: Rogan Lynch Date: Fri, 12 Dec 2025 19:55:59 -0800 Subject: [PATCH] feat: implement dynamic Docker API version negotiation Replace hardcoded API version 1.44 with automatic version detection. The previous approach had issues: - dockertest.NewPool("") auto-negotiated to API 1.25 (too old) - Modern Docker daemons reject old versions causing broken pipe errors - Hardcoded 1.44 worked but wasn't portable across Docker installations New approach: - Query Docker daemon's /version endpoint to get MinAPIVersion - Use the server's minimum supported version (e.g., 1.44 for Docker Desktop) - Fallback to 1.41 if MinAPIVersion not available - Respects DOCKER_HOST environment variable This ensures compatibility across Docker Desktop, Colima, and other installations while avoiding the broken pipe errors from outdated API versions. --- integration/scenario.go | 49 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/integration/scenario.go b/integration/scenario.go index e22026fd..8fa6c80c 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -158,12 +158,55 @@ func (s *Scenario) prefixedNetworkName(name string) string { return s.testHashPrefix + "-" + name } +// createDockerClient creates a Docker client with automatic API version negotiation. +// This queries the Docker daemon to determine the supported API version range and selects +// an appropriate version, ensuring compatibility across different Docker installations. +func createDockerClient() (*docker.Client, error) { + // Determine the Docker endpoint (socket path) + endpoint := "unix:///var/run/docker.sock" + if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" { + endpoint = dockerHost + } + + // First, create a temporary client to query the server's API version. + // We use an empty version string which allows us to call the /version endpoint. + tmpClient, err := docker.NewVersionedClient(endpoint, "") + if err != nil { + return nil, fmt.Errorf("failed to create temporary Docker client: %w", err) + } + tmpClient.SkipServerVersionCheck = true // Allow querying any server + + // Query the server's API version information + env, err := tmpClient.Version() + if err != nil { + return nil, fmt.Errorf("failed to query Docker server version: %w", err) + } + + // Get the server's minimum supported API version + // This is the safe version to use that the server will accept + serverMinVersion := env.Get("MinAPIVersion") + if serverMinVersion == "" { + // Fallback to a reasonable default if MinAPIVersion not available + serverMinVersion = "1.41" + } + + // Now create the actual client with the negotiated version + client, err := docker.NewVersionedClient(endpoint, serverMinVersion) + if err != nil { + return nil, fmt.Errorf("failed to create Docker client with version %s: %w", serverMinVersion, err) + } + client.SkipServerVersionCheck = false + + return client, nil +} + // NewScenario creates a test Scenario which can be used to bootstraps a ControlServer with // a set of Users and TailscaleClients. func NewScenario(spec ScenarioSpec) (*Scenario, error) { - // dockertest.NewPool("") defaults to an old client version (1.25) which is rejected by newer Docker daemons. - // We must force a newer version (1.44) compatible with the host Docker environment. - client, err := docker.NewVersionedClient("unix:///var/run/docker.sock", "1.44") + // Create a Docker client with automatic API version negotiation. + // This ensures compatibility across different Docker installations by automatically + // selecting the highest mutually supported API version between client and server. + client, err := createDockerClient() if err != nil { return nil, fmt.Errorf("could not connect to docker: %w", err) }