1 package healthcheck
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "os"
8 "regexp"
9 "strings"
10 "time"
11
12 "github.com/briandowns/spinner"
13 "github.com/fatih/color"
14 "github.com/mattn/go-isatty"
15 )
16
17 const (
18
19 JSONOutput = "json"
20
21 TableOutput = "table"
22
23 WideOutput = "wide"
24
25 ShortOutput = "short"
26
27
28
29
30 DefaultHintBaseURL = "https://linkerd.io/2/checks/#"
31 )
32
33 var (
34 okStatus = color.New(color.FgGreen, color.Bold).SprintFunc()("\u221A")
35 warnStatus = color.New(color.FgYellow, color.Bold).SprintFunc()("\u203C")
36 failStatus = color.New(color.FgRed, color.Bold).SprintFunc()("\u00D7")
37
38 reStableVersion = regexp.MustCompile(`stable-(\d\.\d+)\.`)
39 )
40
41
42 type Checks string
43
44 const (
45
46
47 ExtensionMetadataSubcommand = "_extension-metadata"
48
49
50 Always Checks = "always"
51
52
53
54
55
56
57 )
58
59
60 type ExtensionMetadataOutput struct {
61 Name string `json:"name"`
62 Checks Checks `json:"checks"`
63 }
64
65
66 type CheckResults struct {
67 Results []CheckResult
68 }
69
70
71 type CheckOutput struct {
72 Success bool `json:"success"`
73 Categories []*CheckCategory `json:"categories"`
74 }
75
76
77 type CheckCategory struct {
78 Name CategoryID `json:"categoryName"`
79 Checks []*Check `json:"checks"`
80 }
81
82
83
84 type Check struct {
85 Description string `json:"description"`
86 Hint string `json:"hint,omitempty"`
87 Error string `json:"error,omitempty"`
88 Result CheckResultStr `json:"result"`
89 }
90
91
92
93 func (cr CheckResults) RunChecks(observer CheckObserver) (bool, bool) {
94 success := true
95 warning := false
96 for _, result := range cr.Results {
97 result := result
98 if result.Err != nil {
99 if !result.Warning {
100 success = false
101 } else {
102 warning = true
103 }
104 }
105 observer(&result)
106 }
107 return success, warning
108 }
109
110
111 func PrintChecksResult(wout io.Writer, output string, success bool, warning bool) {
112 if output == JSONOutput {
113 return
114 }
115
116 switch success {
117 case true:
118 fmt.Fprintf(wout, "Status check results are %s\n", okStatus)
119 case false:
120 fmt.Fprintf(wout, "Status check results are %s\n", failStatus)
121 }
122 }
123
124
125 func RunChecks(wout io.Writer, werr io.Writer, hc Runner, output string) (bool, bool) {
126 if output == JSONOutput {
127 return runChecksJSON(wout, werr, hc)
128 }
129
130 return runChecksTable(wout, hc, output)
131 }
132
133 func runChecksTable(wout io.Writer, hc Runner, output string) (bool, bool) {
134 var lastCategory CategoryID
135 spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
136 spin.Writer = wout
137
138
139
140
141
142 prettyPrintResults := func(result *CheckResult) {
143 lastCategory = printCategory(wout, lastCategory, result)
144
145 spin.Stop()
146 if result.Retry {
147 restartSpinner(spin, result)
148 return
149 }
150
151 status := getResultStatus(result)
152
153 printResultDescription(wout, status, result)
154 }
155
156 prettyPrintResultsShort := func(result *CheckResult) {
157
158 if result.Err == nil {
159 return
160 }
161
162 lastCategory = printCategory(wout, lastCategory, result)
163
164 spin.Stop()
165 if result.Retry {
166 restartSpinner(spin, result)
167 return
168 }
169
170 status := getResultStatus(result)
171
172 printResultDescription(wout, status, result)
173 }
174
175 var (
176 success bool
177 warning bool
178 )
179 switch output {
180 case ShortOutput:
181 success, warning = hc.RunChecks(prettyPrintResultsShort)
182 default:
183 success, warning = hc.RunChecks(prettyPrintResults)
184 }
185
186
187
188
189 if output != ShortOutput || !success || warning {
190 fmt.Fprintln(wout)
191 }
192
193 return success, warning
194 }
195
196
197 type CheckResultStr string
198
199 const (
200 CheckSuccess CheckResultStr = "success"
201 CheckWarn CheckResultStr = "warning"
202 CheckErr CheckResultStr = "error"
203 )
204
205 func runChecksJSON(wout io.Writer, werr io.Writer, hc Runner) (bool, bool) {
206 var categories []*CheckCategory
207
208 collectJSONOutput := func(result *CheckResult) {
209 if categories == nil || categories[len(categories)-1].Name != result.Category {
210 categories = append(categories, &CheckCategory{
211 Name: result.Category,
212 Checks: []*Check{},
213 })
214 }
215
216 if !result.Retry {
217 currentCategory := categories[len(categories)-1]
218
219 status := CheckSuccess
220 if result.Err != nil {
221 status = CheckErr
222 if result.Warning {
223 status = CheckWarn
224 }
225 }
226
227 currentCheck := &Check{
228 Description: result.Description,
229 Result: status,
230 }
231
232 if result.Err != nil {
233 currentCheck.Error = result.Err.Error()
234
235 if result.HintURL != "" {
236 currentCheck.Hint = result.HintURL
237 }
238 }
239 currentCategory.Checks = append(currentCategory.Checks, currentCheck)
240 }
241 }
242
243 success, warning := hc.RunChecks(collectJSONOutput)
244
245 outputJSON := CheckOutput{
246 Success: success,
247 Categories: categories,
248 }
249
250 resultJSON, err := json.MarshalIndent(outputJSON, "", " ")
251 if err == nil {
252 fmt.Fprintf(wout, "%s\n", string(resultJSON))
253 } else {
254 fmt.Fprintf(werr, "JSON serialization of the check result failed with %s", err)
255 }
256 return success, warning
257 }
258
259 func printResultDescription(wout io.Writer, status string, result *CheckResult) {
260 fmt.Fprintf(wout, "%s %s\n", status, result.Description)
261
262 if result.Err == nil {
263 return
264 }
265
266 fmt.Fprintf(wout, " %s\n", result.Err)
267 if result.HintURL != "" {
268 fmt.Fprintf(wout, " see %s for hints\n", result.HintURL)
269 }
270 }
271
272 func getResultStatus(result *CheckResult) string {
273 status := okStatus
274 if result.Err != nil {
275 status = failStatus
276 if result.Warning {
277 status = warnStatus
278 }
279 }
280
281 return status
282 }
283
284 func restartSpinner(spin *spinner.Spinner, result *CheckResult) {
285 if isatty.IsTerminal(os.Stdout.Fd()) {
286 spin.Suffix = fmt.Sprintf(" %s", result.Err)
287 spin.Color("bold")
288 }
289 }
290
291 func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult) CategoryID {
292 if lastCategory == result.Category {
293 return lastCategory
294 }
295
296 if lastCategory != "" {
297 fmt.Fprintln(wout)
298 }
299
300 fmt.Fprintln(wout, result.Category)
301 fmt.Fprintln(wout, strings.Repeat("-", len(result.Category)))
302
303 return result.Category
304 }
305
306
307
308 func HintBaseURL(ver string) string {
309 stableVersion := reStableVersion.FindStringSubmatch(ver)
310 if stableVersion == nil {
311 return DefaultHintBaseURL
312 }
313 return fmt.Sprintf("https://linkerd.io/%s/checks/#", stableVersion[1])
314 }
315
View as plain text