diff --git a/internal/support/cli/agent_command.go b/internal/support/cli/agent_command.go new file mode 100644 index 00000000..428437bd --- /dev/null +++ b/internal/support/cli/agent_command.go @@ -0,0 +1,67 @@ +package cli + +import ( + "context" + "embed" + "fmt" + "io" + "net" + "os" + "os/signal" + "syscall" + + "github.com/amir20/dozzle/internal/agent" + "github.com/amir20/dozzle/internal/docker" + "github.com/rs/zerolog/log" +) + + +type AgentCmd struct { + Addr string `arg:"--agent-addr,env:DOZZLE_AGENT_ADDR" default:":7007" help:"sets the host:port to bind for the agent"` +} + +func (a *AgentCmd) Run(args Args, embeddedCerts embed.FS) error { + if args.Mode != "server" { + return fmt.Errorf("agent command is only available in server mode") + } + client, err := docker.NewLocalClient(args.Hostname) + if err != nil { + return fmt.Errorf("failed to create docker client: %w", err) + } + certs, err := ReadCertificates(embeddedCerts) + if err != nil { + return fmt.Errorf("failed to read certificates: %w", err) + } + + listener, err := net.Listen("tcp", args.Agent.Addr) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + tempFile, err := os.CreateTemp("./", "agent-*.addr") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + io.WriteString(tempFile, listener.Addr().String()) + go StartEvent(args, "", client, "agent") + server, err := agent.NewServer(client, certs, args.Version(), args.Filter) + if err != nil { + return fmt.Errorf("failed to create agent server: %w", err) + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + go func() { + log.Info().Msgf("Dozzle agent version %s", args.Version()) + log.Info().Msgf("Agent listening on %s", listener.Addr().String()) + + if err := server.Serve(listener); err != nil { + log.Error().Err(err).Msg("failed to serve") + } + }() + <-ctx.Done() + stop() + log.Info().Msg("Shutting down agent") + server.Stop() + log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file") + os.Remove(tempFile.Name()) + return nil +} diff --git a/internal/support/cli/agent_test_command.go b/internal/support/cli/agent_test_command.go new file mode 100644 index 00000000..358a1e88 --- /dev/null +++ b/internal/support/cli/agent_test_command.go @@ -0,0 +1,38 @@ +package cli + +import ( + "context" + "embed" + "fmt" + + "github.com/amir20/dozzle/internal/agent" + "github.com/rs/zerolog/log" +) + +type AgentTestCmd struct { + Address string `arg:"positional"` +} + +func (at *AgentTestCmd) Run(args Args, embeddedCerts embed.FS) error { + certs, err := ReadCertificates(embeddedCerts) + if err != nil { + return fmt.Errorf("error reading certificates: %w", err) + } + + log.Info().Str("endpoint", args.AgentTest.Address).Msg("Connecting to agent") + + agent, err := agent.NewClient(args.AgentTest.Address, certs) + if err != nil { + return fmt.Errorf("error connecting to agent: %w", err) + } + ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) + defer cancel() + host, err := agent.Host(ctx) + if err != nil { + return fmt.Errorf("error fetching host info for agent: %w", err) + } + + log.Info().Str("endpoint", args.AgentTest.Address).Str("version", host.AgentVersion).Str("name", host.Name).Str("id", host.ID).Msg("Successfully connected to agent") + + return nil +} diff --git a/internal/support/cli/args.go b/internal/support/cli/args.go index 5b793a36..50d9a58a 100644 --- a/internal/support/cli/args.go +++ b/internal/support/cli/args.go @@ -1,6 +1,7 @@ package cli import ( + "embed" "strings" "time" @@ -36,23 +37,8 @@ type Args struct { AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"` } -type HealthcheckCmd struct { -} - -type AgentCmd struct { - Addr string `arg:"--agent-addr,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"` - 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"` - Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."` +type Runnable interface { + Run(args Args, embeddedCerts embed.FS) error } func (Args) Version() string { diff --git a/internal/support/cli/generate_command.go b/internal/support/cli/generate_command.go new file mode 100644 index 00000000..a50ff910 --- /dev/null +++ b/internal/support/cli/generate_command.go @@ -0,0 +1,38 @@ +package cli + +import ( + "embed" + "fmt" + "os" + + "github.com/amir20/dozzle/internal/auth" +) + +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"` + Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."` +} + +func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error { + StartEvent(args, "", nil, "generate") + if args.Generate.Username == "" || args.Generate.Password == "" { + return fmt.Errorf("Username and password are required") + } + + buffer := auth.GenerateUsers(auth.User{ + Username: args.Generate.Username, + Password: args.Generate.Password, + Name: args.Generate.Name, + Email: args.Generate.Email, + Filter: args.Generate.Filter, + }, true) + + if _, err := os.Stdout.Write(buffer.Bytes()); err != nil { + return fmt.Errorf("Failed to write to stdout: %w", err) + } + + return nil +} diff --git a/internal/support/cli/health_command.go b/internal/support/cli/health_command.go new file mode 100644 index 00000000..02cc3127 --- /dev/null +++ b/internal/support/cli/health_command.go @@ -0,0 +1,50 @@ +package cli + +import ( + "context" + "embed" + "fmt" + "os" + "path/filepath" + + "github.com/amir20/dozzle/internal/healthcheck" +) + +type HealthcheckCmd struct { +} + +func (h *HealthcheckCmd) Run(args Args, embeddedCerts embed.FS) error { + files, err := os.ReadDir(".") + if err != nil { + return fmt.Errorf("failed to read directory: %w", err) + } + + agentAddress := "" + for _, file := range files { + if match, _ := filepath.Match("agent-*.addr", file.Name()); match { + data, err := os.ReadFile(file.Name()) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + agentAddress = string(data) + break + } + } + if agentAddress == "" { + if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + } else { + certs, err := ReadCertificates(embeddedCerts) + if err != nil { + return fmt.Errorf("failed to read certificates: %w", err) + } + ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) + defer cancel() + if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil { + return fmt.Errorf("failed to make request: %w", err) + } + } + + return nil +} diff --git a/main.go b/main.go index 68672f91..a77a18b3 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "context" "embed" - "io" "io/fs" "net" @@ -17,7 +16,6 @@ import ( "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/k8s" "github.com/amir20/dozzle/internal/support/cli" docker_support "github.com/amir20/dozzle/internal/support/docker" @@ -37,122 +35,13 @@ func main() { cli.ValidateEnvVars(cli.Args{}, cli.AgentCmd{}) args, subcommand := cli.ParseArgs() if subcommand != nil { - switch subcommand.(type) { - case *cli.AgentCmd: - if args.Mode != "server" { - log.Fatal().Msg("Dozzle agent command is only available in server mode") - } - client, err := docker.NewLocalClient(args.Hostname) - if err != nil { - log.Fatal().Err(err).Msg("Could not create docker client") - } - certs, err := cli.ReadCertificates(certs) - if err != nil { - log.Fatal().Err(err).Msg("Could not read certificates") - } - - listener, err := net.Listen("tcp", args.Agent.Addr) - if err != nil { - log.Fatal().Err(err).Msg("failed to listen") - } - tempFile, err := os.CreateTemp("./", "agent-*.addr") - if err != nil { - log.Fatal().Err(err).Msg("failed to create temp file") - } - io.WriteString(tempFile, listener.Addr().String()) - go cli.StartEvent(args, "", client, "agent") - server, err := agent.NewServer(client, certs, args.Version(), args.Filter) - if err != nil { - log.Fatal().Err(err).Msg("failed to create agent server") - } - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() - go func() { - log.Info().Msgf("Dozzle agent version %s", args.Version()) - log.Info().Msgf("Agent listening on %s", listener.Addr().String()) - - if err := server.Serve(listener); err != nil { - log.Error().Err(err).Msg("failed to serve") - } - }() - <-ctx.Done() - stop() - log.Info().Msg("Shutting down agent") - server.Stop() - log.Debug().Str("file", tempFile.Name()).Msg("Removing temp file") - os.Remove(tempFile.Name()) - - case *cli.HealthcheckCmd: - files, err := os.ReadDir(".") - if err != nil { - log.Fatal().Err(err).Msg("Failed to read directory") - } - - agentAddress := "" - for _, file := range files { - if match, _ := filepath.Match("agent-*.addr", file.Name()); match { - data, err := os.ReadFile(file.Name()) - if err != nil { - log.Fatal().Err(err).Msg("Failed to read file") - } - agentAddress = string(data) - break - } - } - if agentAddress == "" { - if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil { - log.Fatal().Err(err).Msg("Failed to make request") - } - } else { - certs, err := cli.ReadCertificates(certs) - if err != nil { - log.Fatal().Err(err).Msg("Could not read certificates") - } - ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) - defer cancel() - if err := healthcheck.RPCRequest(ctx, agentAddress, certs); err != nil { - log.Fatal().Err(err).Msg("Failed to make request") - } - } - - case *cli.GenerateCmd: - cli.StartEvent(args, "", nil, "generate") - if args.Generate.Username == "" || args.Generate.Password == "" { - log.Fatal().Msg("Username and password are required") - } - - buffer := auth.GenerateUsers(auth.User{ - Username: args.Generate.Username, - Password: args.Generate.Password, - Name: args.Generate.Name, - Email: args.Generate.Email, - Filter: args.Generate.Filter, - }, true) - - 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") + runnable, ok := subcommand.(cli.Runnable) + if !ok { + log.Fatal().Msg("Invalid command") + } + err := runnable.Run(args, certs) + if err != nil { + log.Fatal().Err(err).Msg("Failed to run command") } os.Exit(0)