From b5e5bdb63f8824f099521088bb8e0633a401f397 Mon Sep 17 00:00:00 2001 From: shwoop Date: Thu, 28 Aug 2025 13:11:32 +0000 Subject: [PATCH 1/5] Add help text to binary --- cmd/gluetun/main.go | 5 +++++ internal/cli/help.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 internal/cli/help.go diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index c5b471264..1eae368bd 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -159,7 +159,11 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return cli.FormatServers(args[2:]) case "genkey": return cli.GenKey(args[2:]) + case "-help", "--help": + cli.Help() + return nil default: + cli.Help() return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) } } @@ -589,6 +593,7 @@ type clier interface { HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error GenKey(args []string) error + Help() } type Tun interface { diff --git a/internal/cli/help.go b/internal/cli/help.go new file mode 100644 index 000000000..75212622a --- /dev/null +++ b/internal/cli/help.go @@ -0,0 +1,17 @@ +package cli + +import ( + "fmt" +) + +func (c *CLI) Help() { + //nolint:lll + fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\nLightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\n") + fmt.Println("Commands:") + fmt.Printf(" version \tPrint the version of gluetun\n") + fmt.Printf(" update \tUpdate the VPN providers and servers\n") + fmt.Printf(" healthcheck \tCheck the health of the VPN connection\n") + fmt.Printf(" openvpnconfig \tPrint the OpenVPN configuration (for debugging)\n") + fmt.Printf(" format-servers \tFormat the servers into a format that can be used by OpenVPN\n") + fmt.Printf(" genkey \tGenerate a new OpenVPN key\n") +} From 58eb69a341c7eee14deea211d7e20e6706925469 Mon Sep 17 00:00:00 2001 From: shwoop Date: Fri, 12 Sep 2025 19:52:09 +0200 Subject: [PATCH 2/5] Update help text language proposed during code review Co-authored-by: Quentin McGaw --- internal/cli/help.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/cli/help.go b/internal/cli/help.go index 75212622a..e35d385e6 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -8,10 +8,9 @@ func (c *CLI) Help() { //nolint:lll fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\nLightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\n") fmt.Println("Commands:") - fmt.Printf(" version \tPrint the version of gluetun\n") - fmt.Printf(" update \tUpdate the VPN providers and servers\n") - fmt.Printf(" healthcheck \tCheck the health of the VPN connection\n") + fmt.Printf(" update \tUpdate the VPN servers data for some or all providers\n") + fmt.Printf(" healthcheck \tCheck the health of the VPN connection of another Gluetun instance\n") fmt.Printf(" openvpnconfig \tPrint the OpenVPN configuration (for debugging)\n") - fmt.Printf(" format-servers \tFormat the servers into a format that can be used by OpenVPN\n") - fmt.Printf(" genkey \tGenerate a new OpenVPN key\n") + fmt.Printf(" format-servers \tFormat the servers data into a Markdown table\n") + fmt.Printf(" genkey \tGenerate a new 32 bytes Wireguard key (base58 encoded)\n") } From 582487baf72093d93888305e2e68e59687d8380e Mon Sep 17 00:00:00 2001 From: shwoop Date: Fri, 12 Sep 2025 18:04:00 +0000 Subject: [PATCH 3/5] make help a command rather than flags --- cmd/gluetun/main.go | 2 +- internal/cli/help.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 1eae368bd..965dc0536 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -159,7 +159,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return cli.FormatServers(args[2:]) case "genkey": return cli.GenKey(args[2:]) - case "-help", "--help": + case "help": cli.Help() return nil default: diff --git a/internal/cli/help.go b/internal/cli/help.go index e35d385e6..54b730c8f 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -13,4 +13,5 @@ func (c *CLI) Help() { fmt.Printf(" openvpnconfig \tPrint the OpenVPN configuration (for debugging)\n") fmt.Printf(" format-servers \tFormat the servers data into a Markdown table\n") fmt.Printf(" genkey \tGenerate a new 32 bytes Wireguard key (base58 encoded)\n") + fmt.Printf(" help \tDisplay available options\n") } From 2af3d4053048e756c61fdc25ac405ef94bfea3cf Mon Sep 17 00:00:00 2001 From: shwoop Date: Fri, 12 Sep 2025 19:22:16 +0000 Subject: [PATCH 4/5] Refactor client to be injected with subcommands --- cmd/gluetun/main.go | 47 +++++++++++------------------------ internal/cli/cli.go | 45 ++++++++++++++++++++++++++++++--- internal/cli/formatservers.go | 23 +++++++++++++++-- internal/cli/genkey.go | 23 +++++++++++++++-- internal/cli/healthcheck.go | 22 ++++++++++++++-- internal/cli/help.go | 17 ------------- internal/cli/interfaces.go | 12 ++++++++- internal/cli/openvpnconfig.go | 34 ++++++++++++++++++++----- internal/cli/update.go | 34 ++++++++++++++++++++----- 9 files changed, 185 insertions(+), 72 deletions(-) delete mode 100644 internal/cli/help.go diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index 965dc0536..fcf9bd8d1 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -78,7 +78,6 @@ func main() { tun := tun.New() netLinkDebugLogger := logger.New(log.SetComponent("netlink")) netLinker := netlink.New(netLinkDebugLogger) - cli := cli.New() cmder := command.New() reader := reader.New(reader.Settings{ @@ -93,13 +92,24 @@ func main() { }, }) + clier, err := cli.New( + cli.NewUpdateCommand("./internal/storage/servers.json", args, logger), + cli.NewHealthCheckCommand(reader), + cli.NewOpenVPNConfigCommand(logger, reader, netLinker), + cli.NewFormatServersCommand(args), + cli.NewGenKeyCommand(args), + ) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + errorCh := make(chan error) go func() { - errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli) + errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, clier) }() // Wait for OS signal or run error - var err error select { case receivedSignal := <-signalCh: signal.Stop(signalCh) @@ -137,8 +147,6 @@ func main() { } } -var errCommandUnknown = errors.New("command is unknown") - //nolint:gocognit,gocyclo,maintidx func _main(ctx context.Context, buildInfo models.BuildInformation, args []string, logger log.LoggerInterface, reader *reader.Reader, @@ -146,26 +154,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, cli clier, ) error { if len(args) > 1 { // cli operation - switch args[1] { - case "healthcheck": - return cli.HealthCheck(ctx, reader, logger) - case "clientkey": - return cli.ClientKey(args[2:]) - case "openvpnconfig": - return cli.OpenvpnConfig(logger, reader, netLinker) - case "update": - return cli.Update(ctx, args[2:], logger) - case "format-servers": - return cli.FormatServers(args[2:]) - case "genkey": - return cli.GenKey(args[2:]) - case "help": - cli.Help() - return nil - default: - cli.Help() - return fmt.Errorf("%w: %s", errCommandUnknown, args[1]) - } + return cli.RunCommand(ctx, args[1]) } announcementExp, err := time.Parse(time.RFC3339, "2024-12-01T00:00:00Z") @@ -587,13 +576,7 @@ type Linker interface { } type clier interface { - ClientKey(args []string) error - FormatServers(args []string) error - OpenvpnConfig(logger cli.OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker cli.IPv6Checker) error - HealthCheck(ctx context.Context, reader *reader.Reader, warner cli.Warner) error - Update(ctx context.Context, args []string, logger cli.UpdaterLogger) error - GenKey(args []string) error - Help() + RunCommand(context.Context, string) error } type Tun interface { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index f3a5f3eae..5703a5e7b 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,11 +1,48 @@ package cli +import ( + "context" + "errors" + "fmt" +) + +var ( + ErrCommandAlreadyRegistered = errors.New("command already registered") + errCommandUnknown = errors.New("command is unknown") +) + type CLI struct { - repoServersPath string + subCommands map[string]SubCommand +} + +func New(subCommands ...SubCommand) (*CLI, error) { + cli := &CLI{ + subCommands: make(map[string]SubCommand), + } + for _, command := range subCommands { + if _, exists := cli.subCommands[command.Name()]; exists { + return nil, fmt.Errorf("%w: %s", ErrCommandAlreadyRegistered, command.Name()) + } + cli.subCommands[command.Name()] = command + } + return cli, nil +} + +func (c *CLI) RunCommand(ctx context.Context, command string) error { + if subCommand, exists := c.subCommands[command]; exists { + return subCommand.Run(ctx) + } + c.help() + if command == "help" { + return nil + } + return fmt.Errorf("%w: %s", errCommandUnknown, command) } -func New() *CLI { - return &CLI{ - repoServersPath: "./internal/storage/servers.json", +func (c *CLI) help() { + //nolint:lll + fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\nLightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\nCommands:\n") + for _, command := range c.subCommands { + fmt.Printf(" %-20s\t%s\n", command.Name(), command.Description()) } } diff --git a/internal/cli/formatservers.go b/internal/cli/formatservers.go index 254c04517..9fd1eaad7 100644 --- a/internal/cli/formatservers.go +++ b/internal/cli/formatservers.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "flag" "fmt" @@ -31,7 +32,25 @@ func addProviderFlag(flagSet *flag.FlagSet, providerToFormat map[string]*bool, flagSet.BoolVar(boolPtr, provider, false, "Format "+titleCaser.String(provider)+" servers") } -func (c *CLI) FormatServers(args []string) error { +type FormatServersCommand struct { + args []string +} + +func NewFormatServersCommand(args []string) *FormatServersCommand { + return &FormatServersCommand{ + args: args, + } +} + +func (c *FormatServersCommand) Name() string { + return "format-servers" +} + +func (c *FormatServersCommand) Description() string { + return "Format the servers data into a Markdown table" +} + +func (c *FormatServersCommand) Run(_ context.Context) error { var format, output string allProviders := providers.All() allProviderFlags := make([]string, len(allProviders)) @@ -50,7 +69,7 @@ func (c *CLI) FormatServers(args []string) error { for _, provider := range allProviderFlags { addProviderFlag(flagSet, providersToFormat, provider, titleCaser) } - if err := flagSet.Parse(args); err != nil { + if err := flagSet.Parse(c.args[2:]); err != nil { return err } diff --git a/internal/cli/genkey.go b/internal/cli/genkey.go index 0b50ab000..5a04bc6b2 100644 --- a/internal/cli/genkey.go +++ b/internal/cli/genkey.go @@ -1,14 +1,33 @@ package cli import ( + "context" "crypto/rand" "flag" "fmt" ) -func (c *CLI) GenKey(args []string) (err error) { +type GenKeyCommand struct { + args []string +} + +func NewGenKeyCommand(args []string) *GenKeyCommand { + return &GenKeyCommand{ + args: args, + } +} + +func (c *GenKeyCommand) Name() string { + return "genkey" +} + +func (c *GenKeyCommand) Description() string { + return "Generate a new 32 bytes Wireguard key (base58 encoded)" +} + +func (c *GenKeyCommand) Run(_ context.Context) (err error) { flagSet := flag.NewFlagSet("genkey", flag.ExitOnError) - err = flagSet.Parse(args) + err = flagSet.Parse(c.args[2:]) if err != nil { return fmt.Errorf("parsing flags: %w", err) } diff --git a/internal/cli/healthcheck.go b/internal/cli/healthcheck.go index 2ceaa6343..7cb548305 100644 --- a/internal/cli/healthcheck.go +++ b/internal/cli/healthcheck.go @@ -11,10 +11,28 @@ import ( "github.com/qdm12/gosettings/reader" ) -func (c *CLI) HealthCheck(ctx context.Context, reader *reader.Reader, _ Warner) (err error) { +type HealthCheckCommand struct { + reader *reader.Reader +} + +func NewHealthCheckCommand(reader *reader.Reader) *HealthCheckCommand { + return &HealthCheckCommand{ + reader: reader, + } +} + +func (c *HealthCheckCommand) Name() string { + return "healthcheck" +} + +func (c *HealthCheckCommand) Description() string { + return "Check the health of the VPN connection of another Gluetun instance" +} + +func (c *HealthCheckCommand) Run(ctx context.Context) (err error) { // Extract the health server port from the configuration. var config settings.Health - err = config.Read(reader) + err = config.Read(c.reader) if err != nil { return err } diff --git a/internal/cli/help.go b/internal/cli/help.go deleted file mode 100644 index 54b730c8f..000000000 --- a/internal/cli/help.go +++ /dev/null @@ -1,17 +0,0 @@ -package cli - -import ( - "fmt" -) - -func (c *CLI) Help() { - //nolint:lll - fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\nLightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\n") - fmt.Println("Commands:") - fmt.Printf(" update \tUpdate the VPN servers data for some or all providers\n") - fmt.Printf(" healthcheck \tCheck the health of the VPN connection of another Gluetun instance\n") - fmt.Printf(" openvpnconfig \tPrint the OpenVPN configuration (for debugging)\n") - fmt.Printf(" format-servers \tFormat the servers data into a Markdown table\n") - fmt.Printf(" genkey \tGenerate a new 32 bytes Wireguard key (base58 encoded)\n") - fmt.Printf(" help \tDisplay available options\n") -} diff --git a/internal/cli/interfaces.go b/internal/cli/interfaces.go index ad5ac64b8..5c992d6a6 100644 --- a/internal/cli/interfaces.go +++ b/internal/cli/interfaces.go @@ -1,9 +1,19 @@ package cli -import "github.com/qdm12/gluetun/internal/configuration/settings" +import ( + "context" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) type Source interface { Read() (settings settings.Settings, err error) ReadHealth() (health settings.Health, err error) String() string } + +type SubCommand interface { + Run(ctx context.Context) (err error) + Name() string + Description() string +} diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 6987d4969..44de11e55 100644 --- a/internal/cli/openvpnconfig.go +++ b/internal/cli/openvpnconfig.go @@ -43,27 +43,49 @@ type IPv6Checker interface { IsIPv6Supported() (supported bool, err error) } -func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader, +type OpenVPNConfigCommand struct { + logger OpenvpnConfigLogger + reader *reader.Reader + ipv6Checker IPv6Checker +} + +func NewOpenVPNConfigCommand(logger OpenvpnConfigLogger, reader *reader.Reader, ipv6Checker IPv6Checker, -) error { - storage, err := storage.New(logger, constants.ServersData) +) *OpenVPNConfigCommand { + return &OpenVPNConfigCommand{ + logger: logger, + reader: reader, + ipv6Checker: ipv6Checker, + } +} + +func (c *OpenVPNConfigCommand) Name() string { + return "openvpnconfig" +} + +func (c *OpenVPNConfigCommand) Description() string { + return "OPrint the OpenVPN configuration (for debugging)" +} + +func (c *OpenVPNConfigCommand) Run(_ context.Context) error { + storage, err := storage.New(c.logger, constants.ServersData) if err != nil { return err } var allSettings settings.Settings - err = allSettings.Read(reader, logger) + err = allSettings.Read(c.reader, c.logger) if err != nil { return err } allSettings.SetDefaults() - ipv6Supported, err := ipv6Checker.IsIPv6Supported() + ipv6Supported, err := c.ipv6Checker.IsIPv6Supported() if err != nil { return fmt.Errorf("checking for IPv6 support: %w", err) } - if err = allSettings.Validate(storage, ipv6Supported, logger); err != nil { + if err = allSettings.Validate(storage, ipv6Supported, c.logger); err != nil { return fmt.Errorf("validating settings: %w", err) } diff --git a/internal/cli/update.go b/internal/cli/update.go index 9e6844905..d2f4f01ce 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -32,7 +32,29 @@ type UpdaterLogger interface { Error(s string) } -func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) error { +type UpdateCommand struct { + repoServersPath string + args []string + logger UpdaterLogger +} + +func NewUpdateCommand(repoServersPath string, args []string, logger UpdaterLogger) *UpdateCommand { + return &UpdateCommand{ + repoServersPath: repoServersPath, + args: args, + logger: logger, + } +} + +func (c *UpdateCommand) Name() string { + return "update" +} + +func (c *UpdateCommand) Description() string { + return "Update the VPN servers information of one or more providers" +} + +func (c *UpdateCommand) Run(ctx context.Context) error { options := settings.Updater{} var endUserMode, maintainerMode, updateAll bool var csvProviders, ipToken string @@ -47,7 +69,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e flagSet.BoolVar(&updateAll, "all", false, "Update servers for all VPN providers") flagSet.StringVar(&csvProviders, "providers", "", "CSV string of VPN providers to update server data for") flagSet.StringVar(&ipToken, "ip-token", "", "IP data service token (e.g. ipinfo.io) to use") - if err := flagSet.Parse(args); err != nil { + if err := flagSet.Parse(c.args[2:]); err != nil { return err } @@ -71,7 +93,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e return fmt.Errorf("options validation failed: %w", err) } - storage, err := storage.New(logger, constants.ServersData) + storage, err := storage.New(c.logger, constants.ServersData) if err != nil { return fmt.Errorf("creating servers storage: %w", err) } @@ -89,14 +111,14 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e if err != nil { return fmt.Errorf("creating public IP fetchers: %w", err) } - ipFetcher := api.NewResilient(fetchers, logger) + ipFetcher := api.NewResilient(fetchers, c.logger) openvpnFileExtractor := extract.New() - providers := provider.NewProviders(storage, time.Now, logger, httpClient, + providers := provider.NewProviders(storage, time.Now, c.logger, httpClient, unzipper, parallelResolver, ipFetcher, openvpnFileExtractor) - updater := updater.New(httpClient, storage, providers, logger) + updater := updater.New(httpClient, storage, providers, c.logger) err = updater.UpdateServers(ctx, options.Providers, options.MinRatio) if err != nil { return fmt.Errorf("updating server information: %w", err) From f144d482fc345dbdf0a2f078b00a18453989ac2c Mon Sep 17 00:00:00 2001 From: shwoop Date: Fri, 12 Sep 2025 19:23:33 +0000 Subject: [PATCH 5/5] avoid long lines --- internal/cli/cli.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 5703a5e7b..96fe0be69 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -40,8 +40,10 @@ func (c *CLI) RunCommand(ctx context.Context, command string) error { } func (c *CLI) help() { - //nolint:lll - fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\nLightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\nCommands:\n") + fmt.Printf("Usage: gluetun [COMMAND [OPTIONS]]\n\n" + + "Lightweight swiss-army-knife-like VPN client to multiple VPN service providers.\n\n" + + "Commands:\n") + for _, command := range c.subCommands { fmt.Printf(" %-20s\t%s\n", command.Name(), command.Description()) }