mirror of https://github.com/linkerd/linkerd2.git
314 lines
8.1 KiB
Go
314 lines
8.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"text/tabwriter"
|
|
|
|
destinationPb "github.com/linkerd/linkerd2-proxy-api/go/destination"
|
|
netPb "github.com/linkerd/linkerd2-proxy-api/go/net"
|
|
"github.com/linkerd/linkerd2/controller/api/public"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type endpointsOptions struct {
|
|
outputFormat string
|
|
}
|
|
|
|
type (
|
|
// map[ServiceID]map[Port][]podData
|
|
endpointsInfo map[string]map[uint32][]podData
|
|
podData struct {
|
|
name string
|
|
address string
|
|
ip 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{
|
|
outputFormat: tableOutput,
|
|
}
|
|
}
|
|
|
|
func newCmdEndpoints() *cobra.Command {
|
|
options := newEndpointsOptions()
|
|
|
|
example := ` # get all endpoints for the authorities emoji-svc.emojivoto.svc.cluster.local:8080 and web-svc.emojivoto.svc.cluster.local:80
|
|
linkerd endpoints emoji-svc.emojivoto.svc.cluster.local:8080 web-svc.emojivoto.svc.cluster.local:80
|
|
|
|
# get that same information in json format
|
|
linkerd endpoints -o json emoji-svc.emojivoto.svc.cluster.local:8080 web-svc.emojivoto.svc.cluster.local:80
|
|
|
|
# get the endpoints for authorities in Linkerd's control-plane itself
|
|
linkerd endpoints linkerd-controller-api.linkerd.svc.cluster.local:8085
|
|
linkerd endpoints linkerd-web.linkerd.svc.cluster.local:8084`
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "endpoints [flags] authorities",
|
|
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. It queries the same Destination service
|
|
endpoint as the linkerd-proxy's, and returns the addresses associated with that
|
|
destination.`,
|
|
Example: example,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := options.validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
endpoints, err := requestEndpointsFromAPI(checkPublicAPIClientOrExit(), args)
|
|
if err != nil {
|
|
return fmt.Errorf("Destination API error: %s", err)
|
|
}
|
|
|
|
output := renderEndpoints(endpoints, options)
|
|
_, err = fmt.Print(output)
|
|
|
|
return err
|
|
},
|
|
}
|
|
|
|
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, authorities []string) (endpointsInfo, error) {
|
|
info := make(endpointsInfo)
|
|
// buffered channels to avoid blocking
|
|
events := make(chan *destinationPb.Update, len(authorities))
|
|
errors := make(chan error, len(authorities))
|
|
var wg sync.WaitGroup
|
|
|
|
for _, authority := range authorities {
|
|
wg.Add(1)
|
|
go func(authority string) {
|
|
defer wg.Done()
|
|
if len(errors) == 0 {
|
|
dest := &destinationPb.GetDestination{
|
|
Scheme: "http:",
|
|
Path: authority,
|
|
}
|
|
|
|
rsp, err := client.Get(context.Background(), dest)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
|
|
event, err := rsp.Recv()
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
events <- event
|
|
}
|
|
}(authority)
|
|
}
|
|
// Block till all goroutines above are done
|
|
wg.Wait()
|
|
|
|
for i := 0; i < len(authorities); i++ {
|
|
select {
|
|
case err := <-errors:
|
|
// we only care about the first error
|
|
return nil, err
|
|
case event := <-events:
|
|
addressSet := event.GetAdd()
|
|
labels := addressSet.GetMetricLabels()
|
|
serviceID := labels["service"] + "." + labels["namespace"]
|
|
if _, ok := info[serviceID]; !ok {
|
|
info[serviceID] = make(map[uint32][]podData)
|
|
}
|
|
|
|
for _, addr := range addressSet.GetAddrs() {
|
|
tcpAddr := addr.GetAddr()
|
|
port := tcpAddr.GetPort()
|
|
|
|
if info[serviceID][port] == nil {
|
|
info[serviceID][port] = make([]podData, 0)
|
|
}
|
|
|
|
labels := addr.GetMetricLabels()
|
|
info[serviceID][port] = append(info[serviceID][port], podData{
|
|
name: labels["pod"],
|
|
address: tcpAddr.String(),
|
|
ip: getIP(tcpAddr),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func getIP(tcpAddr *netPb.TcpAddress) string {
|
|
ip := tcpAddr.GetIp().GetIpv4()
|
|
b := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(b, ip)
|
|
return net.IP(b).String()
|
|
}
|
|
|
|
func renderEndpoints(endpoints endpointsInfo, 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"`
|
|
Service string `json:"service"`
|
|
}
|
|
|
|
func writeEndpointsToBuffer(endpoints endpointsInfo, w *tabwriter.Writer, options *endpointsOptions) {
|
|
maxPodLength := len(podHeader)
|
|
maxNamespaceLength := len(namespaceHeader)
|
|
endpointsTables := map[string][]rowEndpoint{}
|
|
|
|
for serviceID, servicePort := range endpoints {
|
|
namespace := ""
|
|
parts := strings.SplitN(serviceID, ".", 2)
|
|
namespace = parts[1]
|
|
|
|
for port, podAddrs := range servicePort {
|
|
for _, pod := range podAddrs {
|
|
name := pod.name
|
|
parts := strings.SplitN(name, "/", 2)
|
|
if len(parts) == 2 {
|
|
name = parts[1]
|
|
}
|
|
row := rowEndpoint{
|
|
Namespace: namespace,
|
|
IP: pod.ip,
|
|
Port: port,
|
|
Pod: name,
|
|
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, maxPodLength, maxNamespaceLength)
|
|
case jsonOutput:
|
|
printEndpointsJSON(endpointsTables, w)
|
|
}
|
|
}
|
|
|
|
func printEndpointsTables(endpointsTables map[string][]rowEndpoint, w *tabwriter.Writer, 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, maxPodLength, maxNamespaceLength)
|
|
}
|
|
}
|
|
|
|
func printEndpointsTable(namespace string, rows []rowEndpoint, w *tabwriter.Writer, maxPodLength int, maxNamespaceLength int) {
|
|
headers := make([]string, 0)
|
|
templateString := "%s\t%d\t%s\t%s\n"
|
|
|
|
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)),
|
|
"SERVICE",
|
|
}...)
|
|
fmt.Fprintln(w, strings.Join(headers, "\t"))
|
|
|
|
for _, row := range rows {
|
|
values := []interface{}{
|
|
namespace + strings.Repeat(" ", maxNamespaceLength-len(namespace)),
|
|
row.IP,
|
|
row.Port,
|
|
row.Pod,
|
|
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
|
|
}
|