/
mesh_webhook.go
742 lines (634 loc) · 30.1 KB
/
mesh_webhook.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
mapset "github.com/deckarep/golang-set"
"github.com/go-logr/logr"
"gomodules.xyz/jsonpatch/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/hashicorp/consul-k8s/control-plane/connect-inject/common"
"github.com/hashicorp/consul-k8s/control-plane/connect-inject/constants"
"github.com/hashicorp/consul-k8s/control-plane/connect-inject/lifecycle"
"github.com/hashicorp/consul-k8s/control-plane/connect-inject/metrics"
"github.com/hashicorp/consul-k8s/control-plane/consul"
"github.com/hashicorp/consul-k8s/control-plane/namespaces"
"github.com/hashicorp/consul-k8s/control-plane/version"
)
const (
sidecarContainer = "consul-dataplane"
// exposedPathsLivenessPortsRangeStart is the start of the port range that we will use as
// the ListenerPort for the Expose configuration of the proxy registration for a liveness probe.
exposedPathsLivenessPortsRangeStart = 20300
// exposedPathsReadinessPortsRangeStart is the start of the port range that we will use as
// the ListenerPort for the Expose configuration of the proxy registration for a readiness probe.
exposedPathsReadinessPortsRangeStart = 20400
// exposedPathsStartupPortsRangeStart is the start of the port range that we will use as
// the ListenerPort for the Expose configuration of the proxy registration for a startup probe.
exposedPathsStartupPortsRangeStart = 20500
)
// kubeSystemNamespaces is a set of namespaces that are considered
// "system" level namespaces and are always skipped (never injected).
var kubeSystemNamespaces = mapset.NewSetWith(metav1.NamespaceSystem, metav1.NamespacePublic)
// MeshWebhook is the HTTP meshWebhook for admission webhooks.
type MeshWebhook struct {
Clientset kubernetes.Interface
// ConsulClientConfig is the config to create a Consul API client.
ConsulConfig *consul.Config
// ConsulServerConnMgr is the watcher for the Consul server addresses.
ConsulServerConnMgr consul.ServerConnectionManager
// ImageConsul is the container image for Consul to use.
// ImageConsulDataplane is the container image for Envoy to use.
//
// Both of these MUST be set.
ImageConsul string
ImageConsulDataplane string
// ImageConsulK8S is the container image for consul-k8s to use.
// This image is used for the consul-sidecar container.
ImageConsulK8S string
// Optional: set when you need extra options to be set when running envoy
// See a list of args here: https://www.envoyproxy.io/docs/envoy/latest/operations/cli
EnvoyExtraArgs string
// RequireAnnotation means that the annotation must be given to inject.
// If this is false, injection is default.
RequireAnnotation bool
// AuthMethod is the name of the Kubernetes Auth Method to
// use for identity with connectInjection if ACLs are enabled.
AuthMethod string
// The PEM-encoded CA certificate string
// to use when communicating with Consul clients over HTTPS.
// If not set, will use HTTP.
ConsulCACert string
// TLSEnabled indicates whether we should use TLS for communicating to Consul.
TLSEnabled bool
// ConsulAddress is the address of the Consul server. This should be only the
// host (i.e. not including port or protocol).
ConsulAddress string
// ConsulTLSServerName is the SNI header to use to connect to the Consul servers
// over TLS.
ConsulTLSServerName string
// ConsulPartition is the name of the Admin Partition that the controller
// is deployed in. It is an enterprise feature requiring Consul Enterprise 1.11+.
// Its value is an empty string if partitions aren't enabled.
ConsulPartition string
// EnableNamespaces indicates that a user is running Consul Enterprise
// with version 1.7+ which is namespace aware. It enables Consul namespaces,
// with injection into either a single Consul namespace or mirrored from
// k8s namespaces.
EnableNamespaces bool
// AllowK8sNamespacesSet is a set of k8s namespaces to explicitly allow for
// injection. It supports the special character `*` which indicates that
// all k8s namespaces are eligible unless explicitly denied. This filter
// is applied before checking pod annotations.
AllowK8sNamespacesSet mapset.Set
// DenyK8sNamespacesSet is a set of k8s namespaces to explicitly deny
// injection and thus service registration with Consul. An empty set
// means that no namespaces are removed from consideration. This filter
// takes precedence over AllowK8sNamespacesSet.
DenyK8sNamespacesSet mapset.Set
// ConsulDestinationNamespace is the name of the Consul namespace to register all
// injected services into if Consul namespaces are enabled and mirroring
// is disabled. This may be set, but will not be used if mirroring is enabled.
ConsulDestinationNamespace string
// EnableK8SNSMirroring causes Consul namespaces to be created to match the
// k8s namespace of any service being registered into Consul. Services are
// registered into the Consul namespace that mirrors their k8s namespace.
EnableK8SNSMirroring bool
// K8SNSMirroringPrefix is an optional prefix that can be added to the Consul
// namespaces created while mirroring. For example, if it is set to "k8s-",
// then the k8s `default` namespace will be mirrored in Consul's
// `k8s-default` namespace.
K8SNSMirroringPrefix string
// CrossNamespaceACLPolicy is the name of the ACL policy to attach to
// any created Consul namespaces to allow cross namespace service discovery.
// Only necessary if ACLs are enabled.
CrossNamespaceACLPolicy string
// Default resource settings for sidecar proxies. Some of these
// fields may be empty.
DefaultProxyCPURequest resource.Quantity
DefaultProxyCPULimit resource.Quantity
DefaultProxyMemoryRequest resource.Quantity
DefaultProxyMemoryLimit resource.Quantity
DefaultSidecarProxyStartupFailureSeconds int
DefaultSidecarProxyLivenessFailureSeconds int
// LifecycleConfig contains proxy lifecycle management configuration from the inject-connect command and has methods to determine whether
// configuration should come from the default flags or annotations. The meshWebhook uses this to configure container sidecar proxy args.
LifecycleConfig lifecycle.Config
// Default Envoy concurrency flag, this is the number of worker threads to be used by the proxy.
DefaultEnvoyProxyConcurrency int
// MetricsConfig contains metrics configuration from the inject-connect command and has methods to determine whether
// configuration should come from the default flags or annotations. The meshWebhook uses this to configure prometheus
// annotations and the merged metrics server.
MetricsConfig metrics.Config
// Resource settings for init container. All of these fields
// will be populated by the defaults provided in the initial flags.
InitContainerResources corev1.ResourceRequirements
// Resource settings for Consul sidecar. All of these fields
// will be populated by the defaults provided in the initial flags.
DefaultConsulSidecarResources corev1.ResourceRequirements
// EnableTransparentProxy enables transparent proxy mode.
// This means that the injected init container will apply traffic redirection rules
// so that all traffic will go through the Envoy proxy.
EnableTransparentProxy bool
// EnableCNI enables the CNI plugin and prevents the connect-inject init container
// from running the consul redirect-traffic command as the CNI plugin handles traffic
// redirection
EnableCNI bool
// TProxyOverwriteProbes controls whether the webhook should mutate pod's HTTP probes
// to point them to the Envoy proxy.
TProxyOverwriteProbes bool
// EnableConsulDNS enables traffic redirection so that DNS requests are directed to Consul
// from mesh services.
EnableConsulDNS bool
// EnableOpenShift indicates that when tproxy is enabled, the security context for the Envoy and init
// containers should not be added because OpenShift sets a random user for those and will not allow
// those containers to be created otherwise.
EnableOpenShift bool
// SkipServerWatch prevents consul-dataplane from consuming the server update stream. This is useful
// for situations where Consul servers are behind a load balancer.
SkipServerWatch bool
// ReleaseNamespace is the Kubernetes namespace where this webhook is running.
ReleaseNamespace string
// Log
Log logr.Logger
// Log settings for consul-dataplane and connect-init containers.
LogLevel string
LogJSON bool
decoder *admission.Decoder
// etcResolvFile is only used in tests to stub out /etc/resolv.conf file.
etcResolvFile string
}
type multiPortInfo struct {
serviceIndex int
serviceName string
}
// Handle is the admission.Webhook implementation that actually handles the
// webhook request for admission control. This should be registered or
// served via the controller runtime manager.
func (w *MeshWebhook) Handle(ctx context.Context, req admission.Request) admission.Response {
var pod corev1.Pod
// Decode the pod from the request
if err := w.decoder.Decode(req, &pod); err != nil {
w.Log.Error(err, "could not unmarshal request to pod")
return admission.Errored(http.StatusBadRequest, err)
}
// Marshall the contents of the pod that was received. This is compared with the
// marshalled contents of the pod after it has been updated to create the jsonpatch.
origPodJson, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// Setup the default annotation values that are used for the container.
// This MUST be done before shouldInject is called since that function
// uses these annotations.
if err := w.defaultAnnotations(&pod, string(origPodJson)); err != nil {
w.Log.Error(err, "error creating default annotations", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error creating default annotations: %s", err))
}
// Check if we should inject, for example we don't inject in the
// system namespaces.
if shouldInject, err := w.shouldInject(pod, req.Namespace); err != nil {
w.Log.Error(err, "error checking if should inject", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error checking if should inject: %s", err))
} else if !shouldInject {
return admission.Allowed(fmt.Sprintf("%s %s does not require injection", pod.Kind, pod.Name))
}
w.Log.Info("received pod", "name", req.Name, "ns", req.Namespace)
// Add our volume that will be shared by the init container and
// the sidecar for passing data in the pod.
pod.Spec.Volumes = append(pod.Spec.Volumes, w.containerVolume())
// Optionally mount data volume to other containers
w.injectVolumeMount(pod)
// Optionally add any volumes that are to be used by the envoy sidecar.
if _, ok := pod.Annotations[constants.AnnotationConsulSidecarUserVolume]; ok {
var userVolumes []corev1.Volume
err := json.Unmarshal([]byte(pod.Annotations[constants.AnnotationConsulSidecarUserVolume]), &userVolumes)
if err != nil {
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error unmarshalling sidecar user volumes: %s", err))
}
pod.Spec.Volumes = append(pod.Spec.Volumes, userVolumes...)
}
// Add the upstream services as environment variables for easy
// service discovery.
containerEnvVars := w.containerEnvVars(pod)
for i := range pod.Spec.InitContainers {
pod.Spec.InitContainers[i].Env = append(pod.Spec.InitContainers[i].Env, containerEnvVars...)
}
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, containerEnvVars...)
}
// A user can enable/disable tproxy for an entire namespace via a label.
ns, err := w.Clientset.CoreV1().Namespaces().Get(ctx, req.Namespace, metav1.GetOptions{})
if err != nil {
w.Log.Error(err, "error fetching namespace metadata for container", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error getting namespace metadata for container: %s", err))
}
// Get service names from the annotation. If theres 0-1 service names, it's a single port pod, otherwise it's multi
// port.
annotatedSvcNames := w.annotatedServiceNames(pod)
multiPort := len(annotatedSvcNames) > 1
lifecycleEnabled, ok := w.LifecycleConfig.EnableProxyLifecycle(pod)
if ok != nil {
w.Log.Error(err, "unable to get lifecycle enabled status")
}
// For single port pods, add the single init container and envoy sidecar.
if !multiPort {
// Add the init container that registers the service and sets up the Envoy configuration.
initContainer, err := w.containerInit(*ns, pod, multiPortInfo{})
if err != nil {
w.Log.Error(err, "error configuring injection init container", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection init container: %s", err))
}
pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer)
// Add the Envoy sidecar.
envoySidecar, err := w.consulDataplaneSidecar(*ns, pod, multiPortInfo{})
if err != nil {
w.Log.Error(err, "error configuring injection sidecar container", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection sidecar container: %s", err))
}
//Append the Envoy sidecar before the application container only if lifecycle enabled.
if lifecycleEnabled && ok == nil {
pod.Spec.Containers = append([]corev1.Container{envoySidecar}, pod.Spec.Containers...)
} else {
pod.Spec.Containers = append(pod.Spec.Containers, envoySidecar)
}
} else {
// For multi port pods, check for unsupported cases, mount all relevant service account tokens, and mount an init
// container and envoy sidecar per port. Tproxy, metrics, and metrics merging are not supported for multi port pods.
// In a single port pod, the service account specified in the pod is sufficient for mounting the service account
// token to the pod. In a multi port pod, where multiple services are registered with Consul, we also require a
// service account per service. So, this will look for service accounts whose name matches the service and mount
// those tokens if not already specified via the pod's serviceAccountName.
w.Log.Info("processing multiport pod")
err := w.checkUnsupportedMultiPortCases(*ns, pod)
if err != nil {
w.Log.Error(err, "checking unsupported cases for multi port pods")
return admission.Errored(http.StatusInternalServerError, err)
}
//List of sidecar containers for each service. Build as a list to preserve correct ordering in relation
//to services.
sidecarContainers := []corev1.Container{}
for i, svc := range annotatedSvcNames {
w.Log.Info(fmt.Sprintf("service: %s", svc))
if w.AuthMethod != "" {
if svc != "" && pod.Spec.ServiceAccountName != svc {
secretName := ""
sa, err := w.Clientset.CoreV1().ServiceAccounts(req.Namespace).Get(ctx, svc, metav1.GetOptions{})
if err != nil {
w.Log.Error(err, "couldn't get service accounts")
return admission.Errored(http.StatusInternalServerError, err)
}
if len(sa.Secrets) == 0 {
// Check to see if there is a secret with the same name as the ServiceAccount for Kube-1.24+.
w.Log.Info(fmt.Sprintf("service account %s has zero secrets exp at least 1", svc))
sec, err := w.Clientset.CoreV1().Secrets(req.Namespace).Get(ctx, svc, metav1.GetOptions{})
if err != nil {
w.Log.Error(err, "couldn't get Secret associated with Service Account")
return admission.Errored(http.StatusInternalServerError, err)
}
secretName = sec.Name
w.Log.Info(fmt.Sprintf("fetched secret: %s", secretName))
} else {
secretName = sa.Secrets[0].Name
}
w.Log.Info("found service account, mounting service account secret to Pod", "serviceAccountName", secretName)
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: fmt.Sprintf("%s-service-account", svc),
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
},
},
})
}
}
// This will get passed to the init and sidecar containers so they are configured correctly.
mpi := multiPortInfo{
serviceIndex: i,
serviceName: svc,
}
// Add the init container that registers the service and sets up the Envoy configuration.
initContainer, err := w.containerInit(*ns, pod, mpi)
if err != nil {
w.Log.Error(err, "error configuring injection init container", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection init container: %s", err))
}
pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer)
// Add the Envoy sidecar.
envoySidecar, err := w.consulDataplaneSidecar(*ns, pod, mpi)
if err != nil {
w.Log.Error(err, "error configuring injection sidecar container", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring injection sidecar container: %s", err))
}
// If Lifecycle is enabled, add to the list of sidecar containers to be added
// to pod containers at the end in order to preserve relative ordering.
if lifecycleEnabled {
sidecarContainers = append(sidecarContainers, envoySidecar)
} else {
pod.Spec.Containers = append(pod.Spec.Containers, envoySidecar)
}
}
//Add sidecar containers first if lifecycle enabled.
if lifecycleEnabled {
pod.Spec.Containers = append(sidecarContainers, pod.Spec.Containers...)
}
}
// pod.Annotations has already been initialized by h.defaultAnnotations()
// and does not need to be checked for being a nil value.
pod.Annotations[constants.KeyInjectStatus] = constants.Injected
tproxyEnabled, err := common.TransparentProxyEnabled(*ns, pod, w.EnableTransparentProxy)
if err != nil {
w.Log.Error(err, "error determining if transparent proxy is enabled", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error determining if transparent proxy is enabled: %s", err))
}
// Add an annotation to the pod sets transparent-proxy-status to enabled or disabled. Used by the CNI plugin
// to determine if it should traffic redirect or not.
if tproxyEnabled {
pod.Annotations[constants.KeyTransparentProxyStatus] = constants.Enabled
}
// If DNS redirection is enabled, we want to configure dns on the pod.
dnsEnabled, err := consulDNSEnabled(*ns, pod, w.EnableConsulDNS, w.EnableTransparentProxy)
if err != nil {
w.Log.Error(err, "error determining if dns redirection is enabled", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error determining if dns redirection is enabled: %s", err))
}
if dnsEnabled {
if err = w.configureDNS(&pod, req.Namespace); err != nil {
w.Log.Error(err, "error configuring DNS on the pod", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring DNS on the pod: %s", err))
}
}
// Add annotations for metrics.
if err = w.prometheusAnnotations(&pod); err != nil {
w.Log.Error(err, "error configuring prometheus annotations", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring prometheus annotations: %s", err))
}
if pod.Labels == nil {
pod.Labels = make(map[string]string)
}
pod.Labels[constants.KeyInjectStatus] = constants.Injected
// Add the managed-by label since services are now managed by endpoints controller. This is to support upgrading
// from consul-k8s without Endpoints controller to consul-k8s with Endpoints controller.
pod.Labels[constants.KeyManagedBy] = constants.ManagedByValue
// Consul-ENT only: Add the Consul destination namespace as an annotation to the pod.
if w.EnableNamespaces {
pod.Annotations[constants.AnnotationConsulNamespace] = w.consulNamespace(req.Namespace)
}
// Overwrite readiness/liveness probes if needed.
err = w.overwriteProbes(*ns, &pod)
if err != nil {
w.Log.Error(err, "error overwriting readiness or liveness probes", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error overwriting readiness or liveness probes: %s", err))
}
// When CNI and tproxy are enabled, we add an annotation to the pod that contains the iptables config so that the CNI
// plugin can apply redirect traffic rules on the pod.
if w.EnableCNI && tproxyEnabled {
if err = w.addRedirectTrafficConfigAnnotation(&pod, *ns); err != nil {
w.Log.Error(err, "error configuring annotation for CNI traffic redirection", "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error configuring annotation for CNI traffic redirection: %s", err))
}
}
// Marshall the pod into JSON after it has the desired envs, annotations, labels,
// sidecars and initContainers appended to it.
updatedPodJson, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// Create a patches based on the Pod that was received by the meshWebhook
// and the desired Pod spec.
patches, err := jsonpatch.CreatePatch(origPodJson, updatedPodJson)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// Check and potentially create Consul resources. This is done after
// all patches are created to guarantee no errors were encountered in
// that process before modifying the Consul cluster.
if w.EnableNamespaces {
serverState, err := w.ConsulServerConnMgr.State()
if err != nil {
w.Log.Error(err, "error checking or creating namespace",
"ns", w.consulNamespace(req.Namespace), "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error checking or creating namespace: %s", err))
}
apiClient, err := consul.NewClientFromConnMgrState(w.ConsulConfig, serverState)
if err != nil {
w.Log.Error(err, "error checking or creating namespace",
"ns", w.consulNamespace(req.Namespace), "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error checking or creating namespace: %s", err))
}
if _, err := namespaces.EnsureExists(apiClient, w.consulNamespace(req.Namespace), w.CrossNamespaceACLPolicy); err != nil {
w.Log.Error(err, "error checking or creating namespace",
"ns", w.consulNamespace(req.Namespace), "request name", req.Name)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("error checking or creating namespace: %s", err))
}
}
// Return a Patched response along with the patches we intend on applying to the
// Pod received by the meshWebhook.
return admission.Patched(fmt.Sprintf("valid %s request", pod.Kind), patches...)
}
// overwriteProbes overwrites readiness/liveness probes of this pod when
// both transparent proxy is enabled and overwrite probes is true for the pod.
func (w *MeshWebhook) overwriteProbes(ns corev1.Namespace, pod *corev1.Pod) error {
tproxyEnabled, err := common.TransparentProxyEnabled(ns, *pod, w.EnableTransparentProxy)
if err != nil {
return err
}
overwriteProbes, err := common.ShouldOverwriteProbes(*pod, w.TProxyOverwriteProbes)
if err != nil {
return err
}
if tproxyEnabled && overwriteProbes {
// We don't use the loop index because this needs to line up w.withiptablesConfigJSON,
// which is performed before the sidecar is injected.
idx := 0
for _, container := range pod.Spec.Containers {
// skip the "envoy-sidecar" container from having it's probes overridden
if container.Name == sidecarContainer {
continue
}
if container.LivenessProbe != nil && container.LivenessProbe.HTTPGet != nil {
container.LivenessProbe.HTTPGet.Port = intstr.FromInt(exposedPathsLivenessPortsRangeStart + idx)
}
if container.ReadinessProbe != nil && container.ReadinessProbe.HTTPGet != nil {
container.ReadinessProbe.HTTPGet.Port = intstr.FromInt(exposedPathsReadinessPortsRangeStart + idx)
}
if container.StartupProbe != nil && container.StartupProbe.HTTPGet != nil {
container.StartupProbe.HTTPGet.Port = intstr.FromInt(exposedPathsStartupPortsRangeStart + idx)
}
idx++
}
}
return nil
}
func (w *MeshWebhook) injectVolumeMount(pod corev1.Pod) {
containersToInject := splitCommaSeparatedItemsFromAnnotation(constants.AnnotationInjectMountVolumes, pod)
for index, container := range pod.Spec.Containers {
if sliceContains(containersToInject, container.Name) {
pod.Spec.Containers[index].VolumeMounts = append(pod.Spec.Containers[index].VolumeMounts, corev1.VolumeMount{
Name: volumeName,
MountPath: "/consul/connect-inject",
})
}
}
}
func (w *MeshWebhook) shouldInject(pod corev1.Pod, namespace string) (bool, error) {
// Don't inject in the Kubernetes system namespaces
if kubeSystemNamespaces.Contains(namespace) {
return false, nil
}
// Namespace logic
// If in deny list, don't inject
if w.DenyK8sNamespacesSet.Contains(namespace) {
return false, nil
}
// If not in allow list or allow list is not *, don't inject
if !w.AllowK8sNamespacesSet.Contains("*") && !w.AllowK8sNamespacesSet.Contains(namespace) {
return false, nil
}
// If we already injected then don't inject again
if pod.Annotations[constants.KeyInjectStatus] != "" {
return false, nil
}
// If the explicit true/false is on, then take that value. Note that
// this has to be the last check since it sets a default value after
// all other checks.
if raw, ok := pod.Annotations[constants.AnnotationInject]; ok {
return strconv.ParseBool(raw)
}
return !w.RequireAnnotation, nil
}
func (w *MeshWebhook) defaultAnnotations(pod *corev1.Pod, podJson string) error {
if pod.Annotations == nil {
pod.Annotations = make(map[string]string)
}
// Default service port is the first port exported in the container
if _, ok := pod.ObjectMeta.Annotations[constants.AnnotationPort]; !ok {
if cs := pod.Spec.Containers; len(cs) > 0 {
if ps := cs[0].Ports; len(ps) > 0 {
if ps[0].Name != "" {
pod.Annotations[constants.AnnotationPort] = ps[0].Name
} else {
pod.Annotations[constants.AnnotationPort] = strconv.Itoa(int(ps[0].ContainerPort))
}
}
}
}
pod.Annotations[constants.AnnotationOriginalPod] = podJson
pod.Annotations[constants.LegacyAnnotationConsulK8sVersion] = version.GetHumanVersion()
pod.Annotations[constants.AnnotationConsulK8sVersion] = version.GetHumanVersion()
return nil
}
// prometheusAnnotations sets the Prometheus scraping configuration
// annotations on the Pod.
func (w *MeshWebhook) prometheusAnnotations(pod *corev1.Pod) error {
enableMetrics, err := w.MetricsConfig.EnableMetrics(*pod)
if err != nil {
return err
}
prometheusScrapePort, err := w.MetricsConfig.PrometheusScrapePort(*pod)
if err != nil {
return err
}
prometheusScrapePath := w.MetricsConfig.PrometheusScrapePath(*pod)
if enableMetrics {
pod.Annotations[constants.AnnotationPrometheusScrape] = "true"
pod.Annotations[constants.AnnotationPrometheusPort] = prometheusScrapePort
pod.Annotations[constants.AnnotationPrometheusPath] = prometheusScrapePath
}
return nil
}
// consulNamespace returns the namespace that a service should be
// registered in based on the namespace options. It returns an
// empty string if namespaces aren't enabled.
func (w *MeshWebhook) consulNamespace(ns string) string {
return namespaces.ConsulNamespace(ns, w.EnableNamespaces, w.ConsulDestinationNamespace, w.EnableK8SNSMirroring, w.K8SNSMirroringPrefix)
}
func findServiceAccountVolumeMount(pod corev1.Pod, multiPortSvcName string) (corev1.VolumeMount, string, error) {
// In the case of a multiPort pod, there may be another service account
// token mounted as a different volume. Its name must be <svc>-serviceaccount.
// If not we'll fall back to the service account for the pod.
if multiPortSvcName != "" {
for _, v := range pod.Spec.Volumes {
if v.Name == fmt.Sprintf("%s-service-account", multiPortSvcName) {
mountPath := fmt.Sprintf("/consul/serviceaccount-%s", multiPortSvcName)
return corev1.VolumeMount{
Name: v.Name,
ReadOnly: true,
MountPath: mountPath,
}, filepath.Join(mountPath, "token"), nil
}
}
}
// Find the volume mount that is mounted at the known
// service account token location
var volumeMount corev1.VolumeMount
for _, container := range pod.Spec.Containers {
for _, vm := range container.VolumeMounts {
if vm.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" {
volumeMount = vm
break
}
}
}
// Return an error if volumeMount is still empty
if (corev1.VolumeMount{}) == volumeMount {
return volumeMount, "", errors.New("unable to find service account token volumeMount")
}
return volumeMount, "/var/run/secrets/kubernetes.io/serviceaccount/token", nil
}
func (w *MeshWebhook) annotatedServiceNames(pod corev1.Pod) []string {
var annotatedSvcNames []string
if anno, ok := pod.Annotations[constants.AnnotationService]; ok {
annotatedSvcNames = strings.Split(anno, ",")
}
return annotatedSvcNames
}
func (w *MeshWebhook) checkUnsupportedMultiPortCases(ns corev1.Namespace, pod corev1.Pod) error {
tproxyEnabled, err := common.TransparentProxyEnabled(ns, pod, w.EnableTransparentProxy)
if err != nil {
return fmt.Errorf("couldn't check if tproxy is enabled: %s", err)
}
metricsEnabled, err := w.MetricsConfig.EnableMetrics(pod)
if err != nil {
return fmt.Errorf("couldn't check if metrics is enabled: %s", err)
}
metricsMergingEnabled, err := w.MetricsConfig.EnableMetricsMerging(pod)
if err != nil {
return fmt.Errorf("couldn't check if metrics merging is enabled: %s", err)
}
if tproxyEnabled {
return fmt.Errorf("multi port services are not compatible with transparent proxy")
}
if metricsEnabled {
return fmt.Errorf("multi port services are not compatible with metrics")
}
if metricsMergingEnabled {
return fmt.Errorf("multi port services are not compatible with metrics merging")
}
return nil
}
func (w *MeshWebhook) InjectDecoder(d *admission.Decoder) error {
w.decoder = d
return nil
}
func sliceContains(slice []string, entry string) bool {
for _, s := range slice {
if entry == s {
return true
}
}
return false
}