mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-04 03:54:58 +01:00
feat!: implements swarm mode with agents (#3058)
This commit is contained in:
@@ -63,13 +63,13 @@ type DockerCLI interface {
|
||||
type Client interface {
|
||||
ListContainers() ([]Container, error)
|
||||
FindContainer(string) (Container, error)
|
||||
ContainerLogs(context.Context, string, *time.Time, StdType) (io.ReadCloser, error)
|
||||
Events(context.Context, chan<- ContainerEvent) error
|
||||
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerEvents(context.Context, chan<- ContainerEvent) error
|
||||
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
|
||||
ContainerStats(context.Context, string, chan<- ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() *Host
|
||||
ContainerActions(action string, containerID string) error
|
||||
Host() Host
|
||||
ContainerActions(action ContainerAction, containerID string) error
|
||||
IsSwarmMode() bool
|
||||
SystemInfo() system.Info
|
||||
}
|
||||
@@ -77,31 +77,33 @@ type Client interface {
|
||||
type httpClient struct {
|
||||
cli DockerCLI
|
||||
filters filters.Args
|
||||
host *Host
|
||||
host Host
|
||||
info system.Info
|
||||
}
|
||||
|
||||
func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client {
|
||||
func NewClient(cli DockerCLI, filters filters.Args, host Host) Client {
|
||||
client := &httpClient{
|
||||
cli: cli,
|
||||
filters: filters,
|
||||
host: host,
|
||||
}
|
||||
|
||||
var err error
|
||||
client.info, err = cli.Info(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("unable to get docker info: %v", err)
|
||||
}
|
||||
if host.MemTotal == 0 || host.NCPU == 0 {
|
||||
var err error
|
||||
client.info, err = cli.Info(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("unable to get docker info: %v", err)
|
||||
}
|
||||
|
||||
host.NCPU = client.info.NCPU
|
||||
host.MemTotal = client.info.MemTotal
|
||||
host.NCPU = client.info.NCPU
|
||||
host.MemTotal = client.info.MemTotal
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// NewClientWithFilters creates a new instance of Client with docker filters
|
||||
func NewClientWithFilters(f map[string][]string) (Client, error) {
|
||||
func NewLocalClient(f map[string][]string, hostname string) (Client, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
@@ -117,10 +119,27 @@ func NewClientWithFilters(f map[string][]string) (Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil
|
||||
info, err := cli.Info(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host := Host{
|
||||
ID: info.ID,
|
||||
Name: info.Name,
|
||||
MemTotal: info.MemTotal,
|
||||
NCPU: info.NCPU,
|
||||
Endpoint: "local",
|
||||
}
|
||||
|
||||
if hostname != "" {
|
||||
host.Name = hostname
|
||||
}
|
||||
|
||||
return NewClient(cli, filterArgs, host), nil
|
||||
}
|
||||
|
||||
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) {
|
||||
func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
|
||||
filterArgs := filters.NewArgs()
|
||||
for key, values := range f {
|
||||
for _, value := range values {
|
||||
@@ -153,10 +172,11 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewClient(cli, filterArgs, &host), nil
|
||||
return NewClient(cli, filterArgs, host), nil
|
||||
}
|
||||
|
||||
func (d *httpClient) FindContainer(id string) (Container, error) {
|
||||
log.Debugf("finding container with id: %s", id)
|
||||
var container Container
|
||||
containers, err := d.ListContainers()
|
||||
if err != nil {
|
||||
@@ -188,13 +208,13 @@ func (d *httpClient) FindContainer(id string) (Container, error) {
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerActions(action string, containerID string) error {
|
||||
func (d *httpClient) ContainerActions(action ContainerAction, containerID string) error {
|
||||
switch action {
|
||||
case "start":
|
||||
case Start:
|
||||
return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{})
|
||||
case "stop":
|
||||
case Stop:
|
||||
return d.cli.ContainerStop(context.Background(), containerID, container.StopOptions{})
|
||||
case "restart":
|
||||
case Restart:
|
||||
return d.cli.ContainerRestart(context.Background(), containerID, container.StopOptions{})
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %s", action)
|
||||
@@ -299,14 +319,10 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
|
||||
}
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since *time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
|
||||
|
||||
sinceQuery := ""
|
||||
if since != nil {
|
||||
sinceQuery = since.Add(time.Millisecond).Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
sinceQuery := since.Add(time.Millisecond).Format(time.RFC3339Nano)
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: stdType&STDOUT != 0,
|
||||
ShowStderr: stdType&STDERR != 0,
|
||||
@@ -324,7 +340,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since *time.T
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) Events(ctx context.Context, messages chan<- ContainerEvent) error {
|
||||
func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- ContainerEvent) error {
|
||||
dockerMessages, err := d.cli.Events(ctx, events.ListOptions{})
|
||||
|
||||
for {
|
||||
@@ -344,7 +360,6 @@ func (d *httpClient) Events(ctx context.Context, messages chan<- ContainerEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
|
||||
@@ -370,7 +385,7 @@ func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) {
|
||||
return d.cli.Ping(ctx)
|
||||
}
|
||||
|
||||
func (d *httpClient) Host() *Host {
|
||||
func (d *httpClient) Host() Host {
|
||||
return d.host
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, op
|
||||
}
|
||||
|
||||
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
|
||||
args := m.Called(ctx, containerID, options)
|
||||
err := args.Get(0)
|
||||
|
||||
@@ -91,7 +90,7 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
|
||||
func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Empty(t, list, "list should be empty")
|
||||
@@ -103,7 +102,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
|
||||
func Test_dockerClient_ListContainers_error(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
assert.Nil(t, list, "list should be nil")
|
||||
@@ -126,7 +125,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
list, err := client.ListContainers()
|
||||
require.NoError(t, err, "error should not return an error.")
|
||||
@@ -160,8 +159,8 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
|
||||
Since: "2021-01-01T00:00:00.001Z"}
|
||||
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
|
||||
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, &since, STDALL)
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
logReader, _ := client.ContainerLogs(context.Background(), id, since, STDALL)
|
||||
|
||||
actual, _ := io.ReadAll(logReader)
|
||||
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
|
||||
@@ -174,9 +173,9 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
|
||||
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
|
||||
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
reader, err := client.ContainerLogs(context.Background(), id, nil, STDALL)
|
||||
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, STDALL)
|
||||
|
||||
assert.Nil(t, reader, "reader should be nil")
|
||||
assert.Error(t, err, "error should have been returned")
|
||||
@@ -202,7 +201,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
container, err := client.FindContainer("abcdefghijkl")
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
@@ -225,7 +224,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
require.Error(t, err, "error should be thrown")
|
||||
@@ -246,7 +245,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
|
||||
@@ -264,7 +263,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
|
||||
actions := []string{"start", "stop", "restart"}
|
||||
for _, action := range actions {
|
||||
err := client.ContainerActions(action, container.ID)
|
||||
err := client.ContainerActions(ContainerAction(action), container.ID)
|
||||
require.NoError(t, err, "error should not be thrown")
|
||||
assert.Equal(t, err, nil)
|
||||
}
|
||||
@@ -285,7 +284,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
@@ -297,7 +296,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
|
||||
actions := []string{"start", "stop", "restart"}
|
||||
for _, action := range actions {
|
||||
err := client.ContainerActions(action, container.ID)
|
||||
err := client.ContainerActions(ContainerAction(action), container.ID)
|
||||
require.Error(t, err, "error should be thrown")
|
||||
assert.Error(t, err, "error should have been returned")
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ContainerStore struct {
|
||||
containers *xsync.MapOf[string, *Container]
|
||||
subscribers *xsync.MapOf[context.Context, chan ContainerEvent]
|
||||
newContainerSubscribers *xsync.MapOf[context.Context, chan Container]
|
||||
subscribers *xsync.MapOf[context.Context, chan<- ContainerEvent]
|
||||
newContainerSubscribers *xsync.MapOf[context.Context, chan<- Container]
|
||||
client Client
|
||||
statsCollector *StatsCollector
|
||||
wg sync.WaitGroup
|
||||
@@ -26,8 +27,8 @@ func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
|
||||
s := &ContainerStore{
|
||||
containers: xsync.NewMapOf[string, *Container](),
|
||||
client: client,
|
||||
subscribers: xsync.NewMapOf[context.Context, chan ContainerEvent](),
|
||||
newContainerSubscribers: xsync.NewMapOf[context.Context, chan Container](),
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerEvent](),
|
||||
newContainerSubscribers: xsync.NewMapOf[context.Context, chan<- Container](),
|
||||
statsCollector: NewStatsCollector(client),
|
||||
wg: sync.WaitGroup{},
|
||||
events: make(chan ContainerEvent),
|
||||
@@ -41,11 +42,13 @@ func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
|
||||
return s
|
||||
}
|
||||
|
||||
var ErrContainerNotFound = errors.New("container not found")
|
||||
|
||||
func (s *ContainerStore) checkConnectivity() error {
|
||||
if s.connected.CompareAndSwap(false, true) {
|
||||
go func() {
|
||||
log.Debugf("subscribing to docker events from container store %s", s.client.Host())
|
||||
err := s.client.Events(s.ctx, s.events)
|
||||
err := s.client.ContainerEvents(s.ctx, s.events)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
log.Errorf("docker store unexpectedly disconnected from docker events from %s with %v", s.client.Host(), err)
|
||||
}
|
||||
@@ -56,16 +59,17 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
return err
|
||||
} else {
|
||||
s.containers.Clear()
|
||||
for _, c := range containers {
|
||||
s.containers.Store(c.ID, &c)
|
||||
}
|
||||
lop.ForEach(containers, func(c Container, _ int) {
|
||||
container, _ := s.client.FindContainer(c.ID)
|
||||
s.containers.Store(c.ID, &container)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ContainerStore) List() ([]Container, error) {
|
||||
func (s *ContainerStore) ListContainers() ([]Container, error) {
|
||||
s.wg.Wait()
|
||||
|
||||
if err := s.checkConnectivity(); err != nil {
|
||||
@@ -80,11 +84,27 @@ func (s *ContainerStore) List() ([]Container, error) {
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (s *ContainerStore) FindContainer(id string) (Container, error) {
|
||||
list, err := s.ListContainers()
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
for _, c := range list {
|
||||
if c.ID == id {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Warnf("container %s not found in store", id)
|
||||
return Container{}, ErrContainerNotFound
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Client() Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Subscribe(ctx context.Context, events chan ContainerEvent) {
|
||||
func (s *ContainerStore) SubscribeEvents(ctx context.Context, events chan<- ContainerEvent) {
|
||||
go func() {
|
||||
if s.statsCollector.Start(s.ctx) {
|
||||
log.Debug("clearing container stats as stats collector has been stopped")
|
||||
@@ -96,19 +116,23 @@ func (s *ContainerStore) Subscribe(ctx context.Context, events chan ContainerEve
|
||||
}()
|
||||
|
||||
s.subscribers.Store(ctx, events)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.subscribers.Delete(ctx)
|
||||
s.statsCollector.Stop()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Unsubscribe(ctx context.Context) {
|
||||
s.subscribers.Delete(ctx)
|
||||
s.statsCollector.Stop()
|
||||
}
|
||||
|
||||
func (s *ContainerStore) SubscribeStats(ctx context.Context, stats chan ContainerStat) {
|
||||
func (s *ContainerStore) SubscribeStats(ctx context.Context, stats chan<- ContainerStat) {
|
||||
s.statsCollector.Subscribe(ctx, stats)
|
||||
}
|
||||
|
||||
func (s *ContainerStore) SubscribeNewContainers(ctx context.Context, containers chan Container) {
|
||||
func (s *ContainerStore) SubscribeNewContainers(ctx context.Context, containers chan<- Container) {
|
||||
s.newContainerSubscribers.Store(ctx, containers)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.newContainerSubscribers.Delete(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ContainerStore) init() {
|
||||
@@ -128,11 +152,10 @@ func (s *ContainerStore) init() {
|
||||
if container, err := s.client.FindContainer(event.ActorID); err == nil {
|
||||
log.Debugf("container %s started", container.ID)
|
||||
s.containers.Store(container.ID, &container)
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan Container) bool {
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-c.Done():
|
||||
s.newContainerSubscribers.Delete(c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -167,7 +190,7 @@ func (s *ContainerStore) init() {
|
||||
}
|
||||
})
|
||||
}
|
||||
s.subscribers.Range(func(c context.Context, events chan ContainerEvent) bool {
|
||||
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- event:
|
||||
case <-c.Done():
|
||||
|
||||
@@ -24,7 +24,7 @@ func (m *mockedClient) FindContainer(id string) (Container, error) {
|
||||
return args.Get(0).(Container), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockedClient) Events(ctx context.Context, events chan<- ContainerEvent) error {
|
||||
func (m *mockedClient) ContainerEvents(ctx context.Context, events chan<- ContainerEvent) error {
|
||||
args := m.Called(ctx, events)
|
||||
return args.Error(0)
|
||||
}
|
||||
@@ -34,9 +34,9 @@ func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockedClient) Host() *Host {
|
||||
func (m *mockedClient) Host() Host {
|
||||
args := m.Called()
|
||||
return args.Get(0).(*Host)
|
||||
return args.Get(0).(Host)
|
||||
}
|
||||
|
||||
func TestContainerStore_List(t *testing.T) {
|
||||
@@ -48,18 +48,26 @@ func TestContainerStore_List(t *testing.T) {
|
||||
Name: "test",
|
||||
},
|
||||
}, nil)
|
||||
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
<-ctx.Done()
|
||||
})
|
||||
client.On("Host").Return(&Host{
|
||||
client.On("Host").Return(Host{
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
client.On("FindContainer", "1234").Return(Container{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
}, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
store := NewContainerStore(ctx, client)
|
||||
containers, _ := store.List()
|
||||
containers, _ := store.ListContainers()
|
||||
|
||||
assert.Equal(t, containers[0].ID, "1234")
|
||||
}
|
||||
@@ -75,7 +83,7 @@ func TestContainerStore_die(t *testing.T) {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
events := args.Get(1).(chan<- ContainerEvent)
|
||||
@@ -86,21 +94,28 @@ func TestContainerStore_die(t *testing.T) {
|
||||
}
|
||||
<-ctx.Done()
|
||||
})
|
||||
client.On("Host").Return(&Host{
|
||||
client.On("Host").Return(Host{
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.ContainerStat")).Return(nil)
|
||||
|
||||
client.On("FindContainer", "1234").Return(Container{
|
||||
ID: "1234",
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300),
|
||||
}, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
store := NewContainerStore(ctx, client)
|
||||
|
||||
// Wait until we get the event
|
||||
events := make(chan ContainerEvent)
|
||||
store.Subscribe(ctx, events)
|
||||
store.SubscribeEvents(ctx, events)
|
||||
<-events
|
||||
|
||||
containers, _ := store.List()
|
||||
containers, _ := store.ListContainers()
|
||||
assert.Equal(t, containers[0].State, "exited")
|
||||
}
|
||||
|
||||
@@ -197,24 +197,24 @@ func checkPosition(currentEvent *LogEvent, nextEvent *LogEvent) {
|
||||
currentLevel := guessLogLevel(currentEvent)
|
||||
if nextEvent != nil {
|
||||
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "" && !nextEvent.HasLevel() {
|
||||
currentEvent.Position = START
|
||||
nextEvent.Position = MIDDLE
|
||||
currentEvent.Position = Beginning
|
||||
nextEvent.Position = Middle
|
||||
}
|
||||
|
||||
// If next item is not close to current item or has level, set current item position to end
|
||||
if currentEvent.Position == MIDDLE && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
|
||||
currentEvent.Position = END
|
||||
if currentEvent.Position == Middle && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
|
||||
currentEvent.Position = End
|
||||
}
|
||||
|
||||
// If next item is close to current item and has no level, set next item position to middle
|
||||
if currentEvent.Position == MIDDLE && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
|
||||
nextEvent.Position = MIDDLE
|
||||
if currentEvent.Position == Middle && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
|
||||
nextEvent.Position = Middle
|
||||
}
|
||||
// Set next item level to current item level
|
||||
if currentEvent.Position == START || currentEvent.Position == MIDDLE {
|
||||
if currentEvent.Position == Beginning || currentEvent.Position == Middle {
|
||||
nextEvent.Level = currentEvent.Level
|
||||
}
|
||||
} else if currentEvent.Position == MIDDLE {
|
||||
currentEvent.Position = END
|
||||
} else if currentEvent.Position == Middle {
|
||||
currentEvent.Position = End
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -20,10 +20,11 @@ type Host struct {
|
||||
ValidCerts bool `json:"-"`
|
||||
NCPU int `json:"nCPU"`
|
||||
MemTotal int64 `json:"memTotal"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
func (h *Host) String() string {
|
||||
return h.ID
|
||||
func (h Host) String() string {
|
||||
return fmt.Sprintf("ID: %s, Endpoint: %s", h.ID, h.Endpoint)
|
||||
}
|
||||
|
||||
func ParseConnection(connection string) (Host, error) {
|
||||
@@ -72,6 +73,7 @@ func ParseConnection(connection string) (Host, error) {
|
||||
CACertPath: cacertPath,
|
||||
KeyPath: keyPath,
|
||||
ValidCerts: hasCerts,
|
||||
Endpoint: remoteUrl.String(),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
type StatsCollector struct {
|
||||
stream chan ContainerStat
|
||||
subscribers *xsync.MapOf[context.Context, chan ContainerStat]
|
||||
subscribers *xsync.MapOf[context.Context, chan<- ContainerStat]
|
||||
client Client
|
||||
cancelers *xsync.MapOf[string, context.CancelFunc]
|
||||
stopper context.CancelFunc
|
||||
@@ -28,14 +28,18 @@ var timeToStop = 6 * time.Hour
|
||||
func NewStatsCollector(client Client) *StatsCollector {
|
||||
return &StatsCollector{
|
||||
stream: make(chan ContainerStat),
|
||||
subscribers: xsync.NewMapOf[context.Context, chan ContainerStat](),
|
||||
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerStat](),
|
||||
client: client,
|
||||
cancelers: xsync.NewMapOf[string, context.CancelFunc](),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan ContainerStat) {
|
||||
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerStat) {
|
||||
c.subscribers.Store(ctx, stats)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.subscribers.Delete(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *StatsCollector) forceStop() {
|
||||
@@ -109,7 +113,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
|
||||
go func() {
|
||||
log.Debugf("subscribing to docker events from stats collector %s", sc.client.Host())
|
||||
err := sc.client.Events(context.Background(), events)
|
||||
err := sc.client.ContainerEvents(context.Background(), events)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
log.Errorf("stats collector unexpectedly disconnected from docker events from %s with %v", sc.client.Host(), err)
|
||||
}
|
||||
@@ -136,7 +140,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
|
||||
log.Info("stopped collecting container stats")
|
||||
return true
|
||||
case stat := <-sc.stream:
|
||||
sc.subscribers.Range(func(c context.Context, stats chan ContainerStat) bool {
|
||||
sc.subscribers.Range(func(c context.Context, stats chan<- ContainerStat) bool {
|
||||
select {
|
||||
case stats <- stat:
|
||||
case <-c.Done():
|
||||
|
||||
@@ -17,7 +17,7 @@ func startedCollector(ctx context.Context) *StatsCollector {
|
||||
State: "running",
|
||||
},
|
||||
}, nil)
|
||||
client.On("Events", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).
|
||||
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).
|
||||
Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
ctx := args.Get(0).(context.Context)
|
||||
@@ -31,7 +31,7 @@ func startedCollector(ctx context.Context) *StatsCollector {
|
||||
ID: "1234",
|
||||
}
|
||||
})
|
||||
client.On("Host").Return(&Host{
|
||||
client.On("Host").Return(Host{
|
||||
ID: "localhost",
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
@@ -45,11 +46,29 @@ type ContainerEvent struct {
|
||||
type LogPosition string
|
||||
|
||||
const (
|
||||
START LogPosition = "start"
|
||||
MIDDLE LogPosition = "middle"
|
||||
END LogPosition = "end"
|
||||
Beginning LogPosition = "start"
|
||||
Middle LogPosition = "middle"
|
||||
End LogPosition = "end"
|
||||
)
|
||||
|
||||
type ContainerAction string
|
||||
|
||||
const (
|
||||
Start ContainerAction = "start"
|
||||
Stop ContainerAction = "stop"
|
||||
Restart ContainerAction = "restart"
|
||||
)
|
||||
|
||||
func ParseContainerAction(input string) (ContainerAction, error) {
|
||||
action := ContainerAction(input)
|
||||
switch action {
|
||||
case Start, Stop, Restart:
|
||||
return action, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown action: %s", input)
|
||||
}
|
||||
}
|
||||
|
||||
type LogEvent struct {
|
||||
Message any `json:"m,omitempty"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
|
||||
Reference in New Issue
Block a user