1
0
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:
Amir Raminfar
2024-07-05 13:38:10 -07:00
committed by GitHub
parent 2e5fb71938
commit 4de9c775ba
70 changed files with 2681 additions and 963 deletions

View 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")
}

View 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)
}

View 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)
}

View 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")
}