1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "strconv"
11 "strings"
12 "time"
13
14 jsonpatch "github.com/evanphx/json-patch"
15 "github.com/linkerd/linkerd2/cli/flag"
16 "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
17 "github.com/linkerd/linkerd2/pkg/healthcheck"
18 "github.com/linkerd/linkerd2/pkg/inject"
19 "github.com/linkerd/linkerd2/pkg/k8s"
20 log "github.com/sirupsen/logrus"
21 "github.com/spf13/cobra"
22 "sigs.k8s.io/yaml"
23 )
24
25 const (
26
27 hostNetworkDesc = "pods do not use host networking"
28 sidecarDesc = "pods do not have a 3rd party proxy or initContainer already injected"
29 injectDisabledDesc = "pods are not annotated to disable injection"
30 unsupportedDesc = "at least one resource can be injected or annotated"
31 udpDesc = "pod specs do not include UDP ports"
32 automountServiceAccountTokenDesc = "pods do not have automountServiceAccountToken set to \"false\" or service account token projection is enabled"
33 slash = "/"
34 )
35
36 type resourceTransformerInject struct {
37 allowNsInject bool
38 injectProxy bool
39 values *linkerd2.Values
40 overrideAnnotations map[string]string
41 enableDebugSidecar bool
42 closeWaitTimeout time.Duration
43 }
44
45 func runInjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject, output string) int {
46 return transformInput(inputs, errWriter, outWriter, transformer, output)
47 }
48
49 func newCmdInject() *cobra.Command {
50 defaults, err := linkerd2.NewValues()
51 if err != nil {
52 fmt.Fprint(os.Stderr, err.Error())
53 os.Exit(1)
54 }
55 flags, proxyFlagSet := makeProxyFlags(defaults)
56 injectFlags, injectFlagSet := makeInjectFlags(defaults)
57 var manualOption, enableDebugSidecar bool
58 var closeWaitTimeout time.Duration
59 var output string
60
61 cmd := &cobra.Command{
62 Use: "inject [flags] CONFIG-FILE",
63 Short: "Add the Linkerd proxy to a Kubernetes config",
64 Long: `Add the Linkerd proxy to a Kubernetes config.
65
66 You can inject resources contained in a single file, inside a folder and its
67 sub-folders, or coming from stdin.`,
68 Example: ` # Inject all the deployments in the default namespace.
69 kubectl get deploy -o yaml | linkerd inject - | kubectl apply -f -
70
71 # Injecting a file from a remote URL
72 linkerd inject https://url.to/yml | kubectl apply -f -
73
74 # Inject all the resources inside a folder and its sub-folders.
75 linkerd inject <folder> | kubectl apply -f -`,
76 RunE: func(cmd *cobra.Command, args []string) error {
77 if len(args) < 1 {
78 return fmt.Errorf("please specify a kubernetes resource file")
79 }
80
81 values := defaults
82 if !ignoreCluster {
83 values, err = fetchConfigs(cmd.Context())
84 if err != nil {
85 return err
86 }
87 }
88
89 baseValues, err := values.DeepCopy()
90 if err != nil {
91 return err
92 }
93 err = flag.ApplySetFlags(values, append(flags, injectFlags...))
94 if err != nil {
95 return err
96 }
97
98 in, err := read(args[0])
99 if err != nil {
100 return err
101 }
102
103 overrideAnnotations := getOverrideAnnotations(values, baseValues)
104
105 transformer := &resourceTransformerInject{
106 allowNsInject: true,
107 injectProxy: manualOption,
108 values: values,
109 overrideAnnotations: overrideAnnotations,
110 enableDebugSidecar: enableDebugSidecar,
111 closeWaitTimeout: closeWaitTimeout,
112 }
113 exitCode := uninjectAndInject(in, stderr, stdout, transformer, output)
114 os.Exit(exitCode)
115 return nil
116 },
117 }
118
119 cmd.Flags().BoolVar(
120 &manualOption, "manual", manualOption,
121 "Include the proxy sidecar container spec in the YAML output (the auto-injector won't pick it up, so config annotations aren't supported) (default false)",
122 )
123
124 cmd.Flags().BoolVar(
125 &ignoreCluster, "ignore-cluster", false,
126 "Ignore the current Kubernetes cluster when checking for existing cluster configuration (default false)",
127 )
128
129 cmd.Flags().BoolVar(&enableDebugSidecar, "enable-debug-sidecar", enableDebugSidecar,
130 "Inject a debug sidecar for data plane debugging")
131
132 cmd.Flags().DurationVar(
133 &closeWaitTimeout, "close-wait-timeout", closeWaitTimeout,
134 "Sets nf_conntrack_tcp_timeout_close_wait")
135
136 cmd.Flags().StringVarP(&output, "output", "o", "yaml", "Output format, one of: json|yaml")
137
138 cmd.Flags().AddFlagSet(proxyFlagSet)
139 cmd.Flags().AddFlagSet(injectFlagSet)
140
141 return cmd
142 }
143
144 func uninjectAndInject(inputs []io.Reader, errWriter, outWriter io.Writer, transformer *resourceTransformerInject, output string) int {
145 var out bytes.Buffer
146 if exitCode := runUninjectSilentCmd(inputs, errWriter, &out, transformer.values, "yaml"); exitCode != 0 {
147 return exitCode
148 }
149 return runInjectCmd([]io.Reader{&out}, errWriter, outWriter, transformer, output)
150 }
151
152 func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Report, error) {
153 conf := inject.NewResourceConfig(rt.values, inject.OriginCLI, controlPlaneNamespace)
154
155 if rt.enableDebugSidecar {
156 conf.AppendPodAnnotation(k8s.ProxyEnableDebugAnnotation, "true")
157 }
158
159 if rt.closeWaitTimeout != time.Duration(0) {
160 conf.AppendPodAnnotation(k8s.CloseWaitTimeoutAnnotation, rt.closeWaitTimeout.String())
161 }
162
163 report, err := conf.ParseMetaAndYAML(bytes)
164 if err != nil {
165 return nil, nil, err
166 }
167
168 if conf.IsControlPlaneComponent() && !rt.injectProxy {
169 return nil, nil, errors.New("--manual must be set when injecting control plane components")
170 }
171
172 if conf.IsService() {
173 opaquePorts, ok := rt.overrideAnnotations[k8s.ProxyOpaquePortsAnnotation]
174 if ok {
175 annotations := map[string]string{k8s.ProxyOpaquePortsAnnotation: opaquePorts}
176 bytes, err = conf.AnnotateService(annotations)
177 report.Annotated = true
178 }
179 return bytes, []inject.Report{*report}, err
180 }
181 if rt.allowNsInject && conf.IsNamespace() {
182 bytes, err = conf.AnnotateNamespace(rt.overrideAnnotations)
183 report.Annotated = true
184 return bytes, []inject.Report{*report}, err
185 }
186 if conf.HasPodTemplate() && len(rt.overrideAnnotations) > 0 {
187 conf.AppendPodAnnotations(rt.overrideAnnotations)
188 report.Annotated = true
189 }
190
191 if ok, _ := report.Injectable(); !ok {
192 if errs := report.ThrowInjectError(); len(errs) > 0 {
193 return bytes, []inject.Report{*report}, fmt.Errorf("failed to inject %s%s%s: %w", report.Kind, slash, report.Name, concatErrors(errs, ", "))
194 }
195 return bytes, []inject.Report{*report}, nil
196 }
197
198 if rt.injectProxy {
199
200
201 delete(rt.overrideAnnotations, k8s.ProxyInjectAnnotation)
202 conf.AppendPodAnnotation(k8s.CreatedByAnnotation, k8s.CreatedByAnnotationValue())
203 } else if !rt.values.Proxy.IsIngress {
204
205 conf.AppendPodAnnotation(k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled)
206 }
207
208 patchJSON, err := conf.GetPodPatch(rt.injectProxy)
209 if err != nil {
210 return nil, nil, err
211 }
212
213 if len(patchJSON) == 0 {
214 return bytes, []inject.Report{*report}, nil
215 }
216 log.Infof("patch generated for: %s", report.ResName())
217 log.Debugf("patch: %s", patchJSON)
218 patch, err := jsonpatch.DecodePatch(patchJSON)
219 if err != nil {
220 return nil, nil, err
221 }
222 origJSON, err := yaml.YAMLToJSON(bytes)
223 if err != nil {
224 return nil, nil, err
225 }
226 injectedJSON, err := patch.Apply(origJSON)
227 if err != nil {
228 return nil, nil, err
229 }
230 injectedYAML, err := conf.JSONToYAML(injectedJSON)
231 if err != nil {
232 return nil, nil, err
233 }
234 return injectedYAML, []inject.Report{*report}, nil
235 }
236
237 func (resourceTransformerInject) generateReport(reports []inject.Report, output io.Writer) {
238 injected := []inject.Report{}
239 annotatable := false
240 hostNetwork := []string{}
241 sidecar := []string{}
242 udp := []string{}
243 injectDisabled := []string{}
244 automountServiceAccountTokenFalse := []string{}
245 warningsPrinted := verbose
246
247 for _, r := range reports {
248 if b, _ := r.Injectable(); b {
249 injected = append(injected, r)
250 }
251
252 if r.IsAnnotatable() {
253 annotatable = true
254 }
255
256 if r.HostNetwork {
257 hostNetwork = append(hostNetwork, r.ResName())
258 warningsPrinted = true
259 }
260
261 if r.Sidecar {
262 sidecar = append(sidecar, r.ResName())
263 warningsPrinted = true
264 }
265
266 if r.UDP {
267 udp = append(udp, r.ResName())
268 warningsPrinted = true
269 }
270
271 if r.InjectDisabled {
272 injectDisabled = append(injectDisabled, r.ResName())
273 warningsPrinted = true
274 }
275
276 if !r.AutomountServiceAccountToken {
277 automountServiceAccountTokenFalse = append(automountServiceAccountTokenFalse, r.ResName())
278 warningsPrinted = true
279 }
280 }
281
282
283
284
285
286
287 output.Write([]byte("\n"))
288
289 if len(hostNetwork) > 0 {
290 output.Write([]byte(fmt.Sprintf("%s \"hostNetwork: true\" detected in %s\n", warnStatus, strings.Join(hostNetwork, ", "))))
291 } else if verbose {
292 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, hostNetworkDesc)))
293 }
294
295 if len(sidecar) > 0 {
296 output.Write([]byte(fmt.Sprintf("%s known 3rd party sidecar detected in %s\n", warnStatus, strings.Join(sidecar, ", "))))
297 } else if verbose {
298 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, sidecarDesc)))
299 }
300
301 if len(injectDisabled) > 0 {
302 output.Write([]byte(fmt.Sprintf("%s \"%s: %s\" annotation set on %s\n",
303 warnStatus, k8s.ProxyInjectAnnotation, k8s.ProxyInjectDisabled, strings.Join(injectDisabled, ", "))))
304 } else if verbose {
305 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, injectDisabledDesc)))
306 }
307
308 if len(injected) == 0 && !annotatable {
309 output.Write([]byte(fmt.Sprintf("%s no supported objects found\n", warnStatus)))
310 warningsPrinted = true
311 } else if verbose {
312 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, unsupportedDesc)))
313 }
314
315 if len(udp) > 0 {
316 verb := "uses"
317 if len(udp) > 1 {
318 verb = "use"
319 }
320 output.Write([]byte(fmt.Sprintf("%s %s %s \"protocol: UDP\"\n", warnStatus, strings.Join(udp, ", "), verb)))
321 } else if verbose {
322 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, udpDesc)))
323 }
324
325 if len(automountServiceAccountTokenFalse) == 0 && verbose {
326 output.Write([]byte(fmt.Sprintf("%s %s\n", okStatus, automountServiceAccountTokenDesc)))
327 }
328
329
330
331
332 if warningsPrinted {
333 output.Write([]byte("\n"))
334 }
335
336 for _, r := range reports {
337 ok, _ := r.Injectable()
338 if ok {
339 output.Write([]byte(fmt.Sprintf("%s \"%s\" injected\n", r.Kind, r.Name)))
340 }
341 if !ok && !r.Annotated {
342 if r.Kind != "" {
343 output.Write([]byte(fmt.Sprintf("%s \"%s\" skipped\n", r.Kind, r.Name)))
344 } else {
345 output.Write([]byte(fmt.Sprintln("document missing \"kind\" field, skipped")))
346 }
347 }
348 if !ok && r.Annotated {
349 output.Write([]byte(fmt.Sprintf("%s \"%s\" annotated\n", r.Kind, r.Name)))
350 }
351 }
352
353
354 output.Write([]byte("\n"))
355 }
356
357 func fetchConfigs(ctx context.Context) (*linkerd2.Values, error) {
358
359 hc := healthcheck.NewWithCoreChecks(&healthcheck.Options{
360 ControlPlaneNamespace: controlPlaneNamespace,
361 KubeConfig: kubeconfigPath,
362 Impersonate: impersonate,
363 ImpersonateGroup: impersonateGroup,
364 KubeContext: kubeContext,
365 APIAddr: apiAddr,
366 RetryDeadline: time.Time{},
367 })
368 hc.RunWithExitOnError()
369
370 api, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
371 if err != nil {
372 return nil, err
373 }
374
375
376 _, values, err := healthcheck.FetchCurrentConfiguration(ctx, api, controlPlaneNamespace)
377 return values, err
378 }
379
380
381
382
383 func getOverrideAnnotations(values *linkerd2.Values, base *linkerd2.Values) map[string]string {
384 overrideAnnotations := make(map[string]string)
385
386 proxy := values.Proxy
387 baseProxy := base.Proxy
388 if proxy.Image.Version != baseProxy.Image.Version {
389 overrideAnnotations[k8s.ProxyVersionOverrideAnnotation] = proxy.Image.Version
390 }
391
392 if values.ProxyInit.IgnoreInboundPorts != base.ProxyInit.IgnoreInboundPorts {
393 overrideAnnotations[k8s.ProxyIgnoreInboundPortsAnnotation] = values.ProxyInit.IgnoreInboundPorts
394 }
395 if values.ProxyInit.IgnoreOutboundPorts != base.ProxyInit.IgnoreOutboundPorts {
396 overrideAnnotations[k8s.ProxyIgnoreOutboundPortsAnnotation] = values.ProxyInit.IgnoreOutboundPorts
397 }
398
399 if proxy.Ports.Admin != baseProxy.Ports.Admin {
400 overrideAnnotations[k8s.ProxyAdminPortAnnotation] = fmt.Sprintf("%d", proxy.Ports.Admin)
401 }
402 if proxy.Ports.Control != baseProxy.Ports.Control {
403 overrideAnnotations[k8s.ProxyControlPortAnnotation] = fmt.Sprintf("%d", proxy.Ports.Control)
404 }
405 if proxy.Ports.Inbound != baseProxy.Ports.Inbound {
406 overrideAnnotations[k8s.ProxyInboundPortAnnotation] = fmt.Sprintf("%d", proxy.Ports.Inbound)
407 }
408 if proxy.Ports.Outbound != baseProxy.Ports.Outbound {
409 overrideAnnotations[k8s.ProxyOutboundPortAnnotation] = fmt.Sprintf("%d", proxy.Ports.Outbound)
410 }
411 if proxy.OpaquePorts != baseProxy.OpaquePorts {
412 overrideAnnotations[k8s.ProxyOpaquePortsAnnotation] = proxy.OpaquePorts
413 }
414
415 if proxy.Image.Name != baseProxy.Image.Name {
416 overrideAnnotations[k8s.ProxyImageAnnotation] = proxy.Image.Name
417 }
418 if values.ProxyInit.Image.Name != base.ProxyInit.Image.Name {
419 overrideAnnotations[k8s.ProxyInitImageAnnotation] = values.ProxyInit.Image.Name
420 }
421 if values.DebugContainer.Image.Name != base.DebugContainer.Image.Name {
422 overrideAnnotations[k8s.DebugImageAnnotation] = values.DebugContainer.Image.Name
423 }
424
425 if values.ProxyInit.Image.Version != base.ProxyInit.Image.Version {
426 overrideAnnotations[k8s.ProxyInitImageVersionAnnotation] = values.ProxyInit.Image.Version
427 }
428
429 if values.DebugContainer.Image.Version != base.DebugContainer.Image.Version {
430 overrideAnnotations[k8s.DebugImageVersionAnnotation] = values.DebugContainer.Image.Version
431 }
432
433 if proxy.Image.PullPolicy != baseProxy.Image.PullPolicy {
434 overrideAnnotations[k8s.ProxyImagePullPolicyAnnotation] = proxy.Image.PullPolicy
435 }
436
437 if proxy.UID != baseProxy.UID {
438 overrideAnnotations[k8s.ProxyUIDAnnotation] = strconv.FormatInt(proxy.UID, 10)
439 }
440
441 if proxy.GID >= 0 && (baseProxy.GID < 0 || proxy.GID != baseProxy.GID) {
442 overrideAnnotations[k8s.ProxyGIDAnnotation] = strconv.FormatInt(proxy.GID, 10)
443 }
444
445 if proxy.LogLevel != baseProxy.LogLevel {
446 overrideAnnotations[k8s.ProxyLogLevelAnnotation] = proxy.LogLevel
447 }
448
449 if proxy.LogFormat != baseProxy.LogFormat {
450 overrideAnnotations[k8s.ProxyLogFormatAnnotation] = proxy.LogFormat
451 }
452
453 if proxy.RequireIdentityOnInboundPorts != baseProxy.RequireIdentityOnInboundPorts {
454 overrideAnnotations[k8s.ProxyRequireIdentityOnInboundPortsAnnotation] = proxy.RequireIdentityOnInboundPorts
455 }
456
457 if proxy.EnableExternalProfiles != baseProxy.EnableExternalProfiles {
458 overrideAnnotations[k8s.ProxyEnableExternalProfilesAnnotation] = strconv.FormatBool(proxy.EnableExternalProfiles)
459 }
460
461 if proxy.IsIngress != baseProxy.IsIngress {
462 overrideAnnotations[k8s.ProxyInjectAnnotation] = k8s.ProxyInjectIngress
463 }
464
465 if proxy.Resources.CPU.Request != baseProxy.Resources.CPU.Request {
466 overrideAnnotations[k8s.ProxyCPURequestAnnotation] = proxy.Resources.CPU.Request
467 }
468 if proxy.Resources.CPU.Limit != baseProxy.Resources.CPU.Limit {
469 overrideAnnotations[k8s.ProxyCPULimitAnnotation] = proxy.Resources.CPU.Limit
470 }
471 if proxy.Resources.Memory.Request != baseProxy.Resources.Memory.Request {
472 overrideAnnotations[k8s.ProxyMemoryRequestAnnotation] = proxy.Resources.Memory.Request
473 }
474 if proxy.Resources.Memory.Limit != baseProxy.Resources.Memory.Limit {
475 overrideAnnotations[k8s.ProxyMemoryLimitAnnotation] = proxy.Resources.Memory.Limit
476 }
477 if proxy.WaitBeforeExitSeconds != baseProxy.WaitBeforeExitSeconds {
478 overrideAnnotations[k8s.ProxyWaitBeforeExitSecondsAnnotation] = uintToString(proxy.WaitBeforeExitSeconds)
479 }
480
481 if proxy.Await != baseProxy.Await {
482 if proxy.Await {
483 overrideAnnotations[k8s.ProxyAwait] = k8s.Enabled
484 } else {
485 overrideAnnotations[k8s.ProxyAwait] = k8s.Disabled
486 }
487 }
488
489 if proxy.DefaultInboundPolicy != baseProxy.DefaultInboundPolicy {
490 overrideAnnotations[k8s.ProxyDefaultInboundPolicyAnnotation] = proxy.DefaultInboundPolicy
491 }
492
493 if proxy.AccessLog != baseProxy.AccessLog {
494 overrideAnnotations[k8s.ProxyAccessLogAnnotation] = proxy.AccessLog
495 }
496
497 if proxy.ShutdownGracePeriod != baseProxy.ShutdownGracePeriod {
498 overrideAnnotations[k8s.ProxyShutdownGracePeriodAnnotation] = proxy.ShutdownGracePeriod
499 }
500
501 if proxy.NativeSidecar != baseProxy.NativeSidecar {
502 overrideAnnotations[k8s.ProxyEnableNativeSidecarAnnotation] = strconv.FormatBool(proxy.NativeSidecar)
503 }
504
505 return overrideAnnotations
506 }
507
508 func uintToString(v uint64) string {
509 return strconv.FormatUint(v, 10)
510 }
511
View as plain text