1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 21:33:18 +01:00

chore(refactor): refactors docker client for better testing (#2302)

* chore(refactor): refactors docker client for better testing

* more refactoring and clenaing up tests
This commit is contained in:
Amir Raminfar
2023-07-11 13:21:46 -07:00
committed by GitHub
parent bbc6853f96
commit 8c8987b666
9 changed files with 157 additions and 149 deletions

View File

@@ -20,12 +20,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type dockerClient struct {
cli dockerProxy
filters filters.Args
host *Host
}
type StdType int type StdType int
const ( const (
@@ -48,7 +42,7 @@ func (s StdType) String() string {
} }
} }
type dockerProxy interface { type DockerCLI interface {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error) ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error) Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
@@ -57,20 +51,18 @@ type dockerProxy interface {
Ping(ctx context.Context) (types.Ping, error) Ping(ctx context.Context) (types.Ping, error)
} }
// Client is a proxy around the docker client type Client struct {
type Client interface { cli DockerCLI
ListContainers() ([]Container, error) filters filters.Args
FindContainer(string) (Container, error) host *Host
ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error) }
Events(context.Context, chan<- ContainerEvent) <-chan error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error) func NewClient(cli DockerCLI, filters filters.Args, host *Host) *Client {
ContainerStats(context.Context, string, chan<- ContainerStat) error return &Client{cli, filters, host}
Ping(context.Context) (types.Ping, error)
Host() *Host
} }
// NewClientWithFilters creates a new instance of Client with docker filters // NewClientWithFilters creates a new instance of Client with docker filters
func NewClientWithFilters(f map[string][]string) (Client, error) { func NewClientWithFilters(f map[string][]string) (*Client, error) {
filterArgs := filters.NewArgs() filterArgs := filters.NewArgs()
for key, values := range f { for key, values := range f {
for _, value := range values { for _, value := range values {
@@ -86,10 +78,10 @@ func NewClientWithFilters(f map[string][]string) (Client, error) {
return nil, err return nil, err
} }
return &dockerClient{cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}}, nil return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil
} }
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) { func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error) {
filterArgs := filters.NewArgs() filterArgs := filters.NewArgs()
for key, values := range f { for key, values := range f {
for _, value := range values { for _, value := range values {
@@ -122,10 +114,10 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error)
return nil, err return nil, err
} }
return &dockerClient{cli, filterArgs, &host}, nil return NewClient(cli, filterArgs, &host), nil
} }
func (d *dockerClient) FindContainer(id string) (Container, error) { func (d *Client) FindContainer(id string) (Container, error) {
var container Container var container Container
containers, err := d.ListContainers() containers, err := d.ListContainers()
if err != nil { if err != nil {
@@ -153,7 +145,7 @@ func (d *dockerClient) FindContainer(id string) (Container, error) {
return container, nil return container, nil
} }
func (d *dockerClient) ListContainers() ([]Container, error) { func (d *Client) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{ containerListOptions := types.ContainerListOptions{
Filters: d.filters, Filters: d.filters,
All: true, All: true,
@@ -188,7 +180,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
return containers, nil return containers, nil
} }
func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error { func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
response, err := d.cli.ContainerStats(ctx, id, true) response, err := d.cli.ContainerStats(ctx, id, true)
if err != nil { if err != nil {
@@ -240,7 +232,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil return nil
} }
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) { func (d *Client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container") log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" { if since != "" {
@@ -268,7 +260,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
return reader, nil return reader, nil
} }
func (d *dockerClient) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error { func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{}) dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
go func() { go func() {
@@ -296,7 +288,7 @@ func (d *dockerClient) Events(ctx context.Context, messages chan<- ContainerEven
return errors return errors
} }
func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) { func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{ options := types.ContainerLogsOptions{
ShowStdout: stdType&STDOUT != 0, ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0, ShowStderr: stdType&STDERR != 0,
@@ -315,11 +307,11 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
return reader, nil return reader, nil
} }
func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) { func (d *Client) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx) return d.cli.Ping(ctx)
} }
func (d *dockerClient) Host() *Host { func (d *Client) Host() *Host {
return d.host return d.host
} }

View File

@@ -19,7 +19,7 @@ import (
type mockedProxy struct { type mockedProxy struct {
mock.Mock mock.Mock
dockerProxy DockerCLI
} }
func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) { func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
@@ -53,7 +53,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
func Test_dockerClient_ListContainers_null(t *testing.T) { func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy) proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers() list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty") assert.Empty(t, list, "list should be empty")
@@ -65,7 +65,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
func Test_dockerClient_ListContainers_error(t *testing.T) { func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy) proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test")) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers() list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil") assert.Nil(t, list, "list should be nil")
@@ -88,7 +88,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy := new(mockedProxy) proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers() list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.") require.NoError(t, err, "error should not return an error.")
@@ -125,7 +125,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"} options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil) proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL) logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
actual, _ := io.ReadAll(logReader) actual, _ := io.ReadAll(logReader)
@@ -139,7 +139,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test")) proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL) reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
@@ -166,7 +166,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
json := types.ContainerJSON{Config: &container.Config{Tty: false}} json := types.ContainerJSON{Config: &container.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil) proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
container, err := client.FindContainer("abcdefghijkl") container, err := client.FindContainer("abcdefghijkl")
require.NoError(t, err, "error should not be thrown") require.NoError(t, err, "error should not be thrown")
@@ -195,7 +195,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy) proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}} client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
_, err := client.FindContainer("not_valid") _, err := client.FindContainer("not_valid")
require.Error(t, err, "error should be thrown") require.Error(t, err, "error should be thrown")

View File

@@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"io" "io"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -197,38 +196,3 @@ func checkPosition(currentEvent *LogEvent, nextEvent *LogEvent) {
currentEvent.Position = END currentEvent.Position = END
} }
} }
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
var ANSI_COLOR_REGEX = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func guessLogLevel(logEvent *LogEvent) string {
switch value := logEvent.Message.(type) {
case string:
levels := []string{"error", "warn", "warning", "info", "debug", "trace", "fatal"}
stripped := ANSI_COLOR_REGEX.ReplaceAllString(value, "") // remove ansi color codes
for _, level := range levels {
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
return level
}
if strings.Contains(value, "["+strings.ToUpper(level)+"]") {
return level
}
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
return level
}
}
if matches := KEY_VALUE_REGEX.FindStringSubmatch(value); matches != nil {
return matches[1]
}
case map[string]interface{}:
if level, ok := value["level"].(string); ok {
return level
}
}
return ""
}

41
docker/level_guesser.go Normal file
View File

@@ -0,0 +1,41 @@
package docker
import (
"regexp"
"strings"
)
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
var ANSI_COLOR_REGEX = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func guessLogLevel(logEvent *LogEvent) string {
switch value := logEvent.Message.(type) {
case string:
levels := []string{"error", "warn", "warning", "info", "debug", "trace", "fatal"}
stripped := ANSI_COLOR_REGEX.ReplaceAllString(value, "") // remove ansi color codes
for _, level := range levels {
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
return level
}
if strings.Contains(value, "["+strings.ToUpper(level)+"]") {
return level
}
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
return level
}
}
if matches := KEY_VALUE_REGEX.FindStringSubmatch(value); matches != nil {
return matches[1]
}
case map[string]interface{}:
if level, ok := value["level"].(string); ok {
return level
}
}
return ""
}

10
main.go
View File

@@ -137,8 +137,10 @@ func doStartEvent(arg args) {
} }
} }
func createClients(args args, localClientFactory func(map[string][]string) (docker.Client, error), remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error)) map[string]docker.Client { func createClients(args args,
clients := make(map[string]docker.Client) localClientFactory func(map[string][]string) (*docker.Client, error),
remoteClientFactory func(map[string][]string, docker.Host) (*docker.Client, error)) map[string]web.DockerClient {
clients := make(map[string]web.DockerClient)
if localClient := createLocalClient(args, localClientFactory); localClient != nil { if localClient := createLocalClient(args, localClientFactory); localClient != nil {
clients[localClient.Host().ID] = localClient clients[localClient.Host().ID] = localClient
@@ -166,7 +168,7 @@ func createClients(args args, localClientFactory func(map[string][]string) (dock
return clients return clients
} }
func createServer(args args, clients map[string]docker.Client) *http.Server { func createServer(args args, clients map[string]web.DockerClient) *http.Server {
_, dev := os.LookupEnv("DEV") _, dev := os.LookupEnv("DEV")
config := web.Config{ config := web.Config{
Addr: args.Addr, Addr: args.Addr,
@@ -206,7 +208,7 @@ func createServer(args args, clients map[string]docker.Client) *http.Server {
return web.CreateServer(clients, assets, config) return web.CreateServer(clients, assets, config)
} }
func createLocalClient(args args, localClientFactory func(map[string][]string) (docker.Client, error)) docker.Client { func createLocalClient(args args, localClientFactory func(map[string][]string) (*docker.Client, error)) *docker.Client {
for i := 1; ; i++ { for i := 1; ; i++ {
dockerClient, err := localClientFactory(args.Filter) dockerClient, err := localClientFactory(args.Filter)
if err == nil { if err == nil {

View File

@@ -1,37 +1,34 @@
package main package main
import ( import (
"context"
"errors" "errors"
"testing" "testing"
"github.com/amir20/dozzle/docker" "github.com/amir20/dozzle/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
type fakeClient struct { type fakeCLI struct {
docker.Client docker.DockerCLI
mock.Mock mock.Mock
} }
func (f *fakeClient) ListContainers() ([]docker.Container, error) { func (f *fakeCLI) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
args := f.Called() args := f.Called()
return args.Get(0).([]docker.Container), args.Error(1) return args.Get(0).([]types.Container), args.Error(1)
}
func (f *fakeClient) Host() *docker.Host {
args := f.Called()
return args.Get(0).(*docker.Host)
} }
func Test_valid_localhost(t *testing.T) { func Test_valid_localhost(t *testing.T) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { client := new(fakeCLI)
client := new(fakeClient) client.On("ContainerList").Return([]types.Container{}, nil)
client.On("ListContainers").Return([]docker.Container{}, nil) fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "localhost", ID: "localhost",
}) }), nil
return client, nil
} }
args := args{} args := args{}
@@ -39,16 +36,16 @@ func Test_valid_localhost(t *testing.T) {
actualClient := createLocalClient(args, fakeClientFactory) actualClient := createLocalClient(args, fakeClientFactory)
assert.NotNil(t, actualClient) assert.NotNil(t, actualClient)
client.AssertExpectations(t)
} }
func Test_invalid_localhost(t *testing.T) { func Test_invalid_localhost(t *testing.T) {
fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { client := new(fakeCLI)
client := new(fakeClient) client.On("ContainerList").Return([]types.Container{}, errors.New("error"))
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) fakeClientFactory := func(filter map[string][]string) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "localhost", ID: "localhost",
}) }), nil
return client, nil
} }
args := args{} args := args{}
@@ -56,26 +53,24 @@ func Test_invalid_localhost(t *testing.T) {
actualClient := createLocalClient(args, fakeClientFactory) actualClient := createLocalClient(args, fakeClientFactory)
assert.Nil(t, actualClient) assert.Nil(t, actualClient)
client.AssertExpectations(t)
} }
func Test_valid_remote(t *testing.T) { func Test_valid_remote(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { local := new(fakeCLI)
client := new(fakeClient) local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(local, filters.NewArgs(), &docker.Host{
ID: "localhost", ID: "localhost",
}) }), nil
return client, nil
} }
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { remote := new(fakeCLI)
client := new(fakeClient) remote.On("ContainerList").Return([]types.Container{}, nil)
client.On("ListContainers").Return([]docker.Container{}, nil) fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
ID: "test", ID: "test",
}) }), nil
return client, nil
} }
args := args{ args := args{
@@ -87,27 +82,26 @@ func Test_valid_remote(t *testing.T) {
assert.Equal(t, 1, len(clients)) assert.Equal(t, 1, len(clients))
assert.Contains(t, clients, "test") assert.Contains(t, clients, "test")
assert.NotContains(t, clients, "localhost") assert.NotContains(t, clients, "localhost")
local.AssertExpectations(t)
remote.AssertExpectations(t)
} }
func Test_valid_remote_and_local(t *testing.T) { func Test_valid_remote_and_local(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { local := new(fakeCLI)
client := new(fakeClient) local.On("ContainerList").Return([]types.Container{}, nil)
client.On("ListContainers").Return([]docker.Container{}, nil) fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(local, filters.NewArgs(), &docker.Host{
ID: "localhost", ID: "localhost",
}) }), nil
return client, nil
} }
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { remote := new(fakeCLI)
client := new(fakeClient) remote.On("ContainerList").Return([]types.Container{}, nil)
client.On("ListContainers").Return([]docker.Container{}, nil) fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{ return docker.NewClient(remote, filters.NewArgs(), &docker.Host{
ID: "test", ID: "test",
}) }), nil
return client, nil
} }
args := args{ args := args{
RemoteHost: []string{"tcp://test:2375"}, RemoteHost: []string{"tcp://test:2375"},
} }
@@ -117,24 +111,24 @@ func Test_valid_remote_and_local(t *testing.T) {
assert.Equal(t, 2, len(clients)) assert.Equal(t, 2, len(clients))
assert.Contains(t, clients, "test") assert.Contains(t, clients, "test")
assert.Contains(t, clients, "localhost") assert.Contains(t, clients, "localhost")
local.AssertExpectations(t)
remote.AssertExpectations(t)
} }
func Test_no_clients(t *testing.T) { func Test_no_clients(t *testing.T) {
fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { local := new(fakeCLI)
client := new(fakeClient) local.On("ContainerList").Return([]types.Container{}, errors.New("error"))
client.On("ListContainers").Return([]docker.Container{}, errors.New("error")) fakeLocalClientFactory := func(filter map[string][]string) (*docker.Client, error) {
client.On("Host").Return(&docker.Host{
ID: "localhost",
})
return client, nil
}
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{
client := new(fakeClient) ID: "localhost",
client.On("Host").Return(&docker.Host{ }), nil
}
fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (*docker.Client, error) {
client := new(fakeCLI)
return docker.NewClient(client, filters.NewArgs(), &docker.Host{
ID: "test", ID: "test",
}) }), nil
return client, nil
} }
args := args{} args := args{}
@@ -142,4 +136,5 @@ func Test_no_clients(t *testing.T) {
clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory) clients := createClients(args, fakeLocalClientFactory, fakeRemoteClientFactory)
assert.Equal(t, 0, len(clients)) assert.Equal(t, 0, len(clients))
local.AssertExpectations(t)
} }

View File

@@ -39,11 +39,11 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
for _, client := range h.clients { for _, client := range h.clients {
client.Events(ctx, events) client.Events(ctx, events)
go func(client docker.Client) { go func(client DockerClient) {
defer wg.Done() defer wg.Done()
if containers, err := client.ListContainers(); err == nil { if containers, err := client.ListContainers(); err == nil {
results <- containers results <- containers
go func(client docker.Client) { go func(client DockerClient) {
for _, c := range containers { for _, c := range containers {
if c.State == "running" { if c.State == "running" {
if err := client.ContainerStats(ctx, c.ID, stats); err != nil && !errors.Is(err, context.Canceled) { if err := client.ContainerStats(ctx, c.ID, stats); err != nil && !errors.Is(err, context.Canceled) {

View File

@@ -1,12 +1,14 @@
package web package web
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs" "io/fs"
"sort" "sort"
"time"
"net/http" "net/http"
"os" "os"
@@ -15,6 +17,7 @@ import (
"github.com/amir20/dozzle/analytics" "github.com/amir20/dozzle/analytics"
"github.com/amir20/dozzle/docker" "github.com/amir20/dozzle/docker"
"github.com/docker/docker/api/types"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -33,13 +36,24 @@ type Config struct {
} }
type handler struct { type handler struct {
clients map[string]docker.Client clients map[string]DockerClient
content fs.FS content fs.FS
config *Config config *Config
} }
// CreateServer creates a service for http handler // Client is a proxy around the docker client
func CreateServer(clients map[string]docker.Client, content fs.FS, config Config) *http.Server { type DockerClient interface {
ListContainers() ([]docker.Container, error)
FindContainer(string) (docker.Container, error)
ContainerLogs(context.Context, string, string, docker.StdType) (io.ReadCloser, error)
Events(context.Context, chan<- docker.ContainerEvent) <-chan error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, docker.StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() *docker.Host
}
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
handler := &handler{ handler := &handler{
clients: clients, clients: clients,
content: content, content: content,
@@ -98,7 +112,7 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
go func() { go func() {
host, _ := os.Hostname() host, _ := os.Hostname()
var client docker.Client var client DockerClient
for _, v := range h.clients { for _, v := range h.clients {
client = v client = v
break break
@@ -216,7 +230,7 @@ func (h *handler) version(w http.ResponseWriter, r *http.Request) {
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) { func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
log.Trace("Executing healthcheck request") log.Trace("Executing healthcheck request")
var client docker.Client var client DockerClient
for _, v := range h.clients { for _, v := range h.clients {
client = v client = v
break break
@@ -230,7 +244,7 @@ func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *handler) clientFromRequest(r *http.Request) docker.Client { func (h *handler) clientFromRequest(r *http.Request) DockerClient {
host := chi.URLParam(r, "host") host := chi.URLParam(r, "host")
if host == "" { if host == "" {

View File

@@ -17,7 +17,7 @@ import (
type MockedClient struct { type MockedClient struct {
mock.Mock mock.Mock
docker.Client DockerClient
} }
func (m *MockedClient) FindContainer(id string) (docker.Container, error) { func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
@@ -54,7 +54,7 @@ func (m *MockedClient) Host() *docker.Host {
return args.Get(0).(*docker.Host) return args.Get(0).(*docker.Host)
} }
func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux { func createHandler(client DockerClient, content fs.FS, config Config) *chi.Mux {
if client == nil { if client == nil {
client = new(MockedClient) client = new(MockedClient)
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil) client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
@@ -69,7 +69,7 @@ func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux
content = afero.NewIOFS(fs) content = afero.NewIOFS(fs)
} }
clients := map[string]docker.Client{ clients := map[string]DockerClient{
"localhost": client, "localhost": client,
} }
return createRouter(&handler{ return createRouter(&handler{
@@ -79,6 +79,6 @@ func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux
}) })
} }
func createDefaultHandler(client docker.Client) *chi.Mux { func createDefaultHandler(client DockerClient) *chi.Mux {
return createHandler(client, nil, Config{Base: "/"}) return createHandler(client, nil, Config{Base: "/"})
} }