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
8 changes: 8 additions & 0 deletions plugins/inputs/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
defaultHeaders = map[string]string{"User-Agent": "engine-api-cli-1.0"}
)

//nolint:interfacebloat // wrapping upstream docker client which has many methods
type dockerClient interface {
// Info retrieves system-wide information about the Docker server.
Info(ctx context.Context) (system.Info, error)
Expand All @@ -35,6 +36,8 @@ type dockerClient interface {
DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error)
// ClientVersion retrieves the version of the Docker client.
ClientVersion() string
// Ping pings the server and returns information about the server.
Ping(ctx context.Context) (types.Ping, error)
// Close releases any resources held by the client.
Close() error
}
Expand Down Expand Up @@ -114,6 +117,11 @@ func (c *socketClient) ClientVersion() string {
return c.client.ClientVersion()
}

// Ping pings the server and returns information about the server.
func (c *socketClient) Ping(ctx context.Context) (types.Ping, error) {
return c.client.Ping(ctx)
}

// Close releases any resources held by the client.
func (c *socketClient) Close() error {
return c.client.Close()
Expand Down
28 changes: 23 additions & 5 deletions plugins/inputs/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/internal/docker"
docker_stats "github.com/influxdata/telegraf/plugins/common/docker"
Expand Down Expand Up @@ -143,16 +145,28 @@ func (d *Docker) Init() error {
}

func (d *Docker) Start(telegraf.Accumulator) error {
// Get client
// Get client - this only creates the client object, doesn't connect
c, err := d.getNewClient()
if err != nil {
return err
}
d.client = c

// Use Ping to check connectivity - this is a lightweight check
ctxPing, cancelPing := context.WithTimeout(context.Background(), time.Duration(d.Timeout))
defer cancelPing()
if _, err := d.client.Ping(ctxPing); err != nil {
d.Stop()
return &internal.StartupError{
Err: fmt.Errorf("failed to ping Docker daemon: %w", err),
Retry: client.IsErrConnectionFailed(err),
}
}

// Check API version compatibility
version, err := semver.NewVersion(d.client.ClientVersion())
if err != nil {
d.Stop()
return fmt.Errorf("failed to parse client version: %w", err)
}

Expand All @@ -165,12 +179,16 @@ func (d *Docker) Start(telegraf.Accumulator) error {
}

// Get info from docker daemon for Podman detection
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(d.Timeout))
defer cancel()
ctxInfo, cancelInfo := context.WithTimeout(context.Background(), time.Duration(d.Timeout))
defer cancelInfo()

info, err := d.client.Info(ctx)
info, err := d.client.Info(ctxInfo)
if err != nil {
return fmt.Errorf("failed to get Docker info: %w", err)
d.Stop()
return &internal.StartupError{
Err: fmt.Errorf("failed to get Docker info: %w", err),
Retry: client.IsErrConnectionFailed(err),
}
}

d.engineHost = info.Name
Expand Down
125 changes: 124 additions & 1 deletion plugins/inputs/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package docker
import (
"context"
"crypto/tls"
"errors"
"io"
"reflect"
"sort"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/models"
"github.com/influxdata/telegraf/testutil"
)

Expand All @@ -32,6 +34,7 @@ type mockClient struct {
NodeListF func() ([]swarm.Node, error)
DiskUsageF func() (types.DiskUsage, error)
ClientVersionF func() string
PingF func() (types.Ping, error)
CloseF func() error
}

Expand Down Expand Up @@ -71,6 +74,10 @@ func (c *mockClient) ClientVersion() string {
return c.ClientVersionF()
}

func (c *mockClient) Ping(context.Context) (types.Ping, error) {
return c.PingF()
}

func (c *mockClient) Close() error {
return c.CloseF()
}
Expand Down Expand Up @@ -103,6 +110,9 @@ var baseClient = mockClient{
ClientVersionF: func() string {
return version
},
PingF: func() (types.Ping, error) {
return types.Ping{}, nil
},
CloseF: func() error {
return nil
},
Expand Down Expand Up @@ -421,7 +431,8 @@ func TestDocker_WindowsMemoryContainerStats(t *testing.T) {
var acc testutil.Accumulator

d := Docker{
Log: testutil.Logger{},
Log: testutil.Logger{},
Timeout: config.Duration(5 * time.Second),
newClient: func(string, *tls.Config) (dockerClient, error) {
return &mockClient{
InfoF: func() (system.Info, error) {
Expand Down Expand Up @@ -451,6 +462,9 @@ func TestDocker_WindowsMemoryContainerStats(t *testing.T) {
ClientVersionF: func() string {
return version
},
PingF: func() (types.Ping, error) {
return types.Ping{}, nil
},
CloseF: func() error {
return nil
},
Expand Down Expand Up @@ -1693,6 +1707,7 @@ func TestPodmanDetection(t *testing.T) {
var acc testutil.Accumulator
d := Docker{
Endpoint: tt.endpoint,
Timeout: config.Duration(5 * time.Second),
newClient: func(string, *tls.Config) (dockerClient, error) {
return &mockClient{
InfoF: func() (system.Info, error) {
Expand All @@ -1711,6 +1726,9 @@ func TestPodmanDetection(t *testing.T) {
ClientVersionF: func() string {
return "1.24.0"
},
PingF: func() (types.Ping, error) {
return types.Ping{}, nil
},
CloseF: func() error {
return nil
},
Expand Down Expand Up @@ -1774,3 +1792,108 @@ func TestPodmanStatsCache(t *testing.T) {
require.NotContains(t, d.statsCache, "old-container")
require.Contains(t, d.statsCache, testID)
}

func TestStartupErrorBehaviorError(t *testing.T) {
// Test that model.Start returns error when Ping fails with default "error" behavior
// Uses the startup-error-behavior framework (TSD-006)
plugin := &Docker{
Timeout: config.Duration(100 * time.Millisecond),
newClient: func(string, *tls.Config) (dockerClient, error) {
return &mockClient{
PingF: func() (types.Ping, error) {
return types.Ping{}, errors.New("connection refused")
},
CloseF: func() error {
return nil
},
}, nil
},
newEnvClient: func() (dockerClient, error) {
return nil, errors.New("not using env client")
},
}
model := models.NewRunningInput(plugin, &models.InputConfig{
Name: "docker",
Alias: "error-test",
})
model.StartupErrors.Set(0)
require.NoError(t, model.Init())

// Starting the plugin will fail with an error because Ping fails
var acc testutil.Accumulator
err := model.Start(&acc)
model.Stop()
require.ErrorContains(t, err, "failed to ping Docker daemon")
}

func TestStartupErrorBehaviorIgnore(t *testing.T) {
// Test that model.Start returns fatal error with "ignore" behavior when Ping fails
plugin := &Docker{
Timeout: config.Duration(100 * time.Millisecond),
newClient: func(string, *tls.Config) (dockerClient, error) {
return &mockClient{
PingF: func() (types.Ping, error) {
return types.Ping{}, errors.New("connection refused")
},
CloseF: func() error {
return nil
},
}, nil
},
newEnvClient: func() (dockerClient, error) {
return nil, errors.New("not using env client")
},
}
model := models.NewRunningInput(plugin, &models.InputConfig{
Name: "docker",
Alias: "ignore-test",
StartupErrorBehavior: "ignore",
})
model.StartupErrors.Set(0)
require.NoError(t, model.Init())

// Starting the plugin will fail and model should convert to fatal error
var acc testutil.Accumulator
err := model.Start(&acc)
model.Stop()
require.ErrorContains(t, err, "failed to ping Docker daemon")
}

func TestStartSuccess(t *testing.T) {
// Test that Start succeeds when Docker is available
plugin := &Docker{
Timeout: config.Duration(5 * time.Second),
newClient: func(string, *tls.Config) (dockerClient, error) {
return &mockClient{
PingF: func() (types.Ping, error) {
return types.Ping{}, nil
},
InfoF: func() (system.Info, error) {
return system.Info{
Name: "docker-desktop",
ServerVersion: "20.10.0",
}, nil
},
ClientVersionF: func() string {
return "1.24.0"
},
CloseF: func() error {
return nil
},
}, nil
},
newEnvClient: func() (dockerClient, error) {
return nil, errors.New("not using env client")
},
}
model := models.NewRunningInput(plugin, &models.InputConfig{
Name: "docker",
Alias: "success-test",
})
model.StartupErrors.Set(0)
require.NoError(t, model.Init())

var acc testutil.Accumulator
require.NoError(t, model.Start(&acc))
model.Stop()
}
Loading