mirror of
https://github.com/netbirdio/netbird.git
synced 2025-05-31 07:07:42 +02:00
Merge pull request #704 from netbirdio/feature/extend-client-status-cmd-to-print-json-or-yaml
Feature/extend client status cmd to print json or yaml
This commit is contained in:
commit
5bb875a0fa
@ -2,25 +2,74 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
nbStatus "github.com/netbirdio/netbird/client/status"
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type peerStateDetailOutput struct {
|
||||||
|
FQDN string `json:"fqdn" yaml:"fqdn"`
|
||||||
|
IP string `json:"netbirdIp" yaml:"netbirdIp"`
|
||||||
|
PubKey string `json:"publicKey" yaml:"publicKey"`
|
||||||
|
Status string `json:"status" yaml:"status"`
|
||||||
|
LastStatusUpdate time.Time `json:"lastStatusUpdate" yaml:"lastStatusUpdate"`
|
||||||
|
ConnType string `json:"connectionType" yaml:"connectionType"`
|
||||||
|
Direct bool `json:"direct" yaml:"direct"`
|
||||||
|
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type peersStateOutput struct {
|
||||||
|
Total int `json:"total" yaml:"total"`
|
||||||
|
Connected int `json:"connected" yaml:"connected"`
|
||||||
|
Details []peerStateDetailOutput `json:"details" yaml:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type signalStateOutput struct {
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
Connected bool `json:"connected" yaml:"connected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type managementStateOutput struct {
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
Connected bool `json:"connected" yaml:"connected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type iceCandidateType struct {
|
||||||
|
Local string `json:"local" yaml:"local"`
|
||||||
|
Remote string `json:"remote" yaml:"remote"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusOutputOverview struct {
|
||||||
|
Peers peersStateOutput `json:"peers" yaml:"peers"`
|
||||||
|
CliVersion string `json:"cliVersion" yaml:"cliVersion"`
|
||||||
|
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
|
||||||
|
ManagementState managementStateOutput `json:"management" yaml:"management"`
|
||||||
|
SignalState signalStateOutput `json:"signal" yaml:"signal"`
|
||||||
|
IP string `json:"netbirdIp" yaml:"netbirdIp"`
|
||||||
|
PubKey string `json:"publicKey" yaml:"publicKey"`
|
||||||
|
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
|
||||||
|
FQDN string `json:"fqdn" yaml:"fqdn"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
detailFlag bool
|
detailFlag bool
|
||||||
ipv4Flag bool
|
ipv4Flag bool
|
||||||
|
jsonFlag bool
|
||||||
|
yamlFlag bool
|
||||||
ipsFilter []string
|
ipsFilter []string
|
||||||
statusFilter string
|
statusFilter string
|
||||||
ipsFilterMap map[string]struct{}
|
ipsFilterMap map[string]struct{}
|
||||||
@ -29,67 +78,99 @@ var (
|
|||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "status of the Netbird Service",
|
Short: "status of the Netbird Service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: statusFunc,
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
|
||||||
|
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
|
||||||
|
|
||||||
err := parseFilters()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = util.InitLog(logLevel, "console")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed initializing log %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := internal.CtxInitState(context.Background())
|
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
|
||||||
"If the daemon is not running please run: "+
|
|
||||||
"\nnetbird service install \nnetbird service start\n", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
daemonStatus := fmt.Sprintf("Daemon status: %s\n", resp.GetStatus())
|
|
||||||
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
|
||||||
|
|
||||||
cmd.Printf("%s\n"+
|
|
||||||
"Run UP command to log in with SSO (interactive login):\n\n"+
|
|
||||||
" netbird up \n\n"+
|
|
||||||
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
|
||||||
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
|
||||||
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
|
|
||||||
daemonStatus,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pbFullStatus := resp.GetFullStatus()
|
|
||||||
fullStatus := fromProtoFullStatus(pbFullStatus)
|
|
||||||
|
|
||||||
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion(), ipv4Flag))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ipsFilterMap = make(map[string]struct{})
|
ipsFilterMap = make(map[string]struct{})
|
||||||
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
|
||||||
|
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
|
||||||
|
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
|
||||||
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
||||||
|
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
|
||||||
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusFunc(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := parseFilters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(context.Background())
|
||||||
|
|
||||||
|
resp, _ := getStatus(ctx, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
||||||
|
cmd.Printf("Daemon status: %s\n\n"+
|
||||||
|
"Run UP command to log in with SSO (interactive login):\n\n"+
|
||||||
|
" netbird up \n\n"+
|
||||||
|
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
||||||
|
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
||||||
|
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
|
||||||
|
resp.GetStatus(),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4Flag {
|
||||||
|
cmd.Print(parseInterfaceIP(resp.GetFullStatus().GetLocalPeerState().GetIP()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outputInformationHolder := convertToStatusOutputOverview(resp)
|
||||||
|
|
||||||
|
statusOutputString := ""
|
||||||
|
switch {
|
||||||
|
case detailFlag:
|
||||||
|
statusOutputString = parseToFullDetailSummary(outputInformationHolder)
|
||||||
|
case jsonFlag:
|
||||||
|
statusOutputString, err = parseToJSON(outputInformationHolder)
|
||||||
|
case yamlFlag:
|
||||||
|
statusOutputString, err = parseToYAML(outputInformationHolder)
|
||||||
|
default:
|
||||||
|
statusOutputString = parseGeneralSummary(outputInformationHolder, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Print(statusOutputString)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse, error) {
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
switch strings.ToLower(statusFilter) {
|
switch strings.ToLower(statusFilter) {
|
||||||
case "", "disconnected", "connected":
|
case "", "disconnected", "connected":
|
||||||
@ -109,195 +190,229 @@ func parseFilters() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromProtoFullStatus(pbFullStatus *proto.FullStatus) nbStatus.FullStatus {
|
func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
|
||||||
var fullStatus nbStatus.FullStatus
|
pbFullStatus := resp.GetFullStatus()
|
||||||
|
|
||||||
managementState := pbFullStatus.GetManagementState()
|
managementState := pbFullStatus.GetManagementState()
|
||||||
fullStatus.ManagementState.URL = managementState.GetURL()
|
managementOverview := managementStateOutput{
|
||||||
fullStatus.ManagementState.Connected = managementState.GetConnected()
|
URL: managementState.GetURL(),
|
||||||
|
Connected: managementState.GetConnected(),
|
||||||
signalState := pbFullStatus.GetSignalState()
|
|
||||||
fullStatus.SignalState.URL = signalState.GetURL()
|
|
||||||
fullStatus.SignalState.Connected = signalState.GetConnected()
|
|
||||||
|
|
||||||
localPeerState := pbFullStatus.GetLocalPeerState()
|
|
||||||
fullStatus.LocalPeerState.IP = localPeerState.GetIP()
|
|
||||||
fullStatus.LocalPeerState.PubKey = localPeerState.GetPubKey()
|
|
||||||
fullStatus.LocalPeerState.KernelInterface = localPeerState.GetKernelInterface()
|
|
||||||
fullStatus.LocalPeerState.FQDN = localPeerState.GetFqdn()
|
|
||||||
|
|
||||||
var peersState []nbStatus.PeerState
|
|
||||||
|
|
||||||
for _, pbPeerState := range pbFullStatus.GetPeers() {
|
|
||||||
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
|
||||||
peerState := nbStatus.PeerState{
|
|
||||||
IP: pbPeerState.GetIP(),
|
|
||||||
PubKey: pbPeerState.GetPubKey(),
|
|
||||||
ConnStatus: pbPeerState.GetConnStatus(),
|
|
||||||
ConnStatusUpdate: timeLocal,
|
|
||||||
Relayed: pbPeerState.GetRelayed(),
|
|
||||||
Direct: pbPeerState.GetDirect(),
|
|
||||||
LocalIceCandidateType: pbPeerState.GetLocalIceCandidateType(),
|
|
||||||
RemoteIceCandidateType: pbPeerState.GetRemoteIceCandidateType(),
|
|
||||||
FQDN: pbPeerState.GetFqdn(),
|
|
||||||
}
|
|
||||||
peersState = append(peersState, peerState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fullStatus.Peers = peersState
|
signalState := pbFullStatus.GetSignalState()
|
||||||
|
signalOverview := signalStateOutput{
|
||||||
|
URL: signalState.GetURL(),
|
||||||
|
Connected: signalState.GetConnected(),
|
||||||
|
}
|
||||||
|
|
||||||
return fullStatus
|
peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
|
||||||
|
|
||||||
|
overview := statusOutputOverview{
|
||||||
|
Peers: peersOverview,
|
||||||
|
CliVersion: system.NetbirdVersion(),
|
||||||
|
DaemonVersion: resp.GetDaemonVersion(),
|
||||||
|
ManagementState: managementOverview,
|
||||||
|
SignalState: signalOverview,
|
||||||
|
IP: pbFullStatus.GetLocalPeerState().GetIP(),
|
||||||
|
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
|
||||||
|
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
|
||||||
|
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return overview
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string, flag bool) string {
|
func mapPeers(peers []*proto.PeerState) peersStateOutput {
|
||||||
|
var peersStateDetail []peerStateDetailOutput
|
||||||
|
localICE := ""
|
||||||
|
remoteICE := ""
|
||||||
|
connType := ""
|
||||||
|
peersConnected := 0
|
||||||
|
for _, pbPeerState := range peers {
|
||||||
|
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
|
||||||
|
if skipDetailByFilters(pbPeerState, isPeerConnected) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isPeerConnected {
|
||||||
|
peersConnected = peersConnected + 1
|
||||||
|
|
||||||
interfaceIP := fullStatus.LocalPeerState.IP
|
localICE = pbPeerState.GetLocalIceCandidateType()
|
||||||
|
remoteICE = pbPeerState.GetRemoteIceCandidateType()
|
||||||
|
connType = "P2P"
|
||||||
|
if pbPeerState.Relayed {
|
||||||
|
connType = "Relayed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
||||||
|
peerState := peerStateDetailOutput{
|
||||||
|
IP: pbPeerState.GetIP(),
|
||||||
|
PubKey: pbPeerState.GetPubKey(),
|
||||||
|
Status: pbPeerState.GetConnStatus(),
|
||||||
|
LastStatusUpdate: timeLocal.UTC(),
|
||||||
|
ConnType: connType,
|
||||||
|
Direct: pbPeerState.GetDirect(),
|
||||||
|
IceCandidateType: iceCandidateType{
|
||||||
|
Local: localICE,
|
||||||
|
Remote: remoteICE,
|
||||||
|
},
|
||||||
|
FQDN: pbPeerState.GetFqdn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
peersStateDetail = append(peersStateDetail, peerState)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortPeersByIP(peersStateDetail)
|
||||||
|
|
||||||
|
peersOverview := peersStateOutput{
|
||||||
|
Total: len(peersStateDetail),
|
||||||
|
Connected: peersConnected,
|
||||||
|
Details: peersStateDetail,
|
||||||
|
}
|
||||||
|
return peersOverview
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortPeersByIP(peersStateDetail []peerStateDetailOutput) {
|
||||||
|
if len(peersStateDetail) > 0 {
|
||||||
|
sort.SliceStable(peersStateDetail, func(i, j int) bool {
|
||||||
|
iAddr, _ := netip.ParseAddr(peersStateDetail[i].IP)
|
||||||
|
jAddr, _ := netip.ParseAddr(peersStateDetail[j].IP)
|
||||||
|
return iAddr.Compare(jAddr) == -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInterfaceIP(interfaceIP string) string {
|
||||||
ip, _, err := net.ParseCIDR(interfaceIP)
|
ip, _, err := net.ParseCIDR(interfaceIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
return fmt.Sprintf("%s\n", ip)
|
||||||
|
}
|
||||||
|
|
||||||
if ipv4Flag {
|
func parseToJSON(overview statusOutputOverview) (string, error) {
|
||||||
return fmt.Sprintf("%s\n", ip)
|
jsonBytes, err := json.Marshal(overview)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("json marshal failed")
|
||||||
}
|
}
|
||||||
|
return string(jsonBytes), err
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
func parseToYAML(overview statusOutputOverview) (string, error) {
|
||||||
managementStatusURL = ""
|
yamlBytes, err := yaml.Marshal(overview)
|
||||||
signalStatusURL = ""
|
if err != nil {
|
||||||
managementConnString = "Disconnected"
|
return "", fmt.Errorf("yaml marshal failed")
|
||||||
signalConnString = "Disconnected"
|
|
||||||
interfaceTypeString = "Userspace"
|
|
||||||
)
|
|
||||||
|
|
||||||
if printDetail {
|
|
||||||
managementStatusURL = fmt.Sprintf(" to %s", fullStatus.ManagementState.URL)
|
|
||||||
signalStatusURL = fmt.Sprintf(" to %s", fullStatus.SignalState.URL)
|
|
||||||
}
|
}
|
||||||
|
return string(yamlBytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
if fullStatus.ManagementState.Connected {
|
func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
|
||||||
|
|
||||||
|
managementConnString := "Disconnected"
|
||||||
|
if overview.ManagementState.Connected {
|
||||||
managementConnString = "Connected"
|
managementConnString = "Connected"
|
||||||
|
if showURL {
|
||||||
|
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fullStatus.SignalState.Connected {
|
signalConnString := "Disconnected"
|
||||||
|
if overview.SignalState.Connected {
|
||||||
signalConnString = "Connected"
|
signalConnString = "Connected"
|
||||||
|
if showURL {
|
||||||
|
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fullStatus.LocalPeerState.KernelInterface {
|
interfaceTypeString := "Userspace"
|
||||||
|
interfaceIP := overview.IP
|
||||||
|
if overview.KernelInterface {
|
||||||
interfaceTypeString = "Kernel"
|
interfaceTypeString = "Kernel"
|
||||||
} else if fullStatus.LocalPeerState.IP == "" {
|
} else if overview.IP == "" {
|
||||||
interfaceTypeString = "N/A"
|
interfaceTypeString = "N/A"
|
||||||
interfaceIP = "N/A"
|
interfaceIP = "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedPeersString, peersConnected := parsePeers(fullStatus.Peers, printDetail)
|
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
|
||||||
|
|
||||||
peersCountString := fmt.Sprintf("%d/%d Connected", peersConnected, len(fullStatus.Peers))
|
|
||||||
|
|
||||||
summary := fmt.Sprintf(
|
summary := fmt.Sprintf(
|
||||||
"Daemon version: %s\n"+
|
"Daemon version: %s\n"+
|
||||||
"CLI version: %s\n"+
|
"CLI version: %s\n"+
|
||||||
"%s"+ // daemon status
|
"Management: %s\n"+
|
||||||
"Management: %s%s\n"+
|
"Signal: %s\n"+
|
||||||
"Signal: %s%s\n"+
|
"FQDN: %s\n"+
|
||||||
"Domain: %s\n"+
|
|
||||||
"NetBird IP: %s\n"+
|
"NetBird IP: %s\n"+
|
||||||
"Interface type: %s\n"+
|
"Interface type: %s\n"+
|
||||||
"Peers count: %s\n",
|
"Peers count: %s\n",
|
||||||
daemonVersion,
|
overview.DaemonVersion,
|
||||||
system.NetbirdVersion(),
|
system.NetbirdVersion(),
|
||||||
daemonStatus,
|
|
||||||
managementConnString,
|
managementConnString,
|
||||||
managementStatusURL,
|
|
||||||
signalConnString,
|
signalConnString,
|
||||||
signalStatusURL,
|
overview.FQDN,
|
||||||
fullStatus.LocalPeerState.FQDN,
|
|
||||||
interfaceIP,
|
interfaceIP,
|
||||||
interfaceTypeString,
|
interfaceTypeString,
|
||||||
peersCountString,
|
peersCountString,
|
||||||
)
|
)
|
||||||
|
|
||||||
if printDetail {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"Peers detail:"+
|
|
||||||
"%s\n"+
|
|
||||||
"%s",
|
|
||||||
parsedPeersString,
|
|
||||||
summary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePeers(peers []nbStatus.PeerState, printDetail bool) (string, int) {
|
func parseToFullDetailSummary(overview statusOutputOverview) string {
|
||||||
var (
|
parsedPeersString := parsePeers(overview.Peers)
|
||||||
peersString = ""
|
summary := parseGeneralSummary(overview, true)
|
||||||
peersConnected = 0
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Peers detail:"+
|
||||||
|
"%s\n"+
|
||||||
|
"%s",
|
||||||
|
parsedPeersString,
|
||||||
|
summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(peers) > 0 {
|
|
||||||
sort.SliceStable(peers, func(i, j int) bool {
|
|
||||||
iAddr, _ := netip.ParseAddr(peers[i].IP)
|
|
||||||
jAddr, _ := netip.ParseAddr(peers[j].IP)
|
|
||||||
return iAddr.Compare(jAddr) == -1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedStatusString := peer.StatusConnected.String()
|
|
||||||
|
|
||||||
for _, peerState := range peers {
|
|
||||||
peerConnectionStatus := false
|
|
||||||
if peerState.ConnStatus == connectedStatusString {
|
|
||||||
peersConnected = peersConnected + 1
|
|
||||||
peerConnectionStatus = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if printDetail {
|
|
||||||
|
|
||||||
if skipDetailByFilters(peerState, peerConnectionStatus) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
localICE := "-"
|
|
||||||
remoteICE := "-"
|
|
||||||
connType := "-"
|
|
||||||
|
|
||||||
if peerConnectionStatus {
|
|
||||||
localICE = peerState.LocalIceCandidateType
|
|
||||||
remoteICE = peerState.RemoteIceCandidateType
|
|
||||||
connType = "P2P"
|
|
||||||
if peerState.Relayed {
|
|
||||||
connType = "Relayed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peerString := fmt.Sprintf(
|
|
||||||
"\n %s:\n"+
|
|
||||||
" NetBird IP: %s\n"+
|
|
||||||
" Public key: %s\n"+
|
|
||||||
" Status: %s\n"+
|
|
||||||
" -- detail --\n"+
|
|
||||||
" Connection type: %s\n"+
|
|
||||||
" Direct: %t\n"+
|
|
||||||
" ICE candidate (Local/Remote): %s/%s\n"+
|
|
||||||
" Last connection update: %s\n",
|
|
||||||
peerState.FQDN,
|
|
||||||
peerState.IP,
|
|
||||||
peerState.PubKey,
|
|
||||||
peerState.ConnStatus,
|
|
||||||
connType,
|
|
||||||
peerState.Direct,
|
|
||||||
localICE,
|
|
||||||
remoteICE,
|
|
||||||
peerState.ConnStatusUpdate.Format("2006-01-02 15:04:05"),
|
|
||||||
)
|
|
||||||
|
|
||||||
peersString = peersString + peerString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return peersString, peersConnected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipDetailByFilters(peerState nbStatus.PeerState, isConnected bool) bool {
|
func parsePeers(peers peersStateOutput) string {
|
||||||
|
var (
|
||||||
|
peersString = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, peerState := range peers.Details {
|
||||||
|
|
||||||
|
localICE := "-"
|
||||||
|
if peerState.IceCandidateType.Local != "" {
|
||||||
|
localICE = peerState.IceCandidateType.Local
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteICE := "-"
|
||||||
|
if peerState.IceCandidateType.Remote != "" {
|
||||||
|
remoteICE = peerState.IceCandidateType.Remote
|
||||||
|
}
|
||||||
|
|
||||||
|
peerString := fmt.Sprintf(
|
||||||
|
"\n %s:\n"+
|
||||||
|
" NetBird IP: %s\n"+
|
||||||
|
" Public key: %s\n"+
|
||||||
|
" Status: %s\n"+
|
||||||
|
" -- detail --\n"+
|
||||||
|
" Connection type: %s\n"+
|
||||||
|
" Direct: %t\n"+
|
||||||
|
" ICE candidate (Local/Remote): %s/%s\n"+
|
||||||
|
" Last connection update: %s\n",
|
||||||
|
peerState.FQDN,
|
||||||
|
peerState.IP,
|
||||||
|
peerState.PubKey,
|
||||||
|
peerState.Status,
|
||||||
|
peerState.ConnType,
|
||||||
|
peerState.Direct,
|
||||||
|
localICE,
|
||||||
|
remoteICE,
|
||||||
|
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"),
|
||||||
|
)
|
||||||
|
|
||||||
|
peersString = peersString + peerString
|
||||||
|
}
|
||||||
|
return peersString
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
||||||
statusEval := false
|
statusEval := false
|
||||||
ipEval := false
|
ipEval := false
|
||||||
|
|
||||||
|
301
client/cmd/status_test.go
Normal file
301
client/cmd/status_test.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resp = &proto.StatusResponse{
|
||||||
|
Status: "Connected",
|
||||||
|
FullStatus: &proto.FullStatus{
|
||||||
|
Peers: []*proto.PeerState{
|
||||||
|
{
|
||||||
|
IP: "192.168.178.101",
|
||||||
|
PubKey: "Pubkey1",
|
||||||
|
Fqdn: "peer-1.awesome-domain.com",
|
||||||
|
ConnStatus: "Connected",
|
||||||
|
ConnStatusUpdate: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 1, 0, time.UTC)),
|
||||||
|
Relayed: false,
|
||||||
|
Direct: true,
|
||||||
|
LocalIceCandidateType: "",
|
||||||
|
RemoteIceCandidateType: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.102",
|
||||||
|
PubKey: "Pubkey2",
|
||||||
|
Fqdn: "peer-2.awesome-domain.com",
|
||||||
|
ConnStatus: "Connected",
|
||||||
|
ConnStatusUpdate: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 2, 0, time.UTC)),
|
||||||
|
Relayed: true,
|
||||||
|
Direct: false,
|
||||||
|
LocalIceCandidateType: "relay",
|
||||||
|
RemoteIceCandidateType: "prflx",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ManagementState: &proto.ManagementState{
|
||||||
|
URL: "my-awesome-management.com:443",
|
||||||
|
Connected: true,
|
||||||
|
},
|
||||||
|
SignalState: &proto.SignalState{
|
||||||
|
URL: "my-awesome-signal.com:443",
|
||||||
|
Connected: true,
|
||||||
|
},
|
||||||
|
LocalPeerState: &proto.LocalPeerState{
|
||||||
|
IP: "192.168.178.100/16",
|
||||||
|
PubKey: "Some-Pub-Key",
|
||||||
|
KernelInterface: true,
|
||||||
|
Fqdn: "some-localhost.awesome-domain.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DaemonVersion: "0.14.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
var overview = statusOutputOverview{
|
||||||
|
Peers: peersStateOutput{
|
||||||
|
Total: 2,
|
||||||
|
Connected: 2,
|
||||||
|
Details: []peerStateDetailOutput{
|
||||||
|
{
|
||||||
|
IP: "192.168.178.101",
|
||||||
|
PubKey: "Pubkey1",
|
||||||
|
FQDN: "peer-1.awesome-domain.com",
|
||||||
|
Status: "Connected",
|
||||||
|
LastStatusUpdate: time.Date(2001, 1, 1, 1, 1, 1, 0, time.UTC),
|
||||||
|
ConnType: "P2P",
|
||||||
|
Direct: true,
|
||||||
|
IceCandidateType: iceCandidateType{
|
||||||
|
Local: "",
|
||||||
|
Remote: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.102",
|
||||||
|
PubKey: "Pubkey2",
|
||||||
|
FQDN: "peer-2.awesome-domain.com",
|
||||||
|
Status: "Connected",
|
||||||
|
LastStatusUpdate: time.Date(2002, 2, 2, 2, 2, 2, 0, time.UTC),
|
||||||
|
ConnType: "Relayed",
|
||||||
|
Direct: false,
|
||||||
|
IceCandidateType: iceCandidateType{
|
||||||
|
Local: "relay",
|
||||||
|
Remote: "prflx",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CliVersion: system.NetbirdVersion(),
|
||||||
|
DaemonVersion: "0.14.1",
|
||||||
|
ManagementState: managementStateOutput{
|
||||||
|
URL: "my-awesome-management.com:443",
|
||||||
|
Connected: true,
|
||||||
|
},
|
||||||
|
SignalState: signalStateOutput{
|
||||||
|
URL: "my-awesome-signal.com:443",
|
||||||
|
Connected: true,
|
||||||
|
},
|
||||||
|
IP: "192.168.178.100/16",
|
||||||
|
PubKey: "Some-Pub-Key",
|
||||||
|
KernelInterface: true,
|
||||||
|
FQDN: "some-localhost.awesome-domain.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
|
||||||
|
convertedResult := convertToStatusOutputOverview(resp)
|
||||||
|
|
||||||
|
assert.Equal(t, overview, convertedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSortingOfPeers(t *testing.T) {
|
||||||
|
peers := []peerStateDetailOutput{
|
||||||
|
{
|
||||||
|
IP: "192.168.178.104",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.102",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.101",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.105",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "192.168.178.103",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sortPeersByIP(peers)
|
||||||
|
|
||||||
|
assert.Equal(t, peers[3].IP, "192.168.178.104")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingToJSON(t *testing.T) {
|
||||||
|
json, _ := parseToJSON(overview)
|
||||||
|
|
||||||
|
//@formatter:off
|
||||||
|
expectedJSON := "{\"" +
|
||||||
|
"peers\":" +
|
||||||
|
"{" +
|
||||||
|
"\"total\":2," +
|
||||||
|
"\"connected\":2," +
|
||||||
|
"\"details\":" +
|
||||||
|
"[" +
|
||||||
|
"{" +
|
||||||
|
"\"fqdn\":\"peer-1.awesome-domain.com\"," +
|
||||||
|
"\"netbirdIp\":\"192.168.178.101\"," +
|
||||||
|
"\"publicKey\":\"Pubkey1\"," +
|
||||||
|
"\"status\":\"Connected\"," +
|
||||||
|
"\"lastStatusUpdate\":\"2001-01-01T01:01:01Z\"," +
|
||||||
|
"\"connectionType\":\"P2P\"," +
|
||||||
|
"\"direct\":true," +
|
||||||
|
"\"iceCandidateType\":" +
|
||||||
|
"{" +
|
||||||
|
"\"local\":\"\"," +
|
||||||
|
"\"remote\":\"\"" +
|
||||||
|
"}" +
|
||||||
|
"}," +
|
||||||
|
"{" +
|
||||||
|
"\"fqdn\":\"peer-2.awesome-domain.com\"," +
|
||||||
|
"\"netbirdIp\":\"192.168.178.102\"," +
|
||||||
|
"\"publicKey\":\"Pubkey2\"," +
|
||||||
|
"\"status\":\"Connected\"," +
|
||||||
|
"\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," +
|
||||||
|
"\"connectionType\":\"Relayed\"," +
|
||||||
|
"\"direct\":false," +
|
||||||
|
"\"iceCandidateType\":" +
|
||||||
|
"{" +
|
||||||
|
"\"local\":\"relay\"," +
|
||||||
|
"\"remote\":\"prflx\"" +
|
||||||
|
"}" +
|
||||||
|
"}" +
|
||||||
|
"]" +
|
||||||
|
"}," +
|
||||||
|
"\"cliVersion\":\"development\"," +
|
||||||
|
"\"daemonVersion\":\"0.14.1\"," +
|
||||||
|
"\"management\":" +
|
||||||
|
"{" +
|
||||||
|
"\"url\":\"my-awesome-management.com:443\"," +
|
||||||
|
"\"connected\":true" +
|
||||||
|
"}," +
|
||||||
|
"\"signal\":" +
|
||||||
|
"{\"" +
|
||||||
|
"url\":\"my-awesome-signal.com:443\"," +
|
||||||
|
"\"connected\":true" +
|
||||||
|
"}," +
|
||||||
|
"\"netbirdIp\":\"192.168.178.100/16\"," +
|
||||||
|
"\"publicKey\":\"Some-Pub-Key\"," +
|
||||||
|
"\"usesKernelInterface\":true," +
|
||||||
|
"\"fqdn\":\"some-localhost.awesome-domain.com\"" +
|
||||||
|
"}"
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
assert.Equal(t, expectedJSON, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingToYAML(t *testing.T) {
|
||||||
|
yaml, _ := parseToYAML(overview)
|
||||||
|
|
||||||
|
expectedYAML := "peers:\n" +
|
||||||
|
" total: 2\n" +
|
||||||
|
" connected: 2\n" +
|
||||||
|
" details:\n" +
|
||||||
|
" - fqdn: peer-1.awesome-domain.com\n" +
|
||||||
|
" netbirdIp: 192.168.178.101\n" +
|
||||||
|
" publicKey: Pubkey1\n" +
|
||||||
|
" status: Connected\n" +
|
||||||
|
" lastStatusUpdate: 2001-01-01T01:01:01Z\n" +
|
||||||
|
" connectionType: P2P\n" +
|
||||||
|
" direct: true\n" +
|
||||||
|
" iceCandidateType:\n" +
|
||||||
|
" local: \"\"\n" +
|
||||||
|
" remote: \"\"\n" +
|
||||||
|
" - fqdn: peer-2.awesome-domain.com\n" +
|
||||||
|
" netbirdIp: 192.168.178.102\n" +
|
||||||
|
" publicKey: Pubkey2\n" +
|
||||||
|
" status: Connected\n" +
|
||||||
|
" lastStatusUpdate: 2002-02-02T02:02:02Z\n" +
|
||||||
|
" connectionType: Relayed\n" +
|
||||||
|
" direct: false\n" +
|
||||||
|
" iceCandidateType:\n" +
|
||||||
|
" local: relay\n" +
|
||||||
|
" remote: prflx\n" +
|
||||||
|
"cliVersion: development\n" +
|
||||||
|
"daemonVersion: 0.14.1\n" +
|
||||||
|
"management:\n" +
|
||||||
|
" url: my-awesome-management.com:443\n" +
|
||||||
|
" connected: true\n" +
|
||||||
|
"signal:\n" +
|
||||||
|
" url: my-awesome-signal.com:443\n" +
|
||||||
|
" connected: true\n" +
|
||||||
|
"netbirdIp: 192.168.178.100/16\n" +
|
||||||
|
"publicKey: Some-Pub-Key\n" +
|
||||||
|
"usesKernelInterface: true\n" +
|
||||||
|
"fqdn: some-localhost.awesome-domain.com\n"
|
||||||
|
|
||||||
|
assert.Equal(t, expectedYAML, yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingToDetail(t *testing.T) {
|
||||||
|
detail := parseToFullDetailSummary(overview)
|
||||||
|
|
||||||
|
expectedDetail := "Peers detail:\n" +
|
||||||
|
" peer-1.awesome-domain.com:\n" +
|
||||||
|
" NetBird IP: 192.168.178.101\n" +
|
||||||
|
" Public key: Pubkey1\n" +
|
||||||
|
" Status: Connected\n" +
|
||||||
|
" -- detail --\n" +
|
||||||
|
" Connection type: P2P\n" +
|
||||||
|
" Direct: true\n" +
|
||||||
|
" ICE candidate (Local/Remote): -/-\n" +
|
||||||
|
" Last connection update: 2001-01-01 01:01:01\n" +
|
||||||
|
"\n" +
|
||||||
|
" peer-2.awesome-domain.com:\n" +
|
||||||
|
" NetBird IP: 192.168.178.102\n" +
|
||||||
|
" Public key: Pubkey2\n" +
|
||||||
|
" Status: Connected\n" +
|
||||||
|
" -- detail --\n" +
|
||||||
|
" Connection type: Relayed\n" +
|
||||||
|
" Direct: false\n" +
|
||||||
|
" ICE candidate (Local/Remote): relay/prflx\n" +
|
||||||
|
" Last connection update: 2002-02-02 02:02:02\n" +
|
||||||
|
"\n" +
|
||||||
|
"Daemon version: 0.14.1\n" +
|
||||||
|
"CLI version: development\n" +
|
||||||
|
"Management: Connected to my-awesome-management.com:443\n" +
|
||||||
|
"Signal: Connected to my-awesome-signal.com:443\n" +
|
||||||
|
"FQDN: some-localhost.awesome-domain.com\n" +
|
||||||
|
"NetBird IP: 192.168.178.100/16\n" +
|
||||||
|
"Interface type: Kernel\n" +
|
||||||
|
"Peers count: 2/2 Connected\n"
|
||||||
|
|
||||||
|
assert.Equal(t, expectedDetail, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingToShortVersion(t *testing.T) {
|
||||||
|
shortVersion := parseGeneralSummary(overview, false)
|
||||||
|
|
||||||
|
expectedString := "Daemon version: 0.14.1\n" +
|
||||||
|
"CLI version: development\n" +
|
||||||
|
"Management: Connected\n" +
|
||||||
|
"Signal: Connected\n" +
|
||||||
|
"FQDN: some-localhost.awesome-domain.com\n" +
|
||||||
|
"NetBird IP: 192.168.178.100/16\n" +
|
||||||
|
"Interface type: Kernel\n" +
|
||||||
|
"Peers count: 2/2 Connected\n"
|
||||||
|
|
||||||
|
assert.Equal(t, expectedString, shortVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingOfIP(t *testing.T) {
|
||||||
|
InterfaceIP := "192.168.178.123/16"
|
||||||
|
|
||||||
|
parsedIP := parseInterfaceIP(InterfaceIP)
|
||||||
|
|
||||||
|
assert.Equal(t, "192.168.178.123\n", parsedIP)
|
||||||
|
}
|
2
go.mod
2
go.mod
@ -53,6 +53,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v0.33.0
|
go.opentelemetry.io/otel/sdk/metric v0.33.0
|
||||||
golang.org/x/net v0.7.0
|
golang.org/x/net v0.7.0
|
||||||
golang.org/x/term v0.5.0
|
golang.org/x/term v0.5.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -124,7 +125,6 @@ require (
|
|||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
honnef.co/go/tools v0.2.2 // indirect
|
honnef.co/go/tools v0.2.2 // indirect
|
||||||
k8s.io/apimachinery v0.23.5 // indirect
|
k8s.io/apimachinery v0.23.5 // indirect
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user