From 810bf018713af6acdaf265aa92b08f0606d229c0 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Wed, 21 Aug 2024 17:30:49 -0700 Subject: [PATCH] feat: change startup banner to versitygw version This changes the startup banner to report the versitygw version and build info along with interfaces configured for admin and s3 services when quiet option not enabled. Fixes #728 --- cmd/versitygw/main.go | 198 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 190 insertions(+), 8 deletions(-) diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index e8eac072..237739d5 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -19,9 +19,11 @@ import ( "crypto/tls" "fmt" "log" + "net" "net/http" _ "net/http/pprof" "os" + "strings" "github.com/gofiber/fiber/v2" "github.com/urfave/cli/v2" @@ -518,11 +520,12 @@ func runGateway(ctx context.Context, be backend.Backend) error { } app := fiber.New(fiber.Config{ - AppName: "versitygw", - ServerHeader: "VERSITYGW", - StreamRequestBody: true, - DisableKeepalive: true, - Network: fiber.NetworkTCP, + AppName: "versitygw", + ServerHeader: "VERSITYGW", + StreamRequestBody: true, + DisableKeepalive: true, + Network: fiber.NetworkTCP, + DisableStartupMessage: true, }) var opts []s3api.Option @@ -558,9 +561,10 @@ func runGateway(ctx context.Context, be backend.Backend) error { } admApp := fiber.New(fiber.Config{ - AppName: "versitygw", - ServerHeader: "VERSITYGW", - Network: fiber.NetworkTCP, + AppName: "versitygw", + ServerHeader: "VERSITYGW", + Network: fiber.NetworkTCP, + DisableStartupMessage: true, }) var admOpts []s3api.AdminOpt @@ -662,6 +666,10 @@ func runGateway(ctx context.Context, be backend.Backend) error { admSrv := s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, loggers.AdminLogger, admOpts...) + if !quiet { + printBanner(port, admPort, certFile != "", admCertFile != "") + } + c := make(chan error, 2) go func() { c <- srv.Serve() }() if admPort != "" { @@ -740,3 +748,177 @@ Loop: return saveErr } + +func printBanner(port, admPort string, ssl, admSsl bool) { + interfaces, err := getMatchingIPs(port) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to match local IP addresses: %v\n", err) + return + } + + var admInterfaces []string + if admPort != "" { + admInterfaces, err = getMatchingIPs(admPort) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to match admin port local IP addresses: %v\n", err) + return + } + } + + title := "VersityGW" + version := fmt.Sprintf("Version %v, Build %v", Version, Build) + urls := []string{} + + hst, prt, err := net.SplitHostPort(port) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse port: %v\n", err) + return + } + + for _, ip := range interfaces { + url := fmt.Sprintf("http://%s:%s", ip, prt) + if ssl { + url = fmt.Sprintf("https://%s:%s", ip, prt) + } + urls = append(urls, url) + } + + if hst == "" { + hst = "0.0.0.0" + } + + boundHost := fmt.Sprintf("(bound on host %s and port %s)", hst, prt) + + lines := []string{ + centerText(title), + centerText(version), + centerText(boundHost), + centerText(""), + } + + if len(admInterfaces) > 0 { + lines = append(lines, + leftText("S3 service listening on:"), + ) + } else { + lines = append(lines, + leftText("Admin/S3 service listening on:"), + ) + } + + for _, url := range urls { + lines = append(lines, leftText(" "+url)) + } + + if len(admInterfaces) > 0 { + lines = append(lines, + centerText(""), + leftText("Admin service listening on:"), + ) + + _, prt, err := net.SplitHostPort(admPort) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse port: %v\n", err) + return + } + + for _, ip := range admInterfaces { + url := fmt.Sprintf("http://%s:%s", ip, prt) + if admSsl { + url = fmt.Sprintf("https://%s:%s", ip, prt) + } + lines = append(lines, leftText(" "+url)) + } + } + + // Print the top border + fmt.Println("┌" + strings.Repeat("─", columnWidth-2) + "┐") + + // Print each line + for _, line := range lines { + fmt.Printf("│%-*s│\n", columnWidth-2, line) + } + + // Print the bottom border + fmt.Println("└" + strings.Repeat("─", columnWidth-2) + "┘") +} + +// getMatchingIPs returns all IP addresses for local system interfaces that +// match the input address specification. +func getMatchingIPs(spec string) ([]string, error) { + // Split the input spec into IP and port + host, _, err := net.SplitHostPort(spec) + if err != nil { + return nil, fmt.Errorf("parse address/port: %v", err) + } + + // Handle cases where IP is omitted (e.g., ":1234") + if host == "" { + host = "0.0.0.0" + } + + ipaddr, err := net.ResolveIPAddr("ip", host) + if err != nil { + return nil, err + } + + parsedInputIP := ipaddr.IP + + var result []string + + // Get all network interfaces + interfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + for _, iface := range interfaces { + // Get all addresses associated with the interface + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + + for _, addr := range addrs { + // Parse the address to get the IP part + ipAddr, _, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + + if ipAddr.IsLinkLocalUnicast() { + continue + } + if ipAddr.IsInterfaceLocalMulticast() { + continue + } + if ipAddr.IsLinkLocalMulticast() { + continue + } + + // Check if the IP matches the input specification + if parsedInputIP.Equal(net.IPv4(0, 0, 0, 0)) || parsedInputIP.Equal(ipAddr) { + result = append(result, ipAddr.String()) + } + } + } + + return result, nil +} + +const columnWidth = 70 + +func centerText(text string) string { + padding := (columnWidth - 2 - len(text)) / 2 + if padding < 0 { + padding = 0 + } + return strings.Repeat(" ", padding) + text +} + +func leftText(text string) string { + if len(text) > columnWidth-2 { + return text + } + return text + strings.Repeat(" ", columnWidth-2-len(text)) +}