linkerd2/multicluster/cmd/gateways.go

164 lines
3.9 KiB
Go

package cmd
import (
"context"
"fmt"
"io"
"github.com/linkerd/linkerd2/cli/table"
"github.com/linkerd/linkerd2/controller/api/public"
pb "github.com/linkerd/linkerd2/controller/gen/public"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
)
type (
gatewaysOptions struct {
gatewayNamespace string
clusterName string
timeWindow string
}
)
func newGatewaysCommand() *cobra.Command {
opts := gatewaysOptions{}
cmd := &cobra.Command{
Use: "gateways",
Short: "Display stats information about the gateways in target clusters",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
req := &pb.GatewaysRequest{
RemoteClusterName: opts.clusterName,
GatewayNamespace: opts.gatewayNamespace,
TimeWindow: opts.timeWindow,
}
k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
if err != nil {
return err
}
client, err := public.NewExternalClient(cmd.Context(), controlPlaneNamespace, k8sAPI)
if err != nil {
return err
}
resp, err := requestGatewaysFromAPI(client, req)
if err != nil {
return err
}
renderGateways(resp.GetOk().GatewaysTable.Rows, stdout)
return nil
},
}
cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "the name of the target cluster")
cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "", "the namespace in which the gateway resides on the target cluster")
cmd.Flags().StringVarP(&opts.timeWindow, "time-window", "t", "1m", "Time window (for example: \"15s\", \"1m\", \"10m\", \"1h\"). Needs to be at least 15s.")
return cmd
}
func requestGatewaysFromAPI(client pb.ApiClient, req *pb.GatewaysRequest) (*pb.GatewaysResponse, error) {
resp, err := client.Gateways(context.Background(), req)
if err != nil {
return nil, fmt.Errorf("Gateways API error: %v", err)
}
if e := resp.GetError(); e != nil {
return nil, fmt.Errorf("Gateways API response error: %v", e.Error)
}
return resp, nil
}
func renderGateways(rows []*pb.GatewaysTable_Row, w io.Writer) {
t := buildGatewaysTable()
t.Data = []table.Row{}
for _, row := range rows {
row := row // Copy to satisfy golint.
t.Data = append(t.Data, gatewaysRowToTableRow(row))
}
t.Render(w)
}
var (
clusterNameHeader = "CLUSTER"
aliveHeader = "ALIVE"
pairedServicesHeader = "NUM_SVC"
latencyP50Header = "LATENCY_P50"
latencyP95Header = "LATENCY_P95"
latencyP99Header = "LATENCY_P99"
)
func buildGatewaysTable() table.Table {
columns := []table.Column{
table.Column{
Header: clusterNameHeader,
Width: 7,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: aliveHeader,
Width: 5,
Flexible: true,
LeftAlign: true,
},
table.Column{
Header: pairedServicesHeader,
Width: 9,
},
table.Column{
Header: latencyP50Header,
Width: 11,
},
table.Column{
Header: latencyP95Header,
Width: 11,
},
table.Column{
Header: latencyP99Header,
Width: 11,
},
}
t := table.NewTable(columns, []table.Row{})
t.Sort = []int{0, 1} // Sort by namespace, then name.
return t
}
func gatewaysRowToTableRow(row *pb.GatewaysTable_Row) []string {
valueOrPlaceholder := func(value string) string {
if row.Alive {
return value
}
return "-"
}
alive := "False"
if row.Alive {
alive = "True"
}
return []string{
row.ClusterName,
alive,
fmt.Sprint(row.PairedServices),
valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP50)),
valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP95)),
valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP99)),
}
}
func extractGatewayPort(gateway *corev1.Service) (uint32, error) {
for _, port := range gateway.Spec.Ports {
if port.Name == k8s.GatewayPortName {
return uint32(port.Port), nil
}
}
return 0, fmt.Errorf("gateway service %s has no gateway port named %s", gateway.Name, k8s.GatewayPortName)
}