mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-04 12:05:07 +01:00
feat!: implements swarm mode with agents (#3058)
This commit is contained in:
60
internal/support/docker/agent_service.go
Normal file
60
internal/support/docker/agent_service.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
)
|
||||
|
||||
type agentService struct {
|
||||
client *agent.Client
|
||||
}
|
||||
|
||||
func NewAgentService(client *agent.Client) ClientService {
|
||||
return &agentService{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agentService) FindContainer(id string) (docker.Container, error) {
|
||||
return a.client.FindContainer(id)
|
||||
}
|
||||
|
||||
func (a *agentService) RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
|
||||
return a.client.StreamRawBytes(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (a *agentService) LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
|
||||
return a.client.LogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (a *agentService) StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
|
||||
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
|
||||
}
|
||||
|
||||
func (a *agentService) ListContainers() ([]docker.Container, error) {
|
||||
return a.client.ListContainers()
|
||||
}
|
||||
|
||||
func (a *agentService) Host() docker.Host {
|
||||
return a.client.Host()
|
||||
}
|
||||
|
||||
func (a *agentService) SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat) {
|
||||
go a.client.StreamStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (a *agentService) SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent) {
|
||||
go a.client.StreamEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (d *agentService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container) {
|
||||
go d.client.StreamNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (a *agentService) ContainerAction(container docker.Container, action docker.ContainerAction) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
107
internal/support/docker/client_service.go
Normal file
107
internal/support/docker/client_service.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
)
|
||||
|
||||
type ClientService interface {
|
||||
FindContainer(id string) (docker.Container, error)
|
||||
ListContainers() ([]docker.Container, error)
|
||||
Host() docker.Host
|
||||
ContainerAction(container docker.Container, action docker.ContainerAction) error
|
||||
LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error)
|
||||
RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error)
|
||||
|
||||
// Subscriptions
|
||||
SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat)
|
||||
SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent)
|
||||
SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container)
|
||||
|
||||
// Blocking streaming functions that should be used in a goroutine
|
||||
StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error
|
||||
}
|
||||
|
||||
type dockerClientService struct {
|
||||
client docker.Client
|
||||
store *docker.ContainerStore
|
||||
}
|
||||
|
||||
func NewDockerClientService(client docker.Client) ClientService {
|
||||
return &dockerClientService{
|
||||
client: client,
|
||||
store: docker.NewContainerStore(context.Background(), client),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dockerClientService) RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
|
||||
return d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
|
||||
reader, err := d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := docker.NewEventGenerator(reader, container)
|
||||
return g.Events, nil
|
||||
}
|
||||
|
||||
func (d *dockerClientService) StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
|
||||
reader, err := d.client.ContainerLogs(ctx, container.ID, from, stdTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := docker.NewEventGenerator(reader, container)
|
||||
for event := range g.Events {
|
||||
events <- event
|
||||
}
|
||||
select {
|
||||
case e := <-g.Errors:
|
||||
return e
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dockerClientService) FindContainer(id string) (docker.Container, error) {
|
||||
container, err := d.store.FindContainer(id)
|
||||
if err != nil {
|
||||
if err == docker.ErrContainerNotFound {
|
||||
return d.client.FindContainer(id)
|
||||
} else {
|
||||
return docker.Container{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *dockerClientService) ContainerAction(container docker.Container, action docker.ContainerAction) error {
|
||||
return d.client.ContainerActions(action, container.ID)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) ListContainers() ([]docker.Container, error) {
|
||||
return d.store.ListContainers()
|
||||
}
|
||||
|
||||
func (d *dockerClientService) Host() docker.Host {
|
||||
return d.client.Host()
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat) {
|
||||
d.store.SubscribeStats(ctx, stats)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent) {
|
||||
d.store.SubscribeEvents(ctx, events)
|
||||
}
|
||||
|
||||
func (d *dockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container) {
|
||||
d.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
30
internal/support/docker/container_service.go
Normal file
30
internal/support/docker/container_service.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
)
|
||||
|
||||
type containerService struct {
|
||||
clientService ClientService
|
||||
Container docker.Container
|
||||
}
|
||||
|
||||
func (c *containerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
|
||||
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *containerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
|
||||
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
|
||||
}
|
||||
|
||||
func (c *containerService) StreamLogs(ctx context.Context, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
|
||||
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
|
||||
}
|
||||
|
||||
func (c *containerService) Action(action docker.ContainerAction) error {
|
||||
return c.clientService.ContainerAction(c.Container, action)
|
||||
}
|
||||
215
internal/support/docker/multi_host_service.go
Normal file
215
internal/support/docker/multi_host_service.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package docker_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
)
|
||||
|
||||
type ContainerFilter = func(*docker.Container) bool
|
||||
|
||||
type HostUnavailableError struct {
|
||||
Host docker.Host
|
||||
Err error
|
||||
}
|
||||
|
||||
func (h *HostUnavailableError) Error() string {
|
||||
return fmt.Sprintf("host %s unavailable: %v", h.Host.ID, h.Err)
|
||||
}
|
||||
|
||||
type MultiHostService struct {
|
||||
clients map[string]ClientService
|
||||
SwarmMode bool
|
||||
}
|
||||
|
||||
func NewMultiHostService(clients []ClientService) *MultiHostService {
|
||||
m := &MultiHostService{
|
||||
clients: make(map[string]ClientService),
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
if _, ok := m.clients[client.Host().ID]; ok {
|
||||
log.Warnf("duplicate host %s found, skipping", client.Host())
|
||||
continue
|
||||
}
|
||||
m.clients[client.Host().ID] = client
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func NewSwarmService(client docker.Client, certificates tls.Certificate) *MultiHostService {
|
||||
m := &MultiHostService{
|
||||
clients: make(map[string]ClientService),
|
||||
SwarmMode: true,
|
||||
}
|
||||
|
||||
localClient := NewDockerClientService(client)
|
||||
m.clients[localClient.Host().ID] = localClient
|
||||
|
||||
discover := func() {
|
||||
ips, err := net.LookupIP("tasks.dozzle")
|
||||
if err != nil {
|
||||
log.Fatalf("error looking up swarm services: %v", err)
|
||||
}
|
||||
|
||||
found := 0
|
||||
replaced := 0
|
||||
for _, ip := range ips {
|
||||
client, err := agent.NewClient(ip.String()+":7007", certificates)
|
||||
if err != nil {
|
||||
log.Warnf("error creating client for %s: %v", ip, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if client.Host().ID == localClient.Host().ID {
|
||||
continue
|
||||
}
|
||||
|
||||
service := NewAgentService(client)
|
||||
if existing, ok := m.clients[service.Host().ID]; !ok {
|
||||
log.Debugf("adding swarm service %s", service.Host().ID)
|
||||
m.clients[service.Host().ID] = service
|
||||
found++
|
||||
} else if existing.Host().Endpoint != service.Host().Endpoint {
|
||||
log.Debugf("swarm service %s already exists with different endpoint %s and old value %s", service.Host().ID, service.Host().Endpoint, existing.Host().Endpoint)
|
||||
delete(m.clients, existing.Host().ID)
|
||||
m.clients[service.Host().ID] = service
|
||||
replaced++
|
||||
}
|
||||
}
|
||||
|
||||
if found > 0 {
|
||||
log.Infof("found %d new dozzle replicas", found)
|
||||
}
|
||||
if replaced > 0 {
|
||||
log.Infof("replaced %d dozzle replicas", replaced)
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := backoff.NewTicker(backoff.NewExponentialBackOff(
|
||||
backoff.WithMaxElapsedTime(0)),
|
||||
)
|
||||
for range ticker.C {
|
||||
log.Tracef("discovering swarm services")
|
||||
discover()
|
||||
}
|
||||
}()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultiHostService) FindContainer(host string, id string) (*containerService, error) {
|
||||
client, ok := m.clients[host]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("host %s not found", host)
|
||||
}
|
||||
|
||||
container, err := client.FindContainer(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &containerService{
|
||||
clientService: client,
|
||||
Container: container,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListContainersForHost(host string) ([]docker.Container, error) {
|
||||
client, ok := m.clients[host]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("host %s not found", host)
|
||||
}
|
||||
|
||||
return client.ListContainers()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListAllContainers() ([]docker.Container, []error) {
|
||||
containers := make([]docker.Container, 0)
|
||||
var errors []error
|
||||
|
||||
for _, client := range m.clients {
|
||||
list, err := client.ListContainers()
|
||||
if err != nil {
|
||||
log.Debugf("error listing containers for host %s: %v", client.Host().ID, err)
|
||||
errors = append(errors, &HostUnavailableError{Host: client.Host(), Err: err})
|
||||
continue
|
||||
}
|
||||
|
||||
containers = append(containers, list...)
|
||||
}
|
||||
|
||||
return containers, errors
|
||||
}
|
||||
|
||||
func (m *MultiHostService) ListAllContainersFiltered(filter ContainerFilter) ([]docker.Container, []error) {
|
||||
containers, err := m.ListAllContainers()
|
||||
filtered := make([]docker.Container, 0, len(containers))
|
||||
for _, container := range containers {
|
||||
if filter(&container) {
|
||||
filtered = append(filtered, container)
|
||||
}
|
||||
}
|
||||
return filtered, err
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events chan<- docker.ContainerEvent, stats chan<- docker.ContainerStat) {
|
||||
for _, client := range m.clients {
|
||||
client.SubscribeEvents(ctx, events)
|
||||
client.SubscribeStats(ctx, stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container, filter ContainerFilter) {
|
||||
newContainers := make(chan docker.Container)
|
||||
for _, client := range m.clients {
|
||||
client.SubscribeContainersStarted(ctx, newContainers)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for container := range newContainers {
|
||||
if filter(&container) {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *MultiHostService) TotalClients() int {
|
||||
return len(m.clients)
|
||||
}
|
||||
|
||||
func (m *MultiHostService) Hosts() []docker.Host {
|
||||
hosts := make([]docker.Host, 0, len(m.clients))
|
||||
for _, client := range m.clients {
|
||||
hosts = append(hosts, client.Host())
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (m *MultiHostService) LocalHost() (docker.Host, error) {
|
||||
host := docker.Host{}
|
||||
|
||||
for _, host := range m.Hosts() {
|
||||
if host.Endpoint == "local" {
|
||||
|
||||
return host, nil
|
||||
}
|
||||
}
|
||||
|
||||
return host, fmt.Errorf("local host not found")
|
||||
}
|
||||
Reference in New Issue
Block a user