Skip to content

Unify Docker host / scheme resolution into a single seam (refactor follow-up to #606, #607, #609) #617

Description

@CybotTM

Surfaced during the refactor review of #612. Same antipattern family as the bugs fixed by #606, #607, #609 — multiple readers of the same Docker-host configuration that can drift.

Today's smells in core/adapters/docker/client.go

  1. Dual DOCKER_HOST readers. NewClientWithConfig reads os.Getenv("DOCKER_HOST") to compute the normalized host for client.WithHost(...) and createHTTPClient(...). Meanwhile client.FromEnv is still in the SDK options list and reads DOCKER_HOST independently inside the SDK. Same pattern as the original #605 bug. If env changes between the two reads (t.Setenv cross-test contamination, parallel goroutines, dynamic reconfig), SDK and transport disagree.
  2. createHTTPClient re-derives the host via resolveDockerHost(config.Host) (or resolveHostForTransport in #613) instead of being passed the already-resolved values. Caller has the work; callee redoes it.
  3. Two sources of truth for the scheme allow-list: var supportedDockerHostSchemes = []string{...} and the switch in createHTTPClient. Adding a new scheme (e.g. wss://) requires touching both. A map[string]schemeKind (where schemeKind is kindUnix | kindTLS | kindPlain | kindNamedPipe) collapses them.
  4. Magic strings for scheme names appear in: the allow-list, the switch, strings.TrimPrefix(host, "unix://"), formatSupportedSchemes, and tests. const ( schemeUnix = "unix"; ... ) once.
  5. Test boilerplateTestNewClientWithConfig_UnsupportedSchemes and TestValidateAndNormalizeHost duplicate the unsupported-schemes list.
  6. formatSupportedSchemes recomputes per error. Cache once into var supportedSchemesMsg.
  7. The WithHTTPClient / FromEnv TLS material clobber from #607 is the same root pattern (multiple option-ordering writers on the SDK client). The unified seam is the natural place to subsume that path too.

Suggested refactor (single PR)

Introduce one source of truth:

type dockerHostConfig struct {
    URL         string       // resolved + normalized, e.g. "tcp://host:2375"
    Scheme      string       // lowercase, e.g. "tcp"
    SocketPath  string       // for unix:// only
    TLSConfig   *tls.Config  // populated by resolveDockerHost when env / config asks for it
}

func resolveDockerHost(cfg *ClientConfig) (dockerHostConfig, error) { ... }

NewClientWithConfig calls resolveDockerHost once and threads the result through client.WithHost, createHTTPClient, and the TLS branch. Drop client.FromEnv from opts (we already mirror its host + TLS resolution) so there is no second reader.

The dialer-selection switch becomes a map[string]func(transport, dockerHostConfig) so the allow-list is implicit.

Acceptance

Severity

Medium — pure refactor. No new behavior, no security implications. Sequence: land #606, #611, #612, #613 first, then refactor on top so the bisect story for each fix stays clean.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions