1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "fmt"
7 "io"
8 "os"
9 "path"
10 "strconv"
11 "strings"
12 "text/template"
13 "time"
14
15 "github.com/linkerd/linkerd2/cli/flag"
16 "github.com/linkerd/linkerd2/pkg/charts"
17 l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
18 "github.com/linkerd/linkerd2/pkg/charts/static"
19 flagspkg "github.com/linkerd/linkerd2/pkg/flags"
20 "github.com/linkerd/linkerd2/pkg/healthcheck"
21 "github.com/linkerd/linkerd2/pkg/k8s"
22 "github.com/linkerd/linkerd2/pkg/tree"
23 "github.com/spf13/cobra"
24 "helm.sh/helm/v3/pkg/chart/loader"
25 "helm.sh/helm/v3/pkg/chartutil"
26 valuespkg "helm.sh/helm/v3/pkg/cli/values"
27 "helm.sh/helm/v3/pkg/engine"
28 corev1 "k8s.io/api/core/v1"
29 kerrors "k8s.io/apimachinery/pkg/api/errors"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/util/intstr"
32 "sigs.k8s.io/yaml"
33 )
34
35 const (
36 helmDefaultChartNameCrds = "linkerd-crds"
37 helmDefaultChartNameCP = "linkerd-control-plane"
38
39 errMsgCannotInitializeClient = `Unable to install the Linkerd control plane. Cannot connect to the Kubernetes cluster:
40
41 %s
42
43 You can use the --ignore-cluster flag if you just want to generate the installation config.
44 `
45
46 errMsgLinkerdConfigResourceConflict = "Can't install the Linkerd control plane in the '%s' namespace. Reason: %s.\nRun the command `linkerd upgrade`, if you are looking to upgrade Linkerd.\n"
47 )
48
49 var (
50 TemplatesCrdFiles = []string{
51 "templates/policy/authorization-policy.yaml",
52 "templates/policy/httproute.yaml",
53 "templates/policy/meshtls-authentication.yaml",
54 "templates/policy/network-authentication.yaml",
55 "templates/policy/server-authorization.yaml",
56 "templates/policy/server.yaml",
57 "templates/serviceprofile.yaml",
58 "templates/gateway.networking.k8s.io_httproutes.yaml",
59 "templates/gateway.networking.k8s.io_grpcroutes.yaml",
60 "templates/workload/external-workload.yaml",
61 }
62
63 TemplatesControlPlane = []string{
64 "templates/namespace.yaml",
65 "templates/identity-rbac.yaml",
66 "templates/destination-rbac.yaml",
67 "templates/heartbeat-rbac.yaml",
68 "templates/podmonitor.yaml",
69 "templates/proxy-injector-rbac.yaml",
70 "templates/psp.yaml",
71 "templates/config.yaml",
72 "templates/config-rbac.yaml",
73 "templates/identity.yaml",
74 "templates/destination.yaml",
75 "templates/heartbeat.yaml",
76 "templates/proxy-injector.yaml",
77 }
78
79 ignoreCluster bool
80 )
81
82
83 func newCmdInstall() *cobra.Command {
84 values, err := l5dcharts.NewValues()
85 if err != nil {
86 fmt.Fprintln(os.Stderr, err.Error())
87 os.Exit(1)
88 }
89 var crds bool
90 var options valuespkg.Options
91
92 installOnlyFlags, installOnlyFlagSet := makeInstallFlags(values)
93 installUpgradeFlags, installUpgradeFlagSet, err := makeInstallUpgradeFlags(values)
94 if err != nil {
95 fmt.Fprint(os.Stderr, err.Error())
96 os.Exit(1)
97 }
98 proxyFlags, proxyFlagSet := makeProxyFlags(values)
99
100 flags := flattenFlags(installOnlyFlags, installUpgradeFlags, proxyFlags)
101
102 cmd := &cobra.Command{
103 Use: "install [flags]",
104 Args: cobra.NoArgs,
105 Short: "Output Kubernetes configs to install Linkerd",
106 Long: `Output Kubernetes configs to install Linkerd.
107
108 This command provides all Kubernetes configs necessary to install the Linkerd
109 control plane.`,
110 Example: ` # Install CRDs first.
111 linkerd install --crds | kubectl apply -f -
112
113 # Install the core control plane.
114 linkerd install | kubectl apply -f -
115
116 The installation can be configured by using the --set, --values, --set-string and --set-file flags.
117 A full list of configurable values can be found at https://artifacthub.io/packages/helm/linkerd2/linkerd-control-plane#values`,
118 RunE: func(cmd *cobra.Command, args []string) error {
119 var k8sAPI *k8s.KubernetesAPI
120 if !ignoreCluster {
121
122 if err := errAfterRunningChecks(values.CNIEnabled); err != nil {
123 fmt.Fprintf(os.Stderr, errMsgCannotInitializeClient, err)
124 os.Exit(1)
125 }
126
127
128 k8sAPI, err = k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 30*time.Second)
129 if err != nil {
130 return err
131 }
132
133 if !crds {
134 crds := bytes.Buffer{}
135 err := renderCRDs(&crds, options)
136 if err != nil {
137 fmt.Fprintf(os.Stderr, "%q", err)
138 os.Exit(1)
139 }
140 err = healthcheck.CheckCustomResourceDefinitions(cmd.Context(), k8sAPI, crds.String())
141 if err != nil {
142 fmt.Fprintln(os.Stderr, "Linkerd CRDs must be installed first. Run linkerd install with the --crds flag.")
143 os.Exit(1)
144 }
145 }
146 }
147
148 if crds {
149
150
151 if err = installCRDs(cmd.Context(), k8sAPI, os.Stdout, options); err != nil {
152 return err
153 }
154
155 fmt.Fprintln(os.Stderr, "Rendering Linkerd CRDs...")
156 fmt.Fprintln(os.Stderr, "Next, run `linkerd install | kubectl apply -f -` to install the control plane.")
157 fmt.Fprintln(os.Stderr)
158 return nil
159 }
160
161 return installControlPlane(cmd.Context(), k8sAPI, os.Stdout, values, flags, options)
162 },
163 }
164
165 cmd.Flags().AddFlagSet(installOnlyFlagSet)
166 cmd.Flags().AddFlagSet(installUpgradeFlagSet)
167 cmd.Flags().AddFlagSet(proxyFlagSet)
168 cmd.Flags().BoolVar(&crds, "crds", false, "Install Linkerd CRDs")
169 cmd.PersistentFlags().BoolVar(&ignoreCluster, "ignore-cluster", false,
170 "Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)")
171
172 flagspkg.AddValueOptionsFlags(cmd.Flags(), &options)
173
174 return cmd
175 }
176
177 func checkNoConfig(ctx context.Context, k8sAPI *k8s.KubernetesAPI) error {
178 if k8sAPI == nil {
179
180 return nil
181 }
182
183
184 _, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
185 if err == nil {
186 fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
187 os.Exit(1)
188 }
189 if !kerrors.IsNotFound(err) {
190 return err
191 }
192
193 return nil
194 }
195
196 func installCRDs(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, options valuespkg.Options) error {
197 if err := checkNoConfig(ctx, k8sAPI); err != nil {
198 return err
199 }
200
201 return renderCRDs(w, options)
202 }
203
204 func installControlPlane(ctx context.Context, k8sAPI *k8s.KubernetesAPI, w io.Writer, values *l5dcharts.Values, flags []flag.Flag, options valuespkg.Options) error {
205 err := flag.ApplySetFlags(values, flags)
206 if err != nil {
207 return err
208 }
209
210 if err := checkNoConfig(ctx, k8sAPI); err != nil {
211 return err
212 }
213
214
215 valuesOverrides, err := options.MergeValues(nil)
216 if err != nil {
217 return err
218 }
219
220 if k8sAPI != nil {
221
222 _, err = k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{})
223 if err == nil {
224 fmt.Fprintf(os.Stderr, errMsgLinkerdConfigResourceConflict, controlPlaneNamespace, "ConfigMap/linkerd-config already exists")
225 os.Exit(1)
226 }
227 if !kerrors.IsNotFound(err) {
228 return err
229 }
230
231 if !isRunAsRoot(valuesOverrides) {
232 err = healthcheck.CheckNodesHaveNonDockerRuntime(ctx, k8sAPI)
233 if err != nil {
234 fmt.Fprintln(os.Stderr, err)
235 os.Exit(1)
236 }
237 }
238
239
240
241
242 apiSrvPorts := getApiServerPorts(ctx, k8sAPI)
243 if apiSrvPorts != "" {
244 values.ProxyInit.KubeAPIServerPorts = apiSrvPorts
245 }
246 }
247
248 err = initializeIssuerCredentials(ctx, k8sAPI, values)
249 if err != nil {
250 return err
251 }
252
253 err = validateValues(ctx, k8sAPI, values)
254 if err != nil {
255 return err
256 }
257
258 return renderControlPlane(w, values, valuesOverrides)
259 }
260
261 func isRunAsRoot(values map[string]interface{}) bool {
262 if proxyInit, ok := values["proxyInit"]; ok {
263 if val, ok := proxyInit.(map[string]interface{})["runAsRoot"]; ok {
264 if truth, ok := template.IsTrue(val); ok {
265 return truth
266 }
267 }
268 }
269 return false
270 }
271
272
273
274
275 func renderChartToBuffer(files []*loader.BufferedFile, values map[string]interface{}, valuesOverrides map[string]interface{}) (*bytes.Buffer, chartutil.Values, error) {
276
277 var partials []*loader.BufferedFile
278 for _, template := range charts.L5dPartials {
279 partials = append(partials, &loader.BufferedFile{Name: template})
280 }
281 if err := charts.FilesReader(static.Templates, "", partials); err != nil {
282 return nil, nil, err
283 }
284 chart, err := loader.LoadFiles(append(files, partials...))
285 if err != nil {
286 return nil, nil, err
287 }
288
289
290 chart.Values = values
291
292 vals, err := chartutil.CoalesceValues(chart, valuesOverrides)
293 if err != nil {
294 return nil, nil, err
295 }
296
297 fullValues := map[string]interface{}{
298 "Values": vals,
299 "Release": map[string]interface{}{
300 "Namespace": controlPlaneNamespace,
301 "Service": "CLI",
302 },
303 }
304
305
306 renderedTemplates, err := engine.Render(chart, fullValues)
307 if err != nil {
308 return nil, nil, fmt.Errorf("failed to render the template: %w", err)
309 }
310
311
312 var buf bytes.Buffer
313 for _, tmpl := range chart.Templates {
314 t := path.Join(chart.Metadata.Name, tmpl.Name)
315 if _, err := buf.WriteString(renderedTemplates[t]); err != nil {
316 return nil, nil, err
317 }
318 }
319
320 return &buf, vals, nil
321 }
322
323 func renderCRDs(w io.Writer, options valuespkg.Options) error {
324 files := []*loader.BufferedFile{
325 {Name: chartutil.ChartfileName},
326 }
327 for _, template := range TemplatesCrdFiles {
328 files = append(files, &loader.BufferedFile{Name: template})
329 }
330 if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCrds+"/", files); err != nil {
331 return err
332 }
333
334
335 valuesFile := &loader.BufferedFile{Name: l5dcharts.HelmChartDirCrds + "/values.yaml"}
336 if err := charts.ReadFile(static.Templates, "/", valuesFile); err != nil {
337 return err
338 }
339
340
341
342
343 defaultValues := make(map[string]interface{})
344 err := yaml.Unmarshal(valuesFile.Data, &defaultValues)
345 if err != nil {
346 return err
347 }
348 defaultValues["cliVersion"] = k8s.CreatedByAnnotationValue()
349
350
351 valuesOverrides, err := options.MergeValues(nil)
352 if err != nil {
353 return err
354 }
355
356 buf, _, err := renderChartToBuffer(files, defaultValues, valuesOverrides)
357 if err != nil {
358 return err
359 }
360
361 _, err = w.Write(buf.Bytes())
362 return err
363 }
364
365 func renderControlPlane(w io.Writer, values *l5dcharts.Values, valuesOverrides map[string]interface{}) error {
366 files := []*loader.BufferedFile{
367 {Name: chartutil.ChartfileName},
368 }
369 for _, template := range TemplatesControlPlane {
370 files = append(files, &loader.BufferedFile{Name: template})
371 }
372 if err := charts.FilesReader(static.Templates, l5dcharts.HelmChartDirCP+"/", files); err != nil {
373 return err
374 }
375
376 valuesMap, err := values.ToMap()
377 if err != nil {
378 return err
379 }
380 buf, vals, err := renderChartToBuffer(files, valuesMap, valuesOverrides)
381 if err != nil {
382 return err
383 }
384
385 overrides, err := renderOverrides(vals, false)
386 if err != nil {
387 return err
388 }
389 buf.WriteString(yamlSep)
390 buf.WriteString(string(overrides))
391
392 _, err = w.Write(buf.Bytes())
393 return err
394 }
395
396
397
398
399
400
401
402
403
404
405
406
407 func renderOverrides(values chartutil.Values, stringData bool) ([]byte, error) {
408 defaults, err := l5dcharts.NewValues()
409 if err != nil {
410 return nil, err
411 }
412
413 delete(values, "configs")
414 delete(values, "partials")
415
416 overrides, err := tree.Diff(defaults, values)
417 if err != nil {
418 return nil, err
419 }
420
421 overridesBytes, err := yaml.Marshal(overrides)
422 if err != nil {
423 return nil, err
424 }
425
426 secret := corev1.Secret{
427 TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
428 ObjectMeta: metav1.ObjectMeta{
429 Name: "linkerd-config-overrides",
430 Namespace: controlPlaneNamespace,
431 Labels: map[string]string{
432 k8s.ControllerNSLabel: controlPlaneNamespace,
433 },
434 },
435 }
436 if stringData {
437 secret.StringData = map[string]string{
438 "linkerd-config-overrides": string(overridesBytes),
439 }
440 } else {
441 secret.Data = map[string][]byte{
442 "linkerd-config-overrides": overridesBytes,
443 }
444 }
445 bytes, err := yaml.Marshal(secret)
446 if err != nil {
447 return nil, err
448 }
449 return bytes, nil
450 }
451
452 func errAfterRunningChecks(cniEnabled bool) error {
453 checks := []healthcheck.CategoryID{
454 healthcheck.KubernetesAPIChecks,
455 }
456 hc := healthcheck.NewHealthChecker(checks, &healthcheck.Options{
457 ControlPlaneNamespace: controlPlaneNamespace,
458 KubeConfig: kubeconfigPath,
459 Impersonate: impersonate,
460 ImpersonateGroup: impersonateGroup,
461 KubeContext: kubeContext,
462 APIAddr: apiAddr,
463 CNIEnabled: cniEnabled,
464 })
465
466 var err error
467 hc.RunChecks(func(result *healthcheck.CheckResult) {
468 if result.Err != nil {
469 err = result.Err
470 }
471 })
472
473 return err
474 }
475
476
477
478
479
480 func getApiServerPorts(ctx context.Context, api *k8s.KubernetesAPI) string {
481 service, err := api.CoreV1().Services("default").Get(ctx, "kubernetes", metav1.GetOptions{})
482 if err != nil {
483 return ""
484 }
485
486 ports := make([]string, 0)
487 for _, port := range service.Spec.Ports {
488 ports = append(ports, strconv.Itoa(int(port.Port)))
489
490
491 if port.TargetPort.Type == intstr.Int {
492 ports = append(ports, strconv.Itoa(port.TargetPort.IntValue()))
493 }
494 }
495
496 return strings.Join(ports, ",")
497 }
498
View as plain text