mirror of https://github.com/linkerd/linkerd2.git
250 lines
6.7 KiB
Go
250 lines
6.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/linkerd/linkerd2/controller/api/public"
|
|
pb "github.com/linkerd/linkerd2/controller/gen/controller/discovery"
|
|
"github.com/linkerd/linkerd2/pkg/addr"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type endpointsOptions struct {
|
|
namespace string
|
|
outputFormat string
|
|
}
|
|
|
|
const (
|
|
podHeader = "POD"
|
|
)
|
|
|
|
// validate performs all validation on the command-line options.
|
|
// It returns the first error encountered, or `nil` if the options are valid.
|
|
func (o *endpointsOptions) validate() error {
|
|
if o.outputFormat == tableOutput || o.outputFormat == jsonOutput {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("--output currently only supports %s and %s", tableOutput, jsonOutput)
|
|
}
|
|
|
|
func newEndpointsOptions() *endpointsOptions {
|
|
return &endpointsOptions{
|
|
namespace: "",
|
|
outputFormat: tableOutput,
|
|
}
|
|
}
|
|
|
|
func newCmdEndpoints() *cobra.Command {
|
|
options := newEndpointsOptions()
|
|
|
|
example := ` # get all endpoints
|
|
linkerd endpoints
|
|
|
|
# get endpoints in the emojivoto namespace
|
|
linkerd endpoints -n emojivoto
|
|
|
|
# get all endpoints in json
|
|
linkerd endpoints -o json`
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "endpoints [flags]",
|
|
Aliases: []string{"ep"},
|
|
Short: "Introspect Linkerd's service discovery state",
|
|
Long: `Introspect Linkerd's service discovery state.
|
|
|
|
This command provides debug information about the internal state of the
|
|
control-plane's destination container. Note that this cache of service discovery
|
|
information is populated on-demand via linkerd-proxy requests. This command
|
|
will return "No endpoints found." until a linkerd-proxy begins routing
|
|
requests.`,
|
|
Example: example,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := options.validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
endpoints, err := requestEndpointsFromAPI(checkPublicAPIClientOrExit())
|
|
if err != nil {
|
|
return fmt.Errorf("Endpoints API error: %s", err)
|
|
}
|
|
|
|
output := renderEndpoints(endpoints, options)
|
|
_, err = fmt.Print(output)
|
|
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified endpoints (default: all namespaces)")
|
|
cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\" or \"%s\"", tableOutput, jsonOutput))
|
|
|
|
return cmd
|
|
}
|
|
|
|
func requestEndpointsFromAPI(client public.APIClient) (*pb.EndpointsResponse, error) {
|
|
return client.Endpoints(context.Background(), &pb.EndpointsParams{})
|
|
}
|
|
|
|
func renderEndpoints(endpoints *pb.EndpointsResponse, options *endpointsOptions) string {
|
|
var buffer bytes.Buffer
|
|
w := tabwriter.NewWriter(&buffer, 0, 0, padding, ' ', 0)
|
|
writeEndpointsToBuffer(endpoints, w, options)
|
|
w.Flush()
|
|
|
|
return buffer.String()
|
|
}
|
|
|
|
type rowEndpoint struct {
|
|
Namespace string `json:"namespace"`
|
|
IP string `json:"ip"`
|
|
Port uint32 `json:"port"`
|
|
Pod string `json:"pod"`
|
|
Version string `json:"version"`
|
|
Service string `json:"service"`
|
|
}
|
|
|
|
func writeEndpointsToBuffer(endpoints *pb.EndpointsResponse, w *tabwriter.Writer, options *endpointsOptions) {
|
|
maxPodLength := len(podHeader)
|
|
maxNamespaceLength := len(namespaceHeader)
|
|
endpointsTables := map[string][]rowEndpoint{}
|
|
|
|
for serviceID, servicePort := range endpoints.GetServicePorts() {
|
|
namespace := ""
|
|
parts := strings.SplitN(serviceID, ".", 2)
|
|
if len(parts) == 2 {
|
|
namespace = parts[1]
|
|
}
|
|
|
|
if options.namespace != "" && options.namespace != namespace {
|
|
continue
|
|
}
|
|
|
|
for port, podAddrs := range servicePort.GetPortEndpoints() {
|
|
for _, podAddr := range podAddrs.GetPodAddresses() {
|
|
pod := podAddr.GetPod()
|
|
name := pod.GetName()
|
|
parts := strings.SplitN(name, "/", 2)
|
|
if len(parts) == 2 {
|
|
name = parts[1]
|
|
}
|
|
row := rowEndpoint{
|
|
Namespace: namespace,
|
|
IP: addr.PublicIPToString(podAddr.GetAddr().GetIp()),
|
|
Port: port,
|
|
Pod: name,
|
|
Version: pod.GetResourceVersion(),
|
|
Service: serviceID,
|
|
}
|
|
|
|
endpointsTables[namespace] = append(endpointsTables[namespace], row)
|
|
|
|
if len(name) > maxPodLength {
|
|
maxPodLength = len(name)
|
|
}
|
|
if len(namespace) > maxNamespaceLength {
|
|
maxNamespaceLength = len(namespace)
|
|
}
|
|
}
|
|
|
|
sort.Slice(endpointsTables[namespace], func(i, j int) bool {
|
|
return endpointsTables[namespace][i].Service < endpointsTables[namespace][j].Service
|
|
})
|
|
}
|
|
}
|
|
|
|
switch options.outputFormat {
|
|
case tableOutput:
|
|
if len(endpointsTables) == 0 {
|
|
fmt.Fprintln(os.Stderr, "No endpoints found.")
|
|
os.Exit(0)
|
|
}
|
|
printEndpointsTables(endpointsTables, w, options, maxPodLength, maxNamespaceLength)
|
|
case jsonOutput:
|
|
printEndpointsJSON(endpointsTables, w)
|
|
}
|
|
}
|
|
|
|
func printEndpointsTables(endpointsTables map[string][]rowEndpoint, w *tabwriter.Writer, options *endpointsOptions, maxPodLength int, maxNamespaceLength int) {
|
|
firstTable := true // don't print a newline before the first table
|
|
|
|
for _, ns := range sortNamespaceKeys(endpointsTables) {
|
|
if !firstTable {
|
|
fmt.Fprint(w, "\n")
|
|
}
|
|
firstTable = false
|
|
printEndpointsTable(ns, endpointsTables[ns], w, options, maxPodLength, maxNamespaceLength)
|
|
}
|
|
}
|
|
|
|
func printEndpointsTable(namespace string, rows []rowEndpoint, w *tabwriter.Writer, options *endpointsOptions, maxPodLength int, maxNamespaceLength int) {
|
|
headers := make([]string, 0)
|
|
templateString := "%s\t%d\t%s\t%s\t%s\n"
|
|
|
|
if options.namespace == "" {
|
|
headers = append(headers, namespaceHeader+strings.Repeat(" ", maxNamespaceLength-len(namespaceHeader)))
|
|
templateString = "%s\t" + templateString
|
|
}
|
|
|
|
headers = append(headers, []string{
|
|
"IP",
|
|
"PORT",
|
|
podHeader + strings.Repeat(" ", maxPodLength-len(podHeader)),
|
|
"VERSION",
|
|
"SERVICE",
|
|
}...)
|
|
fmt.Fprintln(w, strings.Join(headers, "\t"))
|
|
|
|
for _, row := range rows {
|
|
values := make([]interface{}, 0)
|
|
if options.namespace == "" {
|
|
values = append(values,
|
|
namespace+strings.Repeat(" ", maxNamespaceLength-len(namespace)))
|
|
}
|
|
|
|
values = append(values, []interface{}{
|
|
row.IP,
|
|
row.Port,
|
|
row.Pod,
|
|
row.Version,
|
|
row.Service,
|
|
}...)
|
|
|
|
fmt.Fprintf(w, templateString, values...)
|
|
}
|
|
}
|
|
|
|
func printEndpointsJSON(endpointsTables map[string][]rowEndpoint, w *tabwriter.Writer) {
|
|
entries := []rowEndpoint{}
|
|
|
|
for _, ns := range sortNamespaceKeys(endpointsTables) {
|
|
entries = append(entries, endpointsTables[ns]...)
|
|
}
|
|
|
|
b, err := json.MarshalIndent(entries, "", " ")
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "%s\n", b)
|
|
}
|
|
|
|
func sortNamespaceKeys(endpointsTables map[string][]rowEndpoint) []string {
|
|
var sortedKeys []string
|
|
for key := range endpointsTables {
|
|
sortedKeys = append(sortedKeys, key)
|
|
}
|
|
sort.Strings(sortedKeys)
|
|
return sortedKeys
|
|
}
|