mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat!: implements swarm mode with agents (#3058)
This commit is contained in:
284
main.go
284
main.go
@@ -3,22 +3,25 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg"
|
||||
"github.com/amir20/dozzle/internal/analytics"
|
||||
"github.com/amir20/dozzle/internal/agent"
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/healthcheck"
|
||||
"github.com/amir20/dozzle/internal/support/cli"
|
||||
docker_support "github.com/amir20/dozzle/internal/support/docker"
|
||||
"github.com/amir20/dozzle/internal/web"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -29,28 +32,33 @@ var (
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
|
||||
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
|
||||
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
|
||||
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
|
||||
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
|
||||
AuthHeaderUser string `arg:"--auth-header-user,env:DOZZLE_AUTH_HEADER_USER" default:"Remote-User" help:"sets the HTTP Header to use for username in Forward Proxy configuration."`
|
||||
AuthHeaderEmail string `arg:"--auth-header-email,env:DOZZLE_AUTH_HEADER_EMAIL" default:"Remote-Email" help:"sets the HTTP Header to use for email in Forward Proxy configuration."`
|
||||
AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."`
|
||||
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
|
||||
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
|
||||
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
|
||||
Filter map[string][]string `arg:"-"`
|
||||
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
|
||||
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
||||
|
||||
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"`
|
||||
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
|
||||
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
|
||||
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
|
||||
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
|
||||
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
|
||||
AuthHeaderUser string `arg:"--auth-header-user,env:DOZZLE_AUTH_HEADER_USER" default:"Remote-User" help:"sets the HTTP Header to use for username in Forward Proxy configuration."`
|
||||
AuthHeaderEmail string `arg:"--auth-header-email,env:DOZZLE_AUTH_HEADER_EMAIL" default:"Remote-Email" help:"sets the HTTP Header to use for email in Forward Proxy configuration."`
|
||||
AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."`
|
||||
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
|
||||
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
|
||||
Filter map[string][]string `arg:"-"`
|
||||
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
|
||||
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
|
||||
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
|
||||
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type HealthcheckCmd struct {
|
||||
}
|
||||
|
||||
type AgentCmd struct {
|
||||
Addr string `arg:"env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"`
|
||||
}
|
||||
|
||||
type GenerateCmd struct {
|
||||
Username string `arg:"positional"`
|
||||
Password string `arg:"--password, -p" help:"sets the password for the user"`
|
||||
@@ -65,14 +73,65 @@ func (args) Version() string {
|
||||
//go:embed all:dist
|
||||
var content embed.FS
|
||||
|
||||
//go:embed shared_cert.pem shared_key.pem
|
||||
var certs embed.FS
|
||||
|
||||
//go:generate protoc --go_out=. --go-grpc_out=. --proto_path=./protos ./protos/rpc.proto ./protos/types.proto
|
||||
func main() {
|
||||
cli.ValidateEnvVars(args{}, AgentCmd{})
|
||||
args, subcommand := parseArgs()
|
||||
validateEnvVars()
|
||||
if subcommand != nil {
|
||||
switch subcommand.(type) {
|
||||
case *AgentCmd:
|
||||
client, err := docker.NewLocalClient(args.Filter, args.Hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not create docker client: %v", err)
|
||||
}
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read certificates: %v", err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", args.Agent.Addr)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
tempFile, err := os.CreateTemp("/", "agent-*.addr")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
io.WriteString(tempFile, listener.Addr().String())
|
||||
agent.RunServer(client, certs, listener)
|
||||
case *HealthcheckCmd:
|
||||
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
|
||||
log.Fatal(err)
|
||||
files, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read directory: %v", err)
|
||||
}
|
||||
|
||||
agentAddress := ""
|
||||
for _, file := range files {
|
||||
if match, _ := filepath.Match("agent-*.addr", file.Name()); match {
|
||||
data, err := os.ReadFile(file.Name())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
agentAddress = string(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
if agentAddress == "" {
|
||||
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
|
||||
log.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
} else {
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read certificates: %v", err)
|
||||
}
|
||||
if err := healthcheck.RPCRequest(agentAddress, certs); err != nil {
|
||||
log.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *GenerateCmd:
|
||||
@@ -88,7 +147,7 @@ func main() {
|
||||
}, true)
|
||||
|
||||
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Failed to write to stdout: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +160,35 @@ func main() {
|
||||
|
||||
log.Infof("Dozzle version %s", version)
|
||||
|
||||
clients := createClients(args, docker.NewClientWithFilters, docker.NewClientWithTlsAndFilter, args.Hostname)
|
||||
|
||||
if len(clients) == 0 {
|
||||
log.Fatal("Could not connect to any Docker Engines")
|
||||
var multiHostService *docker_support.MultiHostService
|
||||
if args.Mode == "server" {
|
||||
multiHostService = createMultiHostService(args)
|
||||
if multiHostService.TotalClients() == 0 {
|
||||
log.Fatal("Could not connect to any Docker Engines")
|
||||
} else {
|
||||
log.Infof("Connected to %d Docker Engine(s)", multiHostService.TotalClients())
|
||||
}
|
||||
} else if args.Mode == "swarm" {
|
||||
localClient, err := docker.NewLocalClient(args.Filter, args.Hostname)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to local Docker Engine: %s", err)
|
||||
}
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read certificates: %v", err)
|
||||
}
|
||||
multiHostService = docker_support.NewSwarmService(localClient, certs)
|
||||
log.Infof("Starting in Swarm mode")
|
||||
listener, err := net.Listen("tcp", ":7007")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
go agent.RunServer(localClient, certs, listener)
|
||||
} else {
|
||||
log.Infof("Connected to %d Docker Engine(s)", len(clients))
|
||||
log.Fatalf("Invalid mode %s", args.Mode)
|
||||
}
|
||||
|
||||
srv := createServer(args, clients)
|
||||
go doStartEvent(args, clients)
|
||||
srv := createServer(args, multiHostService)
|
||||
go func() {
|
||||
log.Infof("Accepting connections on %s", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
@@ -131,57 +209,19 @@ func main() {
|
||||
log.Debug("shutdown complete")
|
||||
}
|
||||
|
||||
func doStartEvent(arg args, clients map[string]docker.Client) {
|
||||
if arg.NoAnalytics {
|
||||
log.Debug("Analytics disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
event := analytics.BeaconEvent{
|
||||
Name: "start",
|
||||
Version: version,
|
||||
}
|
||||
|
||||
if client, ok := clients["localhost"]; ok {
|
||||
event.ServerID = client.SystemInfo().ID
|
||||
event.ServerVersion = client.SystemInfo().ServerVersion
|
||||
} else {
|
||||
for _, client := range clients {
|
||||
event.ServerID = client.SystemInfo().ID
|
||||
event.ServerVersion = client.SystemInfo().ServerVersion
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := analytics.SendBeacon(event); err != nil {
|
||||
log.Debug(err)
|
||||
}
|
||||
}
|
||||
|
||||
func createClients(args args,
|
||||
localClientFactory func(map[string][]string) (docker.Client, error),
|
||||
remoteClientFactory func(map[string][]string, docker.Host) (docker.Client, error),
|
||||
hostname string) map[string]docker.Client {
|
||||
clients := make(map[string]docker.Client)
|
||||
|
||||
if localClient, err := createLocalClient(args, localClientFactory); err == nil {
|
||||
if hostname != "" {
|
||||
localClient.Host().Name = hostname
|
||||
}
|
||||
clients[localClient.Host().ID] = localClient
|
||||
}
|
||||
|
||||
func createMultiHostService(args args) *docker_support.MultiHostService {
|
||||
var clients []docker_support.ClientService
|
||||
for _, remoteHost := range args.RemoteHost {
|
||||
host, err := docker.ParseConnection(remoteHost)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse remote host %s: %s", remoteHost, err)
|
||||
}
|
||||
log.Debugf("Creating remote client for %s with %+v", host.Name, host)
|
||||
log.Debugf("creating remote client for %s with %+v", host.Name, host)
|
||||
log.Infof("Creating client for %s with %s", host.Name, host.URL.String())
|
||||
if client, err := remoteClientFactory(args.Filter, host); err == nil {
|
||||
if client, err := docker.NewRemoteClient(args.Filter, host); err == nil {
|
||||
if _, err := client.ListContainers(); err == nil {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
clients[client.Host().ID] = client
|
||||
log.Debugf("connected to local Docker Engine")
|
||||
clients = append(clients, docker_support.NewDockerClientService(client))
|
||||
} else {
|
||||
log.Warnf("Could not connect to remote host %s: %s", host.ID, err)
|
||||
}
|
||||
@@ -189,11 +229,40 @@ func createClients(args args,
|
||||
log.Warnf("Could not create client for %s: %s", host.ID, err)
|
||||
}
|
||||
}
|
||||
certs, err := cli.ReadCertificates(certs)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not read certificates: %v", err)
|
||||
}
|
||||
for _, remoteAgent := range args.RemoteAgent {
|
||||
client, err := agent.NewClient(remoteAgent, certs)
|
||||
if err != nil {
|
||||
log.Warnf("Could not connect to remote agent %s: %s", remoteAgent, err)
|
||||
continue
|
||||
}
|
||||
clients = append(clients, docker_support.NewAgentService(client))
|
||||
}
|
||||
|
||||
return clients
|
||||
localClient, err := docker.NewLocalClient(args.Filter, args.Hostname)
|
||||
if err == nil {
|
||||
_, err := localClient.ListContainers()
|
||||
if err != nil {
|
||||
log.Debugf("could not connect to local Docker Engine: %s", err)
|
||||
if !args.NoAnalytics {
|
||||
go cli.StartEvent(version, args.Mode, args.RemoteAgent, args.RemoteHost, nil)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("connected to local Docker Engine")
|
||||
if !args.NoAnalytics {
|
||||
go cli.StartEvent(version, args.Mode, args.RemoteAgent, args.RemoteHost, localClient)
|
||||
}
|
||||
clients = append(clients, docker_support.NewDockerClientService(localClient))
|
||||
}
|
||||
}
|
||||
|
||||
return docker_support.NewMultiHostService(clients)
|
||||
}
|
||||
|
||||
func createServer(args args, clients map[string]docker.Client) *http.Server {
|
||||
func createServer(args args, multiHostService *docker_support.MultiHostService) *http.Server {
|
||||
_, dev := os.LookupEnv("DEV")
|
||||
|
||||
var provider web.AuthProvider = web.NONE
|
||||
@@ -257,38 +326,14 @@ func createServer(args args, clients map[string]docker.Client) *http.Server {
|
||||
}
|
||||
}
|
||||
|
||||
return web.CreateServer(clients, assets, config)
|
||||
}
|
||||
|
||||
func createLocalClient(args args, localClientFactory func(map[string][]string) (docker.Client, error)) (docker.Client, error) {
|
||||
for i := 1; ; i++ {
|
||||
dockerClient, err := localClientFactory(args.Filter)
|
||||
if err == nil {
|
||||
_, err := dockerClient.ListContainers()
|
||||
if err != nil {
|
||||
log.Debugf("Could not connect to local Docker Engine: %s", err)
|
||||
} else {
|
||||
log.Debugf("Connected to local Docker Engine")
|
||||
return dockerClient, nil
|
||||
}
|
||||
}
|
||||
if args.WaitForDockerSeconds > 0 {
|
||||
log.Infof("Waiting for Docker Engine (attempt %d): %s", i, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
args.WaitForDockerSeconds -= 5
|
||||
} else {
|
||||
log.Debugf("Local Docker Engine not found")
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, errors.New("could not connect to local Docker Engine")
|
||||
return web.CreateServer(multiHostService, assets, config)
|
||||
}
|
||||
|
||||
func parseArgs() (args, interface{}) {
|
||||
var args args
|
||||
parser := arg.MustParse(&args)
|
||||
|
||||
configureLogger(args.Level)
|
||||
cli.ConfigureLogger(args.Level)
|
||||
|
||||
args.Filter = make(map[string][]string)
|
||||
|
||||
@@ -304,36 +349,3 @@ func parseArgs() (args, interface{}) {
|
||||
|
||||
return args, parser.Subcommand()
|
||||
}
|
||||
|
||||
func configureLogger(level string) {
|
||||
if l, err := log.ParseLevel(level); err == nil {
|
||||
log.SetLevel(l)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableLevelTruncation: true,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func validateEnvVars() {
|
||||
argsType := reflect.TypeOf(args{})
|
||||
expectedEnvs := make(map[string]bool)
|
||||
for i := 0; i < argsType.NumField(); i++ {
|
||||
field := argsType.Field(i)
|
||||
for _, tag := range strings.Split(field.Tag.Get("arg"), ",") {
|
||||
if strings.HasPrefix(tag, "env:") {
|
||||
expectedEnvs[strings.TrimPrefix(tag, "env:")] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, env := range os.Environ() {
|
||||
actual := strings.Split(env, "=")[0]
|
||||
if strings.HasPrefix(actual, "DOZZLE_") && !expectedEnvs[actual] {
|
||||
log.Warnf("Unexpected environment variable %s", actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user