mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 06:28:42 +01:00
feat: enables swarm cluster to connect to external agents (#3394)
Co-authored-by: Mitch Brown <mitch@mitchbrown.ca>
This commit is contained in:
@@ -26,9 +26,9 @@ FROM --platform=$BUILDPLATFORM golang:1.23.3-alpine AS builder
|
|||||||
|
|
||||||
# install gRPC dependencies
|
# install gRPC dependencies
|
||||||
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
|
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
|
||||||
&& mkdir /dozzle \
|
&& mkdir /dozzle \
|
||||||
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
WORKDIR /dozzle
|
WORKDIR /dozzle
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
$ 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
|
## Technical Details
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
dozzle-service:
|
dozzle-service:
|
||||||
image: amir20/dozzle:latest
|
image: amir20/dozzle:local-test
|
||||||
environment:
|
environment:
|
||||||
- DOZZLE_LEVEL=debug
|
- DOZZLE_LEVEL=debug
|
||||||
- DOZZLE_MODE=swarm
|
- DOZZLE_MODE=swarm
|
||||||
|
- DOZZLE_REMOTE_AGENT=198.19.248.6:7007
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *httpClient) Host() Host {
|
func (d *httpClient) Host() Host {
|
||||||
|
log.Debug().Str("host", d.host.Name).Msg("Fetching host")
|
||||||
return d.host
|
return d.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Args struct {
|
|||||||
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
|
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"`
|
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
|
||||||
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
|
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
|
||||||
|
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HealthcheckCmd struct {
|
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"`
|
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 {
|
type GenerateCmd struct {
|
||||||
Username string `arg:"positional"`
|
Username string `arg:"positional"`
|
||||||
Password string `arg:"--password, -p" help:"sets the password for the user"`
|
Password string `arg:"--password, -p" help:"sets the password for the user"`
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/amir20/dozzle/internal/agent"
|
"github.com/amir20/dozzle/internal/agent"
|
||||||
"github.com/amir20/dozzle/internal/docker"
|
"github.com/amir20/dozzle/internal/docker"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
"github.com/samber/lo"
|
||||||
lop "github.com/samber/lo/parallel"
|
lop "github.com/samber/lo/parallel"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -136,11 +137,7 @@ func (m *RetriableClientManager) List() []ClientService {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
clients := make([]ClientService, 0, len(m.clients))
|
return lo.Values(m.clients)
|
||||||
for _, client := range m.clients {
|
|
||||||
clients = append(clients, client)
|
|
||||||
}
|
|
||||||
return clients
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RetriableClientManager) Find(id string) (ClientService, bool) {
|
func (m *RetriableClientManager) Find(id string) (ClientService, bool) {
|
||||||
|
|||||||
@@ -19,14 +19,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SwarmClientManager struct {
|
type SwarmClientManager struct {
|
||||||
clients map[string]ClientService
|
clients map[string]ClientService
|
||||||
certs tls.Certificate
|
certs tls.Certificate
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
subscribers *xsync.MapOf[context.Context, chan<- docker.Host]
|
subscribers *xsync.MapOf[context.Context, chan<- docker.Host]
|
||||||
localClient docker.Client
|
localClient docker.Client
|
||||||
localIPs []string
|
localIPs []string
|
||||||
name string
|
name string
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
agentManager *RetriableClientManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func localIPs() []string {
|
func localIPs() []string {
|
||||||
@@ -46,7 +47,7 @@ func localIPs() []string {
|
|||||||
return ips
|
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)
|
clientMap := make(map[string]ClientService)
|
||||||
localService := NewDockerClientService(localClient)
|
localService := NewDockerClientService(localClient)
|
||||||
clientMap[localClient.Host().ID] = localService
|
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")
|
log.Debug().Str("service", serviceName).Msg("found swarm service name")
|
||||||
|
|
||||||
return &SwarmClientManager{
|
return &SwarmClientManager{
|
||||||
localClient: localClient,
|
localClient: localClient,
|
||||||
clients: clientMap,
|
clients: clientMap,
|
||||||
certs: certs,
|
certs: certs,
|
||||||
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
|
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
|
||||||
localIPs: localIPs(),
|
localIPs: localIPs(),
|
||||||
name: serviceName,
|
name: serviceName,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
agentManager: agentManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- docker.Host) {
|
func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- docker.Host) {
|
||||||
m.subscribers.Store(ctx, channel)
|
m.subscribers.Store(ctx, channel)
|
||||||
|
m.agentManager.Subscribe(ctx, channel)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -174,6 +177,8 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
|
|||||||
|
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.agentManager.RetryAndList()
|
||||||
|
|
||||||
return m.List(), errors
|
return m.List(), errors
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +186,10 @@ func (m *SwarmClientManager) List() []ClientService {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
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) {
|
func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
|
||||||
@@ -189,13 +197,20 @@ func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
|
|||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
client, ok := m.clients[id]
|
client, ok := m.clients[id]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
client, ok = m.agentManager.Find(id)
|
||||||
|
}
|
||||||
|
|
||||||
return client, ok
|
return client, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SwarmClientManager) Hosts(ctx context.Context) []docker.Host {
|
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)
|
host, err := client.Host(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client")
|
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 host
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return append(m.agentManager.Hosts(ctx), swarmNodes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SwarmClientManager) String() string {
|
func (m *SwarmClientManager) String() string {
|
||||||
|
|||||||
24
main.go
24
main.go
@@ -126,6 +126,27 @@ func main() {
|
|||||||
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
|
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to write to stdout")
|
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)
|
os.Exit(0)
|
||||||
@@ -157,7 +178,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Could not read certificates")
|
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)
|
multiHostService = docker_support.NewMultiHostService(manager, args.Timeout)
|
||||||
log.Info().Msg("Starting in swarm mode")
|
log.Info().Msg("Starting in swarm mode")
|
||||||
listener, err := net.Listen("tcp", ":7007")
|
listener, err := net.Listen("tcp", ":7007")
|
||||||
|
|||||||
Reference in New Issue
Block a user