github.com/elfadel/cilium@v1.6.12/pkg/health/client/client.go (about)

     1  // Copyright 2018 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package client
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"net"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"sort"
    25  	"strings"
    26  	"time"
    27  
    28  	clientapi "github.com/cilium/cilium/api/v1/health/client"
    29  	"github.com/cilium/cilium/api/v1/health/models"
    30  	"github.com/cilium/cilium/pkg/health/defaults"
    31  
    32  	runtime_client "github.com/go-openapi/runtime/client"
    33  	"github.com/go-openapi/strfmt"
    34  )
    35  
    36  const (
    37  	ipUnavailable = "Unavailable"
    38  )
    39  
    40  // Client is a client for cilium health
    41  type Client struct {
    42  	clientapi.CiliumHealth
    43  }
    44  
    45  func configureTransport(tr *http.Transport, proto, addr string) *http.Transport {
    46  	if tr == nil {
    47  		tr = &http.Transport{}
    48  	}
    49  
    50  	if proto == "unix" {
    51  		// No need for compression in local communications.
    52  		tr.DisableCompression = true
    53  		tr.Dial = func(_, _ string) (net.Conn, error) {
    54  			return net.Dial(proto, addr)
    55  		}
    56  	} else {
    57  		tr.Proxy = http.ProxyFromEnvironment
    58  		tr.Dial = (&net.Dialer{}).Dial
    59  	}
    60  
    61  	return tr
    62  }
    63  
    64  // NewDefaultClient creates a client with default parameters connecting to UNIX domain socket.
    65  func NewDefaultClient() (*Client, error) {
    66  	return NewClient("")
    67  }
    68  
    69  // NewClient creates a client for the given `host`.
    70  func NewClient(host string) (*Client, error) {
    71  	if host == "" {
    72  		// Check if environment variable points to socket
    73  		e := os.Getenv(defaults.SockPathEnv)
    74  		if e == "" {
    75  			// If unset, fall back to default value
    76  			e = defaults.SockPath
    77  		}
    78  		host = "unix://" + e
    79  	}
    80  	tmp := strings.SplitN(host, "://", 2)
    81  	if len(tmp) != 2 {
    82  		return nil, fmt.Errorf("invalid host format '%s'", host)
    83  	}
    84  
    85  	switch tmp[0] {
    86  	case "tcp":
    87  		if _, err := url.Parse("tcp://" + tmp[1]); err != nil {
    88  			return nil, err
    89  		}
    90  		host = "http://" + tmp[1]
    91  	case "unix":
    92  		host = tmp[1]
    93  	}
    94  
    95  	transport := configureTransport(nil, tmp[0], host)
    96  	httpClient := &http.Client{Transport: transport}
    97  	clientTrans := runtime_client.NewWithClient(tmp[1], clientapi.DefaultBasePath,
    98  		clientapi.DefaultSchemes, httpClient)
    99  	return &Client{*clientapi.New(clientTrans, strfmt.Default)}, nil
   100  }
   101  
   102  // Hint tries to improve the error message displayed to the user.
   103  func Hint(err error) error {
   104  	if err == nil {
   105  		return err
   106  	}
   107  	e, _ := url.PathUnescape(err.Error())
   108  	if strings.Contains(err.Error(), defaults.SockPath) {
   109  		return fmt.Errorf("%s\nIs the agent running?", e)
   110  	}
   111  	return fmt.Errorf("%s", e)
   112  }
   113  
   114  func connectivityStatusHealthy(cs *models.ConnectivityStatus) bool {
   115  	return cs != nil && cs.Status == ""
   116  }
   117  
   118  func formatConnectivityStatus(w io.Writer, cs *models.ConnectivityStatus, path, indent string) {
   119  	status := cs.Status
   120  	if connectivityStatusHealthy(cs) {
   121  		latency := time.Duration(cs.Latency)
   122  		status = fmt.Sprintf("OK, RTT=%s", latency)
   123  	}
   124  	fmt.Fprintf(w, "%s%s:\t%s\n", indent, path, status)
   125  }
   126  
   127  func formatPathStatus(w io.Writer, name string, cp *models.PathStatus, indent string, verbose bool) {
   128  	if cp == nil {
   129  		if verbose {
   130  			fmt.Fprintf(w, "%s%s connectivity:\tnil\n", indent, name)
   131  		}
   132  		return
   133  	}
   134  	fmt.Fprintf(w, "%s%s connectivity to %s:\n", indent, name, cp.IP)
   135  	indent = fmt.Sprintf("%s  ", indent)
   136  
   137  	if cp.Icmp != nil {
   138  		formatConnectivityStatus(w, cp.Icmp, "ICMP to stack", indent)
   139  	}
   140  	if cp.HTTP != nil {
   141  		formatConnectivityStatus(w, cp.HTTP, "HTTP to agent", indent)
   142  	}
   143  }
   144  
   145  // PathIsHealthy checks whether ICMP and TCP(HTTP) connectivity to the given
   146  // path is available.
   147  func PathIsHealthy(cp *models.PathStatus) bool {
   148  	if cp == nil {
   149  		return false
   150  	}
   151  
   152  	statuses := []*models.ConnectivityStatus{
   153  		cp.Icmp,
   154  		cp.HTTP,
   155  	}
   156  	for _, status := range statuses {
   157  		if !connectivityStatusHealthy(status) {
   158  			return false
   159  		}
   160  	}
   161  	return true
   162  }
   163  
   164  func nodeIsHealthy(node *models.NodeStatus) bool {
   165  	return PathIsHealthy(GetHostPrimaryAddress(node)) &&
   166  		(node.Endpoint == nil || PathIsHealthy(node.Endpoint))
   167  }
   168  
   169  func nodeIsLocalhost(node *models.NodeStatus, self *models.SelfStatus) bool {
   170  	return self != nil && node.Name == self.Name
   171  }
   172  
   173  func getPrimaryAddressIP(node *models.NodeStatus) string {
   174  	if node.Host == nil || node.Host.PrimaryAddress == nil {
   175  		return ipUnavailable
   176  	}
   177  
   178  	return node.Host.PrimaryAddress.IP
   179  }
   180  
   181  // GetHostPrimaryAddress returns the PrimaryAddress for the Host within node.
   182  // If node.Host is nil, returns nil.
   183  func GetHostPrimaryAddress(node *models.NodeStatus) *models.PathStatus {
   184  	if node.Host == nil {
   185  		return nil
   186  	}
   187  
   188  	return node.Host.PrimaryAddress
   189  }
   190  
   191  func formatNodeStatus(w io.Writer, node *models.NodeStatus, printAll, succinct, verbose, localhost bool) {
   192  	localStr := ""
   193  	if localhost {
   194  		localStr = " (localhost)"
   195  	}
   196  	if succinct {
   197  		if printAll || !nodeIsHealthy(node) {
   198  
   199  			fmt.Fprintf(w, "  %s%s\t%s\t%t\t%t\n", node.Name,
   200  				localStr, getPrimaryAddressIP(node),
   201  				PathIsHealthy(GetHostPrimaryAddress(node)),
   202  				PathIsHealthy(node.Endpoint))
   203  		}
   204  	} else {
   205  		fmt.Fprintf(w, "  %s%s:\n", node.Name, localStr)
   206  		formatPathStatus(w, "Host", GetHostPrimaryAddress(node), "    ", verbose)
   207  		if verbose && node.Host != nil {
   208  			for _, addr := range node.Host.SecondaryAddresses {
   209  				formatPathStatus(w, "Secondary", addr, "      ", verbose)
   210  			}
   211  		}
   212  		formatPathStatus(w, "Endpoint", node.Endpoint, "    ", verbose)
   213  	}
   214  }
   215  
   216  // FormatHealthStatusResponse writes a HealthStatusResponse as a string to the
   217  // writer.
   218  //
   219  // 'printAll', if true, causes all nodes to be printed regardless of status
   220  // 'succinct', if true, causes node health to be output as one line per node
   221  // 'verbose', if true, overrides 'succinct' and prints all information
   222  // 'maxLines', if nonzero, determines the maximum number of lines to print
   223  func FormatHealthStatusResponse(w io.Writer, sr *models.HealthStatusResponse, printAll, succinct, verbose bool, maxLines int) {
   224  	var (
   225  		healthy   int
   226  		localhost *models.NodeStatus
   227  	)
   228  	for _, node := range sr.Nodes {
   229  		if nodeIsHealthy(node) {
   230  			healthy++
   231  		}
   232  		if nodeIsLocalhost(node, sr.Local) {
   233  			localhost = node
   234  		}
   235  	}
   236  	if succinct {
   237  		fmt.Fprintf(w, "Cluster health:\t%d/%d reachable\t(%s)\n",
   238  			healthy, len(sr.Nodes), sr.Timestamp)
   239  		if printAll || healthy < len(sr.Nodes) {
   240  			fmt.Fprintf(w, "  Name\tIP\tReachable\tEndpoints reachable\n")
   241  		}
   242  	} else {
   243  		fmt.Fprintf(w, "Probe time:\t%s\n", sr.Timestamp)
   244  		fmt.Fprintf(w, "Nodes:\n")
   245  	}
   246  
   247  	if localhost != nil {
   248  		formatNodeStatus(w, localhost, printAll, succinct, verbose, true)
   249  		maxLines--
   250  	}
   251  
   252  	nodes := sr.Nodes
   253  	sort.Slice(nodes, func(i, j int) bool {
   254  		return strings.Compare(nodes[i].Name, nodes[j].Name) < 0
   255  	})
   256  	for n, node := range nodes {
   257  		if maxLines > 0 && n > maxLines {
   258  			break
   259  		}
   260  		if node == localhost {
   261  			continue
   262  		}
   263  		formatNodeStatus(w, node, printAll, succinct, verbose, false)
   264  	}
   265  	if maxLines > 0 && len(sr.Nodes)-healthy > maxLines {
   266  		fmt.Fprintf(w, "  ...")
   267  	}
   268  }
   269  
   270  // GetAndFormatHealthStatus fetches the health status from the cilium-health
   271  // daemon via the default channel and formats its output as a string to the
   272  // writer.
   273  //
   274  // 'succinct', 'verbose' and 'maxLines' are handled the same as in
   275  // FormatHealthStatusResponse().
   276  func GetAndFormatHealthStatus(w io.Writer, succinct, verbose bool, maxLines int) {
   277  	client, err := NewClient("")
   278  	if err != nil {
   279  		fmt.Fprintf(w, "Cluster health:\t\t\tClient error: %s\n", err)
   280  		return
   281  	}
   282  	hr, err := client.Connectivity.GetStatus(nil)
   283  	if err != nil {
   284  		// The regular `cilium status` output will print the reason why.
   285  		fmt.Fprintf(w, "Cluster health:\t\t\tWarning\tcilium-health daemon unreachable\n")
   286  		return
   287  	}
   288  	FormatHealthStatusResponse(w, hr.Payload, verbose, succinct, verbose, maxLines)
   289  }