diff --git a/Dockerfile b/Dockerfile index 3197fb4ed228..504a93cace8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,9 +26,9 @@ FROM --platform=$BUILDPLATFORM golang:1.23.3-alpine AS builder # install gRPC dependencies RUN apk add --no-cache ca-certificates protoc protobuf-dev\ - && mkdir /dozzle \ - && go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \ - && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + && mkdir /dozzle \ + && go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \ + && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest WORKDIR /dozzle diff --git a/README.md b/README.md index 160c22f4682f..c47a5391390c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent $ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent -See the [Agent Mode](https://dozzle.dev/guide/agent-mode) documentation for more details. +See the [Agent Mode](https://dozzle.dev/guide/agent) documentation for more details. ## Technical Details diff --git a/docs/guide/swarm-mode.md b/docs/guide/swarm-mode.md index 039dbca61c56..10c7e6aaa83c 100644 --- a/docs/guide/swarm-mode.md +++ b/docs/guide/swarm-mode.md @@ -74,3 +74,34 @@ secrets: ``` In this example, `users.yml` file is stored in a Docker secret. It is the same as the [simple authentication](/guide/authentication#generating-users-yml) example. + +## Adding standalone Agents to Swarm Mode + +From version v8.8.x, Dozzle supports adding standalone [Agents](/guide/agent) when running in Swarm Mode. + +Simply [add the remote agent](/guide/agent#how-to-connect-to-an-agent) to your Swarm compose in the same way you normally would. + +> [!NOTE] +> While remote agents are supported, remote connections such as socket proxy are not supported. + +```yml +services: + dozzle: + image: amir20/dozzle:latest + environment: + - DOZZLE_MODE=swarm + - DOZZLE_REMOTE_AGENT=agent:7007 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - 8080:8080 + networks: + - dozzle + deploy: + mode: global +networks: + dozzle: + driver: overlay +``` + +The remote agent(s) will now display alongside the other nodes in Dozzle. diff --git a/examples/docker.swarm.yml b/examples/docker.swarm.yml index 1bc20e8cd56d..212c686f9855 100644 --- a/examples/docker.swarm.yml +++ b/examples/docker.swarm.yml @@ -1,9 +1,10 @@ services: dozzle-service: - image: amir20/dozzle:latest + image: amir20/dozzle:local-test environment: - DOZZLE_LEVEL=debug - DOZZLE_MODE=swarm + - DOZZLE_REMOTE_AGENT=198.19.248.6:7007 volumes: - /var/run/docker.sock:/var/run/docker.sock ports: diff --git a/internal/docker/client.go b/internal/docker/client.go index c3e5ff9acc45..c1a160ac0b3a 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -342,6 +342,7 @@ func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) { } func (d *httpClient) Host() Host { + log.Debug().Str("host", d.host.Name).Msg("Fetching host") return d.host } diff --git a/internal/support/cli/args.go b/internal/support/cli/args.go index dbbe9adae043..88943af2447d 100644 --- a/internal/support/cli/args.go +++ b/internal/support/cli/args.go @@ -31,6 +31,7 @@ type Args struct { Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"` Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"` Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"` + AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"` } type HealthcheckCmd struct { @@ -40,6 +41,10 @@ type AgentCmd struct { Addr string `arg:"env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"` } +type AgentTestCmd struct { + Address string `arg:"positional"` +} + type GenerateCmd struct { Username string `arg:"positional"` Password string `arg:"--password, -p" help:"sets the password for the user"` diff --git a/internal/support/docker/retriable_client_manager.go b/internal/support/docker/retriable_client_manager.go index 44d29773c8e8..1fd077d44124 100644 --- a/internal/support/docker/retriable_client_manager.go +++ b/internal/support/docker/retriable_client_manager.go @@ -10,6 +10,7 @@ import ( "github.com/amir20/dozzle/internal/agent" "github.com/amir20/dozzle/internal/docker" "github.com/puzpuzpuz/xsync/v3" + "github.com/samber/lo" lop "github.com/samber/lo/parallel" "github.com/rs/zerolog/log" @@ -136,11 +137,7 @@ func (m *RetriableClientManager) List() []ClientService { m.mu.RLock() defer m.mu.RUnlock() - clients := make([]ClientService, 0, len(m.clients)) - for _, client := range m.clients { - clients = append(clients, client) - } - return clients + return lo.Values(m.clients) } func (m *RetriableClientManager) Find(id string) (ClientService, bool) { diff --git a/internal/support/docker/swarm_client_manager.go b/internal/support/docker/swarm_client_manager.go index 4a792971c817..bee761b6a845 100644 --- a/internal/support/docker/swarm_client_manager.go +++ b/internal/support/docker/swarm_client_manager.go @@ -19,14 +19,15 @@ import ( ) type SwarmClientManager struct { - clients map[string]ClientService - certs tls.Certificate - mu sync.RWMutex - subscribers *xsync.MapOf[context.Context, chan<- docker.Host] - localClient docker.Client - localIPs []string - name string - timeout time.Duration + clients map[string]ClientService + certs tls.Certificate + mu sync.RWMutex + subscribers *xsync.MapOf[context.Context, chan<- docker.Host] + localClient docker.Client + localIPs []string + name string + timeout time.Duration + agentManager *RetriableClientManager } func localIPs() []string { @@ -46,7 +47,7 @@ func localIPs() []string { return ips } -func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, timeout time.Duration) *SwarmClientManager { +func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager) *SwarmClientManager { clientMap := make(map[string]ClientService) localService := NewDockerClientService(localClient) clientMap[localClient.Host().ID] = localService @@ -68,18 +69,20 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, tim log.Debug().Str("service", serviceName).Msg("found swarm service name") return &SwarmClientManager{ - localClient: localClient, - clients: clientMap, - certs: certs, - subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](), - localIPs: localIPs(), - name: serviceName, - timeout: timeout, + localClient: localClient, + clients: clientMap, + certs: certs, + subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](), + localIPs: localIPs(), + name: serviceName, + timeout: timeout, + agentManager: agentManager, } } func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- docker.Host) { m.subscribers.Store(ctx, channel) + m.agentManager.Subscribe(ctx, channel) go func() { <-ctx.Done() @@ -174,6 +177,8 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) { m.mu.Unlock() + m.agentManager.RetryAndList() + return m.List(), errors } @@ -181,7 +186,10 @@ func (m *SwarmClientManager) List() []ClientService { m.mu.RLock() defer m.mu.RUnlock() - return lo.Values(m.clients) + agents := m.agentManager.List() + clients := lo.Values(m.clients) + + return append(agents, clients...) } func (m *SwarmClientManager) Find(id string) (ClientService, bool) { @@ -189,13 +197,20 @@ func (m *SwarmClientManager) Find(id string) (ClientService, bool) { defer m.mu.RUnlock() client, ok := m.clients[id] + + if !ok { + client, ok = m.agentManager.Find(id) + } + return client, ok } func (m *SwarmClientManager) Hosts(ctx context.Context) []docker.Host { - clients := m.List() + m.mu.RLock() + clients := lo.Values(m.clients) + m.mu.RUnlock() - return lop.Map(clients, func(client ClientService, _ int) docker.Host { + swarmNodes := lop.Map(clients, func(client ClientService, _ int) docker.Host { host, err := client.Host(ctx) if err != nil { log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client") @@ -208,6 +223,7 @@ func (m *SwarmClientManager) Hosts(ctx context.Context) []docker.Host { return host }) + return append(m.agentManager.Hosts(ctx), swarmNodes...) } func (m *SwarmClientManager) String() string { diff --git a/main.go b/main.go index 2baa3894a302..8ba4f64044c5 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,27 @@ func main() { if _, err := os.Stdout.Write(buffer.Bytes()); err != nil { log.Fatal().Err(err).Msg("Failed to write to stdout") } + + case *cli.AgentTestCmd: + certs, err := cli.ReadCertificates(certs) + if err != nil { + log.Fatal().Err(err).Msg("Could not read certificates") + } + + log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent") + + agent, err := agent.NewClient(args.AgentTest.Address, certs) + if err != nil { + log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error connecting to agent") + } + ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) + defer cancel() + host, err := agent.Host(ctx) + if err != nil { + log.Fatal().Err(err).Str("endpoint", args.AgentTest.Address).Msg("error fetching host info for agent") + } + + log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent") } os.Exit(0) @@ -157,7 +178,8 @@ func main() { if err != nil { log.Fatal().Err(err).Msg("Could not read certificates") } - manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout) + agentManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs) + manager := docker_support.NewSwarmClientManager(localClient, certs, args.Timeout, agentManager) multiHostService = docker_support.NewMultiHostService(manager, args.Timeout) log.Info().Msg("Starting in swarm mode") listener, err := net.Listen("tcp", ":7007")