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 }