diff --git a/go.mod b/go.mod index 4e549009..338f5282 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/oklog/run v1.1.0 github.com/panta/machineid v1.0.2 github.com/signadot/go-sdk v0.3.8-0.20260729140747-c2e52c424d95 - github.com/signadot/libconnect v0.1.1-0.20260527212426-31fdd269494f + github.com/signadot/libconnect v0.1.1-0.20260730200225-ae66f07f8f04 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.11.0 github.com/theckman/yacspin v0.13.12 @@ -87,6 +87,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/miekg/dns v1.1.62 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect @@ -110,8 +111,10 @@ require ( github.com/yuin/goldmark-emoji v1.0.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/sync v0.22.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 48478092..61d3e174 100644 --- a/go.sum +++ b/go.sum @@ -377,6 +377,8 @@ github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkEN github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= @@ -456,8 +458,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/signadot/go-sdk v0.3.8-0.20260729140747-c2e52c424d95 h1:wM5K//1RyBhd1K+9qHqTTzdZvCoGZeV2lS/0apJZqqY= github.com/signadot/go-sdk v0.3.8-0.20260729140747-c2e52c424d95/go.mod h1:P9bcwyPeom4W8XToYDxNpXJ4w0EeKaM5lF+GkoeY3Aw= -github.com/signadot/libconnect v0.1.1-0.20260527212426-31fdd269494f h1:b8ZzCjMOat1IJ9Y5rTRoy7TjD0Ch/QDu8P5lHuSJoBQ= -github.com/signadot/libconnect v0.1.1-0.20260527212426-31fdd269494f/go.mod h1:5/CHotq3FQnSv9XQUy3sQkJz240Al1MsJ2wm1wf3usE= +github.com/signadot/libconnect v0.1.1-0.20260730200225-ae66f07f8f04 h1:CzSUlnyjhmBY+B7hTIedm/BoqweAFoJaaskJc+W+33M= +github.com/signadot/libconnect v0.1.1-0.20260730200225-ae66f07f8f04/go.mod h1:awUWZx6SnE/vqfB1Yh+n6YVUkgc0bkILEx+L55sWze0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= @@ -581,6 +583,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/command/local/connect.go b/internal/command/local/connect.go index 17d3b557..9a47e9db 100644 --- a/internal/command/local/connect.go +++ b/internal/command/local/connect.go @@ -83,6 +83,24 @@ func runConnect(cmd *cobra.Command, out io.Writer, cfg *config.LocalConnect, arg return err } + // Resolve the effective name-resolution mode. The connection's `resolver:` + // config field is the encouraged way to opt in; the --local-dns flag is a + // per-invocation override that wins only when explicitly set (so + // --local-dns=false can also force /etc/hosts over a LocalDNS config). + enableLocalDNS := connConfig.Resolver == connectcfg.LocalDNSResolver + localDNSFromFlag := cmd.Flags().Changed("local-dns") + if localDNSFromFlag { + enableLocalDNS = cfg.LocalDNS + } + if enableLocalDNS && !withRootManager { + source := "the --local-dns flag" + if !localDNSFromFlag { + source = fmt.Sprintf("resolver: LocalDNS in the config for cluster %q", connConfig.Cluster) + } + return fmt.Errorf("%s manages the system DNS resolver and requires root; "+ + "it cannot be used with --unprivileged", source) + } + // Get devbox claim and session ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -162,6 +180,7 @@ func runConnect(cmd *cobra.Command, out io.Writer, cfg *config.LocalConnect, arg }, Env: os.Environ(), ConnectionConfig: connConfig, + EnableLocalDNS: enableLocalDNS, ProxyURL: cfg.ProxyURL, APIURL: cfg.API.APIURL, APIKey: cfg.GetAPIKey(), @@ -209,9 +228,13 @@ func runConnectImpl(out, errOut io.Writer, log *slog.Logger, localConfig *config var cmd *exec.Cmd if ciConfig.WithRootManager { if os.Geteuid() != 0 { + dnsLine := "- updating /etc/hosts with cluster service names" + if ciConfig.EnableLocalDNS { + dnsLine = "- configuring the system DNS resolver for cluster service names" + } fmt.Fprintf(out, "signadot local connect needs root privileges for:\n\t"+ - "- updating /etc/hosts with cluster service names\n\t"+ - "- configuring networking to direct local traffic to the cluster\n") + "%s\n\t"+ + "- configuring networking to direct local traffic to the cluster\n", dnsLine) } // run the root-manager args := []string{ diff --git a/internal/command/local/hosts.go b/internal/command/local/hosts.go new file mode 100644 index 00000000..13a8f8ee --- /dev/null +++ b/internal/command/local/hosts.go @@ -0,0 +1,78 @@ +package local + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + sbmgr "github.com/signadot/cli/internal/locald/sandboxmanager" + "github.com/signadot/cli/internal/print" + "github.com/signadot/cli/internal/sdtab" + "github.com/spf13/cobra" +) + +func newHosts(localConfig *config.Local) *cobra.Command { + cfg := &config.LocalHosts{Local: localConfig} + + cmd := &cobra.Command{ + Use: "hosts", + Short: "List the cluster hosts resolvable from the local machine and their IP addresses", + RunE: func(cmd *cobra.Command, args []string) error { + return runHosts(cfg, cmd.OutOrStdout(), args) + }, + } + cfg.AddFlags(cmd) + + return cmd +} + +// printableHost is the JSON/YAML shape of a single resolvable host. Each host +// carries exactly one address: the root controller assigns a single virtual +// address per name and reports it as HostEntry.Ip (IPv4-preferred). +type printableHost struct { + Name string `json:"name"` + IP string `json:"ip"` +} + +func runHosts(cfg *config.LocalHosts, out io.Writer, args []string) error { + if err := cfg.InitLocalConfig(); err != nil { + return err + } + resp, err := sbmgr.GetHosts() + if err != nil { + return err + } + + hosts := make([]printableHost, 0, len(resp.Entries)) + for _, e := range resp.Entries { + hosts = append(hosts, printableHost{Name: e.Name, IP: e.Ip}) + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printHosts(out, hosts) + case config.OutputFormatJSON: + return print.RawJSON(out, hosts) + case config.OutputFormatYAML: + return print.RawK8SYAML(out, hosts) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} + +type hostRow struct { + Name string `sdtab:"NAME"` + IP string `sdtab:"IP"` +} + +// printHosts renders the default aligned NAME/IP table, consistent with the +// other list commands (cluster, routegroup, ...). The entries arrive already +// sorted by name from the root controller (see rootServer.GetHosts). +func printHosts(out io.Writer, hosts []printableHost) error { + t := sdtab.New[hostRow](out) + t.AddHeader() + for _, h := range hosts { + t.AddRow(hostRow{Name: h.Name, IP: h.IP}) + } + return t.Flush() +} diff --git a/internal/command/local/hosts_test.go b/internal/command/local/hosts_test.go new file mode 100644 index 00000000..7847497a --- /dev/null +++ b/internal/command/local/hosts_test.go @@ -0,0 +1,44 @@ +package local + +import ( + "bytes" + "strings" + "testing" +) + +// TestPrintHosts checks the default table has a header row and one aligned +// NAME/IP row per host, in the order given (already name-sorted by the daemon). +func TestPrintHosts(t *testing.T) { + var b bytes.Buffer + err := printHosts(&b, []printableHost{ + {Name: "a.myns.svc.cluster.local", IP: "242.242.0.3"}, + {Name: "b.other.svc.cluster.local", IP: "242.242.0.4"}, + }) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimRight(b.String(), "\n"), "\n") + if len(lines) != 3 { + t.Fatalf("got %d lines, want 3 (header + 2 rows):\n%s", len(lines), b.String()) + } + if !strings.Contains(lines[0], "NAME") || !strings.Contains(lines[0], "IP") { + t.Errorf("header line missing NAME/IP: %q", lines[0]) + } + if !strings.HasPrefix(lines[1], "a.myns.svc.cluster.local") || !strings.Contains(lines[1], "242.242.0.3") { + t.Errorf("row 1 unexpected: %q", lines[1]) + } + if !strings.HasPrefix(lines[2], "b.other.svc.cluster.local") || !strings.Contains(lines[2], "242.242.0.4") { + t.Errorf("row 2 unexpected: %q", lines[2]) + } +} + +// TestPrintHostsEmpty: no hosts still emits just the header (and no panic). +func TestPrintHostsEmpty(t *testing.T) { + var b bytes.Buffer + if err := printHosts(&b, nil); err != nil { + t.Fatal(err) + } + if !strings.Contains(b.String(), "NAME") { + t.Errorf("empty output missing header: %q", b.String()) + } +} diff --git a/internal/command/local/local.go b/internal/command/local/local.go index 32630b9d..d44405b0 100644 --- a/internal/command/local/local.go +++ b/internal/command/local/local.go @@ -20,6 +20,7 @@ func New(api *config.API) *cobra.Command { newStatus(cfg), newDisconnect(cfg), newProxy(cfg), + newHosts(cfg), override.New(cfg), ) diff --git a/internal/command/local/printers.go b/internal/command/local/printers.go index 12b1432a..e3127f72 100644 --- a/internal/command/local/printers.go +++ b/internal/command/local/printers.go @@ -33,6 +33,7 @@ func printRawStatus(cfg *config.LocalStatus, out io.Writer, printer func(out io. OperatorInfo any `json:"operatorInfo,omitempty"` Localnet any `json:"localnet,omitempty"` Hosts any `json:"hosts,omitempty"` + LocalDNS any `json:"localDNS,omitempty"` Portforward any `json:"portforward,omitempty"` ControlPlaneProxy any `json:"controlPlaneProxy,omitempty"` SandboxesWatcher any `json:"sandboxesWatcher,omitempty"` @@ -45,6 +46,7 @@ func printRawStatus(cfg *config.LocalStatus, out io.Writer, printer func(out io. OperatorInfo: getRawOperatorInfo(cfg, status.OperatorInfo), Localnet: getRawLocalnet(cfg, ciConfig, status.Localnet, statusMap), Hosts: getRawHosts(cfg, ciConfig, status.Hosts, statusMap), + LocalDNS: getRawLocalDNS(cfg, ciConfig, status.LocalDns, statusMap), Portforward: getRawPortforward(cfg, ciConfig, status.Portforward, statusMap), ControlPlaneProxy: getRawControlPlaneProxy(cfg, ciConfig, status.ControlPlaneProxy, statusMap), SandboxesWatcher: getRawWatcher(cfg, status.Watcher, statusMap), @@ -186,6 +188,13 @@ func getRawHosts(cfg *config.LocalStatus, ciConfig *config.ConnectInvocationConf if !ciConfig.WithRootManager { return hosts } + if ciConfig.EnableLocalDNS { + // In --local-dns mode /etc/hosts management is intentionally not + // running, so the root manager reports no hosts status. Omit the section + // (mirroring getRawLocalDNS when local DNS is disabled) rather than + // emitting a bogus {"healthy": false, "numHosts": 0}. + return nil + } if cfg.Details { // Details view @@ -219,6 +228,48 @@ func getRawHosts(cfg *config.LocalStatus, ciConfig *config.ConnectInvocationConf return result } +func getRawLocalDNS(cfg *config.LocalStatus, ciConfig *config.ConnectInvocationConfig, + ldns *commonapi.LocalDNSStatus, statusMap map[string]any) any { + if !ciConfig.WithRootManager || !ciConfig.EnableLocalDNS { + return nil + } + + if cfg.Details { + // Details view + return statusMap["localDns"] + } + + // Standard view + type PrintableLocalDNS struct { + Healthy bool `json:"healthy"` + RecordCount uint32 `json:"recordCount,omitempty"` + HostCount uint32 `json:"hostCount,omitempty"` + BindAddr string `json:"bindAddr,omitempty"` + Warning string `json:"warning,omitempty"` + LastErrorReason string `json:"lastErrorReason,omitempty"` + } + + result := &PrintableLocalDNS{Healthy: false} + if ldns == nil || ldns.Health == nil { + return result + } + if ldns.Health.Healthy { + result = &PrintableLocalDNS{ + Healthy: true, + RecordCount: ldns.RecordCount, + HostCount: ldns.HostCount, + BindAddr: ldns.BindAddr, + Warning: ldns.Warning, + } + } else { + result = &PrintableLocalDNS{ + Healthy: false, + LastErrorReason: ldns.Health.LastErrorReason, + } + } + return result +} + func getRawPortforward(cfg *config.LocalStatus, ciConfig *config.ConnectInvocationConfig, portforward *commonapi.PortForwardStatus, statusMap map[string]any) any { if ciConfig.ConnectionConfig.Type != connectcfg.PortForwardLinkType { @@ -437,7 +488,11 @@ func (p *statusPrinter) printSuccess() { } if p.ciConfig.WithRootManager { p.printLocalnetStatus() - p.printHostsStatus() + if p.ciConfig.EnableLocalDNS { + p.printLocalDNSStatus() + } else { + p.printHostsStatus() + } } p.printSandboxesWatcherStatus() p.printSandboxStatus() @@ -505,6 +560,51 @@ func (p *statusPrinter) printHostsStatus() { p.printLine(p.out, 1, fmt.Sprintf("%d hosts accessible via /etc/hosts", p.status.Hosts.NumHosts), "*") } +func (p *statusPrinter) printLocalDNSStatus() { + ldns := p.status.LocalDns + if ldns == nil || ldns.Health == nil { + p.printLine(p.out, 1, "local DNS resolver is not running", "*") + return + } + if ldns.Health.Healthy { + // RecordCount is the total resolvable DNS names, which includes the + // synthesized short forms (e.g. ., ..svc); HostCount is + // the distinct hosts `local hosts` lists. Show both so the larger name + // count doesn't read as a discrepancy against `local hosts` — but omit the + // host count when it's absent (0), which is what an older daemon that + // predates the host_count field reports (a new CLI vs old locald). + msg := fmt.Sprintf("%d names resolvable via local DNS (%s)", ldns.RecordCount, ldns.BindAddr) + if ldns.HostCount > 0 { + msg = fmt.Sprintf("%d names (%d hosts) resolvable via local DNS (%s)", + ldns.RecordCount, ldns.HostCount, ldns.BindAddr) + } + p.printLine(p.out, 1, msg, "*") + } else { + p.printLine(p.out, 1, fmt.Sprintf("local DNS resolver not healthy (%q)", + ldns.Health.LastErrorReason), "*") + } + if ldns.Warning != "" { + p.printLine(p.out, 2, "warning: "+ldns.Warning, p.red("!")) + } + if p.cfg.Details { + if ldns.Mode != "" { + p.printLine(p.out, 2, "mode: "+ldns.Mode, "*") + } + if len(ldns.Suffixes) > 0 { + p.printLine(p.out, 2, "Suffixes:", "*") + for _, s := range ldns.Suffixes { + p.printLine(p.out, 3, s, "-") + } + } + if len(ldns.Upstreams) > 0 { + p.printLine(p.out, 2, "Upstreams:", "*") + for _, u := range ldns.Upstreams { + p.printLine(p.out, 3, u, "-") + } + } + } +} + func (p *statusPrinter) printSandboxesWatcherStatus() { msg := "sandboxes watcher is not running" if p.status.Watcher != nil && p.status.Watcher.Health != nil { diff --git a/internal/command/local/printers_test.go b/internal/command/local/printers_test.go new file mode 100644 index 00000000..290e6b96 --- /dev/null +++ b/internal/command/local/printers_test.go @@ -0,0 +1,136 @@ +package local + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/signadot/cli/internal/config" + commonapi "github.com/signadot/cli/internal/locald/api" + sbmapi "github.com/signadot/cli/internal/locald/api/sandboxmanager" +) + +// TestLocalDNSStatusLineCrossVersion covers the CLI-vs-daemon version skew for +// the local-DNS status line: a new CLI rendering the status of a daemon that +// does (new) or does not (old, pre-host_count) report the host count. protobuf +// makes host_count decode as 0 against an older daemon, so the line must omit +// "(0 hosts)" rather than read as a discrepancy against `local hosts`. +func TestLocalDNSStatusLineCrossVersion(t *testing.T) { + line := func(ldns *commonapi.LocalDNSStatus) string { + var buf bytes.Buffer + p := &statusPrinter{cfg: &config.LocalStatus{}, status: &sbmapi.StatusResponse{LocalDns: ldns}, out: &buf} + p.printLocalDNSStatus() + return buf.String() + } + + t.Run("new daemon reports host count", func(t *testing.T) { + got := line(&commonapi.LocalDNSStatus{ + Health: &commonapi.ServiceHealth{Healthy: true}, RecordCount: 147, HostCount: 49, BindAddr: "127.0.0.54:53", + }) + if !strings.Contains(got, "147 names (49 hosts) resolvable via local DNS") { + t.Errorf("got %q, want name and host counts", got) + } + }) + + t.Run("old daemon (host_count absent -> 0) omits the host count", func(t *testing.T) { + got := line(&commonapi.LocalDNSStatus{ + Health: &commonapi.ServiceHealth{Healthy: true}, RecordCount: 147, HostCount: 0, BindAddr: "127.0.0.54:53", + }) + if !strings.Contains(got, "147 names resolvable via local DNS") { + t.Errorf("got %q, want the plain name count", got) + } + if strings.Contains(got, "hosts)") { + t.Errorf("got %q, want no host count for a pre-host_count daemon", got) + } + }) + + t.Run("not running", func(t *testing.T) { + if got := line(nil); !strings.Contains(got, "not running") { + t.Errorf("got %q", got) + } + }) +} + +// TestGetRawHostsGating covers the section gating for the JSON/YAML status +// output: /etc/hosts status is omitted under --local-dns (where the root +// manager reports no hosts status) rather than emitted as a bogus failure. +func TestGetRawHostsGating(t *testing.T) { + cfg := &config.LocalStatus{} + statusMap := map[string]any{} + hosts := &commonapi.HostsStatus{ + Health: &commonapi.ServiceHealth{Healthy: true}, + NumHosts: 5, + } + + t.Run("local-dns mode omits hosts", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: true, EnableLocalDNS: true} + if got := getRawHosts(cfg, ci, hosts, statusMap); got != nil { + t.Errorf("getRawHosts under --local-dns = %v, want nil", got) + } + }) + + t.Run("etchosts mode reports hosts", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: true, EnableLocalDNS: false} + got := getRawHosts(cfg, ci, hosts, statusMap) + if got == nil { + t.Fatal("getRawHosts in etchosts mode = nil, want a hosts section") + } + if js := mustJSON(t, got); !strings.Contains(js, `"healthy":true`) || !strings.Contains(js, `"numHosts":5`) { + t.Errorf("unexpected hosts json: %s", js) + } + }) + + t.Run("no root manager returns raw status", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: false} + if got := getRawHosts(cfg, ci, hosts, statusMap); got == nil { + t.Error("getRawHosts without root manager = nil, want the raw status") + } + }) +} + +// TestGetRawLocalDNSGating is the inverse: local DNS status is present only when +// running under --local-dns with the root manager. +func TestGetRawLocalDNSGating(t *testing.T) { + cfg := &config.LocalStatus{} + statusMap := map[string]any{} + ldns := &commonapi.LocalDNSStatus{ + Health: &commonapi.ServiceHealth{Healthy: true}, + RecordCount: 7, + BindAddr: "127.0.0.54:53", + } + + t.Run("local-dns mode reports localDNS", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: true, EnableLocalDNS: true} + got := getRawLocalDNS(cfg, ci, ldns, statusMap) + if got == nil { + t.Fatal("getRawLocalDNS under --local-dns = nil, want a localDNS section") + } + if js := mustJSON(t, got); !strings.Contains(js, `"healthy":true`) { + t.Errorf("unexpected localDNS json: %s", js) + } + }) + + t.Run("etchosts mode omits localDNS", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: true, EnableLocalDNS: false} + if got := getRawLocalDNS(cfg, ci, ldns, statusMap); got != nil { + t.Errorf("getRawLocalDNS in etchosts mode = %v, want nil", got) + } + }) + + t.Run("no root manager omits localDNS", func(t *testing.T) { + ci := &config.ConnectInvocationConfig{WithRootManager: false, EnableLocalDNS: true} + if got := getRawLocalDNS(cfg, ci, ldns, statusMap); got != nil { + t.Errorf("getRawLocalDNS without root manager = %v, want nil", got) + } + }) +} + +func mustJSON(t *testing.T, v any) string { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + return string(b) +} diff --git a/internal/config/local.go b/internal/config/local.go index 2d4be2fe..c4271cf2 100644 --- a/internal/config/local.go +++ b/internal/config/local.go @@ -102,6 +102,7 @@ type LocalConnect struct { // Flags Cluster string Unprivileged bool + LocalDNS bool Wait ConnectWait WaitTimeout time.Duration @@ -115,6 +116,9 @@ func (c *LocalConnect) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&c.Cluster, "cluster", "", "specify cluster connection config") cmd.Flags().BoolVar(&c.Unprivileged, "unprivileged", false, "run without root privileges") + cmd.Flags().BoolVar(&c.LocalDNS, "local-dns", false, + "resolve cluster service names with a local DNS resolver instead of /etc/hosts "+ + "(overrides the connection's 'resolver' config field for this invocation)") cmd.Flags().Var(&c.Wait, "wait", "status to wait for while connecting {none,connect,sandboxes}") cmd.Flags().DurationVar(&c.WaitTimeout, "wait-timeout", 10*time.Second, "timeout to wait") @@ -194,6 +198,13 @@ func (c *LocalStatus) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&c.Details, "details", false, "display status details") } +type LocalHosts struct { + *Local +} + +func (c *LocalHosts) AddFlags(cmd *cobra.Command) { +} + type LocalProxy struct { *Local diff --git a/internal/config/locald.go b/internal/config/locald.go index b54b61a0..2e89c1bf 100644 --- a/internal/config/locald.go +++ b/internal/config/locald.go @@ -76,14 +76,18 @@ type ConnectInvocationConfig struct { Env []string `json:"env"` VirtualIPNet string `json:"virtualIPNet"` ConnectionConfig *connectcfg.ConnectionConfig `json:"connectionConfig"` - ProxyURL string `json:"proxyURL"` - APIURL string `json:"apiURL"` - APIKey string `json:"apiKey"` - ConfigFile string `json:"configFile"` - Debug bool `json:"debug"` - ConnectTimeout string `json:"connectTimeout"` - DevboxID string `json:"devboxID"` - DevboxSessionID string `json:"devboxSessionID"` + + // EnableLocalDNS runs the libconnect localdns resolver (managing the OS + // resolver config) in place of the /etc/hosts mechanism for cluster names. + EnableLocalDNS bool `json:"enableLocalDNS,omitempty"` + ProxyURL string `json:"proxyURL"` + APIURL string `json:"apiURL"` + APIKey string `json:"apiKey"` + ConfigFile string `json:"configFile"` + Debug bool `json:"debug"` + ConnectTimeout string `json:"connectTimeout"` + DevboxID string `json:"devboxID"` + DevboxSessionID string `json:"devboxSessionID"` // LocalNetPath is prepended to PATH when the root-manager invokes // tunnel-setup commands (iptables on linux; pfctl and route on diff --git a/internal/locald/api/common.pb.go b/internal/locald/api/common.pb.go index fd5a311f..5615f1a1 100644 --- a/internal/locald/api/common.pb.go +++ b/internal/locald/api/common.pb.go @@ -229,6 +229,190 @@ func (x *HostsStatus) GetLastUpdateTime() *timestamppb.Timestamp { return nil } +// LocalDNS status (produced by root controller) +type LocalDNSStatus struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Health *ServiceHealth `protobuf:"bytes,1,opt,name=health,proto3" json:"health,omitempty"` + Mode string `protobuf:"bytes,2,opt,name=mode,proto3" json:"mode,omitempty"` + BindAddr string `protobuf:"bytes,3,opt,name=bind_addr,json=bindAddr,proto3" json:"bind_addr,omitempty"` + Suffixes []string `protobuf:"bytes,4,rep,name=suffixes,proto3" json:"suffixes,omitempty"` + Upstreams []string `protobuf:"bytes,5,rep,name=upstreams,proto3" json:"upstreams,omitempty"` + RecordCount uint32 `protobuf:"varint,6,opt,name=record_count,json=recordCount,proto3" json:"record_count,omitempty"` + LastRefresh *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_refresh,json=lastRefresh,proto3" json:"last_refresh,omitempty"` + ResolvConfManaged bool `protobuf:"varint,8,opt,name=resolv_conf_managed,json=resolvConfManaged,proto3" json:"resolv_conf_managed,omitempty"` + Warning string `protobuf:"bytes,9,opt,name=warning,proto3" json:"warning,omitempty"` + // host_count is the number of distinct cluster hosts (FQDNs), i.e. + // record_count excluding synthesized short forms. Matches `local hosts`. + HostCount uint32 `protobuf:"varint,10,opt,name=host_count,json=hostCount,proto3" json:"host_count,omitempty"` +} + +func (x *LocalDNSStatus) Reset() { + *x = LocalDNSStatus{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_common_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LocalDNSStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LocalDNSStatus) ProtoMessage() {} + +func (x *LocalDNSStatus) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_common_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LocalDNSStatus.ProtoReflect.Descriptor instead. +func (*LocalDNSStatus) Descriptor() ([]byte, []int) { + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{3} +} + +func (x *LocalDNSStatus) GetHealth() *ServiceHealth { + if x != nil { + return x.Health + } + return nil +} + +func (x *LocalDNSStatus) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *LocalDNSStatus) GetBindAddr() string { + if x != nil { + return x.BindAddr + } + return "" +} + +func (x *LocalDNSStatus) GetSuffixes() []string { + if x != nil { + return x.Suffixes + } + return nil +} + +func (x *LocalDNSStatus) GetUpstreams() []string { + if x != nil { + return x.Upstreams + } + return nil +} + +func (x *LocalDNSStatus) GetRecordCount() uint32 { + if x != nil { + return x.RecordCount + } + return 0 +} + +func (x *LocalDNSStatus) GetLastRefresh() *timestamppb.Timestamp { + if x != nil { + return x.LastRefresh + } + return nil +} + +func (x *LocalDNSStatus) GetResolvConfManaged() bool { + if x != nil { + return x.ResolvConfManaged + } + return false +} + +func (x *LocalDNSStatus) GetWarning() string { + if x != nil { + return x.Warning + } + return "" +} + +func (x *LocalDNSStatus) GetHostCount() uint32 { + if x != nil { + return x.HostCount + } + return 0 +} + +// HostEntry is a single resolvable cluster name and the synthetic address(es) +// assigned to it. It is the unit returned by the GetHosts RPCs and is +// independent of the name-resolution mechanism (/etc/hosts or local DNS): both +// draw from the same address allocator in the root controller. +type HostEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Single resolvable address for the host. A host is assigned one virtual + // address (IPv4-preferred; IPv6 on an IPv6-only allocation), so there is no + // plurality to carry here. + Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"` +} + +func (x *HostEntry) Reset() { + *x = HostEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_common_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HostEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostEntry) ProtoMessage() {} + +func (x *HostEntry) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_common_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostEntry.ProtoReflect.Descriptor instead. +func (*HostEntry) Descriptor() ([]byte, []int) { + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{4} +} + +func (x *HostEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HostEntry) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + // PortForward status (produced by local controller) type PortForwardStatus struct { state protoimpl.MessageState @@ -242,7 +426,7 @@ type PortForwardStatus struct { func (x *PortForwardStatus) Reset() { *x = PortForwardStatus{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[3] + mi := &file_internal_locald_api_common_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -255,7 +439,7 @@ func (x *PortForwardStatus) String() string { func (*PortForwardStatus) ProtoMessage() {} func (x *PortForwardStatus) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[3] + mi := &file_internal_locald_api_common_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -268,7 +452,7 @@ func (x *PortForwardStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use PortForwardStatus.ProtoReflect.Descriptor instead. func (*PortForwardStatus) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{3} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{5} } func (x *PortForwardStatus) GetHealth() *ServiceHealth { @@ -298,7 +482,7 @@ type ControlPlaneProxyStatus struct { func (x *ControlPlaneProxyStatus) Reset() { *x = ControlPlaneProxyStatus{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[4] + mi := &file_internal_locald_api_common_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -311,7 +495,7 @@ func (x *ControlPlaneProxyStatus) String() string { func (*ControlPlaneProxyStatus) ProtoMessage() {} func (x *ControlPlaneProxyStatus) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[4] + mi := &file_internal_locald_api_common_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -324,7 +508,7 @@ func (x *ControlPlaneProxyStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ControlPlaneProxyStatus.ProtoReflect.Descriptor instead. func (*ControlPlaneProxyStatus) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{4} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{6} } func (x *ControlPlaneProxyStatus) GetHealth() *ServiceHealth { @@ -353,7 +537,7 @@ type WatcherStatus struct { func (x *WatcherStatus) Reset() { *x = WatcherStatus{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[5] + mi := &file_internal_locald_api_common_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -366,7 +550,7 @@ func (x *WatcherStatus) String() string { func (*WatcherStatus) ProtoMessage() {} func (x *WatcherStatus) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[5] + mi := &file_internal_locald_api_common_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -379,7 +563,7 @@ func (x *WatcherStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use WatcherStatus.ProtoReflect.Descriptor instead. func (*WatcherStatus) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{5} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{7} } func (x *WatcherStatus) GetHealth() *ServiceHealth { @@ -403,7 +587,7 @@ type SandboxStatus struct { func (x *SandboxStatus) Reset() { *x = SandboxStatus{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[6] + mi := &file_internal_locald_api_common_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -416,7 +600,7 @@ func (x *SandboxStatus) String() string { func (*SandboxStatus) ProtoMessage() {} func (x *SandboxStatus) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[6] + mi := &file_internal_locald_api_common_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -429,7 +613,7 @@ func (x *SandboxStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStatus.ProtoReflect.Descriptor instead. func (*SandboxStatus) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{6} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{8} } func (x *SandboxStatus) GetName() string { @@ -466,7 +650,7 @@ type OperatorInfo struct { func (x *OperatorInfo) Reset() { *x = OperatorInfo{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[7] + mi := &file_internal_locald_api_common_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -479,7 +663,7 @@ func (x *OperatorInfo) String() string { func (*OperatorInfo) ProtoMessage() {} func (x *OperatorInfo) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[7] + mi := &file_internal_locald_api_common_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -492,7 +676,7 @@ func (x *OperatorInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use OperatorInfo.ProtoReflect.Descriptor instead. func (*OperatorInfo) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{7} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{9} } func (x *OperatorInfo) GetVersion() string { @@ -537,7 +721,7 @@ type DevboxSessionStatus struct { func (x *DevboxSessionStatus) Reset() { *x = DevboxSessionStatus{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[8] + mi := &file_internal_locald_api_common_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -550,7 +734,7 @@ func (x *DevboxSessionStatus) String() string { func (*DevboxSessionStatus) ProtoMessage() {} func (x *DevboxSessionStatus) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[8] + mi := &file_internal_locald_api_common_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -563,7 +747,7 @@ func (x *DevboxSessionStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use DevboxSessionStatus.ProtoReflect.Descriptor instead. func (*DevboxSessionStatus) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{8} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{10} } func (x *DevboxSessionStatus) GetHealthy() bool { @@ -615,7 +799,7 @@ type SandboxStatus_Baseline struct { func (x *SandboxStatus_Baseline) Reset() { *x = SandboxStatus_Baseline{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[9] + mi := &file_internal_locald_api_common_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -628,7 +812,7 @@ func (x *SandboxStatus_Baseline) String() string { func (*SandboxStatus_Baseline) ProtoMessage() {} func (x *SandboxStatus_Baseline) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[9] + mi := &file_internal_locald_api_common_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -641,7 +825,7 @@ func (x *SandboxStatus_Baseline) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStatus_Baseline.ProtoReflect.Descriptor instead. func (*SandboxStatus_Baseline) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{6, 0} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{8, 0} } func (x *SandboxStatus_Baseline) GetApiVersion() string { @@ -684,7 +868,7 @@ type SandboxStatus_BaselineToLocal struct { func (x *SandboxStatus_BaselineToLocal) Reset() { *x = SandboxStatus_BaselineToLocal{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[10] + mi := &file_internal_locald_api_common_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -697,7 +881,7 @@ func (x *SandboxStatus_BaselineToLocal) String() string { func (*SandboxStatus_BaselineToLocal) ProtoMessage() {} func (x *SandboxStatus_BaselineToLocal) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[10] + mi := &file_internal_locald_api_common_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -710,7 +894,7 @@ func (x *SandboxStatus_BaselineToLocal) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStatus_BaselineToLocal.ProtoReflect.Descriptor instead. func (*SandboxStatus_BaselineToLocal) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{6, 1} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{8, 1} } func (x *SandboxStatus_BaselineToLocal) GetBaselinePort() int32 { @@ -746,7 +930,7 @@ type SandboxStatus_LocalWorkload struct { func (x *SandboxStatus_LocalWorkload) Reset() { *x = SandboxStatus_LocalWorkload{} if protoimpl.UnsafeEnabled { - mi := &file_internal_locald_api_common_proto_msgTypes[11] + mi := &file_internal_locald_api_common_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -759,7 +943,7 @@ func (x *SandboxStatus_LocalWorkload) String() string { func (*SandboxStatus_LocalWorkload) ProtoMessage() {} func (x *SandboxStatus_LocalWorkload) ProtoReflect() protoreflect.Message { - mi := &file_internal_locald_api_common_proto_msgTypes[11] + mi := &file_internal_locald_api_common_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -772,7 +956,7 @@ func (x *SandboxStatus_LocalWorkload) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStatus_LocalWorkload.ProtoReflect.Descriptor instead. func (*SandboxStatus_LocalWorkload) Descriptor() ([]byte, []int) { - return file_internal_locald_api_common_proto_rawDescGZIP(), []int{6, 2} + return file_internal_locald_api_common_proto_rawDescGZIP(), []int{8, 2} } func (x *SandboxStatus_LocalWorkload) GetName() string { @@ -843,87 +1027,114 @@ var file_internal_locald_api_common_proto_rawDesc = []byte{ 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, - 0x6d, 0x65, 0x22, 0x6a, 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x70, - 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x50, 0x72, - 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x6c, - 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x22, 0x41, 0x0a, 0x0d, 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x22, 0xe4, 0x04, 0x0a, 0x0d, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x75, - 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x4f, 0x0a, 0x0f, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0e, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x1a, 0x70, 0x0a, 0x08, 0x42, - 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, 0x69, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x1a, 0x5b, 0x0a, - 0x0f, 0x42, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x54, 0x6f, 0x4c, 0x6f, 0x63, 0x61, 0x6c, - 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x70, 0x6f, 0x72, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, - 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, + 0x6d, 0x65, 0x22, 0xf8, 0x02, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x44, 0x4e, 0x53, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x62, + 0x69, 0x6e, 0x64, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x62, 0x69, 0x6e, 0x64, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x75, 0x66, 0x66, + 0x69, 0x78, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x75, 0x66, 0x66, + 0x69, 0x78, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x12, 0x2e, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x43, 0x6f, 0x6e, 0x66, 0x4d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x1d, + 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x2f, 0x0a, + 0x09, 0x48, 0x6f, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x22, 0x6a, + 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x1a, 0xfd, 0x01, 0x0a, 0x0d, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x3d, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x42, 0x61, 0x73, - 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x52, 0x08, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, - 0x5a, 0x0a, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, - 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x42, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x54, - 0x6f, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, - 0x50, 0x6f, 0x72, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x3d, 0x0a, 0x0d, 0x74, - 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x0c, 0x74, 0x75, - 0x6e, 0x6e, 0x65, 0x6c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x22, 0x66, 0x0a, 0x0c, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x69, 0x74, 0x43, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x44, 0x61, - 0x74, 0x65, 0x22, 0xdb, 0x01, 0x0a, 0x13, 0x44, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x49, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, - 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x0f, - 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x69, 0x6d, 0x65, - 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, - 0x69, 0x67, 0x6e, 0x61, 0x64, 0x6f, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x70, 0x0a, 0x17, 0x43, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x41, 0x0a, 0x0d, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x30, 0x0a, + 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x22, + 0xe4, 0x04, 0x0a, 0x0d, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, + 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x4f, 0x0a, 0x0f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, + 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x26, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x61, 0x6e, 0x64, + 0x62, 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x57, + 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x57, 0x6f, + 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x73, 0x1a, 0x70, 0x0a, 0x08, 0x42, 0x61, 0x73, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x1a, 0x5b, 0x0a, 0x0f, 0x42, 0x61, 0x73, + 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x54, 0x6f, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x12, 0x23, 0x0a, 0x0d, + 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0c, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x6f, 0x72, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x1a, 0xfd, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x08, + 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x42, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, + 0x65, 0x52, 0x08, 0x62, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x5a, 0x0a, 0x13, 0x77, + 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, + 0x6e, 0x67, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x2e, 0x42, 0x61, 0x73, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x54, 0x6f, 0x4c, 0x6f, 0x63, + 0x61, 0x6c, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x6f, 0x72, 0x74, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x3d, 0x0a, 0x0d, 0x74, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x0c, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x22, 0x66, 0x0a, 0x0c, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x69, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x44, 0x61, 0x74, 0x65, 0x22, 0xdb, + 0x01, 0x0a, 0x13, 0x44, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, + 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x0f, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x6c, + 0x61, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x2d, 0x5a, 0x2b, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x64, 0x6f, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -938,40 +1149,44 @@ func file_internal_locald_api_common_proto_rawDescGZIP() []byte { return file_internal_locald_api_common_proto_rawDescData } -var file_internal_locald_api_common_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_internal_locald_api_common_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_internal_locald_api_common_proto_goTypes = []interface{}{ (*ServiceHealth)(nil), // 0: apicommon.ServiceHealth (*LocalNetStatus)(nil), // 1: apicommon.LocalNetStatus (*HostsStatus)(nil), // 2: apicommon.HostsStatus - (*PortForwardStatus)(nil), // 3: apicommon.PortForwardStatus - (*ControlPlaneProxyStatus)(nil), // 4: apicommon.ControlPlaneProxyStatus - (*WatcherStatus)(nil), // 5: apicommon.WatcherStatus - (*SandboxStatus)(nil), // 6: apicommon.SandboxStatus - (*OperatorInfo)(nil), // 7: apicommon.OperatorInfo - (*DevboxSessionStatus)(nil), // 8: apicommon.DevboxSessionStatus - (*SandboxStatus_Baseline)(nil), // 9: apicommon.SandboxStatus.Baseline - (*SandboxStatus_BaselineToLocal)(nil), // 10: apicommon.SandboxStatus.BaselineToLocal - (*SandboxStatus_LocalWorkload)(nil), // 11: apicommon.SandboxStatus.LocalWorkload - (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (*LocalDNSStatus)(nil), // 3: apicommon.LocalDNSStatus + (*HostEntry)(nil), // 4: apicommon.HostEntry + (*PortForwardStatus)(nil), // 5: apicommon.PortForwardStatus + (*ControlPlaneProxyStatus)(nil), // 6: apicommon.ControlPlaneProxyStatus + (*WatcherStatus)(nil), // 7: apicommon.WatcherStatus + (*SandboxStatus)(nil), // 8: apicommon.SandboxStatus + (*OperatorInfo)(nil), // 9: apicommon.OperatorInfo + (*DevboxSessionStatus)(nil), // 10: apicommon.DevboxSessionStatus + (*SandboxStatus_Baseline)(nil), // 11: apicommon.SandboxStatus.Baseline + (*SandboxStatus_BaselineToLocal)(nil), // 12: apicommon.SandboxStatus.BaselineToLocal + (*SandboxStatus_LocalWorkload)(nil), // 13: apicommon.SandboxStatus.LocalWorkload + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp } var file_internal_locald_api_common_proto_depIdxs = []int32{ - 12, // 0: apicommon.ServiceHealth.last_error_time:type_name -> google.protobuf.Timestamp + 14, // 0: apicommon.ServiceHealth.last_error_time:type_name -> google.protobuf.Timestamp 0, // 1: apicommon.LocalNetStatus.health:type_name -> apicommon.ServiceHealth 0, // 2: apicommon.HostsStatus.health:type_name -> apicommon.ServiceHealth - 12, // 3: apicommon.HostsStatus.last_update_time:type_name -> google.protobuf.Timestamp - 0, // 4: apicommon.PortForwardStatus.health:type_name -> apicommon.ServiceHealth - 0, // 5: apicommon.ControlPlaneProxyStatus.health:type_name -> apicommon.ServiceHealth - 0, // 6: apicommon.WatcherStatus.health:type_name -> apicommon.ServiceHealth - 11, // 7: apicommon.SandboxStatus.local_workloads:type_name -> apicommon.SandboxStatus.LocalWorkload - 12, // 8: apicommon.DevboxSessionStatus.last_error_time:type_name -> google.protobuf.Timestamp - 9, // 9: apicommon.SandboxStatus.LocalWorkload.baseline:type_name -> apicommon.SandboxStatus.Baseline - 10, // 10: apicommon.SandboxStatus.LocalWorkload.workloadPortMapping:type_name -> apicommon.SandboxStatus.BaselineToLocal - 0, // 11: apicommon.SandboxStatus.LocalWorkload.tunnel_health:type_name -> apicommon.ServiceHealth - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 14, // 3: apicommon.HostsStatus.last_update_time:type_name -> google.protobuf.Timestamp + 0, // 4: apicommon.LocalDNSStatus.health:type_name -> apicommon.ServiceHealth + 14, // 5: apicommon.LocalDNSStatus.last_refresh:type_name -> google.protobuf.Timestamp + 0, // 6: apicommon.PortForwardStatus.health:type_name -> apicommon.ServiceHealth + 0, // 7: apicommon.ControlPlaneProxyStatus.health:type_name -> apicommon.ServiceHealth + 0, // 8: apicommon.WatcherStatus.health:type_name -> apicommon.ServiceHealth + 13, // 9: apicommon.SandboxStatus.local_workloads:type_name -> apicommon.SandboxStatus.LocalWorkload + 14, // 10: apicommon.DevboxSessionStatus.last_error_time:type_name -> google.protobuf.Timestamp + 11, // 11: apicommon.SandboxStatus.LocalWorkload.baseline:type_name -> apicommon.SandboxStatus.Baseline + 12, // 12: apicommon.SandboxStatus.LocalWorkload.workloadPortMapping:type_name -> apicommon.SandboxStatus.BaselineToLocal + 0, // 13: apicommon.SandboxStatus.LocalWorkload.tunnel_health:type_name -> apicommon.ServiceHealth + 14, // [14:14] is the sub-list for method output_type + 14, // [14:14] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name } func init() { file_internal_locald_api_common_proto_init() } @@ -1017,7 +1232,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortForwardStatus); i { + switch v := v.(*LocalDNSStatus); i { case 0: return &v.state case 1: @@ -1029,7 +1244,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ControlPlaneProxyStatus); i { + switch v := v.(*HostEntry); i { case 0: return &v.state case 1: @@ -1041,7 +1256,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WatcherStatus); i { + switch v := v.(*PortForwardStatus); i { case 0: return &v.state case 1: @@ -1053,7 +1268,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SandboxStatus); i { + switch v := v.(*ControlPlaneProxyStatus); i { case 0: return &v.state case 1: @@ -1065,7 +1280,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OperatorInfo); i { + switch v := v.(*WatcherStatus); i { case 0: return &v.state case 1: @@ -1077,7 +1292,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DevboxSessionStatus); i { + switch v := v.(*SandboxStatus); i { case 0: return &v.state case 1: @@ -1089,7 +1304,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SandboxStatus_Baseline); i { + switch v := v.(*OperatorInfo); i { case 0: return &v.state case 1: @@ -1101,7 +1316,7 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SandboxStatus_BaselineToLocal); i { + switch v := v.(*DevboxSessionStatus); i { case 0: return &v.state case 1: @@ -1113,6 +1328,30 @@ func file_internal_locald_api_common_proto_init() { } } file_internal_locald_api_common_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SandboxStatus_Baseline); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_locald_api_common_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SandboxStatus_BaselineToLocal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_locald_api_common_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SandboxStatus_LocalWorkload); i { case 0: return &v.state @@ -1131,7 +1370,7 @@ func file_internal_locald_api_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_locald_api_common_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 14, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/locald/api/common.proto b/internal/locald/api/common.proto index 9e63fd57..d242d3bb 100644 --- a/internal/locald/api/common.proto +++ b/internal/locald/api/common.proto @@ -30,6 +30,34 @@ message HostsStatus { google.protobuf.Timestamp last_update_time = 4; } +// LocalDNS status (produced by root controller) +message LocalDNSStatus { + ServiceHealth health = 1; + string mode = 2; + string bind_addr = 3; + repeated string suffixes = 4; + repeated string upstreams = 5; + uint32 record_count = 6; + google.protobuf.Timestamp last_refresh = 7; + bool resolv_conf_managed = 8; + string warning = 9; + // host_count is the number of distinct cluster hosts (FQDNs), i.e. + // record_count excluding synthesized short forms. Matches `local hosts`. + uint32 host_count = 10; +} + +// HostEntry is a single resolvable cluster name and the synthetic address(es) +// assigned to it. It is the unit returned by the GetHosts RPCs and is +// independent of the name-resolution mechanism (/etc/hosts or local DNS): both +// draw from the same address allocator in the root controller. +message HostEntry { + string name = 1; + // Single resolvable address for the host. A host is assigned one virtual + // address (IPv4-preferred; IPv6 on an IPv6-only allocation), so there is no + // plurality to carry here. + string ip = 2; +} + // PortForward status (produced by local controller) message PortForwardStatus { ServiceHealth health = 1; diff --git a/internal/locald/api/rootmanager/root_manager_api.pb.go b/internal/locald/api/rootmanager/root_manager_api.pb.go index 585f5c4b..909cf245 100644 --- a/internal/locald/api/rootmanager/root_manager_api.pb.go +++ b/internal/locald/api/rootmanager/root_manager_api.pb.go @@ -66,6 +66,7 @@ type StatusResponse struct { Localnet *api.LocalNetStatus `protobuf:"bytes,1,opt,name=localnet,proto3" json:"localnet,omitempty"` Hosts *api.HostsStatus `protobuf:"bytes,2,opt,name=hosts,proto3" json:"hosts,omitempty"` + LocalDns *api.LocalDNSStatus `protobuf:"bytes,3,opt,name=local_dns,json=localDns,proto3" json:"local_dns,omitempty"` } func (x *StatusResponse) Reset() { @@ -114,6 +115,13 @@ func (x *StatusResponse) GetHosts() *api.HostsStatus { return nil } +func (x *StatusResponse) GetLocalDns() *api.LocalDNSStatus { + if x != nil { + return x.LocalDns + } + return nil +} + type ShutdownRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -190,6 +198,91 @@ func (*ShutdownResponse) Descriptor() ([]byte, []int) { return file_internal_locald_api_rootmanager_root_manager_api_proto_rawDescGZIP(), []int{3} } +type GetHostsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetHostsRequest) Reset() { + *x = GetHostsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetHostsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHostsRequest) ProtoMessage() {} + +func (x *GetHostsRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHostsRequest.ProtoReflect.Descriptor instead. +func (*GetHostsRequest) Descriptor() ([]byte, []int) { + return file_internal_locald_api_rootmanager_root_manager_api_proto_rawDescGZIP(), []int{4} +} + +type GetHostsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entries []*api.HostEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` +} + +func (x *GetHostsResponse) Reset() { + *x = GetHostsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetHostsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHostsResponse) ProtoMessage() {} + +func (x *GetHostsResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHostsResponse.ProtoReflect.Descriptor instead. +func (*GetHostsResponse) Descriptor() ([]byte, []int) { + return file_internal_locald_api_rootmanager_root_manager_api_proto_rawDescGZIP(), []int{5} +} + +func (x *GetHostsResponse) GetEntries() []*api.HostEntry { + if x != nil { + return x.Entries + } + return nil +} + var File_internal_locald_api_rootmanager_root_manager_api_proto protoreflect.FileDescriptor var file_internal_locald_api_rootmanager_root_manager_api_proto_rawDesc = []byte{ @@ -200,17 +293,26 @@ var file_internal_locald_api_rootmanager_root_manager_api_proto_rawDesc = []byte 0x6e, 0x61, 0x67, 0x65, 0x72, 0x1a, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x75, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, - 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, 0x65, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x6e, 0x65, - 0x74, 0x12, 0x2c, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x6f, 0x73, - 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, - 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xa0, 0x01, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x74, 0x4d, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xad, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x6c, + 0x6f, 0x63, 0x61, 0x6c, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x4e, + 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x6e, + 0x65, 0x74, 0x12, 0x2c, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x6f, + 0x73, 0x74, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, + 0x12, 0x36, 0x0a, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x44, 0x4e, 0x53, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x08, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x44, 0x6e, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, + 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, + 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x11, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x42, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, + 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x32, 0xeb, 0x01, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x41, 0x50, 0x49, 0x12, 0x43, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, @@ -220,11 +322,16 @@ var file_internal_locald_api_rootmanager_root_manager_api_proto_rawDesc = []byte 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x64, 0x6f, 0x74, - 0x2f, 0x63, 0x6c, 0x69, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, - 0x63, 0x61, 0x6c, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x49, 0x0a, 0x08, 0x47, 0x65, 0x74, + 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x64, 0x6f, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -239,27 +346,35 @@ func file_internal_locald_api_rootmanager_root_manager_api_proto_rawDescGZIP() [ return file_internal_locald_api_rootmanager_root_manager_api_proto_rawDescData } -var file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_internal_locald_api_rootmanager_root_manager_api_proto_goTypes = []interface{}{ (*StatusRequest)(nil), // 0: rootmanager.StatusRequest (*StatusResponse)(nil), // 1: rootmanager.StatusResponse (*ShutdownRequest)(nil), // 2: rootmanager.ShutdownRequest (*ShutdownResponse)(nil), // 3: rootmanager.ShutdownResponse - (*api.LocalNetStatus)(nil), // 4: apicommon.LocalNetStatus - (*api.HostsStatus)(nil), // 5: apicommon.HostsStatus + (*GetHostsRequest)(nil), // 4: rootmanager.GetHostsRequest + (*GetHostsResponse)(nil), // 5: rootmanager.GetHostsResponse + (*api.LocalNetStatus)(nil), // 6: apicommon.LocalNetStatus + (*api.HostsStatus)(nil), // 7: apicommon.HostsStatus + (*api.LocalDNSStatus)(nil), // 8: apicommon.LocalDNSStatus + (*api.HostEntry)(nil), // 9: apicommon.HostEntry } var file_internal_locald_api_rootmanager_root_manager_api_proto_depIdxs = []int32{ - 4, // 0: rootmanager.StatusResponse.localnet:type_name -> apicommon.LocalNetStatus - 5, // 1: rootmanager.StatusResponse.hosts:type_name -> apicommon.HostsStatus - 0, // 2: rootmanager.RootManagerAPI.Status:input_type -> rootmanager.StatusRequest - 2, // 3: rootmanager.RootManagerAPI.Shutdown:input_type -> rootmanager.ShutdownRequest - 1, // 4: rootmanager.RootManagerAPI.Status:output_type -> rootmanager.StatusResponse - 3, // 5: rootmanager.RootManagerAPI.Shutdown:output_type -> rootmanager.ShutdownResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 6, // 0: rootmanager.StatusResponse.localnet:type_name -> apicommon.LocalNetStatus + 7, // 1: rootmanager.StatusResponse.hosts:type_name -> apicommon.HostsStatus + 8, // 2: rootmanager.StatusResponse.local_dns:type_name -> apicommon.LocalDNSStatus + 9, // 3: rootmanager.GetHostsResponse.entries:type_name -> apicommon.HostEntry + 0, // 4: rootmanager.RootManagerAPI.Status:input_type -> rootmanager.StatusRequest + 2, // 5: rootmanager.RootManagerAPI.Shutdown:input_type -> rootmanager.ShutdownRequest + 4, // 6: rootmanager.RootManagerAPI.GetHosts:input_type -> rootmanager.GetHostsRequest + 1, // 7: rootmanager.RootManagerAPI.Status:output_type -> rootmanager.StatusResponse + 3, // 8: rootmanager.RootManagerAPI.Shutdown:output_type -> rootmanager.ShutdownResponse + 5, // 9: rootmanager.RootManagerAPI.GetHosts:output_type -> rootmanager.GetHostsResponse + 7, // [7:10] is the sub-list for method output_type + 4, // [4:7] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_internal_locald_api_rootmanager_root_manager_api_proto_init() } @@ -316,6 +431,30 @@ func file_internal_locald_api_rootmanager_root_manager_api_proto_init() { return nil } } + file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetHostsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_locald_api_rootmanager_root_manager_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetHostsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -323,7 +462,7 @@ func file_internal_locald_api_rootmanager_root_manager_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_locald_api_rootmanager_root_manager_api_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/locald/api/rootmanager/root_manager_api.proto b/internal/locald/api/rootmanager/root_manager_api.proto index 06a2a7eb..46574e56 100644 --- a/internal/locald/api/rootmanager/root_manager_api.proto +++ b/internal/locald/api/rootmanager/root_manager_api.proto @@ -13,6 +13,11 @@ service RootManagerAPI { // This method requests the root controller to shutdown rpc Shutdown(ShutdownRequest) returns (ShutdownResponse) {} + + // This method returns the resolvable cluster hosts and their assigned + // addresses, regardless of whether name resolution is served via /etc/hosts + // or the local DNS resolver. + rpc GetHosts(GetHostsRequest) returns (GetHostsResponse) {} } // Status @@ -24,6 +29,7 @@ message StatusRequest { message StatusResponse { apicommon.LocalNetStatus localnet = 1; apicommon.HostsStatus hosts = 2; + apicommon.LocalDNSStatus local_dns = 3; } // Shutdown @@ -33,4 +39,14 @@ message ShutdownRequest { } message ShutdownResponse { +} + +// Hosts +// ---------------------------------------------------------------------------- + +message GetHostsRequest { +} + +message GetHostsResponse { + repeated apicommon.HostEntry entries = 1; } \ No newline at end of file diff --git a/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go b/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go index 9e070df3..8dbf14d4 100644 --- a/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go +++ b/internal/locald/api/rootmanager/root_manager_api_grpc.pb.go @@ -22,6 +22,10 @@ type RootManagerAPIClient interface { Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) // This method requests the root controller to shutdown Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) + // This method returns the resolvable cluster hosts and their assigned + // addresses, regardless of whether name resolution is served via /etc/hosts + // or the local DNS resolver. + GetHosts(ctx context.Context, in *GetHostsRequest, opts ...grpc.CallOption) (*GetHostsResponse, error) } type rootManagerAPIClient struct { @@ -50,6 +54,15 @@ func (c *rootManagerAPIClient) Shutdown(ctx context.Context, in *ShutdownRequest return out, nil } +func (c *rootManagerAPIClient) GetHosts(ctx context.Context, in *GetHostsRequest, opts ...grpc.CallOption) (*GetHostsResponse, error) { + out := new(GetHostsResponse) + err := c.cc.Invoke(ctx, "/rootmanager.RootManagerAPI/GetHosts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RootManagerAPIServer is the server API for RootManagerAPI service. // All implementations must embed UnimplementedRootManagerAPIServer // for forward compatibility @@ -58,6 +71,10 @@ type RootManagerAPIServer interface { Status(context.Context, *StatusRequest) (*StatusResponse, error) // This method requests the root controller to shutdown Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) + // This method returns the resolvable cluster hosts and their assigned + // addresses, regardless of whether name resolution is served via /etc/hosts + // or the local DNS resolver. + GetHosts(context.Context, *GetHostsRequest) (*GetHostsResponse, error) mustEmbedUnimplementedRootManagerAPIServer() } @@ -71,6 +88,9 @@ func (UnimplementedRootManagerAPIServer) Status(context.Context, *StatusRequest) func (UnimplementedRootManagerAPIServer) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Shutdown not implemented") } +func (UnimplementedRootManagerAPIServer) GetHosts(context.Context, *GetHostsRequest) (*GetHostsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHosts not implemented") +} func (UnimplementedRootManagerAPIServer) mustEmbedUnimplementedRootManagerAPIServer() {} // UnsafeRootManagerAPIServer may be embedded to opt out of forward compatibility for this service. @@ -120,6 +140,24 @@ func _RootManagerAPI_Shutdown_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _RootManagerAPI_GetHosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetHostsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RootManagerAPIServer).GetHosts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rootmanager.RootManagerAPI/GetHosts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RootManagerAPIServer).GetHosts(ctx, req.(*GetHostsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RootManagerAPI_ServiceDesc is the grpc.ServiceDesc for RootManagerAPI service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -135,6 +173,10 @@ var RootManagerAPI_ServiceDesc = grpc.ServiceDesc{ MethodName: "Shutdown", Handler: _RootManagerAPI_Shutdown_Handler, }, + { + MethodName: "GetHosts", + Handler: _RootManagerAPI_GetHosts_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "internal/locald/api/rootmanager/root_manager_api.proto", diff --git a/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go b/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go index 73ba0b3b..5c8ef3d2 100644 --- a/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go +++ b/internal/locald/api/sandboxmanager/sandbox_manager_api.pb.go @@ -76,6 +76,7 @@ type StatusResponse struct { Watcher *api.WatcherStatus `protobuf:"bytes,6,opt,name=watcher,proto3" json:"watcher,omitempty"` Sandboxes []*api.SandboxStatus `protobuf:"bytes,5,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` DevboxSession *api.DevboxSessionStatus `protobuf:"bytes,9,opt,name=devbox_session,json=devboxSession,proto3" json:"devbox_session,omitempty"` + LocalDns *api.LocalDNSStatus `protobuf:"bytes,10,opt,name=local_dns,json=localDns,proto3" json:"local_dns,omitempty"` } func (x *StatusResponse) Reset() { @@ -173,6 +174,13 @@ func (x *StatusResponse) GetDevboxSession() *api.DevboxSessionStatus { return nil } +func (x *StatusResponse) GetLocalDns() *api.LocalDNSStatus { + if x != nil { + return x.LocalDns + } + return nil +} + type ShutdownRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -455,6 +463,93 @@ func (x *ResourceOutputItem) GetValue() string { return "" } +// Hosts +// --------------------------------------------------------------------------- +type GetHostsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetHostsRequest) Reset() { + *x = GetHostsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetHostsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHostsRequest) ProtoMessage() {} + +func (x *GetHostsRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHostsRequest.ProtoReflect.Descriptor instead. +func (*GetHostsRequest) Descriptor() ([]byte, []int) { + return file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDescGZIP(), []int{8} +} + +type GetHostsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Entries []*api.HostEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` +} + +func (x *GetHostsResponse) Reset() { + *x = GetHostsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetHostsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHostsResponse) ProtoMessage() {} + +func (x *GetHostsResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHostsResponse.ProtoReflect.Descriptor instead. +func (*GetHostsResponse) Descriptor() ([]byte, []int) { + return file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDescGZIP(), []int{9} +} + +func (x *GetHostsResponse) GetEntries() []*api.HostEntry { + if x != nil { + return x.Entries + } + return nil +} + var File_internal_locald_api_sandboxmanager_sandbox_manager_api_proto protoreflect.FileDescriptor var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDesc = []byte{ @@ -468,7 +563,7 @@ var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDesc = 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0xb0, 0x04, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0xe8, 0x04, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x63, 0x69, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, @@ -503,49 +598,63 @@ var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDesc = 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0d, 0x64, 0x65, 0x76, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4b, 0x0a, 0x19, 0x47, 0x65, 0x74, + 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x64, 0x6e, 0x73, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x44, 0x4e, 0x53, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x44, 0x6e, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x68, + 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x12, 0x0a, + 0x10, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x4b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, + 0x0a, 0x13, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, + 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x73, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x22, 0x68, + 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x10, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x22, 0x74, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x3c, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x22, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x22, 0x3c, + 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x11, 0x0a, 0x0f, + 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x42, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, + 0x69, 0x65, 0x73, 0x32, 0xef, 0x02, 0x0a, 0x11, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x41, 0x50, 0x49, 0x12, 0x49, 0x0a, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, + 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6d, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x73, 0x61, + 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x11, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x52, 0x6f, 0x75, 0x74, - 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x22, 0x68, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, - 0x22, 0x74, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x73, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x07, 0x6f, - 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x22, 0x3c, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x32, 0x9e, 0x02, 0x0a, 0x11, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, - 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x41, 0x50, 0x49, 0x12, 0x49, 0x0a, 0x06, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x08, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, - 0x6e, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x2e, 0x53, 0x68, 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6d, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x73, - 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, + 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x72, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x64, 0x6f, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x64, @@ -565,7 +674,7 @@ func file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDescGZ return file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDescData } -var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_goTypes = []interface{}{ (*StatusRequest)(nil), // 0: sandboxmanager.StatusRequest (*StatusResponse)(nil), // 1: sandboxmanager.StatusResponse @@ -575,39 +684,47 @@ var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_goTypes = (*GetResourceOutputsResponse)(nil), // 5: sandboxmanager.GetResourceOutputsResponse (*ResourceOutputs)(nil), // 6: sandboxmanager.ResourceOutputs (*ResourceOutputItem)(nil), // 7: sandboxmanager.ResourceOutputItem - (*structpb.Struct)(nil), // 8: google.protobuf.Struct - (*api.OperatorInfo)(nil), // 9: apicommon.OperatorInfo - (*api.LocalNetStatus)(nil), // 10: apicommon.LocalNetStatus - (*api.HostsStatus)(nil), // 11: apicommon.HostsStatus - (*api.PortForwardStatus)(nil), // 12: apicommon.PortForwardStatus - (*api.ControlPlaneProxyStatus)(nil), // 13: apicommon.ControlPlaneProxyStatus - (*api.WatcherStatus)(nil), // 14: apicommon.WatcherStatus - (*api.SandboxStatus)(nil), // 15: apicommon.SandboxStatus - (*api.DevboxSessionStatus)(nil), // 16: apicommon.DevboxSessionStatus + (*GetHostsRequest)(nil), // 8: sandboxmanager.GetHostsRequest + (*GetHostsResponse)(nil), // 9: sandboxmanager.GetHostsResponse + (*structpb.Struct)(nil), // 10: google.protobuf.Struct + (*api.OperatorInfo)(nil), // 11: apicommon.OperatorInfo + (*api.LocalNetStatus)(nil), // 12: apicommon.LocalNetStatus + (*api.HostsStatus)(nil), // 13: apicommon.HostsStatus + (*api.PortForwardStatus)(nil), // 14: apicommon.PortForwardStatus + (*api.ControlPlaneProxyStatus)(nil), // 15: apicommon.ControlPlaneProxyStatus + (*api.WatcherStatus)(nil), // 16: apicommon.WatcherStatus + (*api.SandboxStatus)(nil), // 17: apicommon.SandboxStatus + (*api.DevboxSessionStatus)(nil), // 18: apicommon.DevboxSessionStatus + (*api.LocalDNSStatus)(nil), // 19: apicommon.LocalDNSStatus + (*api.HostEntry)(nil), // 20: apicommon.HostEntry } var file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_depIdxs = []int32{ - 8, // 0: sandboxmanager.StatusResponse.ci_config:type_name -> google.protobuf.Struct - 9, // 1: sandboxmanager.StatusResponse.operator_info:type_name -> apicommon.OperatorInfo - 10, // 2: sandboxmanager.StatusResponse.localnet:type_name -> apicommon.LocalNetStatus - 11, // 3: sandboxmanager.StatusResponse.hosts:type_name -> apicommon.HostsStatus - 12, // 4: sandboxmanager.StatusResponse.portforward:type_name -> apicommon.PortForwardStatus - 13, // 5: sandboxmanager.StatusResponse.control_plane_proxy:type_name -> apicommon.ControlPlaneProxyStatus - 14, // 6: sandboxmanager.StatusResponse.watcher:type_name -> apicommon.WatcherStatus - 15, // 7: sandboxmanager.StatusResponse.sandboxes:type_name -> apicommon.SandboxStatus - 16, // 8: sandboxmanager.StatusResponse.devbox_session:type_name -> apicommon.DevboxSessionStatus - 6, // 9: sandboxmanager.GetResourceOutputsResponse.resource_outputs:type_name -> sandboxmanager.ResourceOutputs - 7, // 10: sandboxmanager.ResourceOutputs.outputs:type_name -> sandboxmanager.ResourceOutputItem - 0, // 11: sandboxmanager.SandboxManagerAPI.Status:input_type -> sandboxmanager.StatusRequest - 2, // 12: sandboxmanager.SandboxManagerAPI.Shutdown:input_type -> sandboxmanager.ShutdownRequest - 4, // 13: sandboxmanager.SandboxManagerAPI.GetResourceOutputs:input_type -> sandboxmanager.GetResourceOutputsRequest - 1, // 14: sandboxmanager.SandboxManagerAPI.Status:output_type -> sandboxmanager.StatusResponse - 3, // 15: sandboxmanager.SandboxManagerAPI.Shutdown:output_type -> sandboxmanager.ShutdownResponse - 5, // 16: sandboxmanager.SandboxManagerAPI.GetResourceOutputs:output_type -> sandboxmanager.GetResourceOutputsResponse - 14, // [14:17] is the sub-list for method output_type - 11, // [11:14] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 10, // 0: sandboxmanager.StatusResponse.ci_config:type_name -> google.protobuf.Struct + 11, // 1: sandboxmanager.StatusResponse.operator_info:type_name -> apicommon.OperatorInfo + 12, // 2: sandboxmanager.StatusResponse.localnet:type_name -> apicommon.LocalNetStatus + 13, // 3: sandboxmanager.StatusResponse.hosts:type_name -> apicommon.HostsStatus + 14, // 4: sandboxmanager.StatusResponse.portforward:type_name -> apicommon.PortForwardStatus + 15, // 5: sandboxmanager.StatusResponse.control_plane_proxy:type_name -> apicommon.ControlPlaneProxyStatus + 16, // 6: sandboxmanager.StatusResponse.watcher:type_name -> apicommon.WatcherStatus + 17, // 7: sandboxmanager.StatusResponse.sandboxes:type_name -> apicommon.SandboxStatus + 18, // 8: sandboxmanager.StatusResponse.devbox_session:type_name -> apicommon.DevboxSessionStatus + 19, // 9: sandboxmanager.StatusResponse.local_dns:type_name -> apicommon.LocalDNSStatus + 6, // 10: sandboxmanager.GetResourceOutputsResponse.resource_outputs:type_name -> sandboxmanager.ResourceOutputs + 7, // 11: sandboxmanager.ResourceOutputs.outputs:type_name -> sandboxmanager.ResourceOutputItem + 20, // 12: sandboxmanager.GetHostsResponse.entries:type_name -> apicommon.HostEntry + 0, // 13: sandboxmanager.SandboxManagerAPI.Status:input_type -> sandboxmanager.StatusRequest + 2, // 14: sandboxmanager.SandboxManagerAPI.Shutdown:input_type -> sandboxmanager.ShutdownRequest + 4, // 15: sandboxmanager.SandboxManagerAPI.GetResourceOutputs:input_type -> sandboxmanager.GetResourceOutputsRequest + 8, // 16: sandboxmanager.SandboxManagerAPI.GetHosts:input_type -> sandboxmanager.GetHostsRequest + 1, // 17: sandboxmanager.SandboxManagerAPI.Status:output_type -> sandboxmanager.StatusResponse + 3, // 18: sandboxmanager.SandboxManagerAPI.Shutdown:output_type -> sandboxmanager.ShutdownResponse + 5, // 19: sandboxmanager.SandboxManagerAPI.GetResourceOutputs:output_type -> sandboxmanager.GetResourceOutputsResponse + 9, // 20: sandboxmanager.SandboxManagerAPI.GetHosts:output_type -> sandboxmanager.GetHostsResponse + 17, // [17:21] is the sub-list for method output_type + 13, // [13:17] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_init() } @@ -712,6 +829,30 @@ func file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_init() { return nil } } + file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetHostsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetHostsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -719,7 +860,7 @@ func file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_locald_api_sandboxmanager_sandbox_manager_api_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 10, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/locald/api/sandboxmanager/sandbox_manager_api.proto b/internal/locald/api/sandboxmanager/sandbox_manager_api.proto index 23f58a82..84288a22 100644 --- a/internal/locald/api/sandboxmanager/sandbox_manager_api.proto +++ b/internal/locald/api/sandboxmanager/sandbox_manager_api.proto @@ -18,6 +18,10 @@ service SandboxManagerAPI { // This method returns all the available resource outputs for a sandbox given // its routing key. rpc GetResourceOutputs(GetResourceOutputsRequest) returns (GetResourceOutputsResponse) {} + + // This method returns the resolvable cluster hosts and their assigned + // addresses (relayed from the root controller). + rpc GetHosts(GetHostsRequest) returns (GetHostsResponse) {} } // Status @@ -38,6 +42,7 @@ message StatusResponse { apicommon.WatcherStatus watcher = 6; repeated apicommon.SandboxStatus sandboxes = 5; apicommon.DevboxSessionStatus devbox_session = 9; + apicommon.LocalDNSStatus local_dns = 10; } // Shutdown @@ -68,3 +73,12 @@ message ResourceOutputItem { string key = 1; string value = 2; } + +// Hosts +// --------------------------------------------------------------------------- +message GetHostsRequest { +} + +message GetHostsResponse { + repeated apicommon.HostEntry entries = 1; +} diff --git a/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go b/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go index 552e58a6..92b70311 100644 --- a/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go +++ b/internal/locald/api/sandboxmanager/sandbox_manager_api_grpc.pb.go @@ -25,6 +25,9 @@ type SandboxManagerAPIClient interface { // This method returns all the available resource outputs for a sandbox given // its routing key. GetResourceOutputs(ctx context.Context, in *GetResourceOutputsRequest, opts ...grpc.CallOption) (*GetResourceOutputsResponse, error) + // This method returns the resolvable cluster hosts and their assigned + // addresses (relayed from the root controller). + GetHosts(ctx context.Context, in *GetHostsRequest, opts ...grpc.CallOption) (*GetHostsResponse, error) } type sandboxManagerAPIClient struct { @@ -62,6 +65,15 @@ func (c *sandboxManagerAPIClient) GetResourceOutputs(ctx context.Context, in *Ge return out, nil } +func (c *sandboxManagerAPIClient) GetHosts(ctx context.Context, in *GetHostsRequest, opts ...grpc.CallOption) (*GetHostsResponse, error) { + out := new(GetHostsResponse) + err := c.cc.Invoke(ctx, "/sandboxmanager.SandboxManagerAPI/GetHosts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SandboxManagerAPIServer is the server API for SandboxManagerAPI service. // All implementations must embed UnimplementedSandboxManagerAPIServer // for forward compatibility @@ -73,6 +85,9 @@ type SandboxManagerAPIServer interface { // This method returns all the available resource outputs for a sandbox given // its routing key. GetResourceOutputs(context.Context, *GetResourceOutputsRequest) (*GetResourceOutputsResponse, error) + // This method returns the resolvable cluster hosts and their assigned + // addresses (relayed from the root controller). + GetHosts(context.Context, *GetHostsRequest) (*GetHostsResponse, error) mustEmbedUnimplementedSandboxManagerAPIServer() } @@ -89,6 +104,9 @@ func (UnimplementedSandboxManagerAPIServer) Shutdown(context.Context, *ShutdownR func (UnimplementedSandboxManagerAPIServer) GetResourceOutputs(context.Context, *GetResourceOutputsRequest) (*GetResourceOutputsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetResourceOutputs not implemented") } +func (UnimplementedSandboxManagerAPIServer) GetHosts(context.Context, *GetHostsRequest) (*GetHostsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetHosts not implemented") +} func (UnimplementedSandboxManagerAPIServer) mustEmbedUnimplementedSandboxManagerAPIServer() {} // UnsafeSandboxManagerAPIServer may be embedded to opt out of forward compatibility for this service. @@ -156,6 +174,24 @@ func _SandboxManagerAPI_GetResourceOutputs_Handler(srv interface{}, ctx context. return interceptor(ctx, in, info, handler) } +func _SandboxManagerAPI_GetHosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetHostsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SandboxManagerAPIServer).GetHosts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/sandboxmanager.SandboxManagerAPI/GetHosts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SandboxManagerAPIServer).GetHosts(ctx, req.(*GetHostsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // SandboxManagerAPI_ServiceDesc is the grpc.ServiceDesc for SandboxManagerAPI service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -175,6 +211,10 @@ var SandboxManagerAPI_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetResourceOutputs", Handler: _SandboxManagerAPI_GetResourceOutputs_Handler, }, + { + MethodName: "GetHosts", + Handler: _SandboxManagerAPI_GetHosts_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "internal/locald/api/sandboxmanager/sandbox_manager_api.proto", diff --git a/internal/locald/rootmanager/root_manager.go b/internal/locald/rootmanager/root_manager.go index 52614ffb..cea88182 100644 --- a/internal/locald/rootmanager/root_manager.go +++ b/internal/locald/rootmanager/root_manager.go @@ -15,9 +15,11 @@ import ( "github.com/signadot/cli/internal/config" rootapi "github.com/signadot/cli/internal/locald/api/rootmanager" "github.com/signadot/libconnect/apiv1" + "github.com/signadot/libconnect/common/apiclient" connectcfg "github.com/signadot/libconnect/config" "github.com/signadot/libconnect/fwdtun/etchosts" "github.com/signadot/libconnect/fwdtun/ipmap" + "github.com/signadot/libconnect/fwdtun/localdns" "github.com/signadot/libconnect/fwdtun/localnet" "google.golang.org/grpc" ) @@ -67,6 +69,19 @@ func (m *rootManager) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("error creating ipmap: %w", err) } + // Publish the ipMap on the root API server so GetHosts can enumerate the + // host->address table regardless of which name-resolution service runs. + m.root.setIPMap(ipMap) + + // Repair OS resolver config left behind by a crashed prior --local-dns + // session, regardless of this session's mode. A --local-dns session that + // dies uncleanly can leave the system resolver pointing at our dead server + // (all DNS broken on Linux); the natural next move is a plain + // `signadot local connect`, so recovery must not be gated on --local-dns. + // Best-effort: never block connect on it. No-op when there's nothing to fix. + if err := localdns.Recover(localdns.Config{LocalNetPath: m.ciConfig.LocalNetPath}, m.log); err != nil { + m.log.Warn("localdns crash-recovery failed", "error", err) + } // Run the sandbox manager go m.sbmMonitor.run() @@ -77,9 +92,15 @@ func (m *rootManager) Run(ctx context.Context) error { // starting/restarting the localnet and etchost services m.tpMonitor = NewTunnelProxyMonitor(ctx, m, ipMap) default: - // Start localnet and etchost services + // Start localnet and the name-resolution service (localdns or etchosts) m.runLocalnetService(ctx, m.ciConfig.ConnectionConfig.ProxyAddress, ipMap) - m.runEtcHostsService(ctx, m.ciConfig.ConnectionConfig.ProxyAddress, ipMap) + if err := m.runNameService(ctx, m.ciConfig.ConnectionConfig.ProxyAddress, ipMap); err != nil { + // Route through the full cleanup: the sandbox manager was already + // spawned above as a daemonized child and stop() is the only thing + // that reaps it, so a bare return here would orphan it. + _ = m.cleanup() + return fmt.Errorf("error starting name resolution: %w", err) + } } // Wait until termination @@ -90,14 +111,23 @@ func (m *rootManager) Run(ctx context.Context) error { } // Clean up + return m.cleanup() +} + +// cleanup tears down everything Run started. It is safe to call on the startup +// error path as well as the normal shutdown path: each stop helper is a no-op +// when its service was never started. +func (m *rootManager) cleanup() error { m.log.Info("Shutting down") var me *multierror.Error if m.tpMonitor != nil { m.tpMonitor.Stop() } me = multierror.Append(me, m.sbmMonitor.stop()) + // Name resolution down before NAT: localdns restores the OS resolver path + // before the cluster becomes unreachable. (Harmless ordering for etchosts.) + me = multierror.Append(me, m.stopNameService()) me = multierror.Append(me, m.stopLocalnetService()) - me = multierror.Append(me, m.stopEtcHostsService()) return me.ErrorOrNil() } @@ -142,6 +172,62 @@ func (m *rootManager) runEtcHostsService(ctx context.Context, socks5Addr string, m.root.setEtcHostsService(etcHostsSVC) } +// runNameService starts cluster-name resolution: the localdns resolver when +// --local-dns is set, otherwise the legacy /etc/hosts mechanism. +func (m *rootManager) runNameService(ctx context.Context, socks5Addr string, ipMap *ipmap.IPMap) error { + if m.ciConfig.EnableLocalDNS { + return m.runLocalDNSService(ctx, socks5Addr, ipMap) + } + m.runEtcHostsService(ctx, socks5Addr, ipMap) + return nil +} + +// stopNameService stops whichever name-resolution service is running. Both stop +// helpers are no-ops when their service was never started. +func (m *rootManager) stopNameService() error { + var me *multierror.Error + me = multierror.Append(me, m.stopLocalDNSService()) + me = multierror.Append(me, m.stopEtcHostsService()) + return me.ErrorOrNil() +} + +func (m *rootManager) runLocalDNSService(ctx context.Context, socks5Addr string, ipMap *ipmap.IPMap) error { + // localdns takes an injected tunnel-api client (unlike etchosts, which + // builds its own); the CLI owns it and closes it on teardown. + apiClient, err := apiclient.NewClient(socks5Addr, apiclient.DefaultClientKeepaliveParams()) + if err != nil { + return fmt.Errorf("creating tunnel-api client for localdns: %w", err) + } + svc, err := localdns.New(localdns.Config{ + ManageResolver: true, // install OS resolver config (we are root) + Forward: true, // forward non-cluster queries to captured upstreams + LocalNetPath: m.ciConfig.LocalNetPath, + Log: m.log, + // BindIP/BindPort left zero: localdns picks 127.0.0.54:53 on Linux, + // an ephemeral 127.0.0.1 port on macOS. + }, apiClient, ipMap) + if err != nil { + _ = apiClient.Close() + return fmt.Errorf("starting localdns: %w", err) + } + m.root.setLocalDNSService(svc, apiClient) + return nil +} + +func (m *rootManager) stopLocalDNSService() error { + svc, apiClient := m.root.getLocalDNSService() + if svc == nil { + return nil + } + m.root.setLocalDNSService(nil, nil) + var me *multierror.Error + me = multierror.Append(me, svc.Close()) + if apiClient != nil { + me = multierror.Append(me, apiClient.Close()) + } + return me.ErrorOrNil() +} + func (m *rootManager) stopEtcHostsService() error { etcHostsSVC := m.root.getEtcHostsService() if etcHostsSVC != nil { diff --git a/internal/locald/rootmanager/root_server.go b/internal/locald/rootmanager/root_server.go index 5268bd80..731a861f 100644 --- a/internal/locald/rootmanager/root_server.go +++ b/internal/locald/rootmanager/root_server.go @@ -2,11 +2,16 @@ package rootmanager import ( "context" + "net" + "sort" "sync" commonapi "github.com/signadot/cli/internal/locald/api" rootapi "github.com/signadot/cli/internal/locald/api/rootmanager" + "github.com/signadot/libconnect/common/apiclient" "github.com/signadot/libconnect/fwdtun/etchosts" + "github.com/signadot/libconnect/fwdtun/ipmap" + "github.com/signadot/libconnect/fwdtun/localdns" "github.com/signadot/libconnect/fwdtun/localnet" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -17,7 +22,15 @@ type rootServer struct { mu sync.RWMutex localnetSVC *localnet.Service etcHostsSVC *etchosts.EtcHosts - shutdownCh chan struct{} + // localDNSSVC is the alternative to etcHostsSVC, selected by --local-dns. + // The CLI owns the injected tunnel-api client and closes it on teardown. + localDNSSVC *localdns.Service + localDNSAPIClient apiclient.Client + // ipMap is the shared host->address allocator that backs whichever + // name-resolution service is running; it is the source of truth for + // GetHosts in both /etc/hosts and local-DNS modes. + ipMap *ipmap.IPMap + shutdownCh chan struct{} } var _ rootapi.RootManagerAPIServer = &rootServer{} @@ -77,9 +90,42 @@ func (s *rootServer) Status(ctx context.Context, req *rootapi.StatusRequest) (*r } } + // Local DNS + var localDNSSt *commonapi.LocalDNSStatus + if s.localDNSSVC != nil { + status := s.localDNSSVC.Status() + + var lastErrortime *timestamppb.Timestamp + if status.LastErrorTime != nil { + lastErrortime = timestamppb.New(*status.LastErrorTime) + } + var lastRefresh *timestamppb.Timestamp + if status.LastRefresh != nil { + lastRefresh = timestamppb.New(*status.LastRefresh) + } + localDNSSt = &commonapi.LocalDNSStatus{ + Health: &commonapi.ServiceHealth{ + Healthy: status.Healthy, + ErrorCount: uint32(status.ErrorCount), + LastErrorReason: status.LastErrorReason, + LastErrorTime: lastErrortime, + }, + Mode: status.Mode, + BindAddr: status.BindAddr, + Suffixes: status.Suffixes, + Upstreams: status.Upstreams, + RecordCount: uint32(status.RecordCount), + HostCount: uint32(status.HostCount), + LastRefresh: lastRefresh, + ResolvConfManaged: status.ResolvConfManaged, + Warning: status.Warning, + } + } + resp := &rootapi.StatusResponse{ Localnet: localnetSt, Hosts: etcHostsSt, + LocalDns: localDNSSt, } return resp, nil } @@ -93,6 +139,63 @@ func (s *rootServer) Shutdown(ctx context.Context, req *rootapi.ShutdownRequest) return &rootapi.ShutdownResponse{}, nil } +func (s *rootServer) GetHosts(ctx context.Context, req *rootapi.GetHostsRequest) (*rootapi.GetHostsResponse, error) { + s.mu.RLock() + ipMap := s.ipMap + s.mu.RUnlock() + + resp := &rootapi.GetHostsResponse{} + if ipMap == nil { + // No name-resolution service has started yet (e.g. tunnel-proxy link + // still coming up): report an empty set rather than an error. + return resp, nil + } + + entries := ipMap.Entries() + resp.Entries = make([]*commonapi.HostEntry, 0, len(entries)) + for name, ips := range entries { + ip := pickIP(ips) + if ip == "" { + // A host with no assigned address is not resolvable; skip it. + continue + } + resp.Entries = append(resp.Entries, &commonapi.HostEntry{ + Name: name, + Ip: ip, + }) + } + // Stable, name-sorted output so callers (and `signadot local hosts`) get a + // deterministic listing. + sort.Slice(resp.Entries, func(i, j int) bool { + return resp.Entries[i].Name < resp.Entries[j].Name + }) + return resp, nil +} + +// pickIP returns the single address to report for a host: the IPv4 address when +// one is present, otherwise the first IPv6 address (covering an IPv6-only +// allocation). It returns "" when the host has no address. A host is assigned +// one virtual address in practice; the preference only makes the choice +// deterministic should it ever carry both families. +func pickIP(ips []net.IP) string { + var fallback string + for _, ip := range ips { + if ip.To4() != nil { + return ip.String() + } + if fallback == "" { + fallback = ip.String() + } + } + return fallback +} + +func (s *rootServer) setIPMap(ipMap *ipmap.IPMap) { + s.mu.Lock() + defer s.mu.Unlock() + s.ipMap = ipMap +} + func (s *rootServer) setLocalnetService(localnetSVC *localnet.Service) { s.mu.Lock() defer s.mu.Unlock() @@ -116,3 +219,16 @@ func (s *rootServer) getEtcHostsService() *etchosts.EtcHosts { defer s.mu.RUnlock() return s.etcHostsSVC } + +func (s *rootServer) setLocalDNSService(svc *localdns.Service, apiClient apiclient.Client) { + s.mu.Lock() + defer s.mu.Unlock() + s.localDNSSVC = svc + s.localDNSAPIClient = apiClient +} + +func (s *rootServer) getLocalDNSService() (*localdns.Service, apiclient.Client) { + s.mu.RLock() + defer s.mu.RUnlock() + return s.localDNSSVC, s.localDNSAPIClient +} diff --git a/internal/locald/rootmanager/root_server_test.go b/internal/locald/rootmanager/root_server_test.go new file mode 100644 index 00000000..da4619cd --- /dev/null +++ b/internal/locald/rootmanager/root_server_test.go @@ -0,0 +1,37 @@ +package rootmanager + +import ( + "net" + "testing" +) + +// TestPickIP covers the single-address selection GetHosts reports per host: +// IPv4 preferred, IPv6 fallback, empty when there is nothing to report. +func TestPickIP(t *testing.T) { + v4 := net.ParseIP("242.242.0.3") + v4b := net.ParseIP("242.242.0.9") + v6 := net.ParseIP("fd00:5161::3") + v6b := net.ParseIP("fd00:5161::9") + + tests := []struct { + name string + in []net.IP + want string + }{ + {"nil", nil, ""}, + {"empty", []net.IP{}, ""}, + {"v4 only", []net.IP{v4}, "242.242.0.3"}, + {"v6 only", []net.IP{v6}, "fd00:5161::3"}, + {"v4 before v6", []net.IP{v4, v6}, "242.242.0.3"}, + {"v6 before v4 -> still v4", []net.IP{v6, v4}, "242.242.0.3"}, + {"two v6 -> first", []net.IP{v6, v6b}, "fd00:5161::3"}, + {"two v4 -> first", []net.IP{v4, v4b}, "242.242.0.3"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pickIP(tt.in); got != tt.want { + t.Errorf("pickIP(%v) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} diff --git a/internal/locald/rootmanager/tp_monitor.go b/internal/locald/rootmanager/tp_monitor.go index 868f9763..dc76d56e 100644 --- a/internal/locald/rootmanager/tp_monitor.go +++ b/internal/locald/rootmanager/tp_monitor.go @@ -160,15 +160,17 @@ func (mon *tpMonitor) checkTunnelProxyAccess(ctx context.Context) (success bool, return true, false } - mon.log.Info("restarting localnet and etchosts services") + mon.log.Info("restarting localnet and name-resolution services") // Restart localnet mon.root.stopLocalnetService() mon.root.runLocalnetService(ctx, mon.tpLocalAddr, mon.ipMap) - // Restart etc hosts - mon.root.stopEtcHostsService() - mon.root.runEtcHostsService(ctx, mon.tpLocalAddr, mon.ipMap) + // Restart name resolution (localdns or etchosts) + mon.root.stopNameService() + if err := mon.root.runNameService(ctx, mon.tpLocalAddr, mon.ipMap); err != nil { + mon.log.Error("error starting name resolution; will retry", "error", err) + } return false, true } @@ -202,20 +204,50 @@ func (mon *tpMonitor) checkLinkStatus(status *sbmanagerapi.StatusResponse) (ok, } func (mon *tpMonitor) checkRootServer(r *rootServer) (ok, restart bool) { - if r.localnetSVC == nil || !r.localnetSVC.Status().Healthy { - if mon.shouldRestartDueToUnhealthy() { - return true, true - } else { - return false, false - } + // Use the mutex-guarded accessors: these fields are written from the + // name-service (re)start paths on other goroutines. The inactive + // name-resolution service is nil (etcHostsSVC under --local-dns, localDNSSVC + // otherwise), and the nil check short-circuits its Status() call. + localnetSVC := r.getLocalnetService() + localDNSSVC, _ := r.getLocalDNSService() + etcHostsSVC := r.getEtcHostsService() + return evalRootServerHealth(rootServerHealth{ + enableLocalDNS: mon.root.ciConfig.EnableLocalDNS, + localnetHealthy: localnetSVC != nil && localnetSVC.Status().Healthy, + localDNSHealthy: localDNSSVC != nil && localDNSSVC.Status().Healthy, + etcHostsHealthy: etcHostsSVC != nil && etcHostsSVC.Status().Healthy, + allowRestart: mon.shouldRestartDueToUnhealthy(), + }) +} + +// rootServerHealth is the input to evalRootServerHealth: the presence-and-health +// of each root-managed service plus whether a restart is currently allowed. +type rootServerHealth struct { + enableLocalDNS bool + localnetHealthy bool + localDNSHealthy bool + etcHostsHealthy bool + allowRestart bool +} + +// evalRootServerHealth is the pure decision behind checkRootServer, extracted so +// the mode matrix (which name-resolution service gates health) is unit-testable +// without standing up real localnet/localdns/etchosts services. Health gates on +// localnet and on whichever name service is active for the mode: with +// --local-dns that is localdns, otherwise etchosts. Gating on the wrong one +// would report perpetual unhealthiness and restart the services every cycle. +// allowRestart mirrors shouldRestartDueToUnhealthy: during the startup grace +// period an unhealthy service is tolerated rather than restarted. +func evalRootServerHealth(h rootServerHealth) (ok, restart bool) { + nameSvcHealthy := h.etcHostsHealthy + if h.enableLocalDNS { + nameSvcHealthy = h.localDNSHealthy } - // localnet ok, check etc hosts - if r.etcHostsSVC == nil || !r.etcHostsSVC.Status().Healthy { - if mon.shouldRestartDueToUnhealthy() { + if !h.localnetHealthy || !nameSvcHealthy { + if h.allowRestart { return true, true - } else { - return false, false } + return false, false } return true, false } diff --git a/internal/locald/rootmanager/tp_monitor_test.go b/internal/locald/rootmanager/tp_monitor_test.go new file mode 100644 index 00000000..e2c423f6 --- /dev/null +++ b/internal/locald/rootmanager/tp_monitor_test.go @@ -0,0 +1,117 @@ +package rootmanager + +import "testing" + +// TestEvalRootServerHealth is the mode matrix behind checkRootServer: health +// must gate on the name-resolution service that is actually active for the mode +// (localdns under --local-dns, etchosts otherwise). The regression it guards +// against is the restart loop where --local-dns gated on the always-nil +// etcHostsSVC and restarted the services every cycle. +func TestEvalRootServerHealth(t *testing.T) { + tests := []struct { + name string + in rootServerHealth + wantOK bool + wantRestart bool + }{ + // --- localdns mode --- + { + // The regression scenario: localdns healthy, etchosts absent + // (nil->false). Must be healthy, NOT a restart. + name: "localdns healthy, etchosts absent -> healthy", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: true, + localDNSHealthy: true, etcHostsHealthy: false, allowRestart: true, + }, + wantOK: true, wantRestart: false, + }, + { + name: "localdns unhealthy past grace -> restart", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: true, + localDNSHealthy: false, etcHostsHealthy: false, allowRestart: true, + }, + wantOK: true, wantRestart: true, + }, + { + name: "localdns unhealthy in grace -> wait", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: true, + localDNSHealthy: false, etcHostsHealthy: false, allowRestart: false, + }, + wantOK: false, wantRestart: false, + }, + { + // etchosts health must be ignored in localdns mode. + name: "localdns healthy, etchosts unhealthy -> healthy", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: true, + localDNSHealthy: true, etcHostsHealthy: false, allowRestart: true, + }, + wantOK: true, wantRestart: false, + }, + + // --- etchosts mode --- + { + name: "etchosts healthy, localdns absent -> healthy", + in: rootServerHealth{ + enableLocalDNS: false, localnetHealthy: true, + localDNSHealthy: false, etcHostsHealthy: true, allowRestart: true, + }, + wantOK: true, wantRestart: false, + }, + { + // localdns health must be ignored in etchosts mode. + name: "etchosts unhealthy, localdns healthy -> restart", + in: rootServerHealth{ + enableLocalDNS: false, localnetHealthy: true, + localDNSHealthy: true, etcHostsHealthy: false, allowRestart: true, + }, + wantOK: true, wantRestart: true, + }, + { + name: "etchosts unhealthy in grace -> wait", + in: rootServerHealth{ + enableLocalDNS: false, localnetHealthy: true, + localDNSHealthy: false, etcHostsHealthy: false, allowRestart: false, + }, + wantOK: false, wantRestart: false, + }, + + // --- localnet gating (independent of mode) --- + { + name: "localnet unhealthy past grace -> restart", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: false, + localDNSHealthy: true, etcHostsHealthy: true, allowRestart: true, + }, + wantOK: true, wantRestart: true, + }, + { + name: "localnet unhealthy in grace -> wait", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: false, + localDNSHealthy: true, etcHostsHealthy: true, allowRestart: false, + }, + wantOK: false, wantRestart: false, + }, + { + name: "all healthy (localdns) -> healthy", + in: rootServerHealth{ + enableLocalDNS: true, localnetHealthy: true, + localDNSHealthy: true, etcHostsHealthy: false, allowRestart: false, + }, + wantOK: true, wantRestart: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ok, restart := evalRootServerHealth(tt.in) + if ok != tt.wantOK || restart != tt.wantRestart { + t.Errorf("evalRootServerHealth(%+v) = (ok=%v, restart=%v), want (ok=%v, restart=%v)", + tt.in, ok, restart, tt.wantOK, tt.wantRestart) + } + }) + } +} diff --git a/internal/locald/sandboxmanager/sandbox_server.go b/internal/locald/sandboxmanager/sandbox_server.go index 8f2799fb..702031b4 100644 --- a/internal/locald/sandboxmanager/sandbox_server.go +++ b/internal/locald/sandboxmanager/sandbox_server.go @@ -87,7 +87,7 @@ func (s *sbmServer) Status(ctx context.Context, req *sbapi.StatusRequest) (*sbap Sandboxes: s.sbStatuses(), DevboxSession: s.devboxSessionStatus(), } - resp.Hosts, resp.Localnet = s.rootStatus() + resp.Hosts, resp.Localnet, resp.LocalDns = s.rootStatus() return resp, nil } @@ -125,16 +125,35 @@ func (s *sbmServer) GetResourceOutputs(ctx context.Context, req *sbapi.GetResour return res, nil } -func (s *sbmServer) rootStatus() (*commonapi.HostsStatus, *commonapi.LocalNetStatus) { +func (s *sbmServer) GetHosts(ctx context.Context, req *sbapi.GetHostsRequest) (*sbapi.GetHostsResponse, error) { + if !s.ciConfig.WithRootManager { + // Without a root manager there is no name-resolution service and hence + // no managed hosts. + return &sbapi.GetHostsResponse{}, nil + } + rootClient := s.getRootClient() + if rootClient == nil { + return nil, fmt.Errorf("no root manager client available") + } + rctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + resp, err := rootClient.GetHosts(rctx, &rootapi.GetHostsRequest{}) + if err != nil { + return nil, fmt.Errorf("error getting hosts from root manager: %w", err) + } + return &sbapi.GetHostsResponse{Entries: resp.Entries}, nil +} + +func (s *sbmServer) rootStatus() (*commonapi.HostsStatus, *commonapi.LocalNetStatus, *commonapi.LocalDNSStatus) { if !s.ciConfig.WithRootManager { // We are running without a root manager - return nil, nil + return nil, nil, nil } rootClient := s.getRootClient() if rootClient == nil { s.log.Debug("no root client available for rootStatus()") - return &commonapi.HostsStatus{}, &commonapi.LocalNetStatus{} + return &commonapi.HostsStatus{}, &commonapi.LocalNetStatus{}, nil } req := &rootapi.StatusRequest{} ctx, cancel := context.WithTimeout(context.Background(), @@ -143,9 +162,9 @@ func (s *sbmServer) rootStatus() (*commonapi.HostsStatus, *commonapi.LocalNetSta resp, err := rootClient.Status(ctx, req) if err != nil { s.log.Error("error getting status from root manager", "error", err) - return &commonapi.HostsStatus{}, &commonapi.LocalNetStatus{} + return &commonapi.HostsStatus{}, &commonapi.LocalNetStatus{}, nil } - return resp.Hosts, resp.Localnet + return resp.Hosts, resp.Localnet, resp.LocalDns } func (s *sbmServer) getRootClient() rootapi.RootManagerAPIClient { diff --git a/internal/locald/sandboxmanager/sdk.go b/internal/locald/sandboxmanager/sdk.go index a62d15c3..51a1b0aa 100644 --- a/internal/locald/sandboxmanager/sdk.go +++ b/internal/locald/sandboxmanager/sdk.go @@ -38,6 +38,31 @@ func GetStatus() (*sbmapi.StatusResponse, error) { return sbStatus, nil } +func GetHosts() (*sbmapi.GetHostsResponse, error) { + // get a sandbox manager API client + grpcConn, err := connectSandboxManager() + if err != nil { + return nil, err + } + defer grpcConn.Close() + + // get the hosts + sbManagerClient := sbmapi.NewSandboxManagerAPIClient(grpcConn) + resp, err := sbManagerClient.GetHosts(context.Background(), &sbmapi.GetHostsRequest{}) + if err != nil { + // This call is CLI-to-local-daemon, so Unimplemented means the running + // locald predates 'local hosts' (the user upgraded the CLI while + // connected), not a stale operator. Give restart guidance rather than + // processGRPCError's "upgrade the operator" message. + if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { + return nil, fmt.Errorf("the running local daemon predates 'signadot local hosts'; " + + "run 'signadot local disconnect && signadot local connect' to restart it") + } + return nil, processGRPCError("unable to get hosts from sandboxmanager", err) + } + return resp, nil +} + func CheckStatusConnectErrors(status *sbmapi.StatusResponse, ciConfig *config.ConnectInvocationConfig) []error { var errs []error @@ -72,8 +97,13 @@ func CheckStatusConnectErrors(status *sbmapi.StatusResponse, ciConfig *config.Co if err != nil { errs = append(errs, err) } - // check hosts service - err = checkHostsStatus(status.Hosts) + // check name-resolution service: localdns when --local-dns, + // otherwise the /etc/hosts mechanism + if ciConfig.EnableLocalDNS { + err = checkLocalDNSStatus(status.LocalDns) + } else { + err = checkHostsStatus(status.Hosts) + } if err != nil { errs = append(errs, err) } @@ -148,6 +178,21 @@ func checkHostsStatus(hosts *commonapi.HostsStatus) error { return errors.New(errorMsg) } +func checkLocalDNSStatus(localDNS *commonapi.LocalDNSStatus) error { + errorMsg := "failed to configure local DNS resolver" + if localDNS != nil { + if localDNS.Health != nil { + if localDNS.Health.Healthy { + return nil + } + if localDNS.Health.LastErrorReason != "" { + errorMsg += fmt.Sprintf(" (%q)", localDNS.Health.LastErrorReason) + } + } + } + return errors.New(errorMsg) +} + func checkDevboxSessionStatus(devboxSession *commonapi.DevboxSessionStatus) error { if devboxSession == nil { // Devbox session not initialized yet, not an error diff --git a/internal/locald/sandboxmanager/sdk_test.go b/internal/locald/sandboxmanager/sdk_test.go new file mode 100644 index 00000000..37feab6e --- /dev/null +++ b/internal/locald/sandboxmanager/sdk_test.go @@ -0,0 +1,62 @@ +package sandboxmanager + +import ( + "strings" + "testing" + + commonapi "github.com/signadot/cli/internal/locald/api" +) + +func TestCheckLocalDNSStatus(t *testing.T) { + tests := []struct { + name string + in *commonapi.LocalDNSStatus + wantErr bool + wantSubstr string + }{ + {"nil status", nil, true, "failed to configure local DNS resolver"}, + {"nil health", &commonapi.LocalDNSStatus{}, true, "failed to configure local DNS resolver"}, + { + "healthy", + &commonapi.LocalDNSStatus{Health: &commonapi.ServiceHealth{Healthy: true}}, + false, "", + }, + { + "unhealthy with reason", + &commonapi.LocalDNSStatus{Health: &commonapi.ServiceHealth{ + Healthy: false, LastErrorReason: "bind failed"}}, + true, "bind failed", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkLocalDNSStatus(tt.in) + if tt.wantErr != (err != nil) { + t.Fatalf("checkLocalDNSStatus err = %v, wantErr = %v", err, tt.wantErr) + } + if err != nil && tt.wantSubstr != "" && !strings.Contains(err.Error(), tt.wantSubstr) { + t.Errorf("error %q does not contain %q", err.Error(), tt.wantSubstr) + } + }) + } +} + +func TestCheckHostsStatus(t *testing.T) { + tests := []struct { + name string + in *commonapi.HostsStatus + wantErr bool + }{ + {"nil status", nil, true}, + {"nil health", &commonapi.HostsStatus{}, true}, + {"healthy", &commonapi.HostsStatus{Health: &commonapi.ServiceHealth{Healthy: true}}, false}, + {"unhealthy", &commonapi.HostsStatus{Health: &commonapi.ServiceHealth{Healthy: false}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := checkHostsStatus(tt.in); tt.wantErr != (err != nil) { + t.Errorf("checkHostsStatus err = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +}