Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions cmd/portal-tunnel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,7 @@ type exposeFlags struct {
metricsAddr string
}

func runExposeCommand(args []string) error {
installer.StartUpdateCheck(types.ReleaseVersion)

flags := exposeFlags{}
fs := utils.NewFlagSet("expose", printExposeUsage)

func registerExposeFlags(fs *flag.FlagSet, flags *exposeFlags) {
utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
utils.StringFlagEnv(fs, &flags.multiHopCSV, "multi-hop", "", "Ordered multi-hop relay API URLs, comma-separated", "MULTI_HOP")
utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include bootstrap relays and discover additional relays")
Expand Down Expand Up @@ -113,7 +108,14 @@ func runExposeCommand(args []string) error {
utils.IntFlagEnv(fs, &flags.maxActiveRelays, "max-active-relays", 3, nil, "Maximum auto-selected single-hop relays to keep connected; multi-hop uses every eligible relay as an entry", "MAX_ACTIVE_RELAYS")
utils.IntFlagEnv(fs, &flags.multiHopDepth, "multi-hop-depth", 0, nil, "Automatically create multi-hop routes at this hop count for every eligible entry relay; 0 or 1 disables multi-hop", "MULTI_HOP_DEPTH")
utils.StringFlag(fs, &flags.metricsAddr, "metrics-addr", "", "Optional address (host:port) to serve Prometheus /metrics. Empty = disabled.")
}

func runExposeCommand(args []string) error {
installer.StartUpdateCheck(types.ReleaseVersion)

flags := exposeFlags{}
fs := utils.NewFlagSet("expose", printExposeUsage)
registerExposeFlags(fs, &flags)
if err := utils.ParseFlagSet(fs, args, printExposeUsage); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
Expand Down Expand Up @@ -435,6 +437,15 @@ func printExposeUsage(w io.Writer) {
"portal expose 3000 --multi-hop-depth 3",
},
)
fs := utils.NewFlagSet("expose", nil)
registerExposeFlags(fs, &exposeFlags{})
utils.WriteFlagDefaults(w, fs)
utils.WriteHelpSection(w, "Loopback", []string{
"portal expose 127.0.0.1:8080 --identity-path /absolute/path/outside/repo/identity.json --relays https://127.0.0.1:4017 --discovery=false",
})
utils.WriteHelpSection(w, "Ready", []string{
"On success the process logs a line starting with: service ready at",
})
}

func printListUsage(w io.Writer) {
Expand Down
33 changes: 23 additions & 10 deletions cmd/relay-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,20 @@ func resolveRelayServerConfig(args []string) (relayServerConfig, error) {

cfg := relayServerConfig{}
fs := utils.NewFlagSet("relay-server", printRootUsage)
registerRelayServerFlags(fs, &cfg)

if err := utils.ParseFlagSet(fs, args, printRootUsage); err != nil {
return relayServerConfig{}, err
}
if err := utils.RequireNoArgs(fs.Args(), "relay-server"); err != nil {
printRootUsage(os.Stderr)
return relayServerConfig{}, err
}
cfg.IdentityPath = identity.ResolveRelayStateDir(cfg.IdentityPath)
return cfg, nil
}

func registerRelayServerFlags(fs *flag.FlagSet, cfg *relayServerConfig) {
utils.StringFlagEnv(fs, &cfg.PortalURL, "portal-url", "https://localhost", "portal base URL", "PORTAL_URL")
utils.StringFlagEnv(fs, &cfg.FrontendDir, "frontend-dir", "", "custom SPA directory containing index.html; embedded frontend is used when empty", "PORTAL_FRONTEND_DIR")
utils.StringFlagEnv(fs, &cfg.IdentityPath, "identity-path", "./.portal-certs", "directory path for relay identity, policy state, and keyless materials", "IDENTITY_PATH")
Expand Down Expand Up @@ -126,16 +139,6 @@ func resolveRelayServerConfig(args []string) (relayServerConfig, error) {
utils.StringFlagEnv(fs, &cfg.AWSDNSSECKMSKeyARN, "aws-dnssec-kms-key-arn", "", "AWS KMS key ARN used to create a Route53 DNSSEC key-signing key when needed", "AWS_DNSSEC_KMS_KEY_ARN")
utils.StringFlagEnv(fs, &cfg.VultrAPIKey, "vultr-api-key", "", "Vultr API key for DNS automation (required when acme-dns-provider=vultr)", "VULTR_API_KEY")
utils.StringFlagEnv(fs, &cfg.NjallaToken, "njalla-token", "", "Njalla API token for DNS automation (required when acme-dns-provider=njalla)", "NJALLA_TOKEN")

if err := utils.ParseFlagSet(fs, args, printRootUsage); err != nil {
return relayServerConfig{}, err
}
if err := utils.RequireNoArgs(fs.Args(), "relay-server"); err != nil {
printRootUsage(os.Stderr)
return relayServerConfig{}, err
}
cfg.IdentityPath = identity.ResolveRelayStateDir(cfg.IdentityPath)
return cfg, nil
}

func runServeCommand(args []string) error {
Expand Down Expand Up @@ -275,4 +278,14 @@ func printRootUsage(w io.Writer) {
"relay-server help",
},
)
fs := utils.NewFlagSet("relay-server", nil)
registerRelayServerFlags(fs, &relayServerConfig{})
utils.WriteFlagDefaults(w, fs)
utils.WriteHelpSection(w, "Loopback", []string{
"relay-server --portal-url https://127.0.0.1:4017 --api-port 4017 --sni-port 8443",
"portal expose 127.0.0.1:8080 --relays https://127.0.0.1:4017 --discovery=false",
})
utils.WriteHelpSection(w, "Ready", []string{
"After portal expose succeeds, it logs a line starting with: service ready at",
})
}
36 changes: 33 additions & 3 deletions utils/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ func StringFlag(fs *flag.FlagSet, target *string, name, fallback, usage string)
func StringFlagEnv(fs *flag.FlagSet, target *string, name, fallback, usage string, envNames ...string) {
value, setBy := resolveStringEnv(fallback, envNames...)
registerEnvVar(name, usage, fallback, value, setBy, envNames)
ensureFlagSet(fs).StringVar(target, name, value, flagUsage(usage, envNames...))
flagSet := ensureFlagSet(fs)
flagSet.StringVar(target, name, value, flagUsage(usage, envNames...))
flagSet.Lookup(name).DefValue = fallback
}

func BoolFlag(fs *flag.FlagSet, target *bool, name string, fallback bool, usage string) {
Expand All @@ -234,13 +236,17 @@ func BoolFlag(fs *flag.FlagSet, target *bool, name string, fallback bool, usage
func BoolFlagEnv(fs *flag.FlagSet, target *bool, name string, fallback bool, usage string, envNames ...string) {
value, setBy := resolveBoolEnv(fallback, envNames...)
registerEnvVar(name, usage, strconv.FormatBool(fallback), strconv.FormatBool(value), setBy, envNames)
ensureFlagSet(fs).BoolVar(target, name, value, flagUsage(usage, envNames...))
flagSet := ensureFlagSet(fs)
flagSet.BoolVar(target, name, value, flagUsage(usage, envNames...))
flagSet.Lookup(name).DefValue = strconv.FormatBool(fallback)
}

func IntFlagEnv(fs *flag.FlagSet, target *int, name string, fallback int, parse IntEnvParser, usage string, envNames ...string) {
value, setBy := resolveIntEnv(fallback, parse, envNames...)
registerEnvVar(name, usage, strconv.Itoa(fallback), strconv.Itoa(value), setBy, envNames)
ensureFlagSet(fs).IntVar(target, name, value, flagUsage(usage, envNames...))
flagSet := ensureFlagSet(fs)
flagSet.IntVar(target, name, value, flagUsage(usage, envNames...))
flagSet.Lookup(name).DefValue = strconv.Itoa(fallback)
}

func RepeatedStringFlag(fs *flag.FlagSet, target *[]string, name, usage string) {
Expand Down Expand Up @@ -523,3 +529,27 @@ func WriteCommandUsage(w io.Writer, usage []string, examples []string) {
fmt.Fprintln(w, " "+strings.TrimSpace(line))
}
}

// WriteFlagDefaults prints the registered flags. Custom Usage printers replace
// FlagSet defaults, so --help would otherwise list examples and omit the flags
// agents need (identity-path, relays, api-port).
func WriteFlagDefaults(w io.Writer, fs *flag.FlagSet) {
if w == nil || fs == nil {
return
}
fmt.Fprintln(w)
fmt.Fprintln(w, "Flags:")
fs.SetOutput(w)
fs.PrintDefaults()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func WriteHelpSection(w io.Writer, heading string, lines []string) {
if w == nil || strings.TrimSpace(heading) == "" || len(lines) == 0 {
return
}
fmt.Fprintln(w)
fmt.Fprintln(w, strings.TrimSpace(heading)+":")
for _, line := range lines {
fmt.Fprintln(w, " "+strings.TrimSpace(line))
}
}
74 changes: 74 additions & 0 deletions utils/cmd_usage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package utils

import (
"bytes"
"strings"
"testing"
)

func TestWriteFlagDefaultsListsRegisteredFlags(t *testing.T) {
ResetEnvRegistry()
t.Cleanup(ResetEnvRegistry)
t.Setenv("IDENTITY_PATH", "/private/runtime/identity.json")
t.Setenv("ADMIN_TOKEN", "admin-token-secret")
t.Setenv("AWS_SECRET_ACCESS_KEY", "aws-secret-access-key")

var buf bytes.Buffer
var identityPath string
var adminToken string
var awsSecretAccessKey string
fs := NewFlagSet("expose", nil)
StringFlagEnv(fs, &identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
StringFlagEnv(fs, &adminToken, "admin-token", "", "admin bearer token", "ADMIN_TOKEN")
StringFlagEnv(fs, &awsSecretAccessKey, "aws-secret-access-key", "", "AWS secret access key", "AWS_SECRET_ACCESS_KEY")
if identityPath != "/private/runtime/identity.json" || adminToken != "admin-token-secret" || awsSecretAccessKey != "aws-secret-access-key" {
t.Fatal("environment values were not resolved into flag targets")
}

WriteCommandUsage(&buf, []string{"portal expose [flags] <target>"}, []string{"portal expose 3000"})
WriteFlagDefaults(&buf, fs)

got := buf.String()
for _, want := range []string{"Usage:", "Examples:", "Flags:", "-identity-path", "identity.json", "IDENTITY_PATH"} {
if !strings.Contains(got, want) {
t.Fatalf("help missing %q\n%s", want, got)
}
}
for _, secret := range []string{"/private/runtime/identity.json", "admin-token-secret", "aws-secret-access-key"} {
if strings.Contains(got, secret) {
t.Fatalf("help contains environment value %q\n%s", secret, got)
}
}
}

func TestWriteFlagDefaultsNilIsNoop(t *testing.T) {
WriteFlagDefaults(nil, nil)
var buf bytes.Buffer
WriteFlagDefaults(&buf, nil)
if buf.Len() != 0 {
t.Fatalf("nil FlagSet should print nothing, got %q", buf.String())
}
}

func TestWriteHelpSectionLoopbackAndReady(t *testing.T) {
var buf bytes.Buffer
WriteHelpSection(&buf, "Loopback", []string{
"portal expose 127.0.0.1:8080 --identity-path /absolute/path/outside/repo/identity.json --relays https://127.0.0.1:4017 --discovery=false",
})
WriteHelpSection(&buf, "Ready", []string{
"After portal expose succeeds, it logs a line starting with: service ready at",
})
got := buf.String()
for _, want := range []string{
"Loopback:",
"--identity-path /absolute/path/outside/repo/identity.json",
"--relays https://127.0.0.1:4017",
"--discovery=false",
"Ready:",
"service ready at",
} {
if !strings.Contains(got, want) {
t.Fatalf("help missing %q\n%s", want, got)
}
}
}
Loading