mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 20:49:56 +00:00
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
// Package proxycmd provides reusable cobra commands for managing reverse proxy instances.
|
|
// Both the management and combined binaries use these commands, each providing
|
|
// their own StoreOpener to handle config loading and store initialization.
|
|
package proxycmd
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
)
|
|
|
|
// StoreOpener initializes a store from the command context and calls fn.
|
|
type StoreOpener func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error
|
|
|
|
const disconnectAllConfirmation = "disconnect all proxies"
|
|
|
|
// NewCommands creates the proxy command tree with the given store opener.
|
|
// Returns the parent "proxy" command with the disconnect-all subcommand.
|
|
func NewCommands(opener StoreOpener) *cobra.Command {
|
|
var dryRun bool
|
|
var force bool
|
|
|
|
proxyCmd := &cobra.Command{
|
|
Use: "proxy",
|
|
Short: "Manage reverse proxy instances",
|
|
Long: "Commands for inspecting and repairing the reverse proxy instances registered with the management server.",
|
|
}
|
|
|
|
disconnectAllCmd := &cobra.Command{
|
|
Use: "disconnect-all",
|
|
Short: "Force-mark all reverse proxy instances as disconnected",
|
|
Long: "Lists all reverse proxy instances and force-marks them as disconnected, regardless of their session state. " +
|
|
"Use this to repair stale connection state, e.g. after an unclean management server shutdown. " +
|
|
"By default, it asks for manual confirmation before changing state. Use --dry-run to preview without changing state, or --force to skip confirmation. " +
|
|
"Run during a maintenance window; affected live proxies may stay hidden until their next heartbeat or reconnect/re-register.",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
|
return runDisconnectAll(ctx, s, cmd.OutOrStdout(), cmd.InOrStdin(), dryRun, force)
|
|
})
|
|
},
|
|
}
|
|
disconnectAllCmd.Flags().BoolVar(&dryRun, "dry-run", false, "List reverse proxy instances that would be disconnected without changing state")
|
|
disconnectAllCmd.Flags().BoolVar(&force, "force", false, "Skip the confirmation prompt and apply the repair")
|
|
|
|
proxyCmd.AddCommand(disconnectAllCmd)
|
|
return proxyCmd
|
|
}
|
|
|
|
func runDisconnectAll(ctx context.Context, s store.Store, out io.Writer, in io.Reader, dryRun, force bool) error {
|
|
proxies, err := s.GetAllProxies(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list proxies: %w", err)
|
|
}
|
|
|
|
if len(proxies) == 0 {
|
|
_, _ = fmt.Fprintln(out, "No reverse proxy instances found.")
|
|
return nil
|
|
}
|
|
|
|
toDisconnect := 0
|
|
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
|
|
_, _ = fmt.Fprintln(w, "ID\tCLUSTER\tIP\tACCOUNT\tSTATUS\tLAST SEEN")
|
|
_, _ = fmt.Fprintln(w, "--\t-------\t--\t-------\t------\t---------")
|
|
|
|
for _, p := range proxies {
|
|
if p.Status != rpproxy.StatusDisconnected {
|
|
toDisconnect++
|
|
}
|
|
|
|
account := "-"
|
|
if p.AccountID != nil {
|
|
account = *p.AccountID
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
p.ID,
|
|
p.ClusterAddress,
|
|
p.IPAddress,
|
|
account,
|
|
p.Status,
|
|
p.LastSeen.Format("2006-01-02 15:04:05"),
|
|
)
|
|
}
|
|
if err := w.Flush(); err != nil {
|
|
return fmt.Errorf("write proxy list: %w", err)
|
|
}
|
|
|
|
if dryRun {
|
|
_, _ = fmt.Fprintf(out, "\nDry run: would force-mark %d of %d reverse proxy instance(s) as disconnected.\n", toDisconnect, len(proxies))
|
|
return nil
|
|
}
|
|
|
|
if !force {
|
|
confirmed, err := confirmDisconnectAll(out, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !confirmed {
|
|
_, _ = fmt.Fprintln(out, "Aborted. No reverse proxy instances were changed.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
disconnected, err := s.DisconnectAllProxies(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("disconnect proxies: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(out, "\nForce-marked %d of %d reverse proxy instance(s) as disconnected.\n", disconnected, len(proxies))
|
|
return nil
|
|
}
|
|
|
|
func confirmDisconnectAll(out io.Writer, in io.Reader) (bool, error) {
|
|
if in == nil {
|
|
in = strings.NewReader("")
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(out, "\nWARNING: This command changes stored reverse proxy state for every non-disconnected instance.")
|
|
_, _ = fmt.Fprintln(out, "Run it during a maintenance window; affected live proxies may stay hidden until "+
|
|
"their next heartbeat or reconnect/re-register.")
|
|
_, _ = fmt.Fprintf(out, "Type %q to continue: ", disconnectAllConfirmation)
|
|
|
|
scanner := bufio.NewScanner(in)
|
|
if !scanner.Scan() {
|
|
if err := scanner.Err(); err != nil {
|
|
return false, fmt.Errorf("read confirmation: %w", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return strings.EqualFold(strings.TrimSpace(scanner.Text()), disconnectAllConfirmation), nil
|
|
}
|