From 46e78ec36f39fddc07a6cf96b0436c5e24734e6c Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sat, 11 Apr 2026 02:48:00 +0200 Subject: [PATCH] First stab at maglevc --- Makefile | 7 +- cmd/maglevc/commands.go | 307 ++++++++++++++++++++++++++++++++++++++++ cmd/maglevc/complete.go | 113 +++++++++++++++ cmd/maglevc/main.go | 49 +++++++ cmd/maglevc/shell.go | 94 ++++++++++++ cmd/maglevc/tree.go | 127 +++++++++++++++++ go.mod | 1 + go.sum | 5 + 8 files changed, 700 insertions(+), 3 deletions(-) create mode 100644 cmd/maglevc/commands.go create mode 100644 cmd/maglevc/complete.go create mode 100644 cmd/maglevc/main.go create mode 100644 cmd/maglevc/shell.go create mode 100644 cmd/maglevc/tree.go diff --git a/Makefile b/Makefile index 189f5f2..f6a1fa2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -BINARY := maglevd +BINARIES := maglevd maglevc MODULE := git.ipng.ch/ipng/vpp-maglev PROTO_DIR := proto PROTO_FILE := $(PROTO_DIR)/maglev.proto @@ -9,7 +9,8 @@ GEN_FILES := internal/grpcapi/maglev.pb.go internal/grpcapi/maglev_grpc.pb.go all: build build: $(GEN_FILES) - go build -o bin/$(BINARY) ./cmd/maglevd/ + go build -o bin/maglevd ./cmd/maglevd/ + go build -o bin/maglevc ./cmd/maglevc/ test: $(GEN_FILES) go test ./... @@ -26,5 +27,5 @@ lint: golangci-lint run ./... clean: - rm -f bin/$(BINARY) + rm -f $(addprefix bin/,$(BINARIES)) rm -f $(GEN_FILES) diff --git a/cmd/maglevc/commands.go b/cmd/maglevc/commands.go new file mode 100644 index 0000000..355857d --- /dev/null +++ b/cmd/maglevc/commands.go @@ -0,0 +1,307 @@ +// Copyright (c) 2026, Pim van Pelt + +package main + +import ( + "context" + "fmt" + "text/tabwriter" + "os" + "time" + + "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" +) + +const callTimeout = 10 * time.Second + +// buildTree constructs the full command tree. +func buildTree() *Node { + root := &Node{Word: "", Help: ""} + + show := &Node{Word: "show", Help: "show information"} + set := &Node{Word: "set", Help: "modify configuration"} + quit := &Node{Word: "quit", Help: "exit the shell", Run: runQuit} + exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit} + + // show frontends + showFrontends := &Node{Word: "frontends", Help: "list all frontends", Run: runShowFrontends} + // show frontend + showFrontendName := &Node{ + Word: "", + Help: "frontend name", + Dynamic: dynFrontends, + Run: runShowFrontend, + } + showFrontend := &Node{ + Word: "frontend", + Help: "show a single frontend", + Children: []*Node{showFrontendName}, + } + + // show backends + showBackends := &Node{Word: "backends", Help: "list all backends", Run: runShowBackends} + // show backend + showBackendName := &Node{ + Word: "", + Help: "backend name", + Dynamic: dynBackends, + Run: runShowBackend, + } + showBackend := &Node{ + Word: "backend", + Help: "show a single backend", + Children: []*Node{showBackendName}, + } + + // show healthchecks + showHealthChecks := &Node{Word: "healthchecks", Help: "list all health checks", Run: runShowHealthChecks} + // show healthcheck + showHealthCheckName := &Node{ + Word: "", + Help: "health check name", + Dynamic: dynHealthChecks, + Run: runShowHealthCheck, + } + showHealthCheck := &Node{ + Word: "healthcheck", + Help: "show a single health check", + Children: []*Node{showHealthCheckName}, + } + + show.Children = []*Node{ + showFrontends, showFrontend, + showBackends, showBackend, + showHealthChecks, showHealthCheck, + } + + // set backend pause|resume|disabled|enabled + setPause := &Node{Word: "pause", Help: "pause health checking", Run: runPauseBackend} + setResume := &Node{Word: "resume", Help: "resume health checking", Run: runResumeBackend} + setDisabled := &Node{Word: "disabled", Help: "disable backend (not implemented)", Run: runNotImplemented} + setEnabled := &Node{Word: "enabled", Help: "enable backend (not implemented)", Run: runNotImplemented} + setBackendName := &Node{ + Word: "", + Help: "backend name", + Dynamic: dynBackends, + Children: []*Node{setPause, setResume, setDisabled, setEnabled}, + } + setBackend := &Node{ + Word: "backend", + Help: "modify a backend", + Children: []*Node{setBackendName}, + } + set.Children = []*Node{setBackend} + + root.Children = []*Node{show, set, quit, exit} + return root +} + +// ---- dynamic enumerators --------------------------------------------------- + +func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string { + resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{}) + if err != nil { + return nil + } + return resp.FrontendNames +} + +func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string { + resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{}) + if err != nil { + return nil + } + return resp.BackendNames +} + +func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string { + resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{}) + if err != nil { + return nil + } + return resp.Names +} + +// ---- run functions --------------------------------------------------------- + +func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { + return errQuit +} + +func runShowFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{}) + if err != nil { + return err + } + for _, name := range resp.FrontendNames { + fmt.Println(name) + } + return nil +} + +func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: show frontend ") + } + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + info, err := client.GetFrontend(ctx, &grpcapi.GetFrontendRequest{Name: args[0]}) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "name\t%s\n", info.Name) + fmt.Fprintf(w, "address\t%s\n", info.Address) + fmt.Fprintf(w, "protocol\t%s\n", info.Protocol) + fmt.Fprintf(w, "port\t%d\n", info.Port) + for i, b := range info.BackendNames { + if i == 0 { + fmt.Fprintf(w, "backends\t%s\n", b) + } else { + fmt.Fprintf(w, "\t%s\n", b) + } + } + if info.Description != "" { + fmt.Fprintf(w, "description\t%s\n", info.Description) + } + return w.Flush() +} + +func runShowBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{}) + if err != nil { + return err + } + for _, name := range resp.BackendNames { + fmt.Println(name) + } + return nil +} + +func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: show backend ") + } + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + info, err := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: args[0]}) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "name\t%s\n", info.Name) + fmt.Fprintf(w, "address\t%s\n", info.Address) + fmt.Fprintf(w, "state\t%s\n", info.State) + fmt.Fprintf(w, "enabled\t%v\n", info.Enabled) + fmt.Fprintf(w, "weight\t%d\n", info.Weight) + fmt.Fprintf(w, "healthcheck\t%s\n", info.Healthcheck) + for i, t := range info.Transitions { + if i == 0 { + fmt.Fprintf(w, "transitions\t%s → %s\n", t.From, t.To) + } else { + fmt.Fprintf(w, "\t%s → %s\n", t.From, t.To) + } + } + return w.Flush() +} + +func runShowHealthChecks(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{}) + if err != nil { + return err + } + for _, name := range resp.Names { + fmt.Println(name) + } + return nil +} + +func runShowHealthCheck(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: show healthcheck ") + } + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + info, err := client.GetHealthCheck(ctx, &grpcapi.GetHealthCheckRequest{Name: args[0]}) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "name\t%s\n", info.Name) + fmt.Fprintf(w, "type\t%s\n", info.Type) + if info.Port > 0 { + fmt.Fprintf(w, "port\t%d\n", info.Port) + } + fmt.Fprintf(w, "interval\t%s\n", time.Duration(info.IntervalNs)) + if info.FastIntervalNs > 0 { + fmt.Fprintf(w, "fast-interval\t%s\n", time.Duration(info.FastIntervalNs)) + } + if info.DownIntervalNs > 0 { + fmt.Fprintf(w, "down-interval\t%s\n", time.Duration(info.DownIntervalNs)) + } + fmt.Fprintf(w, "timeout\t%s\n", time.Duration(info.TimeoutNs)) + fmt.Fprintf(w, "rise\t%d\n", info.Rise) + fmt.Fprintf(w, "fall\t%d\n", info.Fall) + if info.ProbeIpv4Src != "" { + fmt.Fprintf(w, "probe-ipv4-src\t%s\n", info.ProbeIpv4Src) + } + if info.ProbeIpv6Src != "" { + fmt.Fprintf(w, "probe-ipv6-src\t%s\n", info.ProbeIpv6Src) + } + if h := info.Http; h != nil { + fmt.Fprintf(w, "http.path\t%s\n", h.Path) + if h.Host != "" { + fmt.Fprintf(w, "http.host\t%s\n", h.Host) + } + fmt.Fprintf(w, "http.response-code\t%d-%d\n", h.ResponseCodeMin, h.ResponseCodeMax) + if h.ResponseRegexp != "" { + fmt.Fprintf(w, "http.response-regexp\t%s\n", h.ResponseRegexp) + } + } + if t := info.Tcp; t != nil { + fmt.Fprintf(w, "tcp.ssl\t%v\n", t.Ssl) + if t.ServerName != "" { + fmt.Fprintf(w, "tcp.server-name\t%s\n", t.ServerName) + } + } + return w.Flush() +} + +func runPauseBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: set backend pause") + } + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + info, err := client.PauseBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]}) + if err != nil { + return err + } + fmt.Printf("%s: setting state to '%s'\n", info.Name, info.State) + return nil +} + +func runResumeBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + if len(args) == 0 { + return fmt.Errorf("usage: set backend resume") + } + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + info, err := client.ResumeBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]}) + if err != nil { + return err + } + fmt.Printf("%s: setting state to '%s'\n", info.Name, info.State) + return nil +} + +func runNotImplemented(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { + fmt.Println("not implemented yet") + return nil +} diff --git a/cmd/maglevc/complete.go b/cmd/maglevc/complete.go new file mode 100644 index 0000000..6235e15 --- /dev/null +++ b/cmd/maglevc/complete.go @@ -0,0 +1,113 @@ +// Copyright (c) 2026, Pim van Pelt + +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/chzyer/readline" + + "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" +) + +const completeTimeout = 1 * time.Second + +// Completer implements readline.AutoCompleter for the command tree. +type Completer struct { + root *Node + client grpcapi.MaglevClient +} + +// Do implements readline.AutoCompleter. +// line is the full current line; pos is the cursor position. +// Returns (newLine [][]rune, length int) where length is how many rune bytes +// before pos should be replaced by each candidate in newLine. +func (co *Completer) Do(line []rune, pos int) (newLine [][]rune, length int) { + before := string(line[:pos]) + tokens := splitTokens(before) + + // Determine the partial token being completed. + var partial string + var prefix []string + if len(tokens) == 0 || (len(before) > 0 && before[len(before)-1] == ' ') { + // Cursor is after a space — completing a new token. + prefix = tokens + partial = "" + } else { + // Cursor is within the last token. + prefix = tokens[:len(tokens)-1] + partial = tokens[len(tokens)-1] + } + + ctx, cancel := context.WithTimeout(context.Background(), completeTimeout) + defer cancel() + + candidates := Candidates(co.root, prefix, partial, ctx, co.client) + + var suffixes [][]rune + for _, c := range candidates { + suffix := c.Word[len(partial):] + suffixes = append(suffixes, []rune(suffix+" ")) + } + return suffixes, len([]rune(partial)) +} + +// questionListener intercepts the '?' key and prints inline help. +type questionListener struct { + root *Node + client grpcapi.MaglevClient + rl *readline.Instance +} + +func (ql *questionListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { + if key != '?' { + return line, pos, false + } + + // line[:pos] includes the '?' just typed — strip it before tokenizing. + before := string(line[:pos]) + if len(before) > 0 && before[len(before)-1] == '?' { + before = before[:len(before)-1] + } + tokens := splitTokens(before) + + var partial string + var prefix []string + if len(before) == 0 || before[len(before)-1] == ' ' { + prefix = tokens + partial = "" + } else if len(tokens) > 0 { + prefix = tokens[:len(tokens)-1] + partial = tokens[len(tokens)-1] + } + + ctx, cancel := context.WithTimeout(context.Background(), completeTimeout) + defer cancel() + + candidates := Candidates(ql.root, prefix, partial, ctx, ql.client) + + fmt.Fprintf(ql.rl.Stderr(), "\r\n") + if len(candidates) == 0 { + fmt.Fprintf(ql.rl.Stderr(), " \r\n") + } else { + for _, c := range candidates { + if c.Help != "" { + fmt.Fprintf(ql.rl.Stderr(), " %-20s %s\r\n", c.Word, c.Help) + } else { + fmt.Fprintf(ql.rl.Stderr(), " %s\r\n", c.Word) + } + } + } + + // Remove the '?' from the line and return with cursor one step back. + newLine = append(append([]rune{}, line[:pos-1]...), line[pos:]...) + return newLine, pos - 1, true +} + +// splitTokens splits a string into whitespace-separated tokens. +func splitTokens(s string) []string { + return strings.Fields(s) +} diff --git a/cmd/maglevc/main.go b/cmd/maglevc/main.go new file mode 100644 index 0000000..6381dce --- /dev/null +++ b/cmd/maglevc/main.go @@ -0,0 +1,49 @@ +// Copyright (c) 2026, Pim van Pelt + +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + serverAddr := flag.String("server", "localhost:9090", "maglev server address") + flag.Parse() + + conn, err := grpc.NewClient(*serverAddr, + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("connect %s: %w", *serverAddr, err) + } + defer conn.Close() + + client := grpcapi.NewMaglevClient(conn) + ctx := context.Background() + + args := flag.Args() + if len(args) == 0 { + // Interactive shell. + return runShell(ctx, client) + } + + // One-shot command from CLI arguments. + root := buildTree() + tokens := splitTokens(strings.Join(args, " ")) + return dispatch(ctx, root, client, tokens) +} diff --git a/cmd/maglevc/shell.go b/cmd/maglevc/shell.go new file mode 100644 index 0000000..5a30a72 --- /dev/null +++ b/cmd/maglevc/shell.go @@ -0,0 +1,94 @@ +// Copyright (c) 2026, Pim van Pelt + +package main + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/chzyer/readline" + + "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" +) + +// errQuit is a sentinel returned by runQuit to exit the REPL. +var errQuit = errors.New("quit") + +// runShell runs the interactive REPL until the user types quit/exit or EOF. +func runShell(ctx context.Context, client grpcapi.MaglevClient) error { + root := buildTree() + + comp := &Completer{root: root, client: client} + ql := &questionListener{root: root, client: client} + + cfg := &readline.Config{ + Prompt: "maglev> ", + AutoComplete: comp, + InterruptPrompt: "^C", + EOFPrompt: "exit", + Listener: ql, + } + rl, err := readline.NewEx(cfg) + if err != nil { + return fmt.Errorf("readline init: %w", err) + } + ql.rl = rl + defer rl.Close() + + for { + line, err := rl.Readline() + if err == readline.ErrInterrupt { + continue + } + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + tokens := splitTokens(line) + if len(tokens) == 0 { + continue + } + + if err := dispatch(ctx, root, client, tokens); err != nil { + if errors.Is(err, errQuit) { + return nil + } + fmt.Fprintf(rl.Stderr(), "error: %v\n", err) + } + } +} + +// dispatch walks the tree and executes the matched command. +func dispatch(ctx context.Context, root *Node, client grpcapi.MaglevClient, tokens []string) error { + node, args := Walk(root, tokens) + + if node.Run == nil { + showHelp(root, tokens) + return nil + } + + return node.Run(ctx, client, args) +} + +// showHelp prints available completions for the given token path. +func showHelp(root *Node, tokens []string) { + node, _ := Walk(root, tokens) + + candidates := filterFixedChildren(node.Children, "") + if len(candidates) == 0 { + fmt.Println(" ") + return + } + for _, c := range candidates { + if c.Help != "" { + fmt.Printf(" %-20s %s\n", c.Word, c.Help) + } else { + fmt.Printf(" %s\n", c.Word) + } + } +} diff --git a/cmd/maglevc/tree.go b/cmd/maglevc/tree.go new file mode 100644 index 0000000..f8e508c --- /dev/null +++ b/cmd/maglevc/tree.go @@ -0,0 +1,127 @@ +// Copyright (c) 2026, Pim van Pelt + +package main + +import ( + "context" + "strings" + + "git.ipng.ch/ipng/vpp-maglev/internal/grpcapi" +) + +// Node is one word in the command tree. Leaf nodes have a Run function. +// Slot nodes have Dynamic set (and no fixed Word to match against); they +// accept any single token as an argument and may have further Children. +type Node struct { + Word string + Help string + Dynamic func(context.Context, grpcapi.MaglevClient) []string // non-nil → slot node + Children []*Node + Run func(context.Context, grpcapi.MaglevClient, []string) error +} + +// Walk descends the tree following tokens. At each step it tries fixed +// children first (exact then prefix), then falls back to a slot child +// (Dynamic != nil). Tokens consumed by slot children are collected as args. +// Returns the deepest node reached and the args collected from slot nodes. +func Walk(root *Node, tokens []string) (*Node, []string) { + node := root + var args []string + for len(tokens) > 0 { + tok := tokens[0] + + // Try fixed children (exact, then unique prefix). + next := matchFixedChild(node.Children, tok) + if next != nil { + node = next + tokens = tokens[1:] + continue + } + + // Try a slot child. + slot := findSlotChild(node.Children) + if slot != nil { + args = append(args, tok) + tokens = tokens[1:] + node = slot + continue + } + + // Dead end — no match. + break + } + return node, args +} + +// matchFixedChild returns the child matching tok by exact then unique prefix, +// considering only non-slot children. +func matchFixedChild(children []*Node, tok string) *Node { + var fixed []*Node + for _, c := range children { + if c.Dynamic == nil { + fixed = append(fixed, c) + } + } + // Exact match. + for _, c := range fixed { + if c.Word == tok { + return c + } + } + // Unique prefix match. + var matches []*Node + for _, c := range fixed { + if strings.HasPrefix(c.Word, tok) { + matches = append(matches, c) + } + } + if len(matches) == 1 { + return matches[0] + } + return nil +} + +// findSlotChild returns the first child that is a slot node (Dynamic != nil). +func findSlotChild(children []*Node) *Node { + for _, c := range children { + if c.Dynamic != nil { + return c + } + } + return nil +} + +// Candidates returns the completable children at the current position given +// the already-typed tokens and the partial token being completed. +func Candidates(root *Node, tokens []string, partial string, ctx context.Context, client grpcapi.MaglevClient) []*Node { + // Walk the already-confirmed tokens. + node, _ := Walk(root, tokens) + + // Now look at what could follow at this node. + // Check fixed children filtered by partial. + fixedMatches := filterFixedChildren(node.Children, partial) + + // Check dynamic slot child if present. + var dynMatches []*Node + slot := findSlotChild(node.Children) + if slot != nil && slot.Dynamic != nil { + vals := slot.Dynamic(ctx, client) + for _, v := range vals { + if strings.HasPrefix(v, partial) { + dynMatches = append(dynMatches, &Node{Word: v, Help: slot.Help}) + } + } + } + + return append(fixedMatches, dynMatches...) +} + +func filterFixedChildren(children []*Node, prefix string) []*Node { + var out []*Node + for _, c := range children { + if c.Dynamic == nil && strings.HasPrefix(c.Word, prefix) { + out = append(out, c) + } + } + return out +} diff --git a/go.mod b/go.mod index 1dcaa71..8097e04 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( + github.com/chzyer/readline v1.5.1 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect diff --git a/go.sum b/go.sum index 2516efb..5816856 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -26,6 +30,7 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=