1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11 "strings"
12 "time"
13
14 charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
15 pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
16 "github.com/linkerd/linkerd2/pkg/healthcheck"
17 "github.com/linkerd/linkerd2/pkg/k8s"
18 "github.com/linkerd/linkerd2/pkg/version"
19 "github.com/spf13/cobra"
20 "github.com/spf13/pflag"
21 valuespkg "helm.sh/helm/v3/pkg/cli/values"
22 utilsexec "k8s.io/utils/exec"
23 )
24
25 type checkOptions struct {
26 versionOverride string
27 preInstallOnly bool
28 crdsOnly bool
29 dataPlaneOnly bool
30 wait time.Duration
31 namespace string
32 cniEnabled bool
33 output string
34 cliVersionOverride string
35 }
36
37 func newCheckOptions() *checkOptions {
38 return &checkOptions{
39 versionOverride: "",
40 preInstallOnly: false,
41 crdsOnly: false,
42 dataPlaneOnly: false,
43 wait: 300 * time.Second,
44 namespace: "",
45 cniEnabled: false,
46 output: tableOutput,
47 cliVersionOverride: "",
48 }
49 }
50
51
52 func (options *checkOptions) nonConfigFlagSet() *pflag.FlagSet {
53 flags := pflag.NewFlagSet("non-config-check", pflag.ExitOnError)
54
55 flags.BoolVar(&options.cniEnabled, "linkerd-cni-enabled", options.cniEnabled, "When running pre-installation checks (--pre), assume the linkerd-cni plugin is already installed, and a NET_ADMIN check is not needed")
56 flags.StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
57 flags.BoolVar(&options.preInstallOnly, "pre", options.preInstallOnly, "Only run pre-installation checks, to determine if the control plane can be installed")
58 flags.BoolVar(&options.crdsOnly, "crds", options.crdsOnly, "Only run checks which determine if the Linkerd CRDs have been installed")
59 flags.BoolVar(&options.dataPlaneOnly, "proxy", options.dataPlaneOnly, "Only run data-plane checks, to determine if the data plane is healthy")
60
61 return flags
62 }
63
64
65 func (options *checkOptions) checkFlagSet() *pflag.FlagSet {
66 flags := pflag.NewFlagSet("check", pflag.ExitOnError)
67
68 flags.StringVar(&options.versionOverride, "expected-version", options.versionOverride, "Overrides the version used when checking if Linkerd is running the latest version (mostly for testing)")
69 flags.StringVar(&options.cliVersionOverride, "cli-version-override", "", "Used to override the version of the cli (mostly for testing)")
70 flags.StringVarP(&options.output, "output", "o", options.output, "Output format. One of: table, json, short")
71 flags.DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
72
73 return flags
74 }
75
76 func (options *checkOptions) validate() error {
77 if options.preInstallOnly && options.dataPlaneOnly {
78 return errors.New("--pre and --proxy flags are mutually exclusive")
79 }
80 if options.preInstallOnly && options.crdsOnly {
81 return errors.New("--pre and --crds flags are mutually exclusive")
82 }
83 if !options.preInstallOnly && options.cniEnabled {
84 return errors.New("--linkerd-cni-enabled can only be used with --pre")
85 }
86 if options.output != tableOutput && options.output != jsonOutput && options.output != shortOutput {
87 return fmt.Errorf("Invalid output type '%s'. Supported output types are: %s, %s, %s", options.output, jsonOutput, tableOutput, shortOutput)
88 }
89 return nil
90 }
91
92 func newCmdCheck() *cobra.Command {
93 options := newCheckOptions()
94 checkFlags := options.checkFlagSet()
95 nonConfigFlags := options.nonConfigFlagSet()
96
97 cmd := &cobra.Command{
98 Use: "check [flags]",
99 Args: cobra.NoArgs,
100 Short: "Check the Linkerd installation for potential problems",
101 Long: `Check the Linkerd installation for potential problems.
102
103 The check command will perform a series of checks to validate that the linkerd
104 CLI and control plane are configured correctly. If the command encounters a
105 failure it will print additional information about the failure and exit with a
106 non-zero exit code.`,
107 Example: ` # Check that the Linkerd control plane is up and running
108 linkerd check
109
110 # Check that the Linkerd control plane can be installed in the "test" namespace
111 linkerd check --pre --linkerd-namespace test
112
113 # Check that the Linkerd data plane proxies in the "app" namespace are up and running
114 linkerd check --proxy --namespace app`,
115 RunE: func(cmd *cobra.Command, args []string) error {
116 return configureAndRunChecks(cmd, stdout, stderr, options)
117 },
118 }
119
120 cmd.PersistentFlags().AddFlagSet(checkFlags)
121 cmd.Flags().AddFlagSet(nonConfigFlags)
122
123 pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
124 kubeconfigPath, impersonate, impersonateGroup, kubeContext)
125
126 return cmd
127 }
128
129 func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, options *checkOptions) error {
130 err := options.validate()
131 if err != nil {
132 return fmt.Errorf("Validation error when executing check command: %w", err)
133 }
134
135 if options.cliVersionOverride != "" {
136 version.Version = options.cliVersionOverride
137 }
138
139 checks := []healthcheck.CategoryID{
140 healthcheck.KubernetesAPIChecks,
141 healthcheck.KubernetesVersionChecks,
142 healthcheck.LinkerdVersionChecks,
143 }
144
145 crdManifest := bytes.Buffer{}
146 err = renderCRDs(&crdManifest, valuespkg.Options{})
147 if err != nil {
148 return err
149 }
150 var installManifest string
151 var values *charts.Values
152 if options.preInstallOnly {
153 checks = append(checks, healthcheck.LinkerdPreInstallChecks)
154 if options.cniEnabled {
155 checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
156 }
157 values, installManifest, err = renderInstallManifest(cmd.Context())
158 if err != nil {
159 fmt.Fprintf(os.Stderr, "Error rendering install manifest: %s\n", err)
160 os.Exit(1)
161 }
162 } else if options.crdsOnly {
163 checks = append(checks, healthcheck.LinkerdCRDChecks)
164 } else {
165 checks = append(checks, healthcheck.LinkerdConfigChecks)
166
167 checks = append(checks, healthcheck.LinkerdControlPlaneExistenceChecks)
168 checks = append(checks, healthcheck.LinkerdIdentity)
169 checks = append(checks, healthcheck.LinkerdWebhooksAndAPISvcTLS)
170 checks = append(checks, healthcheck.LinkerdControlPlaneProxyChecks)
171
172 if options.dataPlaneOnly {
173 checks = append(checks, healthcheck.LinkerdDataPlaneChecks)
174 checks = append(checks, healthcheck.LinkerdIdentityDataPlane)
175 checks = append(checks, healthcheck.LinkerdOpaquePortsDefinitionChecks)
176 } else {
177 checks = append(checks, healthcheck.LinkerdControlPlaneVersionChecks)
178 checks = append(checks, healthcheck.LinkerdExtensionChecks)
179 }
180 checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
181 checks = append(checks, healthcheck.LinkerdHAChecks)
182 }
183
184 hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
185 IsMainCheckCommand: true,
186 ControlPlaneNamespace: controlPlaneNamespace,
187 CNINamespace: cniNamespace,
188 DataPlaneNamespace: options.namespace,
189 KubeConfig: kubeconfigPath,
190 KubeContext: kubeContext,
191 Impersonate: impersonate,
192 ImpersonateGroup: impersonateGroup,
193 APIAddr: apiAddr,
194 VersionOverride: options.versionOverride,
195 RetryDeadline: time.Now().Add(options.wait),
196 CNIEnabled: options.cniEnabled,
197 InstallManifest: installManifest,
198 CRDManifest: crdManifest.String(),
199 ChartValues: values,
200 })
201
202 success, warning := healthcheck.RunChecks(wout, werr, hc, options.output)
203
204 if !options.preInstallOnly && !options.crdsOnly {
205 extensionSuccess, extensionWarning, err := runExtensionChecks(cmd, wout, werr, options)
206 if err != nil {
207 fmt.Fprintf(werr, "Failed to run extensions checks: %s\n", err)
208 os.Exit(1)
209 }
210
211 success = success && extensionSuccess
212 warning = warning || extensionWarning
213 }
214
215 healthcheck.PrintChecksResult(wout, options.output, success, warning)
216
217 if !success {
218 os.Exit(1)
219 }
220
221 return nil
222 }
223
224 func runExtensionChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, opts *checkOptions) (bool, bool, error) {
225 kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
226 if err != nil {
227 return false, false, err
228 }
229
230 namespaces, err := kubeAPI.GetAllNamespacesWithExtensionLabel(cmd.Context())
231 if err != nil {
232 return false, false, err
233 }
234 nsLabels := []string{}
235 for _, ns := range namespaces {
236 ext := ns.Labels[k8s.LinkerdExtensionLabel]
237 nsLabels = append(nsLabels, ext)
238 }
239
240 exec := utilsexec.New()
241
242 extensions, missing := findExtensions(os.Getenv("PATH"), filepath.Glob, exec, nsLabels)
243
244
245 if len(extensions) == 0 && len(missing) == 0 {
246 return true, false, nil
247 }
248
249 extensionSuccess, extensionWarning := runExtensionsChecks(
250 wout, werr, extensions, missing, exec, getExtensionCheckFlags(cmd.Flags()), opts.output,
251 )
252 return extensionSuccess, extensionWarning, nil
253 }
254
255 func getExtensionCheckFlags(lf *pflag.FlagSet) []string {
256 extensionFlags := []string{
257 "api-addr", "context", "as", "as-group", "kubeconfig", "linkerd-namespace", "verbose",
258 "namespace", "proxy", "wait",
259 }
260 cmdLineFlags := []string{}
261 for _, flag := range extensionFlags {
262 f := lf.Lookup(flag)
263 if f != nil {
264 val := f.Value.String()
265 if val != "" {
266 cmdLineFlags = append(cmdLineFlags, fmt.Sprintf("--%s=%s", f.Name, val))
267 }
268 }
269 }
270 cmdLineFlags = append(cmdLineFlags, "--output=json")
271 return cmdLineFlags
272 }
273
274 func renderInstallManifest(ctx context.Context) (*charts.Values, string, error) {
275
276 k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 30*time.Second)
277 if err != nil {
278 return nil, "", err
279 }
280 values, err := charts.NewValues()
281 if err != nil {
282 return nil, "", err
283 }
284 err = initializeIssuerCredentials(ctx, k8sAPI, values)
285 if err != nil {
286 return nil, "", err
287 }
288
289
290 var b strings.Builder
291 err = renderControlPlane(&b, values, map[string]interface{}{})
292 if err != nil {
293 return nil, "", err
294 }
295 return values, b.String(), nil
296 }
297
View as plain text