diff --git a/Dockerfile b/Dockerfile index 6de86cff..460e8cec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ ARG TARGETOS TARGETARCH RUN go generate # Build binary -RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle +RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X cli.version=$TAG" -o dozzle RUN mkdir /data diff --git a/internal/analytics/types.go b/internal/analytics/types.go index 548d433a..a5f7400a 100644 --- a/internal/analytics/types.go +++ b/internal/analytics/types.go @@ -18,4 +18,5 @@ type BeaconEvent struct { Mode string `json:"mode"` RemoteAgents int `json:"remoteAgents"` RemoteClients int `json:"remoteClients"` + SubCommand string `json:"subCommand"` } diff --git a/internal/support/cli/analytics.go b/internal/support/cli/analytics.go index 2d73839b..556858a8 100644 --- a/internal/support/cli/analytics.go +++ b/internal/support/cli/analytics.go @@ -6,13 +6,14 @@ import ( log "github.com/sirupsen/logrus" ) -func StartEvent(version string, mode string, agents []string, remoteClients []string, client docker.Client) { +func StartEvent(version string, mode string, agents []string, remoteClients []string, client docker.Client, subCommand string) { event := analytics.BeaconEvent{ Name: "start", Version: version, Mode: mode, RemoteAgents: len(agents), RemoteClients: len(remoteClients), + SubCommand: subCommand, } if client != nil { diff --git a/internal/support/cli/args.go b/internal/support/cli/args.go new file mode 100644 index 00000000..d34150ea --- /dev/null +++ b/internal/support/cli/args.go @@ -0,0 +1,71 @@ +package cli + +import ( + "strings" + + "github.com/alexflint/go-arg" +) + +var ( + version = "head" +) + +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."` + 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"` + Name string `arg:"--name, -n" help:"sets the display name for the user"` + Email string `arg:"--email, -e" help:"sets the email for the user"` +} + +func (Args) Version() string { + return version +} + +func ParseArgs() (Args, interface{}) { + var args Args + parser := arg.MustParse(&args) + + ConfigureLogger(args.Level) + + args.Filter = make(map[string][]string) + + for _, filter := range args.FilterStrings { + pos := strings.Index(filter, "=") + if pos == -1 { + parser.Fail("each filter should be of the form key=value") + } + key := filter[:pos] + val := filter[pos+1:] + args.Filter[key] = append(args.Filter[key], val) + } + + return args, parser.Subcommand() +} diff --git a/internal/support/cli/clients.go b/internal/support/cli/clients.go new file mode 100644 index 00000000..d921d84d --- /dev/null +++ b/internal/support/cli/clients.go @@ -0,0 +1,63 @@ +package cli + +import ( + "embed" + + "github.com/amir20/dozzle/internal/agent" + "github.com/amir20/dozzle/internal/docker" + docker_support "github.com/amir20/dozzle/internal/support/docker" + log "github.com/sirupsen/logrus" +) + +func CreateMultiHostService(embededCerts embed.FS, 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.Infof("Creating client for %s with %s", host.Name, host.URL.String()) + if client, err := docker.NewRemoteClient(args.Filter, host); err == nil { + if _, err := client.ListContainers(); err == nil { + 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) + } + } else { + log.Warnf("Could not create client for %s: %s", host.ID, err) + } + } + certs, err := ReadCertificates(embededCerts) + 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)) + } + + 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 StartEvent(version, args.Mode, args.RemoteAgent, args.RemoteHost, nil, "") + } + } else { + log.Debugf("connected to local Docker Engine") + if !args.NoAnalytics { + go StartEvent(version, args.Mode, args.RemoteAgent, args.RemoteHost, localClient, "") + } + clients = append(clients, docker_support.NewDockerClientService(localClient)) + } + } + + return docker_support.NewMultiHostService(clients) +} diff --git a/main.go b/main.go index 33a3d313..f19e1988 100644 --- a/main.go +++ b/main.go @@ -11,11 +11,9 @@ import ( "os" "os/signal" "path/filepath" - "strings" "syscall" "time" - "github.com/alexflint/go-arg" "github.com/amir20/dozzle/internal/agent" "github.com/amir20/dozzle/internal/auth" "github.com/amir20/dozzle/internal/docker" @@ -27,49 +25,6 @@ import ( log "github.com/sirupsen/logrus" ) -var ( - version = "head" -) - -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."` - 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"` - Name string `arg:"--name, -n" help:"sets the display name for the user"` - Email string `arg:"--email, -e" help:"sets the email for the user"` -} - -func (args) Version() string { - return version -} - //go:embed all:dist var content embed.FS @@ -78,11 +33,11 @@ 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() + cli.ValidateEnvVars(cli.Args{}, cli.AgentCmd{}) + args, subcommand := cli.ParseArgs() if subcommand != nil { switch subcommand.(type) { - case *AgentCmd: + case *cli.AgentCmd: client, err := docker.NewLocalClient(args.Filter, args.Hostname) if err != nil { log.Fatalf("Could not create docker client: %v", err) @@ -102,8 +57,10 @@ func main() { } defer os.Remove(tempFile.Name()) io.WriteString(tempFile, listener.Addr().String()) + go cli.StartEvent(args.Version(), "", args.RemoteAgent, args.RemoteHost, client, "agent") agent.RunServer(client, certs, listener) - case *HealthcheckCmd: + case *cli.HealthcheckCmd: + go cli.StartEvent(args.Version(), "", args.RemoteAgent, args.RemoteHost, nil, "healthcheck") files, err := os.ReadDir(".") if err != nil { log.Fatalf("Failed to read directory: %v", err) @@ -134,7 +91,8 @@ func main() { } } - case *GenerateCmd: + case *cli.GenerateCmd: + go cli.StartEvent(args.Version(), "", args.RemoteAgent, args.RemoteHost, nil, "generate") if args.Generate.Username == "" || args.Generate.Password == "" { log.Fatal("Username and password are required") } @@ -158,11 +116,11 @@ func main() { log.Fatalf("Invalid auth provider %s", args.AuthProvider) } - log.Infof("Dozzle version %s", version) + log.Infof("Dozzle version %s", args.Version()) var multiHostService *docker_support.MultiHostService if args.Mode == "server" { - multiHostService = createMultiHostService(args) + multiHostService = cli.CreateMultiHostService(certs, args) if multiHostService.TotalClients() == 0 { log.Fatal("Could not connect to any Docker Engines") } else { @@ -209,60 +167,7 @@ func main() { log.Debug("shutdown complete") } -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.Infof("Creating client for %s with %s", host.Name, host.URL.String()) - if client, err := docker.NewRemoteClient(args.Filter, host); err == nil { - if _, err := client.ListContainers(); err == nil { - 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) - } - } else { - 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)) - } - - 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, multiHostService *docker_support.MultiHostService) *http.Server { +func createServer(args cli.Args, multiHostService *docker_support.MultiHostService) *http.Server { _, dev := os.LookupEnv("DEV") var provider web.AuthProvider = web.NONE @@ -291,7 +196,7 @@ func createServer(args args, multiHostService *docker_support.MultiHostService) config := web.Config{ Addr: args.Addr, Base: args.Base, - Version: version, + Version: args.Version(), Hostname: args.Hostname, NoAnalytics: args.NoAnalytics, Dev: dev, @@ -328,24 +233,3 @@ func createServer(args args, multiHostService *docker_support.MultiHostService) return web.CreateServer(multiHostService, assets, config) } - -func parseArgs() (args, interface{}) { - var args args - parser := arg.MustParse(&args) - - cli.ConfigureLogger(args.Level) - - args.Filter = make(map[string][]string) - - for _, filter := range args.FilterStrings { - pos := strings.Index(filter, "=") - if pos == -1 { - parser.Fail("each filter should be of the form key=value") - } - key := filter[:pos] - val := filter[pos+1:] - args.Filter[key] = append(args.Filter[key], val) - } - - return args, parser.Subcommand() -}