Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/briandowns/spinner v1.23.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
Expand Down Expand Up @@ -227,6 +228,7 @@ require (
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect
Expand Down Expand Up @@ -272,6 +274,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
Expand Down Expand Up @@ -367,6 +370,7 @@ require (
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
Expand Down Expand Up @@ -394,10 +398,14 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
Expand Down
69 changes: 68 additions & 1 deletion go.sum

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions pkg/fanal/image/daemon/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package daemon

import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
cliflags "github.com/docker/cli/cli/flags"
"golang.org/x/xerrors"
)

// resolveDockerHost resolves the Docker daemon host based on priority:
// 1. --docker-host flag (highest priority)
// 2. DOCKER_HOST environment variable (handled by NewAPIClientFromFlags)
// 3. DOCKER_CONTEXT environment variable (handled by NewAPIClientFromFlags)
// 4. Current Docker context (default, handled by NewAPIClientFromFlags)
func resolveDockerHost(hostFlag string) (string, error) {
// --docker-host flag
if hostFlag != "" {
return hostFlag, nil
}

// For DOCKER_HOST, DOCKER_CONTEXT and current context resolution, use docker/cli
// This approach validates context existence and returns proper errors
opts := &cliflags.ClientOptions{}

// Load config from DOCKER_CONFIG or default location
configFile, err := config.Load("")
if err != nil {
return "", xerrors.Errorf("failed to load Docker config: %w", err)
}

apiClient, err := command.NewAPIClientFromFlags(opts, configFile)
if err != nil {
return "", xerrors.Errorf("failed to create Docker API client: %w", err)
}
defer apiClient.Close()

// Get the host from the client
return apiClient.DaemonHost(), nil
}
145 changes: 145 additions & 0 deletions pkg/fanal/image/daemon/context_private_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package daemon

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
dockerclient "github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const testContextName = "test-context"

// createTestContext creates a Docker context using docker/cli context store API
func createTestContext(dockerConfigDir string) error {
// Create context store with proper configuration
cfg := store.NewConfig(
func() any { return &store.Metadata{} },
store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
)
contextStore := store.New(dockerConfigDir, cfg)

// Create context metadata
contextMetadata := store.Metadata{
Name: testContextName,
Endpoints: map[string]any{
docker.DockerEndpoint: docker.EndpointMeta{
Host: testContextHost,
},
},
}

// Create or update the context
return contextStore.CreateOrUpdate(contextMetadata)
}

// TestResolveDockerHost tests Docker host resolution with various scenarios
// It's challenging to test it through DockerImage due to the need for a Docker daemon,
// so we test the resolveDockerHost function directly, although it's private.
func TestResolveDockerHost(t *testing.T) {
tests := []struct {
name string
hostFlag string
hostEnv string
contextEnv string
currentContext string
want string
wantErr string
}{
{
name: "flag takes highest priority",
hostFlag: testFlagHost,
hostEnv: testEnvHost,
contextEnv: "",
currentContext: "",
want: testFlagHost,
},
{
name: "DOCKER_HOST takes priority over context",
hostFlag: "",
hostEnv: testEnvHost,
contextEnv: "",
currentContext: "",
want: testEnvHost,
},
{
name: "valid context is used",
hostFlag: "",
hostEnv: "",
contextEnv: testContextName,
currentContext: "",
want: testContextHost,
},
{
name: "current context is used when no options",
hostFlag: "",
hostEnv: "",
contextEnv: "",
currentContext: testContextName,
want: testContextHost,
},
{
name: "default context uses default socket when no options",
hostFlag: "",
hostEnv: "",
contextEnv: "",
currentContext: "",
want: dockerclient.DefaultDockerHost,
},
{
name: "invalid context fails",
hostFlag: "",
hostEnv: "",
contextEnv: "non-existent-context",
currentContext: "",
wantErr: "failed to create Docker API client",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create temporary Docker config directory
testDir := t.TempDir()

t.Setenv("DOCKER_CONFIG", testDir)
t.Setenv("DOCKER_HOST", tt.hostEnv)
t.Setenv("DOCKER_CONTEXT", tt.contextEnv)

// Set the config directory for docker/cli to use
// This is required to handle global state in docker/cli config.
// Due to sync.Once in docker/cli, this cannot be fully cleaned up after tests.
config.SetDir(testDir)

// Always create a test context
contextDir := filepath.Join(testDir, "contexts")

err := createTestContext(contextDir)
require.NoError(t, err)

// Create config.json
configData := map[string]any{
"currentContext": tt.currentContext,
}

configJSON, err := json.MarshalIndent(configData, "", " ")
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(testDir, "config.json"), configJSON, 0o644))

// Test resolveDockerHost
got, err := resolveDockerHost(tt.hostFlag)
if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}

require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
11 changes: 11 additions & 0 deletions pkg/fanal/image/daemon/context_private_test_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !windows

package daemon

const (
testContextHost = "unix:///tmp/test-context.sock"

// Test socket paths for Unix systems
testFlagHost = "unix:///tmp/flag-docker.sock"
testEnvHost = "unix:///tmp/env-docker.sock"
)
9 changes: 9 additions & 0 deletions pkg/fanal/image/daemon/context_private_test_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package daemon

const (
testContextHost = "npipe:////./pipe/test_docker_engine"

// Test socket paths for Windows systems
testFlagHost = "npipe:////./pipe/flag_docker"
testEnvHost = "npipe:////./pipe/env_docker"
)
11 changes: 8 additions & 3 deletions pkg/fanal/image/daemon/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import (
func DockerImage(ref name.Reference, host string) (Image, func(), error) {
cleanup := func() {}

// Resolve Docker host based on priority: --docker-host > DOCKER_HOST > DOCKER_CONTEXT > current context
resolvedHost, err := resolveDockerHost(host)
if err != nil {
return nil, cleanup, xerrors.Errorf("failed to resolve Docker host: %w", err)
}

opts := []client.Opt{
client.FromEnv,
client.WithAPIVersionNegotiation(),
}
if host != "" {
// adding host parameter to the last assuming it will pick up more preference
opts = append(opts, client.WithHost(host))
if resolvedHost != "" {
opts = append(opts, client.WithHost(resolvedHost))
}
c, err := client.NewClientWithOpts(opts...)

Expand Down