mirror of https://github.com/linkerd/linkerd2.git
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:
parent
495c580efc
commit
e71266f2c9
|
|
@ -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
|
In particular, the `linkerd check` command will invoke the check command for
|
||||||
each extension installed in the cluster and will request json output. To
|
each extension installed in the cluster and will request json output.
|
||||||
preserve forwards compatibility, it is recommended that the check command should
|
`linkerd check` may optinally invoke your extension if not installed in the
|
||||||
ignore any unknown flags.
|
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
|
The extension may also implement further commands in addition to the ones
|
||||||
defined here.
|
defined here.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
valuespkg "helm.sh/helm/v3/pkg/cli/values"
|
valuespkg "helm.sh/helm/v3/pkg/cli/values"
|
||||||
|
utilsexec "k8s.io/utils/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type checkOptions struct {
|
type checkOptions struct {
|
||||||
|
|
@ -196,9 +198,6 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, o
|
||||||
ChartValues: values,
|
ChartValues: values,
|
||||||
})
|
})
|
||||||
|
|
||||||
if options.output == tableOutput {
|
|
||||||
healthcheck.PrintChecksHeader(wout, healthcheck.CoreHeader)
|
|
||||||
}
|
|
||||||
success, warning := healthcheck.RunChecks(wout, werr, hc, options.output)
|
success, warning := healthcheck.RunChecks(wout, werr, hc, options.output)
|
||||||
|
|
||||||
if !options.preInstallOnly && !options.crdsOnly {
|
if !options.preInstallOnly && !options.crdsOnly {
|
||||||
|
|
@ -231,19 +230,24 @@ func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
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
|
// no extensions to check
|
||||||
if len(namespaces) == 0 {
|
if len(extensions) == 0 && len(missing) == 0 {
|
||||||
return success, false, nil
|
return true, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
nsLabels := make([]string, len(namespaces))
|
extensionSuccess, extensionWarning := runExtensionsChecks(
|
||||||
for i, ns := range namespaces {
|
wout, werr, extensions, missing, exec, getExtensionCheckFlags(cmd.Flags()), opts.output,
|
||||||
nsLabels[i] = ns.Labels[k8s.LinkerdExtensionLabel]
|
)
|
||||||
}
|
|
||||||
|
|
||||||
extensionSuccess, extensionWarning := healthcheck.RunExtensionsChecks(wout, werr, nsLabels, getExtensionCheckFlags(cmd.Flags()), opts.output)
|
|
||||||
return extensionSuccess, extensionWarning, nil
|
return extensionSuccess, extensionWarning, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
package healthcheck
|
package healthcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
"github.com/briandowns/spinner"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/linkerd/linkerd2/pkg/version"
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,11 +24,6 @@ const (
|
||||||
// ShortOutput is used to specify the short output format
|
// ShortOutput is used to specify the short output format
|
||||||
ShortOutput = "short"
|
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
|
// DefaultHintBaseURL is the default base URL on the linkerd.io website
|
||||||
// that all check hints for the latest linkerd version point to. Each
|
// that all check hints for the latest linkerd version point to. Each
|
||||||
// check adds its own `hintAnchor` to specify a location on the page.
|
// check adds its own `hintAnchor` to specify a location on the page.
|
||||||
|
|
@ -47,6 +38,30 @@ var (
|
||||||
reStableVersion = regexp.MustCompile(`stable-(\d\.\d+)\.`)
|
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.
|
// CheckResults contains a slice of CheckResult structs.
|
||||||
type CheckResults struct {
|
type CheckResults struct {
|
||||||
Results []CheckResult
|
Results []CheckResult
|
||||||
|
|
@ -92,14 +107,6 @@ func (cr CheckResults) RunChecks(observer CheckObserver) (bool, bool) {
|
||||||
return success, warning
|
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.
|
// PrintChecksResult writes the checks result.
|
||||||
func PrintChecksResult(wout io.Writer, output string, success bool, warning bool) {
|
func PrintChecksResult(wout io.Writer, output string, success bool, warning bool) {
|
||||||
if output == JSONOutput {
|
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
|
// RunChecks runs the checks that are part of hc
|
||||||
func RunChecks(wout io.Writer, werr io.Writer, hc Runner, output string) (bool, bool) {
|
func RunChecks(wout io.Writer, werr io.Writer, hc Runner, output string) (bool, bool) {
|
||||||
if output == JSONOutput {
|
if output == JSONOutput {
|
||||||
|
|
@ -235,14 +153,12 @@ func runChecksTable(wout io.Writer, hc Runner, output string) (bool, bool) {
|
||||||
printResultDescription(wout, status, result)
|
printResultDescription(wout, status, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerPrinted bool
|
|
||||||
prettyPrintResultsShort := func(result *CheckResult) {
|
prettyPrintResultsShort := func(result *CheckResult) {
|
||||||
// bail out early and skip printing if we've got an okStatus
|
// bail out early and skip printing if we've got an okStatus
|
||||||
if result.Err == nil {
|
if result.Err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
headerPrinted = printHeader(wout, headerPrinted, hc)
|
|
||||||
lastCategory = printCategory(wout, lastCategory, result)
|
lastCategory = printCategory(wout, lastCategory, result)
|
||||||
|
|
||||||
spin.Stop()
|
spin.Stop()
|
||||||
|
|
@ -340,35 +256,6 @@ func runChecksJSON(wout io.Writer, werr io.Writer, hc Runner) (bool, bool) {
|
||||||
return success, warning
|
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) {
|
func printResultDescription(wout io.Writer, status string, result *CheckResult) {
|
||||||
fmt.Fprintf(wout, "%s %s\n", status, result.Description)
|
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 {
|
func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult) CategoryID {
|
||||||
if lastCategory == result.Category {
|
if lastCategory == result.Category {
|
||||||
return lastCategory
|
return lastCategory
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestSmoke(t *testing.T) {
|
||||||
"'kubectl apply' command failed\n%s", out)
|
"'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"} {
|
for _, deploy := range []string{"smoke-test-terminus", "smoke-test-gateway"} {
|
||||||
if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
|
if err := TestHelper.CheckPods(ctx, ns, deploy, 1); err != nil {
|
||||||
//nolint:errorlint
|
//nolint:errorlint
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
Linkerd core checks
|
|
||||||
===================
|
|
||||||
|
|
||||||
kubernetes-api
|
kubernetes-api
|
||||||
--------------
|
--------------
|
||||||
√ can initialize the client
|
√ can initialize the client
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
Linkerd core checks
|
|
||||||
===================
|
|
||||||
|
|
||||||
kubernetes-api
|
kubernetes-api
|
||||||
--------------
|
--------------
|
||||||
√ can initialize the client
|
√ can initialize the client
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue