1 package cmd
2
3 import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "sort"
13 "strings"
14 "time"
15
16 "github.com/briandowns/spinner"
17 "github.com/linkerd/linkerd2/pkg/healthcheck"
18 "github.com/linkerd/linkerd2/pkg/version"
19 "github.com/mattn/go-isatty"
20 utilsexec "k8s.io/utils/exec"
21 )
22
23
24 type glob func(string) ([]string, error)
25
26
27
28
29 type extension struct {
30 path string
31 builtin string
32 }
33
34 var (
35 builtInChecks = map[string]struct{}{
36 "jaeger": {},
37 "multicluster": {},
38 "viz": {},
39 }
40 )
41
42
43
44 func findExtensions(pathEnv string, glob glob, exec utilsexec.Interface, nsLabels []string) ([]extension, []string) {
45 cliExtensions := findCLIExtensionsOnPath(pathEnv, glob, exec)
46
47
48 extensions := findAlwaysChecks(cliExtensions, exec)
49
50 alwaysSuffixSet := map[string]struct{}{}
51 for _, e := range extensions {
52 alwaysSuffixSet[suffix(e.path)] = struct{}{}
53 }
54
55
56
57 nsLabelSet := map[string]struct{}{}
58 for _, label := range nsLabels {
59 if _, ok := alwaysSuffixSet[label]; !ok {
60 nsLabelSet[label] = struct{}{}
61 }
62 }
63
64
65 for _, e := range cliExtensions {
66 suffix := suffix(e)
67 if _, ok := nsLabelSet[suffix]; ok {
68 extensions = append(extensions, extension{path: e})
69 delete(nsLabelSet, suffix)
70 }
71 }
72
73
74 for label := range nsLabelSet {
75 if _, ok := builtInChecks[label]; ok {
76 extensions = append(extensions, extension{path: os.Args[0], builtin: label})
77 delete(nsLabelSet, label)
78 }
79 }
80
81
82 missing := []string{}
83 for label := range nsLabelSet {
84 missing = append(missing, fmt.Sprintf("linkerd-%s", label))
85 }
86
87 sort.Slice(extensions, func(i, j int) bool {
88 if extensions[i].path != extensions[j].path {
89 _, filename1 := filepath.Split(extensions[i].path)
90 _, filename2 := filepath.Split(extensions[j].path)
91 return filename1 < filename2
92 }
93 return extensions[i].builtin < extensions[j].builtin
94 })
95 sort.Strings(missing)
96
97 return extensions, missing
98 }
99
100
101
102
103 func findCLIExtensionsOnPath(pathEnv string, glob glob, exec utilsexec.Interface) []string {
104 executables := []string{}
105 seen := map[string]struct{}{}
106
107 for _, dir := range filepath.SplitList(pathEnv) {
108 matches, err := glob(filepath.Join(dir, "linkerd-*"))
109 if err != nil {
110 continue
111 }
112 sort.Strings(matches)
113
114 for _, match := range matches {
115 suffix := suffix(match)
116 if _, ok := seen[suffix]; ok {
117 continue
118 }
119
120 path, err := exec.LookPath(match)
121 if err != nil {
122 continue
123 }
124
125 executables = append(executables, path)
126 seen[suffix] = struct{}{}
127 }
128 }
129
130 return executables
131 }
132
133
134
135
136 func findAlwaysChecks(cliExtensions []string, exec utilsexec.Interface) []extension {
137 extensions := []extension{}
138
139 for _, e := range cliExtensions {
140 if isAlwaysCheck(e, exec) {
141 extensions = append(extensions, extension{path: e})
142 }
143 }
144
145 return extensions
146 }
147
148
149
150 func isAlwaysCheck(path string, exec utilsexec.Interface) bool {
151 cmd := exec.Command(path, healthcheck.ExtensionMetadataSubcommand)
152 var stdout, stderr bytes.Buffer
153 cmd.SetStdout(&stdout)
154 cmd.SetStderr(&stderr)
155 err := cmd.Run()
156 if err != nil {
157 return false
158 }
159
160 metadataOutput, err := parseJSONMetadataOutput(stdout.Bytes())
161 if err != nil {
162 return false
163 }
164
165
166
167
168 _, filename := filepath.Split(path)
169 return strings.EqualFold(metadataOutput.Name, filename) && metadataOutput.Checks == healthcheck.Always
170 }
171
172
173
174
175 func parseJSONMetadataOutput(data []byte) (healthcheck.ExtensionMetadataOutput, error) {
176 var metadata healthcheck.ExtensionMetadataOutput
177 err := json.Unmarshal(data, &metadata)
178 if err != nil {
179 return healthcheck.ExtensionMetadataOutput{}, err
180 }
181 return metadata, nil
182 }
183
184
185
186
187 func runExtensionsChecks(
188 wout io.Writer, werr io.Writer, extensions []extension, missing []string, utilsexec utilsexec.Interface, flags []string, output string,
189 ) (bool, bool) {
190 spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
191 spin.Writer = wout
192
193 success := true
194 warning := false
195 for _, extension := range extensions {
196 args := append([]string{"check"}, flags...)
197 if extension.builtin != "" {
198 args = append([]string{extension.builtin}, args...)
199 }
200
201 if isatty.IsTerminal(os.Stdout.Fd()) {
202 name := suffix(extension.path)
203 if extension.builtin != "" {
204 name = extension.builtin
205 }
206
207 spin.Suffix = fmt.Sprintf(" Running %s extension check", name)
208 spin.Color("bold")
209 }
210
211 plugin := utilsexec.Command(extension.path, args...)
212 var stdout, stderr bytes.Buffer
213 plugin.SetStdout(&stdout)
214 plugin.SetStderr(&stderr)
215 plugin.Run()
216 results, err := parseJSONCheckOutput(stdout.Bytes())
217 spin.Stop()
218 if err != nil {
219 success = false
220
221 command := fmt.Sprintf("%s %s", extension.path, strings.Join(args, " "))
222 if len(stderr.String()) > 0 {
223 err = errors.New(stderr.String())
224 } else {
225 err = fmt.Errorf("invalid extension check output from \"%s\" (JSON object expected):\n%s\n[%w]", command, stdout.String(), err)
226 }
227 _, filename := filepath.Split(extension.path)
228 results = healthcheck.CheckResults{
229 Results: []healthcheck.CheckResult{
230 {
231 Category: healthcheck.CategoryID(filename),
232 Description: fmt.Sprintf("Running: %s", command),
233 Err: err,
234 HintURL: healthcheck.HintBaseURL(version.Version) + "extensions",
235 },
236 },
237 }
238 }
239
240 extensionSuccess, extensionWarning := healthcheck.RunChecks(wout, werr, results, output)
241 if !extensionSuccess {
242 success = false
243 }
244 if extensionWarning {
245 warning = true
246 }
247 }
248
249 for _, m := range missing {
250 results := healthcheck.CheckResults{
251 Results: []healthcheck.CheckResult{
252 {
253 Category: healthcheck.CategoryID(m),
254 Description: fmt.Sprintf("Linkerd extension command %s exists", m),
255 Err: &exec.Error{Name: m, Err: exec.ErrNotFound},
256 HintURL: healthcheck.HintBaseURL(version.Version) + "extensions",
257 Warning: true,
258 },
259 },
260 }
261
262 extensionSuccess, extensionWarning := healthcheck.RunChecks(wout, werr, results, output)
263 if !extensionSuccess {
264 success = false
265 }
266 if extensionWarning {
267 warning = true
268 }
269 }
270
271 return success, warning
272 }
273
274
275
276
277
278 func parseJSONCheckOutput(data []byte) (healthcheck.CheckResults, error) {
279 var checks healthcheck.CheckOutput
280 err := json.Unmarshal(data, &checks)
281 if err != nil {
282 return healthcheck.CheckResults{}, err
283 }
284 results := []healthcheck.CheckResult{}
285 for _, category := range checks.Categories {
286 for _, check := range category.Checks {
287 var err error
288 if check.Error != "" {
289 err = errors.New(check.Error)
290 }
291 results = append(results, healthcheck.CheckResult{
292 Category: category.Name,
293 Description: check.Description,
294 Err: err,
295 HintURL: check.Hint,
296 Warning: check.Result == healthcheck.CheckWarn,
297 })
298 }
299 }
300 return healthcheck.CheckResults{Results: results}, nil
301 }
302
303
304
305
306
307
308 func suffix(s string) string {
309 _, filename := filepath.Split(s)
310 suffix := strings.TrimPrefix(filename, "linkerd-")
311 if suffix == filename {
312
313 return ""
314 }
315 return suffix
316 }
317
View as plain text