Run extension checks when linkerd check is invoked (#5647)

* Run extension checks when linkerd check is invoked

This change allows the linkerd check command to also run any known
linkerd extension commands that have been installed in the cluster. It
does this by first querying for any namespace that has the label
selector `linkerd.io/extension` and then runs the subcommands for either
`jaeger`, `multicluster` or `viz`. This change runs basic namespace
healthchecks for extensions that aren't part of the Linkerd extension suite.

Fixes #5233
This commit is contained in:
Dennis Adjei-Baah 2021-02-11 10:50:16 -06:00 committed by GitHub
parent 96e078421c
commit e4069b47e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 329 additions and 94 deletions

View File

@ -10,9 +10,14 @@ import (
"time"
"github.com/linkerd/linkerd2/cli/flag"
jaegerCmd "github.com/linkerd/linkerd2/jaeger/cmd"
mcCmd "github.com/linkerd/linkerd2/multicluster/cmd"
charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
"github.com/linkerd/linkerd2/pkg/healthcheck"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/version"
vizCmd "github.com/linkerd/linkerd2/viz/cmd"
vizHealthCheck "github.com/linkerd/linkerd2/viz/pkg/healthcheck"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
valuespkg "helm.sh/helm/v3/pkg/cli/values"
@ -95,7 +100,7 @@ code.`,
Example: ` # Check that the Linkerd cluster-wide resource are installed correctly
linkerd check config`,
RunE: func(cmd *cobra.Command, args []string) error {
return configureAndRunChecks(cmd.Context(), stdout, stderr, configStage, options)
return configureAndRunChecks(cmd, stdout, stderr, configStage, options)
},
}
@ -129,7 +134,7 @@ non-zero exit code.`,
# Check that the Linkerd data plane proxies in the "app" namespace are up and running
linkerd check --proxy --namespace app`,
RunE: func(cmd *cobra.Command, args []string) error {
return configureAndRunChecks(cmd.Context(), stdout, stderr, "", options)
return configureAndRunChecks(cmd, stdout, stderr, "", options)
},
}
@ -141,7 +146,7 @@ non-zero exit code.`,
return cmd
}
func configureAndRunChecks(ctx context.Context, wout io.Writer, werr io.Writer, stage string, options *checkOptions) error {
func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, stage string, options *checkOptions) error {
err := options.validate()
if err != nil {
return fmt.Errorf("Validation error when executing check command: %v", err)
@ -165,7 +170,7 @@ func configureAndRunChecks(ctx context.Context, wout io.Writer, werr io.Writer,
} else {
checks = append(checks, healthcheck.LinkerdPreInstallCapabilityChecks)
}
installManifest, err = renderInstallManifest(ctx)
installManifest, err = renderInstallManifest(cmd.Context())
if err != nil {
return fmt.Errorf("Error rendering install manifest: %v", err)
}
@ -211,9 +216,141 @@ func configureAndRunChecks(ctx context.Context, wout io.Writer, werr io.Writer,
os.Exit(1)
}
err = runExtensionChecks(cmd, wout, werr, options)
if err != nil {
err = fmt.Errorf("failed to run extensions checks: %s", err)
fmt.Fprintln(werr, err)
os.Exit(1)
}
return nil
}
func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts *checkOptions) error {
kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
if err != nil {
return err
}
namespaces, err := kubeAPI.GetAllNamespacesWithExtensionLabel(cmd.Context())
if err != nil {
return err
}
// no extensions to check
if len(namespaces) == 0 {
return nil
}
if opts.output != healthcheck.JSONOutput {
headerTxt := "Linkerd extensions checks"
fmt.Fprintln(wout)
fmt.Fprintln(wout, headerTxt)
fmt.Fprintln(wout, strings.Repeat("=", len(headerTxt)))
}
for i, ns := range namespaces {
if opts.output != healthcheck.JSONOutput && i < len(namespaces) {
// add a new line to space out each check output
fmt.Fprintln(wout)
}
switch ns.Labels[k8s.LinkerdExtensionLabel] {
case jaegerCmd.JaegerExtensionName:
jaegerCheckCmd := jaegerCmd.NewCmdCheck()
err = executeExtensionCheck(cmd, "jaeger", jaegerCheckCmd.Flags())
case vizHealthCheck.VizExtensionName:
vizCmd := vizCmd.NewCmdCheck()
err = executeExtensionCheck(cmd, "viz", vizCmd.Flags())
case mcCmd.MulticlusterExtensionName:
mcCheckCmd := mcCmd.NewCmdCheck()
err = executeExtensionCheck(cmd, "multicluster", mcCheckCmd.Flags())
default:
// Since we don't have checks for extensions we don't support
// create a healthchecker that checks if the namespace exists
categoryID := healthcheck.CategoryID(ns.Labels[k8s.LinkerdExtensionLabel])
checkers := []healthcheck.Checker{}
checkers = append(checkers,
*healthcheck.NewChecker(fmt.Sprintf("%s extension Namespace exists", categoryID)).
WithHintAnchor(fmt.Sprintf("%s-ns-exists", categoryID)).
Fatal().
WithCheck(func(ctx context.Context) error {
_, err := kubeAPI.GetNamespaceWithExtensionLabel(ctx, string(categoryID))
if err != nil {
return err
}
return nil
}))
hc := healthcheck.NewHealthChecker([]healthcheck.CategoryID{categoryID},
&healthcheck.Options{
ControlPlaneNamespace: controlPlaneNamespace,
CNINamespace: cniNamespace,
DataPlaneNamespace: opts.namespace,
KubeConfig: kubeconfigPath,
KubeContext: kubeContext,
Impersonate: impersonate,
ImpersonateGroup: impersonateGroup,
APIAddr: apiAddr,
VersionOverride: opts.versionOverride,
RetryDeadline: time.Now().Add(opts.wait),
CNIEnabled: opts.cniEnabled,
})
hc.AppendCategories(*healthcheck.NewCategory(categoryID, checkers, true))
success := healthcheck.RunChecks(wout, werr, hc, opts.output)
if !success {
os.Exit(1)
}
}
if err != nil {
return err
}
}
return nil
}
func getGlobalFlags(lf *pflag.FlagSet) []string {
cmdLineFlags := []string{}
lf.VisitAll(func(f *pflag.Flag) {
val := f.Value.String()
if val != "" {
cmdLineFlags = append(cmdLineFlags, fmt.Sprintf("--%s=%s", f.Name, val))
}
})
return cmdLineFlags
}
func getSharedFlags(lf *pflag.FlagSet, lf2 *pflag.FlagSet) []string {
cmdLineFlags := []string{}
lf.VisitAll(func(f *pflag.Flag) {
val := f.Value.String()
if val == "" {
// skip processing empty flags
return
}
if lf2.Lookup(f.Name) != nil {
cmdLineFlags = append(cmdLineFlags, fmt.Sprintf("--%s=%s", f.Name, val))
}
})
return cmdLineFlags
}
func executeExtensionCheck(currentCmd *cobra.Command, extension string, lf *pflag.FlagSet) error {
rootCmd := currentCmd.Root()
globalFlags := getGlobalFlags(rootCmd.PersistentFlags())
localFlags := getSharedFlags(currentCmd.Flags(), lf)
args := []string{extension, "check"}
args = append(args, globalFlags...)
args = append(args, localFlags...)
rootCmd.SetArgs(args)
return rootCmd.Execute()
}
func renderInstallManifest(ctx context.Context) (string, error) {
values, err := charts.NewValues()
if err != nil {

View File

@ -14,11 +14,11 @@ import (
const (
// jaegerExtensionName is the name of jaeger extension
jaegerExtensionName = "linkerd-jaeger"
// JaegerExtensionName is the name of jaeger extension
JaegerExtensionName = "linkerd-jaeger"
// linkerdJaegerExtensionCheck adds checks related to the jaeger extension
linkerdJaegerExtensionCheck healthcheck.CategoryID = jaegerExtensionName
linkerdJaegerExtensionCheck healthcheck.CategoryID = JaegerExtensionName
)
var (
@ -40,7 +40,7 @@ func jaegerCategory(hc *healthcheck.HealthChecker) *healthcheck.Category {
Fatal().
WithCheck(func(ctx context.Context) error {
// Get jaeger Extension Namespace
ns, err := hc.KubeAPIClient().GetNamespaceWithExtensionLabel(ctx, jaegerExtensionName)
ns, err := hc.KubeAPIClient().GetNamespaceWithExtensionLabel(ctx, JaegerExtensionName)
if err != nil {
return err
}
@ -146,7 +146,8 @@ func (options *checkOptions) validate() error {
return nil
}
func newCmdCheck() *cobra.Command {
// NewCmdCheck generates a new cobra command for the jaeger extension.
func NewCmdCheck() *cobra.Command {
options := newCheckOptions()
cmd := &cobra.Command{
Use: "check [flags]",
@ -165,8 +166,8 @@ code.`,
},
}
cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.PersistentFlags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
cmd.Flags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.Flags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
return cmd
}
@ -178,8 +179,6 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
}
checks := []healthcheck.CategoryID{
healthcheck.KubernetesAPIChecks,
healthcheck.LinkerdControlPlaneExistenceChecks,
linkerdJaegerExtensionCheck,
}
@ -193,6 +192,13 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
RetryDeadline: time.Now().Add(options.wait),
})
err = hc.InitializeKubeAPIClient()
if err != nil {
err = fmt.Errorf("Error initializing k8s API client: %s", err)
fmt.Fprintln(werr, err)
os.Exit(1)
}
hc.AppendCategories(*jaegerCategory(hc))
success := healthcheck.RunChecks(wout, werr, hc, options.output)

View File

@ -79,7 +79,7 @@ func newCmdDashboard() *cobra.Command {
return err
}
jaegerNamespace, err := k8sAPI.GetNamespaceWithExtensionLabel(cmd.Context(), jaegerExtensionName)
jaegerNamespace, err := k8sAPI.GetNamespaceWithExtensionLabel(cmd.Context(), JaegerExtensionName)
if err != nil {
return err
}

View File

@ -64,7 +64,7 @@ func NewCmdJaeger() *cobra.Command {
jaegerCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
jaegerCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
jaegerCmd.AddCommand(newCmdInstall())
jaegerCmd.AddCommand(newCmdCheck())
jaegerCmd.AddCommand(NewCmdCheck())
jaegerCmd.AddCommand(newCmdUninstall())
jaegerCmd.AddCommand(newCmdDashboard())

View File

@ -25,11 +25,11 @@ import (
)
const (
// multiclusterExtensionName is the name of the multicluster extension
multiclusterExtensionName = "linkerd-multicluster"
// MulticlusterExtensionName is the name of the multicluster extension
MulticlusterExtensionName = "linkerd-multicluster"
// linkerdMulticlusterExtensionCheck adds checks related to the multicluster extension
linkerdMulticlusterExtensionCheck healthcheck.CategoryID = multiclusterExtensionName
linkerdMulticlusterExtensionCheck healthcheck.CategoryID = MulticlusterExtensionName
linkerdServiceMirrorServiceAccountName = "linkerd-service-mirror-%s"
linkerdServiceMirrorComponentName = "service-mirror"
@ -67,7 +67,8 @@ func newHealthChecker(linkerdHC *healthcheck.HealthChecker) *healthChecker {
}
}
func newCmdCheck() *cobra.Command {
// NewCmdCheck generates a new cobra command for the multicluster extension.
func NewCmdCheck() *cobra.Command {
options := newCheckOptions()
cmd := &cobra.Command{
Use: "check [flags]",
@ -84,7 +85,7 @@ non-zero exit code.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Get the multicluster extension namespace
kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
_, err = kubeAPI.GetNamespaceWithExtensionLabel(context.Background(), multiclusterExtensionName)
_, err = kubeAPI.GetNamespaceWithExtensionLabel(context.Background(), MulticlusterExtensionName)
if err != nil {
err = fmt.Errorf("%w; install by running `linkerd multicluster install | kubectl apply -f -`", err)
fmt.Fprintln(os.Stderr, err.Error())
@ -93,8 +94,8 @@ non-zero exit code.`,
return configureAndRunChecks(stdout, stderr, options)
},
}
cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.PersistentFlags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
cmd.Flags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.Flags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
return cmd
}
@ -104,8 +105,6 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
return fmt.Errorf("Validation error when executing check command: %v", err)
}
checks := []healthcheck.CategoryID{
healthcheck.KubernetesAPIChecks,
healthcheck.LinkerdControlPlaneExistenceChecks,
linkerdMulticlusterExtensionCheck,
}
linkerdHC := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
@ -117,6 +116,21 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
APIAddr: apiAddr,
RetryDeadline: time.Now().Add(options.wait),
})
err = linkerdHC.InitializeKubeAPIClient()
if err != nil {
err = fmt.Errorf("Error initializing k8s API client: %s", err)
fmt.Fprintln(werr, err)
os.Exit(1)
}
err = linkerdHC.InitializeLinkerdGlobalConfig(context.Background())
if err != nil {
err = fmt.Errorf("Failed to fetch linkerd config: %s", err)
fmt.Fprintln(werr, err)
os.Exit(1)
}
hc := newHealthChecker(linkerdHC)
category := multiclusterCategory(hc)
if err != nil {

View File

@ -81,7 +81,7 @@ components on a cluster, manage credentials and link clusters together.`,
multiclusterCmd.AddCommand(newLinkCommand())
multiclusterCmd.AddCommand(newUnlinkCommand())
multiclusterCmd.AddCommand(newMulticlusterInstallCommand())
multiclusterCmd.AddCommand(newCmdCheck())
multiclusterCmd.AddCommand(NewCmdCheck())
multiclusterCmd.AddCommand(newMulticlusterUninstallCommand())
multiclusterCmd.AddCommand(newGatewaysCommand())
multiclusterCmd.AddCommand(newAllowCommand())

View File

@ -442,6 +442,37 @@ func NewHealthChecker(categoryIDs []CategoryID, options *Options) *HealthChecker
return hc
}
// InitializeKubeAPIClient creates a client for the HealthChecker. It avoids
// having to require the KubernetesAPIChecks check to run in order for the
// HealthChecker to run other checks.
func (hc *HealthChecker) InitializeKubeAPIClient() error {
k8sAPI, err := k8s.NewAPI(hc.KubeConfig, hc.KubeContext, hc.Impersonate, hc.ImpersonateGroup, RequestTimeout)
if err != nil {
return err
}
hc.kubeAPI = k8sAPI
return nil
}
// InitializeLinkerdGlobalConfig populates the linkerd config object in the
// healthchecker. It avoids having to require the LinkerdControlPlaneExistenceChecks
// check to run before running other checks
func (hc *HealthChecker) InitializeLinkerdGlobalConfig(ctx context.Context) error {
uuid, l5dConfig, err := hc.checkLinkerdConfigConfigMap(ctx)
if err != nil {
return err
}
if l5dConfig != nil {
hc.CNIEnabled = l5dConfig.GetGlobal().CNIEnabled
}
hc.uuid = uuid
hc.linkerdConfig = l5dConfig
return nil
}
// AppendCategories returns a HealthChecker instance appending the provided Categories
func (hc *HealthChecker) AppendCategories(categories ...Category) *HealthChecker {
hc.categories = append(hc.categories, categories...)
@ -474,7 +505,7 @@ func (hc *HealthChecker) allCategories() []Category {
hintAnchor: "k8s-api",
fatal: true,
check: func(context.Context) (err error) {
hc.kubeAPI, err = k8s.NewAPI(hc.KubeConfig, hc.KubeContext, hc.Impersonate, hc.ImpersonateGroup, RequestTimeout)
err = hc.InitializeKubeAPIClient()
return
},
},
@ -667,11 +698,7 @@ func (hc *HealthChecker) allCategories() []Category {
hintAnchor: "l5d-existence-linkerd-config",
fatal: true,
check: func(ctx context.Context) (err error) {
hc.uuid, hc.linkerdConfig, err = hc.checkLinkerdConfigConfigMap(ctx)
if hc.linkerdConfig == nil {
return errors.New("failed to load linkerd-config")
}
hc.CNIEnabled = hc.linkerdConfig.GetGlobal().CNIEnabled
err = hc.InitializeLinkerdGlobalConfig(ctx)
return
},
},

View File

@ -166,13 +166,24 @@ func (kubeAPI *KubernetesAPI) GetReplicaSets(ctx context.Context, namespace stri
return replicaSetList.Items, nil
}
// GetNamespaceWithExtensionLabel gets the namespace with the LinkerdExtensionLabel label value of `value`
func (kubeAPI *KubernetesAPI) GetNamespaceWithExtensionLabel(ctx context.Context, value string) (*corev1.Namespace, error) {
// GetAllNamespacesWithExtensionLabel gets all namespaces with the linkerd.io/extension label key
func (kubeAPI *KubernetesAPI) GetAllNamespacesWithExtensionLabel(ctx context.Context) ([]corev1.Namespace, error) {
namespaces, err := kubeAPI.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: LinkerdExtensionLabel})
if err != nil {
return nil, err
}
for _, ns := range namespaces.Items {
return namespaces.Items, nil
}
// GetNamespaceWithExtensionLabel gets the namespace with the LinkerdExtensionLabel label value of `value`
func (kubeAPI *KubernetesAPI) GetNamespaceWithExtensionLabel(ctx context.Context, value string) (*corev1.Namespace, error) {
namespaces, err := kubeAPI.GetAllNamespacesWithExtensionLabel(ctx)
if err != nil {
return nil, err
}
for _, ns := range namespaces {
if ns.Labels[LinkerdExtensionLabel] == value {
return &ns, err
}

View File

@ -84,7 +84,9 @@ var (
//skippedInboundPorts lists some ports to be marked as skipped, which will
// be verified in test/integration/inject
skippedInboundPorts = "1234,5678"
skippedInboundPorts = "1234,5678"
multiclusterExtensionName = "linkerd-multicluster"
vizExtensionName = "linkerd-viz"
)
//////////////////////
@ -537,6 +539,7 @@ func TestInstallMulticluster(t *testing.T) {
testutil.AnnotatedFatalf(t, "'helm install' command failed",
"'helm install' command failed\n%s\n%s", stdout, stderr)
}
TestHelper.AddInstalledExtension(multiclusterExtensionName)
} else if TestHelper.Multicluster() {
exec := append([]string{"multicluster"}, []string{
"install",
@ -552,6 +555,7 @@ func TestInstallMulticluster(t *testing.T) {
testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
"'kubectl apply' command failed\n%s", out)
}
TestHelper.AddInstalledExtension(multiclusterExtensionName)
}
}
@ -625,6 +629,7 @@ func TestUpgradeHelm(t *testing.T) {
testutil.AnnotatedFatalf(t, "'helm install' command failed",
"'helm install' command failed\n%s\n%s", stdout, stderr)
}
TestHelper.AddInstalledExtension(vizExtensionName)
}
func TestRetrieveUidPostUpgrade(t *testing.T) {
@ -730,7 +735,8 @@ func TestVersionPostInstall(t *testing.T) {
func testCheckCommand(t *testing.T, stage string, expectedVersion string, namespace string, cliVersionOverride string, compareOutput bool) {
var cmd []string
var golden string
if stage == "proxy" {
proxyStage := "proxy"
if stage == proxyStage {
cmd = []string{"check", "--proxy", "--expected-version", expectedVersion, "--namespace", namespace, "--wait=0"}
// if TestHelper.GetMulticlusterHelmReleaseName() != "" || TestHelper.Multicluster() {
// golden = "check.multicluster.proxy.golden"
@ -771,11 +777,34 @@ func testCheckCommand(t *testing.T, stage string, expectedVersion string, namesp
return nil
}
err = TestHelper.ValidateOutput(out, golden)
err = TestHelper.ContainsOutput(out, golden)
if err != nil {
return fmt.Errorf("received unexpected output\n%s", err.Error())
}
for _, ext := range TestHelper.GetInstalledExtensions() {
if ext == multiclusterExtensionName {
// multicluster check --proxy and multicluster check have the same output
// so use the same golden file.
err = TestHelper.ContainsOutput(out, "check.multicluster.golden")
if err != nil {
return fmt.Errorf("received unexpected output\n%s", err.Error())
}
} else if ext == vizExtensionName {
if stage == proxyStage {
err = TestHelper.ContainsOutput(out, "check.viz.proxy.golden")
if err != nil {
return fmt.Errorf("received unexpected output\n%s", err.Error())
}
} else {
err = TestHelper.ContainsOutput(out, "check.viz.golden")
if err != nil {
return fmt.Errorf("received unexpected output\n%s", err.Error())
}
}
}
}
return nil
})
if err != nil {

View File

@ -1,18 +1,3 @@
kubernetes-api
--------------
√ can initialize the client
√ can query the Kubernetes API
linkerd-existence
-----------------
√ 'linkerd-config' config map exists
√ heartbeat ServiceAccount exist
√ control plane replica sets are ready
√ no unschedulable pods
√ controller pod is running
√ can initialize the client
√ can query the control plane API
linkerd-multicluster
--------------------
√ Link CRD exists

View File

@ -1,18 +1,3 @@
kubernetes-api
--------------
√ can initialize the client
√ can query the Kubernetes API
linkerd-existence
-----------------
√ 'linkerd-config' config map exists
√ heartbeat ServiceAccount exist
√ control plane replica sets are ready
√ no unschedulable pods
√ controller pod is running
√ can initialize the client
√ can query the control plane API
linkerd-viz
-----------
√ linkerd-viz Namespace exists

View File

@ -0,0 +1,22 @@
linkerd-viz
-----------
√ linkerd-viz Namespace exists
√ linkerd-viz ClusterRoles exist
√ linkerd-viz ClusterRoleBindings exist
√ tap API server has valid cert
√ tap API server cert is valid for at least 60 days
√ tap API service is running
√ linkerd-viz pods are injected
√ viz extension pods are running
√ prometheus is installed and configured correctly
√ grafana is installed and configured correctly
√ can initialize the client
√ viz extension self-check
linkerd-viz-data-plane
----------------------
√ data plane namespace exists
√ data plane proxy metrics are present in Prometheus
√ data-plane pods have tap enabled
Status check results are √

View File

@ -1,18 +1,3 @@
kubernetes-api
--------------
√ can initialize the client
√ can query the Kubernetes API
linkerd-existence
-----------------
√ 'linkerd-config' config map exists
√ heartbeat ServiceAccount exist
√ control plane replica sets are ready
√ no unschedulable pods
√ controller pod is running
√ can initialize the client
√ can query the control plane API
linkerd-jaeger
--------------
√ linkerd-jaeger extension Namespace exists

View File

@ -38,6 +38,7 @@ type TestHelper struct {
httpClient http.Client
KubernetesHelper
helm
installedExtensions []string
}
type helm struct {
@ -354,6 +355,18 @@ func (h *TestHelper) Calico() bool {
return h.calico
}
// AddInstalledExtension adds an extension name to installedExtensions to
// track the currently installed linkerd extensions.
func (h *TestHelper) AddInstalledExtension(extensionName string) {
h.installedExtensions = append(h.installedExtensions, extensionName)
}
// GetInstalledExtensions gets a list currently installed extensions
// in a test run.
func (h *TestHelper) GetInstalledExtensions() []string {
return h.installedExtensions
}
// CreateTLSSecret creates a TLS Kubernetes secret
func (h *TestHelper) CreateTLSSecret(name, root, cert, key string) error {
secret := fmt.Sprintf(`
@ -527,6 +540,22 @@ func (h *TestHelper) ValidateOutput(out, fixtureFile string) error {
return nil
}
// ContainsOutput validates that a string is a substring in the contents
// of a file in the test's testdata directory.
func (h *TestHelper) ContainsOutput(out, fixtureFile string) error {
expected, err := ReadFile("testdata/" + fixtureFile)
if err != nil {
return err
}
if !strings.Contains(out, expected) {
return fmt.Errorf(
"Expected:\n%s\nActual:\n%s", expected, out)
}
return nil
}
// CheckVersion validates the output of the "linkerd version" command.
func (h *TestHelper) CheckVersion(serverVersion string) error {
out, err := h.LinkerdRun("version")

View File

@ -32,7 +32,8 @@ func (options *checkOptions) validate() error {
return nil
}
func newCmdCheck() *cobra.Command {
// NewCmdCheck generates a new cobra command for the viz extension.
func NewCmdCheck() *cobra.Command {
options := newCheckOptions()
cmd := &cobra.Command{
Use: "check [flags]",
@ -52,10 +53,10 @@ code.`,
},
}
cmd.PersistentFlags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.PersistentFlags().BoolVar(&options.proxy, "proxy", options.proxy, "Also run data-plane checks, to determine if the data plane is healthy")
cmd.PersistentFlags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
cmd.Flags().StringVarP(&options.output, "output", "o", options.output, "Output format. One of: basic, json")
cmd.Flags().BoolVar(&options.proxy, "proxy", options.proxy, "Also run data-plane checks, to determine if the data plane is healthy")
cmd.Flags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
return cmd
}
@ -65,12 +66,7 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
return fmt.Errorf("Validation error when executing check command: %v", err)
}
checks := []healthcheck.CategoryID{
healthcheck.KubernetesAPIChecks,
healthcheck.LinkerdControlPlaneExistenceChecks,
}
hc := vizHealthCheck.NewHealthChecker(checks, &healthcheck.Options{
hc := vizHealthCheck.NewHealthChecker([]healthcheck.CategoryID{}, &healthcheck.Options{
ControlPlaneNamespace: controlPlaneNamespace,
KubeConfig: kubeconfigPath,
KubeContext: kubeContext,
@ -80,6 +76,12 @@ func configureAndRunChecks(wout io.Writer, werr io.Writer, options *checkOptions
RetryDeadline: time.Now().Add(options.wait),
DataPlaneNamespace: options.namespace,
})
err = hc.InitializeKubeAPIClient()
if err != nil {
err = fmt.Errorf("Error initializing k8s API client: %s", err)
fmt.Fprintln(werr, err)
os.Exit(1)
}
hc.AppendCategories(hc.VizCategory())
if options.proxy {

View File

@ -78,8 +78,8 @@ func NewCmdViz() *cobra.Command {
vizCmd.AddCommand(NewCmdEdges())
vizCmd.AddCommand(NewCmdDashboard())
vizCmd.AddCommand(newCmdUninstall())
vizCmd.AddCommand(newCmdCheck())
vizCmd.AddCommand(newCmdProfile())
vizCmd.AddCommand(NewCmdCheck())
return vizCmd
}

View File

@ -20,6 +20,9 @@ import (
)
const (
// VizExtensionName is the name of the viz extension
VizExtensionName = "linkerd-viz"
// LinkerdVizExtensionCheck adds checks related to the Linkerd Viz extension
LinkerdVizExtensionCheck healthcheck.CategoryID = "linkerd-viz"