From 5ffc3f14bd2b49a2ee09f36fe9e66bcf7b5283e8 Mon Sep 17 00:00:00 2001 From: Utku Ozdemir Date: Fri, 14 Jun 2024 11:24:07 +0200 Subject: [PATCH] feat: show siderolink status on dashboard Add a new resource, `SiderolinkStatus`, which combines the following info: - The Siderolink API endpoint without the query parameters or fragments (potentially sensitive info due to the join token) - The status of the Siderolink connection This resource is not set as sensitive, so it can be retrieved by the users with `os:operator` role (e.g., using `talosctl dashboard` through Omni). Make use of this resource in the dashboard to display the status of the Siderolink connection. Additionally, rework the status columns in the dashboard to: - Display a Linux terminal compatible "tick" or a "cross" prefix for statuses in addition to the red/green color coding. - Move and combine some statuses to save rows and make them more even. Closes siderolabs/talos#8643. Signed-off-by: Utku Ozdemir --- .../definitions/siderolink/siderolink.proto | 6 + .../pkg/controllers/runtime/diagnostics.go | 2 +- .../pkg/controllers/siderolink/manager.go | 28 +-- .../pkg/controllers/siderolink/siderolink.go | 32 ++++ .../pkg/controllers/siderolink/status.go | 158 ++++++++++++++++ .../pkg/controllers/siderolink/status_test.go | 132 ++++++++++++++ .../runtime/v1alpha2/v1alpha2_controller.go | 1 + .../pkg/runtime/v1alpha2/v1alpha2_state.go | 1 + .../pkg/dashboard/components/components.go | 16 +- .../dashboard/components/kubernetesinfo.go | 14 +- .../pkg/dashboard/components/networkinfo.go | 4 +- .../pkg/dashboard/components/talosinfo.go | 104 ++++------- .../dashboard/resourcedata/resourcedata.go | 16 +- internal/pkg/dashboard/summary.go | 2 +- .../definitions/siderolink/siderolink.pb.go | 91 +++++++++- .../siderolink/siderolink_vtproto.pb.go | 170 ++++++++++++++++++ pkg/machinery/resources/runtime/diagnostic.go | 4 +- .../siderolink/deep_copy.generated.go | 8 +- .../resources/siderolink/siderolink.go | 2 +- .../resources/siderolink/siderolink_status.go | 73 ++++++++ website/content/v1.8/reference/api.md | 17 ++ 21 files changed, 758 insertions(+), 123 deletions(-) create mode 100644 internal/app/machined/pkg/controllers/siderolink/status.go create mode 100644 internal/app/machined/pkg/controllers/siderolink/status_test.go create mode 100644 pkg/machinery/resources/siderolink/siderolink_status.go diff --git a/api/resource/definitions/siderolink/siderolink.proto b/api/resource/definitions/siderolink/siderolink.proto index dca66038e..c5a737f0c 100755 --- a/api/resource/definitions/siderolink/siderolink.proto +++ b/api/resource/definitions/siderolink/siderolink.proto @@ -15,6 +15,12 @@ message ConfigSpec { bool tunnel = 5; } +// StatusSpec describes Siderolink status. +message StatusSpec { + string host = 1; + bool connected = 2; +} + // TunnelSpec describes Siderolink GRPC Tunnel configuration. message TunnelSpec { string api_endpoint = 1; diff --git a/internal/app/machined/pkg/controllers/runtime/diagnostics.go b/internal/app/machined/pkg/controllers/runtime/diagnostics.go index c1043734a..963c55c3e 100644 --- a/internal/app/machined/pkg/controllers/runtime/diagnostics.go +++ b/internal/app/machined/pkg/controllers/runtime/diagnostics.go @@ -118,7 +118,7 @@ func (ctrl *DiagnosticsController) Run(ctx context.Context, r controller.Runtime return nil } - return safe.WriterModify(ctx, r, runtime.NewDiagnstic(runtime.NamespaceName, checkDescription.ID), func(res *runtime.Diagnostic) error { + return safe.WriterModify(ctx, r, runtime.NewDiagnostic(runtime.NamespaceName, checkDescription.ID), func(res *runtime.Diagnostic) error { *res.TypedSpec() = *warning return nil diff --git a/internal/app/machined/pkg/controllers/siderolink/manager.go b/internal/app/machined/pkg/controllers/siderolink/manager.go index 8d7cdfd19..ebc00ace1 100644 --- a/internal/app/machined/pkg/controllers/siderolink/manager.go +++ b/internal/app/machined/pkg/controllers/siderolink/manager.go @@ -138,8 +138,13 @@ func (ctrl *ManagerController) Run(ctx context.Context, r controller.Runtime, lo case <-ctx.Done(): return nil case <-ticker.C: - reconnect, err := ctrl.shouldReconnect(wgClient) + reconnect, err := peerDown(wgClient) if err != nil { + if errors.Is(err, os.ErrNotExist) { + // no Wireguard device, so no need to reconnect + continue + } + return err } @@ -476,27 +481,6 @@ func (ctrl *ManagerController) cleanupAddressSpecs(ctx context.Context, r contro return nil } -func (ctrl *ManagerController) shouldReconnect(wgClient *wgctrl.Client) (bool, error) { - wgDevice, err := wgClient.Device(constants.SideroLinkName) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - // no Wireguard device, so no need to reconnect - return false, nil - } - - return false, fmt.Errorf("error reading Wireguard device: %w", err) - } - - if len(wgDevice.Peers) != 1 { - return false, fmt.Errorf("unexpected number of Wireguard peers: %d", len(wgDevice.Peers)) - } - - peer := wgDevice.Peers[0] - since := time.Since(peer.LastHandshakeTime) - - return since >= wireguard.PeerDownInterval, nil -} - func withTransportCredentials(insec bool) grpc.DialOption { var transportCredentials credentials.TransportCredentials diff --git a/internal/app/machined/pkg/controllers/siderolink/siderolink.go b/internal/app/machined/pkg/controllers/siderolink/siderolink.go index d34db5f20..a8e89021b 100644 --- a/internal/app/machined/pkg/controllers/siderolink/siderolink.go +++ b/internal/app/machined/pkg/controllers/siderolink/siderolink.go @@ -4,3 +4,35 @@ // Package siderolink provides controllers which manage file resources. package siderolink + +import ( + "fmt" + "time" + + "github.com/siderolabs/siderolink/pkg/wireguard" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/siderolabs/talos/pkg/machinery/constants" +) + +// WireguardClient allows mocking Wireguard client. +type WireguardClient interface { + Device(string) (*wgtypes.Device, error) + Close() error +} + +func peerDown(wgClient WireguardClient) (bool, error) { + wgDevice, err := wgClient.Device(constants.SideroLinkName) + if err != nil { + return false, fmt.Errorf("error reading Wireguard device: %w", err) + } + + if len(wgDevice.Peers) != 1 { + return false, fmt.Errorf("unexpected number of Wireguard peers: %d", len(wgDevice.Peers)) + } + + peer := wgDevice.Peers[0] + since := time.Since(peer.LastHandshakeTime) + + return since >= wireguard.PeerDownInterval, nil +} diff --git a/internal/app/machined/pkg/controllers/siderolink/status.go b/internal/app/machined/pkg/controllers/siderolink/status.go new file mode 100644 index 000000000..1cbd1b579 --- /dev/null +++ b/internal/app/machined/pkg/controllers/siderolink/status.go @@ -0,0 +1,158 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package siderolink + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + "os" + "time" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/gen/optional" + "go.uber.org/zap" + "golang.zx2c4.com/wireguard/wgctrl" + + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/siderolink" +) + +// DefaultStatusUpdateInterval is the default interval between status updates. +const DefaultStatusUpdateInterval = 30 * time.Second + +// StatusController reports siderolink status. +type StatusController struct { + // WGClientFunc is a function that returns a WireguardClient. + // + // When nil, it defaults to an actual Wireguard client. + WGClientFunc func() (WireguardClient, error) + + // Interval is the time between peer status checks. + // + // When zero, it defaults to DefaultStatusUpdateInterval. + Interval time.Duration +} + +// Name implements controller.Controller interface. +func (ctrl *StatusController) Name() string { + return "siderolink.StatusController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *StatusController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: siderolink.ConfigType, + ID: optional.Some(siderolink.ConfigID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *StatusController) Outputs() []controller.Output { + return []controller.Output{ + { + Type: siderolink.StatusType, + Kind: controller.OutputExclusive, + }, + } +} + +// Run implements controller.Controller interface. +func (ctrl *StatusController) Run(ctx context.Context, r controller.Runtime, _ *zap.Logger) error { + interval := ctrl.Interval + if interval == 0 { + interval = DefaultStatusUpdateInterval + } + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + wgClientFunc := ctrl.WGClientFunc + if wgClientFunc == nil { + wgClientFunc = func() (WireguardClient, error) { + return wgctrl.New() + } + } + + wgClient, err := wgClientFunc() + if err != nil { + return fmt.Errorf("failed to create wireguard client: %w", err) + } + + for { + select { + case <-ctx.Done(): + return nil + case <-r.EventCh(): + case <-ticker.C: + } + + r.StartTrackingOutputs() + + if err = ctrl.reconcileStatus(ctx, r, wgClient); err != nil { + return err + } + + if err = safe.CleanupOutputs[*siderolink.Status](ctx, r); err != nil { + return err + } + + r.ResetRestartBackoff() + } +} + +func (ctrl *StatusController) reconcileStatus(ctx context.Context, r controller.Runtime, wgClient WireguardClient) (err error) { + cfg, err := safe.ReaderGetByID[*siderolink.Config](ctx, r, siderolink.ConfigID) + if err != nil { + if state.IsNotFoundError(err) { + return nil + } + + return err + } + + if cfg.TypedSpec().APIEndpoint == "" { + return nil + } + + var parsed *url.URL + + if parsed, err = url.Parse(cfg.TypedSpec().APIEndpoint); err != nil { + return fmt.Errorf("failed to parse siderolink API endpoint: %w", err) + } + + host, _, err := net.SplitHostPort(parsed.Host) + if err != nil { + host = parsed.Host + } + + down, err := peerDown(wgClient) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + + down = true // wireguard device does not exist, we mark it as down + } + + if err = safe.WriterModify(ctx, r, siderolink.NewStatus(), func(status *siderolink.Status) error { + status.TypedSpec().Host = host + status.TypedSpec().Connected = !down + + return nil + }); err != nil { + return fmt.Errorf("failed to update status: %w", err) + } + + return nil +} diff --git a/internal/app/machined/pkg/controllers/siderolink/status_test.go b/internal/app/machined/pkg/controllers/siderolink/status_test.go new file mode 100644 index 000000000..708045c7e --- /dev/null +++ b/internal/app/machined/pkg/controllers/siderolink/status_test.go @@ -0,0 +1,132 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package siderolink_test + +import ( + "os" + "sync" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/rtestutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest" + siderolinkctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/siderolink" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/siderolink" +) + +type StatusSuite struct { + ctest.DefaultSuite +} + +func TestStatusSuite(t *testing.T) { + suite.Run(t, &StatusSuite{ + DefaultSuite: ctest.DefaultSuite{ + Timeout: 3 * time.Second, + }, + }) +} + +func (suite *StatusSuite) TestStatus() { + wgClient := &mockWgClient{ + device: &wgtypes.Device{ + Peers: []wgtypes.Peer{ + { + LastHandshakeTime: time.Now().Add(-time.Minute), + }, + }, + }, + } + + suite.Require().NoError(suite.Runtime().RegisterController(&siderolinkctrl.StatusController{ + WGClientFunc: func() (siderolinkctrl.WireguardClient, error) { + return wgClient, nil + }, + Interval: 100 * time.Millisecond, + })) + + rtestutils.AssertNoResource[*siderolink.Status](suite.Ctx(), suite.T(), suite.State(), siderolink.StatusID) + + siderolinkConfig := siderolink.NewConfig(config.NamespaceName, siderolink.ConfigID) + + siderolinkConfig.TypedSpec().APIEndpoint = "https://siderolink.example.org:1234?jointoken=supersecret&foo=bar#some=fragment" + + suite.Require().NoError(suite.State().Create(suite.Ctx(), siderolinkConfig)) + + suite.assertStatus("siderolink.example.org", true) + + // disconnect the peer + + wgClient.setDevice(&wgtypes.Device{ + Peers: []wgtypes.Peer{ + {LastHandshakeTime: time.Now().Add(-time.Hour)}, + }, + }) + + // no device + wgClient.setDevice(nil) + suite.assertStatus("siderolink.example.org", false) + + // reconnect the peer + wgClient.setDevice(&wgtypes.Device{ + Peers: []wgtypes.Peer{ + {LastHandshakeTime: time.Now().Add(-5 * time.Second)}, + }, + }) + + suite.assertStatus("siderolink.example.org", true) + + // update API endpoint + + siderolinkConfig.TypedSpec().APIEndpoint = "https://new.example.org?jointoken=supersecret" + + suite.Require().NoError(suite.State().Update(suite.Ctx(), siderolinkConfig)) + suite.assertStatus("new.example.org", true) + + // no config + + suite.Require().NoError(suite.State().Destroy(suite.Ctx(), siderolinkConfig.Metadata())) + rtestutils.AssertNoResource[*siderolink.Status](suite.Ctx(), suite.T(), suite.State(), siderolink.StatusID) +} + +func (suite *StatusSuite) assertStatus(endpoint string, connected bool) { + rtestutils.AssertResources(suite.Ctx(), suite.T(), suite.State(), []resource.ID{siderolink.StatusID}, + func(c *siderolink.Status, assert *assert.Assertions) { + assert.Equal(endpoint, c.TypedSpec().Host) + assert.Equal(connected, c.TypedSpec().Connected) + }) +} + +type mockWgClient struct { + mu sync.Mutex + device *wgtypes.Device +} + +func (m *mockWgClient) setDevice(device *wgtypes.Device) { + m.mu.Lock() + defer m.mu.Unlock() + + m.device = device +} + +func (m *mockWgClient) Device(string) (*wgtypes.Device, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.device == nil { + return nil, os.ErrNotExist + } + + return m.device, nil +} + +func (m *mockWgClient) Close() error { + return nil +} diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index ca25c9d25..057babe98 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -336,6 +336,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(), }, &siderolink.ManagerController{}, + &siderolink.StatusController{}, &siderolink.UserspaceWireguardController{ RelayRetryTimeout: 10 * time.Second, }, diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go index 39c47c616..6debe2447 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_state.go @@ -214,6 +214,7 @@ func NewState() (*State, error) { &secrets.OSRoot{}, &secrets.Trustd{}, &siderolink.Config{}, + &siderolink.Status{}, &siderolink.Tunnel{}, &time.AdjtimeStatus{}, &time.Status{}, diff --git a/internal/pkg/dashboard/components/components.go b/internal/pkg/dashboard/components/components.go index b75dff1ef..018209cd2 100644 --- a/internal/pkg/dashboard/components/components.go +++ b/internal/pkg/dashboard/components/components.go @@ -75,14 +75,26 @@ func formatStatus(status any) string { switch strings.ToLower(statusStr) { case "running", "healthy", "true": - return fmt.Sprintf("[green]%s[-]", statusStr) + return formatText(statusStr, true) case "stopped", "unhealthy", "false": - return fmt.Sprintf("[red]%s[-]", statusStr) + return formatText(statusStr, false) default: return statusStr } } +func formatText(text string, ok bool) string { + if text == "" { + return "" + } + + if ok { + return fmt.Sprintf("[green]√ %s[-]", text) + } + + return fmt.Sprintf("[red]× %s[-]", text) +} + // capitalizeFirst capitalizes the first character of string. func capitalizeFirst(s string) string { if s == "" { diff --git a/internal/pkg/dashboard/components/kubernetesinfo.go b/internal/pkg/dashboard/components/kubernetesinfo.go index a8d468eb7..99b1a19f0 100644 --- a/internal/pkg/dashboard/components/kubernetesinfo.go +++ b/internal/pkg/dashboard/components/kubernetesinfo.go @@ -26,6 +26,7 @@ type staticPodStatuses struct { type kubernetesInfoData struct { isControlPlane bool + typ string kubernetesVersion string kubeletStatus string @@ -106,7 +107,13 @@ func (widget *KubernetesInfo) updateNodeData(data resourcedata.Data) { nodeData.podStatuses = widget.staticPodStatuses(maps.Values(nodeData.staticPodStatusMap)) case *config.MachineType: - nodeData.isControlPlane = !data.Deleted && res.MachineType() == machine.TypeControlPlane + if data.Deleted { + nodeData.isControlPlane = false + nodeData.typ = notAvailable + } else { + nodeData.isControlPlane = res.MachineType() == machine.TypeControlPlane + nodeData.typ = res.MachineType().String() + } } } @@ -128,6 +135,7 @@ func (widget *KubernetesInfo) getOrCreateNodeData(node string) *kubernetesInfoDa nodeData, ok := widget.nodeMap[node] if !ok { nodeData = &kubernetesInfoData{ + typ: notAvailable, kubernetesVersion: notAvailable, kubeletStatus: notAvailable, podStatuses: staticPodStatuses{ @@ -150,6 +158,10 @@ func (widget *KubernetesInfo) redraw() { fieldList := make([]field, 0, 5) fieldList = append(fieldList, + field{ + Name: "TYPE", + Value: data.typ, + }, field{ Name: "KUBERNETES", Value: data.kubernetesVersion, diff --git a/internal/pkg/dashboard/components/networkinfo.go b/internal/pkg/dashboard/components/networkinfo.go index 96feafc15..063a994ac 100644 --- a/internal/pkg/dashboard/components/networkinfo.go +++ b/internal/pkg/dashboard/components/networkinfo.go @@ -266,8 +266,8 @@ func (widget *NetworkInfo) timeservers(status *network.TimeServerStatus) string func (widget *NetworkInfo) connectivity(status *network.Status) string { if status.TypedSpec().ConnectivityReady { - return "[green]OK[-]" + return "[green]√ OK[-]" } - return "[red]FAILED[-]" + return "[red]× FAILED[-]" } diff --git a/internal/pkg/dashboard/components/talosinfo.go b/internal/pkg/dashboard/components/talosinfo.go index 57244921c..060ade6aa 100644 --- a/internal/pkg/dashboard/components/talosinfo.go +++ b/internal/pkg/dashboard/components/talosinfo.go @@ -6,29 +6,24 @@ package components import ( "fmt" - "strconv" - "strings" "github.com/rivo/tview" "github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata" - "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/cluster" - "github.com/siderolabs/talos/pkg/machinery/resources/config" "github.com/siderolabs/talos/pkg/machinery/resources/hardware" "github.com/siderolabs/talos/pkg/machinery/resources/runtime" + "github.com/siderolabs/talos/pkg/machinery/resources/siderolink" ) type talosInfoData struct { - uuid string - clusterName string - stage string - ready string - typ string - numMachinesText string - secureBootState string - statePartitionMountStatus string - ephemeralPartitionMountStatus string + uuid string + clusterName string + siderolink string + stage string + ready string + numMachinesText string + secureBootState string machineIDSet map[string]struct{} } @@ -91,6 +86,12 @@ func (widget *TalosInfo) updateNodeData(data resourcedata.Data) { } else { nodeData.clusterName = clusterName } + case *siderolink.Status: + if data.Deleted { + nodeData.siderolink = notAvailable + } else { + nodeData.siderolink = formatText(res.TypedSpec().Host, res.TypedSpec().Connected) + } case *runtime.MachineStatus: if data.Deleted { nodeData.stage = notAvailable @@ -105,28 +106,6 @@ func (widget *TalosInfo) updateNodeData(data resourcedata.Data) { } else { nodeData.secureBootState = formatStatus(res.TypedSpec().SecureBoot) } - case *runtime.MountStatus: - switch res.Metadata().ID() { - case constants.StatePartitionLabel: - if data.Deleted { - nodeData.statePartitionMountStatus = notAvailable - } else { - nodeData.statePartitionMountStatus = mountStatus(res.TypedSpec().Encrypted, res.TypedSpec().EncryptionProviders) - } - case constants.EphemeralPartitionLabel: - if data.Deleted { - nodeData.ephemeralPartitionMountStatus = notAvailable - } else { - nodeData.ephemeralPartitionMountStatus = mountStatus(res.TypedSpec().Encrypted, res.TypedSpec().EncryptionProviders) - } - } - - case *config.MachineType: - if data.Deleted { - nodeData.typ = notAvailable - } else { - nodeData.typ = res.MachineType().String() - } case *cluster.Member: if data.Deleted { delete(nodeData.machineIDSet, res.Metadata().ID()) @@ -134,7 +113,12 @@ func (widget *TalosInfo) updateNodeData(data resourcedata.Data) { nodeData.machineIDSet[res.Metadata().ID()] = struct{}{} } - nodeData.numMachinesText = strconv.Itoa(len(nodeData.machineIDSet)) + suffix := "" + if len(nodeData.machineIDSet) != 1 { + suffix = "s" + } + + nodeData.numMachinesText = fmt.Sprintf("(%d machine%s)", len(nodeData.machineIDSet), suffix) } } @@ -142,16 +126,14 @@ func (widget *TalosInfo) getOrCreateNodeData(node string) *talosInfoData { nodeData, ok := widget.nodeMap[node] if !ok { nodeData = &talosInfoData{ - uuid: notAvailable, - clusterName: notAvailable, - stage: notAvailable, - ready: notAvailable, - typ: notAvailable, - numMachinesText: notAvailable, - secureBootState: notAvailable, - statePartitionMountStatus: notAvailable, - ephemeralPartitionMountStatus: notAvailable, - machineIDSet: make(map[string]struct{}), + uuid: notAvailable, + clusterName: notAvailable, + siderolink: notAvailable, + stage: notAvailable, + ready: notAvailable, + numMachinesText: notAvailable, + secureBootState: notAvailable, + machineIDSet: make(map[string]struct{}), } widget.nodeMap[node] = nodeData @@ -171,7 +153,11 @@ func (widget *TalosInfo) redraw() { }, { Name: "CLUSTER", - Value: data.clusterName, + Value: data.clusterName + " " + data.numMachinesText, + }, + { + Name: "SIDEROLINK", + Value: data.siderolink, }, { Name: "STAGE", @@ -181,36 +167,12 @@ func (widget *TalosInfo) redraw() { Name: "READY", Value: data.ready, }, - { - Name: "TYPE", - Value: data.typ, - }, - { - Name: "MACHINES", - Value: data.numMachinesText, - }, { Name: "SECUREBOOT", Value: data.secureBootState, }, - { - Name: "STATE", - Value: data.statePartitionMountStatus, - }, - { - Name: "EPHEMERAL", - Value: data.ephemeralPartitionMountStatus, - }, }, } widget.SetText(fields.String()) } - -func mountStatus(encrypted bool, providers []string) string { - if !encrypted { - return "[green]OK[-]" - } - - return fmt.Sprintf("[green]OK - encrypted[-] (%s)", strings.Join(providers, ",")) -} diff --git a/internal/pkg/dashboard/resourcedata/resourcedata.go b/internal/pkg/dashboard/resourcedata/resourcedata.go index e23a0855d..e31d7e23d 100644 --- a/internal/pkg/dashboard/resourcedata/resourcedata.go +++ b/internal/pkg/dashboard/resourcedata/resourcedata.go @@ -20,13 +20,13 @@ import ( "github.com/siderolabs/talos/internal/pkg/dashboard/util" "github.com/siderolabs/talos/pkg/machinery/client" - "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/resources/cluster" "github.com/siderolabs/talos/pkg/machinery/resources/config" "github.com/siderolabs/talos/pkg/machinery/resources/hardware" "github.com/siderolabs/talos/pkg/machinery/resources/k8s" "github.com/siderolabs/talos/pkg/machinery/resources/network" "github.com/siderolabs/talos/pkg/machinery/resources/runtime" + "github.com/siderolabs/talos/pkg/machinery/resources/siderolink" "github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1" ) @@ -114,14 +114,6 @@ func (source *Source) runResourceWatch(ctx context.Context, node string) error { return err } - if err := source.COSI.Watch(ctx, runtime.NewMountStatus(v1alpha1.NamespaceName, constants.StatePartitionLabel).Metadata(), eventCh); err != nil { - return err - } - - if err := source.COSI.Watch(ctx, runtime.NewMountStatus(v1alpha1.NamespaceName, constants.EphemeralPartitionLabel).Metadata(), eventCh); err != nil { - return err - } - if err := source.COSI.Watch(ctx, config.NewMachineType().Metadata(), eventCh); err != nil { return err } @@ -178,7 +170,11 @@ func (source *Source) runResourceWatch(ctx context.Context, node string) error { return err } - if err := source.COSI.WatchKind(ctx, runtime.NewDiagnstic(runtime.NamespaceName, "").Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil { + if err := source.COSI.WatchKind(ctx, siderolink.NewStatus().Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil { + return err + } + + if err := source.COSI.WatchKind(ctx, runtime.NewDiagnostic(runtime.NamespaceName, "").Metadata(), eventCh, state.WithBootstrapContents(true)); err != nil { if client.StatusCode(err) != codes.PermissionDenied { // ignore permission denied, means resource is not supported yet return err diff --git a/internal/pkg/dashboard/summary.go b/internal/pkg/dashboard/summary.go index 87a43a414..57762f40e 100644 --- a/internal/pkg/dashboard/summary.go +++ b/internal/pkg/dashboard/summary.go @@ -30,7 +30,7 @@ type SummaryGrid struct { diagnosticsVisible bool } -const summaryTopFixedRows = 8 +const summaryTopFixedRows = 7 // NewSummaryGrid initializes SummaryGrid. func NewSummaryGrid(app *tview.Application) *SummaryGrid { diff --git a/pkg/machinery/api/resource/definitions/siderolink/siderolink.pb.go b/pkg/machinery/api/resource/definitions/siderolink/siderolink.pb.go index 84fff11db..caf3e6791 100644 --- a/pkg/machinery/api/resource/definitions/siderolink/siderolink.pb.go +++ b/pkg/machinery/api/resource/definitions/siderolink/siderolink.pb.go @@ -103,6 +103,62 @@ func (x *ConfigSpec) GetTunnel() bool { return false } +// StatusSpec describes Siderolink status. +type StatusSpec struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Connected bool `protobuf:"varint,2,opt,name=connected,proto3" json:"connected,omitempty"` +} + +func (x *StatusSpec) Reset() { + *x = StatusSpec{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StatusSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusSpec) ProtoMessage() {} + +func (x *StatusSpec) ProtoReflect() protoreflect.Message { + mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[1] + 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 StatusSpec.ProtoReflect.Descriptor instead. +func (*StatusSpec) Descriptor() ([]byte, []int) { + return file_resource_definitions_siderolink_siderolink_proto_rawDescGZIP(), []int{1} +} + +func (x *StatusSpec) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *StatusSpec) GetConnected() bool { + if x != nil { + return x.Connected + } + return false +} + // TunnelSpec describes Siderolink GRPC Tunnel configuration. type TunnelSpec struct { state protoimpl.MessageState @@ -118,7 +174,7 @@ type TunnelSpec struct { func (x *TunnelSpec) Reset() { *x = TunnelSpec{} if protoimpl.UnsafeEnabled { - mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[1] + mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -131,7 +187,7 @@ func (x *TunnelSpec) String() string { func (*TunnelSpec) ProtoMessage() {} func (x *TunnelSpec) ProtoReflect() protoreflect.Message { - mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[1] + mi := &file_resource_definitions_siderolink_siderolink_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -144,7 +200,7 @@ func (x *TunnelSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use TunnelSpec.ProtoReflect.Descriptor instead. func (*TunnelSpec) Descriptor() ([]byte, []int) { - return file_resource_definitions_siderolink_siderolink_proto_rawDescGZIP(), []int{1} + return file_resource_definitions_siderolink_siderolink_proto_rawDescGZIP(), []int{2} } func (x *TunnelSpec) GetApiEndpoint() string { @@ -194,7 +250,11 @@ var file_resource_definitions_siderolink_siderolink_proto_rawDesc = []byte{ 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x22, 0x94, 0x01, 0x0a, 0x0a, 0x54, 0x75, 0x6e, 0x6e, + 0x06, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x22, 0x3e, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x94, 0x01, 0x0a, 0x0a, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x70, 0x65, 0x63, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, @@ -224,14 +284,15 @@ func file_resource_definitions_siderolink_siderolink_proto_rawDescGZIP() []byte return file_resource_definitions_siderolink_siderolink_proto_rawDescData } -var file_resource_definitions_siderolink_siderolink_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_resource_definitions_siderolink_siderolink_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_resource_definitions_siderolink_siderolink_proto_goTypes = []interface{}{ (*ConfigSpec)(nil), // 0: talos.resource.definitions.siderolink.ConfigSpec - (*TunnelSpec)(nil), // 1: talos.resource.definitions.siderolink.TunnelSpec - (*common.NetIPPort)(nil), // 2: common.NetIPPort + (*StatusSpec)(nil), // 1: talos.resource.definitions.siderolink.StatusSpec + (*TunnelSpec)(nil), // 2: talos.resource.definitions.siderolink.TunnelSpec + (*common.NetIPPort)(nil), // 3: common.NetIPPort } var file_resource_definitions_siderolink_siderolink_proto_depIdxs = []int32{ - 2, // 0: talos.resource.definitions.siderolink.TunnelSpec.node_address:type_name -> common.NetIPPort + 3, // 0: talos.resource.definitions.siderolink.TunnelSpec.node_address:type_name -> common.NetIPPort 1, // [1:1] is the sub-list for method output_type 1, // [1:1] is the sub-list for method input_type 1, // [1:1] is the sub-list for extension type_name @@ -258,6 +319,18 @@ func file_resource_definitions_siderolink_siderolink_proto_init() { } } file_resource_definitions_siderolink_siderolink_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StatusSpec); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_definitions_siderolink_siderolink_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TunnelSpec); i { case 0: return &v.state @@ -276,7 +349,7 @@ func file_resource_definitions_siderolink_siderolink_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_definitions_siderolink_siderolink_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/machinery/api/resource/definitions/siderolink/siderolink_vtproto.pb.go b/pkg/machinery/api/resource/definitions/siderolink/siderolink_vtproto.pb.go index 85e9b0912..40ceafc89 100644 --- a/pkg/machinery/api/resource/definitions/siderolink/siderolink_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/siderolink/siderolink_vtproto.pb.go @@ -96,6 +96,56 @@ func (m *ConfigSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *StatusSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *StatusSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *StatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Connected { + i-- + if m.Connected { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } + if len(m.Host) > 0 { + i -= len(m.Host) + copy(dAtA[i:], m.Host) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Host))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *TunnelSpec) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -198,6 +248,23 @@ func (m *ConfigSpec) SizeVT() (n int) { return n } +func (m *StatusSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Host) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Connected { + n += 2 + } + n += len(m.unknownFields) + return n +} + func (m *TunnelSpec) SizeVT() (n int) { if m == nil { return 0 @@ -416,6 +483,109 @@ func (m *ConfigSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *StatusSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: StatusSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: StatusSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Host", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Host = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Connected", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Connected = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *TunnelSpec) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/pkg/machinery/resources/runtime/diagnostic.go b/pkg/machinery/resources/runtime/diagnostic.go index 62be781e2..ff8df5a28 100644 --- a/pkg/machinery/resources/runtime/diagnostic.go +++ b/pkg/machinery/resources/runtime/diagnostic.go @@ -34,8 +34,8 @@ func (spec *DiagnosticSpec) DocumentationURL(id string) string { return "https://talos.dev/diagnostic/" + id } -// NewDiagnstic initializes a Diagnostic resource. -func NewDiagnstic(namespace resource.Namespace, id resource.ID) *Diagnostic { +// NewDiagnostic initializes a Diagnostic resource. +func NewDiagnostic(namespace resource.Namespace, id resource.ID) *Diagnostic { return typed.NewResource[DiagnosticSpec, DiagnosticExtension]( resource.NewMetadata(namespace, DiagnosticType, id, resource.VersionUndefined), DiagnosticSpec{}, diff --git a/pkg/machinery/resources/siderolink/deep_copy.generated.go b/pkg/machinery/resources/siderolink/deep_copy.generated.go index d2e783cba..03b1c9509 100644 --- a/pkg/machinery/resources/siderolink/deep_copy.generated.go +++ b/pkg/machinery/resources/siderolink/deep_copy.generated.go @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "deep-copy -type ConfigSpec -type TunnelSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. +// Code generated by "deep-copy -type ConfigSpec -type StatusSpec -type TunnelSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT. package siderolink @@ -12,6 +12,12 @@ func (o ConfigSpec) DeepCopy() ConfigSpec { return cp } +// DeepCopy generates a deep copy of StatusSpec. +func (o StatusSpec) DeepCopy() StatusSpec { + var cp StatusSpec = o + return cp +} + // DeepCopy generates a deep copy of TunnelSpec. func (o TunnelSpec) DeepCopy() TunnelSpec { var cp TunnelSpec = o diff --git a/pkg/machinery/resources/siderolink/siderolink.go b/pkg/machinery/resources/siderolink/siderolink.go index cbb2ede41..66b32b81a 100644 --- a/pkg/machinery/resources/siderolink/siderolink.go +++ b/pkg/machinery/resources/siderolink/siderolink.go @@ -15,7 +15,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/resources/config" ) -//go:generate deep-copy -type ConfigSpec -type TunnelSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . +//go:generate deep-copy -type ConfigSpec -type StatusSpec -type TunnelSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go . // ConfigType is type of Config resource. const ConfigType = resource.Type("SiderolinkConfigs.siderolink.talos.dev") diff --git a/pkg/machinery/resources/siderolink/siderolink_status.go b/pkg/machinery/resources/siderolink/siderolink_status.go new file mode 100644 index 000000000..16322b87a --- /dev/null +++ b/pkg/machinery/resources/siderolink/siderolink_status.go @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package siderolink + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/talos/pkg/machinery/proto" + "github.com/siderolabs/talos/pkg/machinery/resources/config" +) + +// StatusType is the type of Status resource. +const StatusType = resource.Type("SiderolinkStatuses.siderolink.talos.dev") + +// StatusID the singleton status resource ID. +const StatusID = resource.ID("siderolink-status") + +// Status resource holds Siderolink status. +type Status = typed.Resource[StatusSpec, StatusExtension] + +// StatusSpec describes Siderolink status. +// +//gotagsrewrite:gen +type StatusSpec struct { + // Host is the Siderolink target host. + Host string `yaml:"host" protobuf:"1"` + // Connected is the status of the Siderolink GRPC connection. + Connected bool `yaml:"connected" protobuf:"2"` +} + +// NewStatus initializes a Status resource. +func NewStatus() *Status { + return typed.NewResource[StatusSpec, StatusExtension]( + resource.NewMetadata(config.NamespaceName, StatusType, StatusID, resource.VersionUndefined), + StatusSpec{}, + ) +} + +// StatusExtension provides auxiliary methods for Status. +type StatusExtension struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (StatusExtension) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: StatusType, + Aliases: []resource.Type{}, + DefaultNamespace: config.NamespaceName, + PrintColumns: []meta.PrintColumn{ + { + Name: "Host", + JSONPath: `{.host}`, + }, + { + Name: "Connected", + JSONPath: `{.connected}`, + }, + }, + } +} + +func init() { + proto.RegisterDefaultTypes() + + err := protobuf.RegisterDynamic[StatusSpec](StatusType, &Status{}) + if err != nil { + panic(err) + } +} diff --git a/website/content/v1.8/reference/api.md b/website/content/v1.8/reference/api.md index 4fa75f599..ef5693416 100644 --- a/website/content/v1.8/reference/api.md +++ b/website/content/v1.8/reference/api.md @@ -255,6 +255,7 @@ description: Talos gRPC API reference. - [resource/definitions/siderolink/siderolink.proto](#resource/definitions/siderolink/siderolink.proto) - [ConfigSpec](#talos.resource.definitions.siderolink.ConfigSpec) + - [StatusSpec](#talos.resource.definitions.siderolink.StatusSpec) - [TunnelSpec](#talos.resource.definitions.siderolink.TunnelSpec) - [resource/definitions/time/time.proto](#resource/definitions/time/time.proto) @@ -4612,6 +4613,22 @@ ConfigSpec describes Siderolink configuration. + + +### StatusSpec +StatusSpec describes Siderolink status. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| host | [string](#string) | | | +| connected | [bool](#bool) | | | + + + + + + ### TunnelSpec