Skip to content
Open
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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ lint-all:
@echo "Running lint in all modules..."
$(call for-all-modules,make lint)

tidy-all:
@echo "Running lint in all modules..."
$(call for-all-modules,go mod tidy)

tidy-all:
@echo "Running tidy in all modules..."
$(call for-all-modules,go mod tidy)
Expand Down
31 changes: 0 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,35 +237,6 @@ func main() {

</details>

#### Reading the current Docker context

With the `moby/moby/client` package, you basically can't do it, as this functionality is part of the client code of the Docker CLI.

With the `go-sdk`, you can do:

<details>
<summary>See the code</summary>

```go
package main

import (
"fmt"

"github.com/docker/go-sdk/context"
)

func main() {
ctx, err := context.Current()
if err != nil {
panic(err)
}
fmt.Println("Current Docker context name:", ctx)
}
```

</details>

## Features

- Initialize a Docker client, using the current Docker context to resolve the Docker host and socket
Expand All @@ -278,9 +249,7 @@ func main() {

```bash
go get github.com/docker/go-sdk/client
go get github.com/docker/go-sdk/config
go get github.com/docker/go-sdk/container
go get github.com/docker/go-sdk/context
go get github.com/docker/go-sdk/image
go get github.com/docker/go-sdk/network
go get github.com/docker/go-sdk/volume
Expand Down
128 changes: 15 additions & 113 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,17 @@ import (
"fmt"
"io"
"log/slog"
"maps"
"path/filepath"
"time"

"github.com/docker/docker/client"
dockercontext "github.com/docker/go-sdk/context"
)

const (
// Headers used for docker client requests.
headerUserAgent = "User-Agent"

// TLS certificate files.
tlsCACertFile = "ca.pem"
tlsCertFile = "cert.pem"
tlsKeyFile = "key.pem"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
)

var (
defaultLogger = slog.New(slog.NewTextHandler(io.Discard, nil))

defaultUserAgent = "docker-go-sdk/" + Version()

defaultOpts = []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}

defaultHealthCheck = func(ctx context.Context) func(c SDKClient) error {
return func(c SDKClient) error {
var pingErr error
Expand Down Expand Up @@ -71,112 +57,28 @@ func New(ctx context.Context, options ...ClientOption) (SDKClient, error) {
log: defaultLogger,
healthCheck: defaultHealthCheck,
}
for _, opt := range options {
if err := opt.Apply(c); err != nil {
return nil, fmt.Errorf("apply option: %w", err)
}
}

if err := c.init(); err != nil {
return nil, fmt.Errorf("load config: %w", err)
}

if err := c.healthCheck(ctx)(c); err != nil {
return nil, fmt.Errorf("health check: %w", err)
}

return c, nil
}

// init initializes the client.
// This method is safe for concurrent use by multiple goroutines.
func (c *sdkClient) init() error {
if c.APIClient != nil || c.err != nil {
return c.err
}

// Set the default values for the client:
// - log
// - dockerHost
// - currentContext
if c.err = c.defaultValues(); c.err != nil {
return fmt.Errorf("default values: %w", c.err)
}

if c.cfg, c.err = newConfig(c.dockerHost); c.err != nil {
return c.err
}

opts := make([]client.Opt, len(defaultOpts), len(defaultOpts)+len(c.dockerOpts))
copy(opts, defaultOpts)

// Add all collected Docker options
opts = append(opts, c.dockerOpts...)

if c.cfg.TLSVerify {
// For further information see:
// https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket
opts = append(opts, client.WithTLSClientConfig(
filepath.Join(c.cfg.CertPath, tlsCACertFile),
filepath.Join(c.cfg.CertPath, tlsCertFile),
filepath.Join(c.cfg.CertPath, tlsKeyFile),
))
}
if c.cfg.Host != "" {
// apply the host from the config if it is set
opts = append(opts, client.WithHost(c.cfg.Host))
}

httpHeaders := make(map[string]string)
maps.Copy(httpHeaders, c.extraHeaders)

// Append the SDK headers last.
httpHeaders[headerUserAgent] = defaultUserAgent

opts = append(opts, client.WithHTTPHeaders(httpHeaders))

api, err := client.NewClientWithOpts(opts...)
cli, err := command.NewDockerCli(command.WithUserAgent(defaultUserAgent))
if err != nil {
return fmt.Errorf("new client: %w", err)
return nil, err
}
c.APIClient = api
return nil
}

// defaultValues sets the default values for the client.
// If no logger is provided, the default one is used.
// If no docker host is provided and no docker context is provided, the current docker host and context are used.
// If no docker host is provided but a docker context is provided, the docker host from the context is used.
// If a docker host is provided, it is used as is.
func (c *sdkClient) defaultValues() error {
if c.log == nil {
c.log = defaultLogger
err = cli.Initialize(flags.NewClientOptions())
if err != nil {
return nil, err
}
c.APIClient = cli.Client()
c.config = cli.ConfigFile()

if c.dockerHost == "" && c.dockerContext == "" {
currentDockerHost, err := dockercontext.CurrentDockerHost()
if err != nil {
return fmt.Errorf("current docker host: %w", err)
}
currentContext, err := dockercontext.Current()
if err != nil {
return fmt.Errorf("current context: %w", err)
for _, opt := range options {
if err := opt.Apply(c); err != nil {
return nil, fmt.Errorf("apply option: %w", err)
}

c.dockerHost = currentDockerHost
c.dockerContext = currentContext

return nil
}

if c.dockerContext != "" {
dockerHost, err := dockercontext.DockerHostFromContext(c.dockerContext)
if err != nil {
return fmt.Errorf("docker host from context: %w", err)
}

c.dockerHost = dockerHost
if err := c.healthCheck(ctx)(c); err != nil {
return nil, fmt.Errorf("health check: %w", err)
}

return nil
return c, nil
}
80 changes: 0 additions & 80 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,13 @@ package client_test

import (
"context"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

dockerclient "github.com/docker/docker/client"
"github.com/docker/go-sdk/client"
dockercontext "github.com/docker/go-sdk/context"
)

var noopHealthCheck = func(_ context.Context) func(c client.SDKClient) error {
return func(_ client.SDKClient) error {
return nil
}
}

func TestNew(t *testing.T) {
t.Run("success", func(t *testing.T) {
cli, err := client.New(context.Background())
Expand Down Expand Up @@ -60,75 +51,4 @@ func TestNew(t *testing.T) {
require.NoError(t, cli.Close())
require.NoError(t, cli.Close())
})

t.Run("success/tls-verify", func(t *testing.T) {
t.Setenv("DOCKER_TLS_VERIFY", "1")
t.Setenv("DOCKER_CERT_PATH", filepath.Join("testdata", "certificates"))

cli, err := client.New(context.Background())
require.Error(t, err)
require.Nil(t, cli)
})

t.Run("success/apply-option", func(t *testing.T) {
cli, err := client.New(context.Background(), client.FromDockerOpt(dockerclient.WithHost("tcp://foobar:2375")))
require.NoError(t, err)
require.NotNil(t, cli)
})

t.Run("error", func(t *testing.T) {
cli, err := client.New(context.Background(), client.FromDockerOpt(dockerclient.WithHost("foobar")))
require.Error(t, err)
require.Nil(t, cli)
})

t.Run("healthcheck/nil", func(t *testing.T) {
cli, err := client.New(context.Background(), client.WithHealthCheck(nil))
require.ErrorContains(t, err, "health check is nil")
require.Nil(t, cli)
})

t.Run("healthcheck/noop", func(t *testing.T) {
cli, err := client.New(context.Background(), client.WithHealthCheck(noopHealthCheck))
require.NoError(t, err)
require.NotNil(t, cli)
})

t.Run("healthcheck/info", func(t *testing.T) {
t.Setenv(dockercontext.EnvOverrideHost, "tcp://foobar:2375") // this URL is parseable, although not reachable

infoHealthCheck := func(ctx context.Context) func(c client.SDKClient) error {
return func(c client.SDKClient) error {
_, err := c.Info(ctx)
return err
}
}

cli, err := client.New(context.Background(), client.WithHealthCheck(infoHealthCheck))
require.Error(t, err)
require.Nil(t, cli)
})

t.Run("docker-host/precedence", func(t *testing.T) {
t.Run("env-var-wins", func(t *testing.T) {
t.Setenv(dockercontext.EnvOverrideHost, "tcp://foobar:2375") // this URL is parseable, although not reachable
cli, err := client.New(context.Background())
require.Error(t, err)
require.Nil(t, cli)
})

t.Run("context-wins/found", func(t *testing.T) {
t.Setenv(dockercontext.EnvOverrideContext, dockercontext.DefaultContextName)
cli, err := client.New(context.Background(), client.WithHealthCheck(noopHealthCheck))
require.NoError(t, err)
require.NotNil(t, cli)
})

t.Run("context-wins/not-found", func(t *testing.T) {
t.Setenv(dockercontext.EnvOverrideContext, "foocontext") // this context does not exist
cli, err := client.New(context.Background())
require.Error(t, err)
require.Nil(t, cli)
})
})
}
Loading
Loading