cli: Support running `check` on CLI-only extensions (#10588)

The existing `linkerd check` command runs extension checks based on extension namespaces already on-cluster. This approach does not permit running extension checks without cluster-side components.

Introduce "CLI Checks". These extensions run as part of `linkerd check`, if they satisfy the following criteria:
1) executable in PATH
2) prefixed by `linkerd-`
3) supports an `_extension-metadata` subcommand, that outputs self-identifying
   JSON, for example:
   ```
   $ linkerd-foo _extension-metadata
   {
     "name": "linkerd-foo",
     "checks": "always"
   }
   ```
4) The `name` value from `_extension-metadata` must match the filename. And `checks` must equal `always`.

If a CLI Check is found that also would have run as an on-cluster extension check, it is run as a CLI Check only.

Fixes #10544
This commit is contained in:
Andrew Seigner 2023-03-29 12:07:36 -07:00 committed by GitHub
parent 495c580efc
commit e71266f2c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 767 additions and 181 deletions

View File

@ -117,9 +117,28 @@ instead of the output format described above. E.g.
```
In particular, the `linkerd check` command will invoke the check command for
each extension installed in the cluster and will request json output. To
preserve forwards compatibility, it is recommended that the check command should
ignore any unknown flags.
each extension installed in the cluster and will request json output.
`linkerd check` may optinally invoke your extension if not installed in the
cluster (see `linkerd-name _extension-metadata` below to opt-in). To preserve
forwards compatibility, it is recommended that the check command should ignore
any unknown flags.
### `linkerd-name _extension-metadata`
This subcommand is optional, and enables an extension to opt-in to being
executed as part of `linkerd check`, even when there is no corresponding
extension on the cluster. To opt-in, the output of
`linkerd-name _extension-metadata` should be json of the form:
```json
{
"name": "linkerd-name",
"checks": "always",
}
```
Note that for `linkerd check` to validate which extensions are opting-in, it
runs `linkerd-* _extension-metadata` against every executable in the PATH.
The extension may also implement further commands in addition to the ones
defined here.

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
@ -18,6 +19,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
valuespkg "helm.sh/helm/v3/pkg/cli/values"
utilsexec "k8s.io/utils/exec"
)
type checkOptions struct {
@ -196,9 +198,6 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, o
ChartValues: values,
})
if options.output == tableOutput {
healthcheck.PrintChecksHeader(wout, healthcheck.CoreHeader)
}
success, warning := healthcheck.RunChecks(wout, werr, hc, options.output)
if !options.preInstallOnly && !options.crdsOnly {
@ -231,19 +230,24 @@ func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts
if err != nil {
return false, false, err
}
nsLabels := []string{}
for _, ns := range namespaces {
ext := ns.Labels[k8s.LinkerdExtensionLabel]
nsLabels = append(nsLabels, ext)
}
exec := utilsexec.New()
extensions, missing := findExtensions(os.Getenv("PATH"), filepath.Glob, exec, nsLabels)
success := true
// no extensions to check
if len(namespaces) == 0 {
return success, false, nil
if len(extensions) == 0 && len(missing) == 0 {
return true, false, nil
}
nsLabels := make([]string, len(namespaces))
for i, ns := range namespaces {
nsLabels[i] = ns.Labels[k8s.LinkerdExtensionLabel]
}
extensionSuccess, extensionWarning := healthcheck.RunExtensionsChecks(wout, werr, nsLabels, getExtensionCheckFlags(cmd.Flags()), opts.output)
extensionSuccess, extensionWarning := runExtensionsChecks(
wout, werr, extensions, missing, exec, getExtensionCheckFlags(cmd.Flags()), opts.output,
)
return extensionSuccess, extensionWarning, nil
}

316
cli/cmd/check_extensions.go Normal file
View File

@ -0,0 +1,316 @@
package cmd
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/linkerd/linkerd2/pkg/healthcheck"
"github.com/linkerd/linkerd2/pkg/version"
"github.com/mattn/go-isatty"
utilsexec "k8s.io/utils/exec"
)
// glob is satisfied by filepath.Glob.
type glob func(string) ([]string, error)
// extension contains the full path of an extension executable. If it's a
// a built-in extension, path will be the `linkerd` executable and builtin will
// be the extension name (jaeger, multicluster, or viz).
type extension struct {
path string
builtin string
}
var (
builtInChecks = map[string]struct{}{
"jaeger": {},
"multicluster": {},
"viz": {},
}
)
// findExtensions searches the path for all linkerd-* executables and returns a
// slice of check commands, and a slice of missing checks.
func findExtensions(pathEnv string, glob glob, exec utilsexec.Interface, nsLabels []string) ([]extension, []string) {
cliExtensions := findCLIExtensionsOnPath(pathEnv, glob, exec)
// first, collect extensions that are "always" enabled
extensions := findAlwaysChecks(cliExtensions, exec)
alwaysSuffixSet := map[string]struct{}{}
for _, e := range extensions {
alwaysSuffixSet[suffix(e.path)] = struct{}{}
}
// nsLabelSet is the set of extension names which are installed on the cluster
// but are not "always" checks
nsLabelSet := map[string]struct{}{}
for _, label := range nsLabels {
if _, ok := alwaysSuffixSet[label]; !ok {
nsLabelSet[label] = struct{}{}
}
}
// second, collect on-cluster extensions
for _, e := range cliExtensions {
suffix := suffix(e)
if _, ok := nsLabelSet[suffix]; ok {
extensions = append(extensions, extension{path: e})
delete(nsLabelSet, suffix)
}
}
// third, collect built-in extensions
for label := range nsLabelSet {
if _, ok := builtInChecks[label]; ok {
extensions = append(extensions, extension{path: os.Args[0], builtin: label})
delete(nsLabelSet, label)
}
}
// anything left in nsLabelSet is a missing executable
missing := []string{}
for label := range nsLabelSet {
missing = append(missing, fmt.Sprintf("linkerd-%s", label))
}
sort.Slice(extensions, func(i, j int) bool {
if extensions[i].path != extensions[j].path {
_, filename1 := filepath.Split(extensions[i].path)
_, filename2 := filepath.Split(extensions[j].path)
return filename1 < filename2
}
return extensions[i].builtin < extensions[j].builtin
})
sort.Strings(missing)
return extensions, missing
}
// findCLIExtensionsOnPath searches the path for all linkerd-* executables and
// returns a slice of unique filepaths. if multiple executables have the same
// name, only the one which comes earliest in the pathEnv is returned.
func findCLIExtensionsOnPath(pathEnv string, glob glob, exec utilsexec.Interface) []string {
executables := []string{}
seen := map[string]struct{}{}
for _, dir := range filepath.SplitList(pathEnv) {
matches, err := glob(filepath.Join(dir, "linkerd-*"))
if err != nil {
continue
}
sort.Strings(matches)
for _, match := range matches {
suffix := suffix(match)
if _, ok := seen[suffix]; ok {
continue
}
path, err := exec.LookPath(match)
if err != nil {
continue
}
executables = append(executables, path)
seen[suffix] = struct{}{}
}
}
return executables
}
// findAlwaysChecks filters a slice of linkerd-* executables to only those that
// support the "_extension-metadata" subcommand, and announce themselves to
// "always" run.
func findAlwaysChecks(cliExtensions []string, exec utilsexec.Interface) []extension {
extensions := []extension{}
for _, e := range cliExtensions {
if isAlwaysCheck(e, exec) {
extensions = append(extensions, extension{path: e})
}
}
return extensions
}
// isAlwaysCheck executes a command with an "_extension-metadata" subcommand,
// and returns true if the output is a valid ExtensionMetadataOutput struct.
func isAlwaysCheck(path string, exec utilsexec.Interface) bool {
cmd := exec.Command(path, healthcheck.ExtensionMetadataSubcommand)
var stdout, stderr bytes.Buffer
cmd.SetStdout(&stdout)
cmd.SetStderr(&stderr)
err := cmd.Run()
if err != nil {
return false
}
metadataOutput, err := parseJSONMetadataOutput(stdout.Bytes())
if err != nil {
return false
}
// output of _extension-metadata must match the executable name, and specific
// "always"
// i.e. linkerd-foo is allowed, linkerd-foo-v0.XX.X is not
_, filename := filepath.Split(path)
return strings.EqualFold(metadataOutput.Name, filename) && metadataOutput.Checks == healthcheck.Always
}
// parseJSONMetadataOutput parses the output of an _extension-metadata
// subcommand. The data is expected to be a ExtensionMetadataOutput struct
// serialized to json.
func parseJSONMetadataOutput(data []byte) (healthcheck.ExtensionMetadataOutput, error) {
var metadata healthcheck.ExtensionMetadataOutput
err := json.Unmarshal(data, &metadata)
if err != nil {
return healthcheck.ExtensionMetadataOutput{}, err
}
return metadata, nil
}
// runExtensionsChecks runs checks for each extension name passed into the
// `extensions` parameter and handles formatting the output for each extension's
// check. This function also prints check warnings for missing extensions.
func runExtensionsChecks(
wout io.Writer, werr io.Writer, extensions []extension, missing []string, utilsexec utilsexec.Interface, flags []string, output string,
) (bool, bool) {
spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spin.Writer = wout
success := true
warning := false
for _, extension := range extensions {
args := append([]string{"check"}, flags...)
if extension.builtin != "" {
args = append([]string{extension.builtin}, args...)
}
if isatty.IsTerminal(os.Stdout.Fd()) {
name := suffix(extension.path)
if extension.builtin != "" {
name = extension.builtin
}
spin.Suffix = fmt.Sprintf(" Running %s extension check", name)
spin.Color("bold") // this calls spin.Restart()
}
plugin := utilsexec.Command(extension.path, args...)
var stdout, stderr bytes.Buffer
plugin.SetStdout(&stdout)
plugin.SetStderr(&stderr)
plugin.Run()
results, err := parseJSONCheckOutput(stdout.Bytes())
spin.Stop()
if err != nil {
success = false
command := fmt.Sprintf("%s %s", extension.path, strings.Join(args, " "))
if len(stderr.String()) > 0 {
err = errors.New(stderr.String())
} else {
err = fmt.Errorf("invalid extension check output from \"%s\" (JSON object expected):\n%s\n[%w]", command, stdout.String(), err)
}
_, filename := filepath.Split(extension.path)
results = healthcheck.CheckResults{
Results: []healthcheck.CheckResult{
{
Category: healthcheck.CategoryID(filename),
Description: fmt.Sprintf("Running: %s", command),
Err: err,
HintURL: healthcheck.HintBaseURL(version.Version) + "extensions",
},
},
}
}
extensionSuccess, extensionWarning := healthcheck.RunChecks(wout, werr, results, output)
if !extensionSuccess {
success = false
}
if extensionWarning {
warning = true
}
}
for _, m := range missing {
results := healthcheck.CheckResults{
Results: []healthcheck.CheckResult{
{
Category: healthcheck.CategoryID(m),
Description: fmt.Sprintf("Linkerd extension command %s exists", m),
Err: &exec.Error{Name: m, Err: exec.ErrNotFound},
HintURL: healthcheck.HintBaseURL(version.Version) + "extensions",
Warning: true,
},
},
}
extensionSuccess, extensionWarning := healthcheck.RunChecks(wout, werr, results, output)
if !extensionSuccess {
success = false
}
if extensionWarning {
warning = true
}
}
return success, warning
}
// parseJSONCheckOutput parses the output of a check command run with json
// output mode. The data is expected to be a CheckOutput struct serialized
// to json. In addition to deserializing, this function will convert the result
// to a CheckResults struct.
func parseJSONCheckOutput(data []byte) (healthcheck.CheckResults, error) {
var checks healthcheck.CheckOutput
err := json.Unmarshal(data, &checks)
if err != nil {
return healthcheck.CheckResults{}, err
}
results := []healthcheck.CheckResult{}
for _, category := range checks.Categories {
for _, check := range category.Checks {
var err error
if check.Error != "" {
err = errors.New(check.Error)
}
results = append(results, healthcheck.CheckResult{
Category: category.Name,
Description: check.Description,
Err: err,
HintURL: check.Hint,
Warning: check.Result == healthcheck.CheckWarn,
})
}
}
return healthcheck.CheckResults{Results: results}, nil
}
// suffix returns the last part of a CLI check name, e.g.:
// linkerd-foo => foo
// linkerd-foo-bar => foo-bar
// /usr/local/bin/linkerd-foo => foo
// s is assumed to be a filepath where the filename begins with "linkerd-"
func suffix(s string) string {
_, filename := filepath.Split(s)
suffix := strings.TrimPrefix(filename, "linkerd-")
if suffix == filename {
// we should never get here
return ""
}
return suffix
}

View File

@ -0,0 +1,388 @@
package cmd
import (
"bytes"
"errors"
"path/filepath"
"reflect"
"testing"
"k8s.io/utils/exec"
fakeexec "k8s.io/utils/exec/testing"
)
func TestFindExtensions(t *testing.T) {
fakeGlob := func(path string) ([]string, error) {
dir, _ := filepath.Split(path)
return []string{
filepath.Join(dir, "linkerd-bar"),
filepath.Join(dir, "linkerd-baz"),
filepath.Join(dir, "linkerd-foo"),
}, nil
}
fcmd := fakeexec.FakeCmd{
RunScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte(`{"name":"linkerd-baz","checks":"always"}`), nil, nil
},
func() ([]byte, []byte, error) {
return []byte(`{"name":"linkerd-foo-no-match","checks":"always"}`), nil, nil
},
func() ([]byte, []byte, error) { return []byte(`{"name":"linkerd-bar","checks":"always"}`), nil, nil },
},
}
lookPathSuccess := false
fexec := &fakeexec.FakeExec{
CommandScript: []fakeexec.FakeCommandAction{
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
},
LookPathFunc: func(cmd string) (string, error) {
if lookPathSuccess {
return cmd, nil
}
lookPathSuccess = true
return "", errors.New("fake-error")
},
}
extensions, missing := findExtensions("/path1:/this/is/a/fake/path2", fakeGlob, fexec, []string{"foo", "missing-cli"})
expExtensions := []extension{
{path: "/this/is/a/fake/path2/linkerd-bar"},
{path: "/path1/linkerd-baz"},
{path: "/path1/linkerd-foo"},
}
expMissing := []string{"linkerd-missing-cli"}
if !reflect.DeepEqual(expExtensions, extensions) {
t.Errorf("Expected [%+v] Got [%+v]", expExtensions, extensions)
}
if !reflect.DeepEqual(expMissing, missing) {
t.Errorf("Expected [%+v] Got [%+v]", expMissing, missing)
}
}
func TestRunExtensionsChecks(t *testing.T) {
successJSON := `
{
"success": true,
"categories": [
{
"categoryName": "success check name",
"checks": [
{
"description": "success check desc",
"result": "success"
}
]
}
]
}
`
warningJSON := `
{
"success": true,
"categories": [
{
"categoryName": "warning check name",
"checks": [
{
"description": "warning check desc",
"hint": "https://example.com/warning",
"error": "this is the warning message",
"result": "warning"
}
]
}
]
}
`
errorJSON := `
{
"success": false,
"categories": [
{
"categoryName": "error check name",
"checks": [
{
"description": "error check desc",
"hint": "https://example.com/error",
"error": "this is the error message",
"result": "error"
}
]
}
]
}
`
multiJSON := `
{
"success": true,
"categories": [
{
"categoryName": "multi check name",
"checks": [
{
"description": "multi check desc success",
"result": "success"
},
{
"description": "multi check desc warning",
"hint": "https://example.com/multi",
"error": "this is the multi warning message",
"result": "warning"
}
]
}
]
}
`
testCases := []struct {
name string
extensions []extension
missing []string
fakeActions []fakeexec.FakeAction
expSuccess bool
expWarning bool
expOutput string
}{
{
"no checks",
nil,
nil,
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return nil, nil, nil
},
},
true,
false,
"",
},
{
"invalid JSON",
[]extension{{path: "/path/linkerd-invalid"}},
nil,
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte("bad json"), nil, nil
},
},
false,
false,
`linkerd-invalid
---------------
× Running: /path/linkerd-invalid check
invalid extension check output from "/path/linkerd-invalid check" (JSON object expected):
bad json
[invalid character 'b' looking for beginning of value]
see https://linkerd.io/2/checks/#extensions for hints
`,
},
{
"one successful check",
[]extension{{path: "/path/linkerd-success"}},
nil,
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte(successJSON), nil, nil
},
},
true,
false,
`success check name
------------------
success check desc
`,
},
{
"one warning check",
[]extension{{path: "/path/linkerd-warning"}},
nil,
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte(warningJSON), nil, nil
},
},
true,
true,
`warning check name
------------------
warning check desc
this is the warning message
see https://example.com/warning for hints
`,
},
{
"one error check",
[]extension{{path: "/path/linkerd-error"}},
nil,
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte(errorJSON), nil, nil
},
},
false,
false,
`error check name
----------------
× error check desc
this is the error message
see https://example.com/error for hints
`,
},
{
"one missing check",
nil,
[]string{"missing"},
nil,
true,
true,
`missing
-------
Linkerd extension command missing exists
exec: "missing": executable file not found in $PATH
see https://linkerd.io/2/checks/#extensions for hints
`,
},
{
"multiple checks with success, warnings, errors, and missing",
[]extension{{path: "/path/linkerd-success"}, {path: "/path/linkerd-warning"}, {path: "/path/linkerd-error"}, {path: "/path/linkerd-multi"}},
[]string{"missing1", "missing2"},
[]fakeexec.FakeAction{
func() ([]byte, []byte, error) {
return []byte(successJSON), nil, nil
},
func() ([]byte, []byte, error) {
return []byte(warningJSON), nil, nil
},
func() ([]byte, []byte, error) {
return []byte(errorJSON), nil, nil
},
func() ([]byte, []byte, error) {
return []byte(multiJSON), nil, nil
},
},
false,
true,
`success check name
------------------
success check desc
warning check name
------------------
warning check desc
this is the warning message
see https://example.com/warning for hints
error check name
----------------
× error check desc
this is the error message
see https://example.com/error for hints
multi check name
----------------
multi check desc success
multi check desc warning
this is the multi warning message
see https://example.com/multi for hints
missing1
--------
Linkerd extension command missing1 exists
exec: "missing1": executable file not found in $PATH
see https://linkerd.io/2/checks/#extensions for hints
missing2
--------
Linkerd extension command missing2 exists
exec: "missing2": executable file not found in $PATH
see https://linkerd.io/2/checks/#extensions for hints
`,
},
}
for _, tc := range testCases {
tc := tc // pin
t.Run(tc.name, func(t *testing.T) {
fcmd := fakeexec.FakeCmd{
RunScript: tc.fakeActions,
}
fakeCommandActions := make([]fakeexec.FakeCommandAction, len(tc.fakeActions))
for i := 0; i < len(tc.fakeActions); i++ {
fakeCommandActions[i] = func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }
}
fexec := &fakeexec.FakeExec{
CommandScript: fakeCommandActions,
}
var stdout, stderr bytes.Buffer
success, warning := runExtensionsChecks(&stdout, &stderr, tc.extensions, tc.missing, fexec, nil, "")
if tc.expSuccess != success {
t.Errorf("Expected success to be %t, got %t", tc.expSuccess, success)
}
if tc.expWarning != warning {
t.Errorf("Expected warning to be %t, got %t", tc.expWarning, warning)
}
output := stdout.String()
if tc.expOutput != output {
t.Errorf("Expected output to be:\n%s\nGot:\n%s", tc.expOutput, output)
}
})
}
}
func TestSuffix(t *testing.T) {
testCases := []*struct {
testName string
input string
exp string
}{
{
"empty",
"",
"",
},
{
"no path",
"linkerd-foo",
"foo",
},
{
"extra dash",
"linkerd-foo-bar",
"foo-bar",
},
{
"with path",
"/tmp/linkerd-foo",
"foo",
},
}
for _, tc := range testCases {
tc := tc // pin
t.Run(tc.testName, func(t *testing.T) {
result := suffix(tc.input)
if !reflect.DeepEqual(tc.exp, result) {
t.Fatalf("Expected [%s] Got [%s]", tc.exp, result)
}
})
}
}

View File

@ -1,20 +1,16 @@
package healthcheck
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/fatih/color"
"github.com/linkerd/linkerd2/pkg/version"
"github.com/mattn/go-isatty"
)
@ -28,11 +24,6 @@ const (
// ShortOutput is used to specify the short output format
ShortOutput = "short"
// CoreHeader is used when printing core header checks
CoreHeader = "core"
// extensionsHeader is used when printing extensions header checks
extensionsHeader = "extensions"
// DefaultHintBaseURL is the default base URL on the linkerd.io website
// that all check hints for the latest linkerd version point to. Each
// check adds its own `hintAnchor` to specify a location on the page.
@ -47,6 +38,30 @@ var (
reStableVersion = regexp.MustCompile(`stable-(\d\.\d+)\.`)
)
// Checks describes the "checks" field on a CheckCLIOutput
type Checks string
const (
// ExtensionMetadataSubcommand is the subcommand name an extension must
// support in order to provide config metadata to the "linkerd" CLI.
ExtensionMetadataSubcommand = "_extension-metadata"
// Always run the check, regardless of cluster state
Always Checks = "always"
// // TODO:
// // Cluster informs "linkerd check" to only run this extension if there are
// // on-cluster resources.
// Cluster Checks = "cluster"
// // Never informs "linkerd check" to never run this extension.
// Never Checks = "never"
)
// ExtensionMetadataOutput contains the output of a _extension-metadata subcommand.
type ExtensionMetadataOutput struct {
Name string `json:"name"`
Checks Checks `json:"checks"`
}
// CheckResults contains a slice of CheckResult structs.
type CheckResults struct {
Results []CheckResult
@ -92,14 +107,6 @@ func (cr CheckResults) RunChecks(observer CheckObserver) (bool, bool) {
return success, warning
}
// PrintChecksHeader writes the header text for a check.
func PrintChecksHeader(wout io.Writer, header string) {
headerText := fmt.Sprintf("Linkerd %s checks", header)
fmt.Fprintln(wout, headerText)
fmt.Fprintln(wout, strings.Repeat("=", len(headerText)))
fmt.Fprintln(wout)
}
// PrintChecksResult writes the checks result.
func PrintChecksResult(wout io.Writer, output string, success bool, warning bool) {
if output == JSONOutput {
@ -114,95 +121,6 @@ func PrintChecksResult(wout io.Writer, output string, success bool, warning bool
}
}
// RunExtensionsChecks runs checks for each extension name passed into the `extensions` parameter
// and handles formatting the output for each extension's check. This function also handles
// finding the extension in the user's path and runs it.
func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, flags []string, output string) (bool, bool) {
if output == TableOutput {
PrintChecksHeader(wout, extensionsHeader)
}
spin := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spin.Writer = wout
success := true
warning := false
for _, extension := range extensions {
var path string
args := append([]string{"check"}, flags...)
var err error
results := CheckResults{
Results: []CheckResult{},
}
extensionCmd := fmt.Sprintf("linkerd-%s", extension)
switch extension {
case "jaeger":
path = os.Args[0]
args = append([]string{"jaeger"}, args...)
case "viz":
path = os.Args[0]
args = append([]string{"viz"}, args...)
case "multicluster":
path = os.Args[0]
args = append([]string{"multicluster"}, args...)
default:
path, err = exec.LookPath(extensionCmd)
results.Results = []CheckResult{
{
Category: CategoryID(extensionCmd),
Description: fmt.Sprintf("Linkerd extension command %s exists", extensionCmd),
Err: err,
HintURL: HintBaseURL(version.Version) + "extensions",
Warning: true,
},
}
}
if err == nil {
if isatty.IsTerminal(os.Stdout.Fd()) {
spin.Suffix = fmt.Sprintf(" Running %s extension check", extension)
spin.Color("bold") // this calls spin.Restart()
}
// Path is constructed from the switch statements above and will
// be a valid Linkerd subcommand.
//nolint:gosec
plugin := exec.Command(path, args...)
var stdout, stderr bytes.Buffer
plugin.Stdout = &stdout
plugin.Stderr = &stderr
plugin.Run()
extensionResults, err := parseJSONCheckOutput(stdout.Bytes())
spin.Stop()
if err != nil {
command := fmt.Sprintf("%s %s", path, strings.Join(args, " "))
if len(stderr.String()) > 0 {
err = errors.New(stderr.String())
} else {
err = fmt.Errorf("invalid extension check output from \"%s\" (JSON object expected):\n%s\n[%w]", command, stdout.String(), err)
}
results.Results = append(results.Results, CheckResult{
Category: CategoryID(extensionCmd),
Description: fmt.Sprintf("Running: %s", command),
Err: err,
HintURL: HintBaseURL(version.Version) + "extensions",
})
success = false
} else {
results.Results = append(results.Results, extensionResults.Results...)
}
}
var extensionSuccess bool
extensionSuccess, warning = RunChecks(wout, werr, results, output)
if !extensionSuccess {
success = false
}
}
return success, warning
}
// RunChecks runs the checks that are part of hc
func RunChecks(wout io.Writer, werr io.Writer, hc Runner, output string) (bool, bool) {
if output == JSONOutput {
@ -235,14 +153,12 @@ func runChecksTable(wout io.Writer, hc Runner, output string) (bool, bool) {
printResultDescription(wout, status, result)
}
var headerPrinted bool
prettyPrintResultsShort := func(result *CheckResult) {
// bail out early and skip printing if we've got an okStatus
if result.Err == nil {
return
}
headerPrinted = printHeader(wout, headerPrinted, hc)
lastCategory = printCategory(wout, lastCategory, result)
spin.Stop()
@ -340,35 +256,6 @@ func runChecksJSON(wout io.Writer, werr io.Writer, hc Runner) (bool, bool) {
return success, warning
}
// parseJSONCheckOutput parses the output of a check command run with json
// output mode. The data is expected to be a CheckOutput struct serialized
// to json. In addition to deserializing, this function will convert the result
// to a CheckResults struct.
func parseJSONCheckOutput(data []byte) (CheckResults, error) {
var checks CheckOutput
err := json.Unmarshal(data, &checks)
if err != nil {
return CheckResults{}, err
}
results := []CheckResult{}
for _, category := range checks.Categories {
for _, check := range category.Checks {
var err error
if check.Error != "" {
err = errors.New(check.Error)
}
results = append(results, CheckResult{
Category: category.Name,
Description: check.Description,
Err: err,
HintURL: check.Hint,
Warning: check.Result == CheckWarn,
})
}
}
return CheckResults{results}, nil
}
func printResultDescription(wout io.Writer, status string, result *CheckResult) {
fmt.Fprintf(wout, "%s %s\n", status, result.Description)
@ -401,28 +288,6 @@ func restartSpinner(spin *spinner.Spinner, result *CheckResult) {
}
}
// When running in short mode, we defer writing the header
// until the first time we print a warning or error result.
func printHeader(wout io.Writer, headerPrinted bool, hc Runner) bool {
if headerPrinted {
return headerPrinted
}
switch v := hc.(type) {
case *HealthChecker:
if v.IsMainCheckCommand {
PrintChecksHeader(wout, CoreHeader)
headerPrinted = true
}
// When RunExtensionChecks called
case CheckResults:
PrintChecksHeader(wout, extensionsHeader)
headerPrinted = true
}
return headerPrinted
}
func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult) CategoryID {
if lastCategory == result.Category {
return lastCategory

View File

@ -56,7 +56,7 @@ func TestSmoke(t *testing.T) {
"'kubectl apply' command failed\n%s", out)
}
// Wait for pods to in smoke-test dpeloyment to come up
// Wait for pods to in smoke-test deployment to come up
for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
//nolint:errorlint

View File

@ -1,6 +1,3 @@
Linkerd core checks
===================
kubernetes-api
--------------
√ can initialize the client

View File

@ -1,6 +1,3 @@
Linkerd core checks
===================
kubernetes-api
--------------
√ can initialize the client