Skip to content

Commit

Permalink
feat: enables swarm cluster to connect to external agents (#3394)
Browse files Browse the repository at this point in the history
Co-authored-by: Mitch Brown <mitch@mitchbrown.ca>
  • Loading branch information
amir20 and mitchplze authored Nov 16, 2024
1 parent 9a296ca commit 1144bdf
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 30 deletions.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions docs/guide/swarm-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion examples/docker.swarm.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
1 change: 1 addition & 0 deletions internal/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions internal/support/cli/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"`
Expand Down
7 changes: 2 additions & 5 deletions internal/support/docker/retriable_client_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
54 changes: 35 additions & 19 deletions internal/support/docker/swarm_client_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -174,28 +177,40 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {

m.mu.Unlock()

m.agentManager.RetryAndList()

return m.List(), errors
}

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) {
m.mu.RLock()
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")
Expand All @@ -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 {
Expand Down
24 changes: 23 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 1144bdf

Please sign in to comment.