istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/inject/webhook.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package inject 16 17 import ( 18 "crypto/sha256" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "net/http" 23 "os" 24 "strconv" 25 "strings" 26 "sync" 27 "text/template" 28 "time" 29 30 "github.com/prometheus/prometheus/util/strutil" 31 "gomodules.xyz/jsonpatch/v2" 32 admissionv1 "k8s.io/api/admission/v1" 33 kubeApiAdmissionv1beta1 "k8s.io/api/admission/v1beta1" 34 corev1 "k8s.io/api/core/v1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/serializer" 38 kjson "k8s.io/apimachinery/pkg/runtime/serializer/json" 39 "k8s.io/apimachinery/pkg/types" 40 "k8s.io/apimachinery/pkg/util/mergepatch" 41 "k8s.io/apimachinery/pkg/util/strategicpatch" 42 "sigs.k8s.io/yaml" 43 44 "istio.io/api/annotation" 45 "istio.io/api/label" 46 meshconfig "istio.io/api/mesh/v1alpha1" 47 opconfig "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 48 "istio.io/istio/pilot/cmd/pilot-agent/status" 49 "istio.io/istio/pilot/pkg/model" 50 "istio.io/istio/pkg/cluster" 51 "istio.io/istio/pkg/config/constants" 52 "istio.io/istio/pkg/config/mesh" 53 "istio.io/istio/pkg/kube" 54 "istio.io/istio/pkg/kube/kubetypes" 55 "istio.io/istio/pkg/kube/multicluster" 56 "istio.io/istio/pkg/log" 57 "istio.io/istio/pkg/platform" 58 "istio.io/istio/pkg/slices" 59 "istio.io/istio/pkg/util/protomarshal" 60 "istio.io/istio/pkg/util/sets" 61 iptablesconstants "istio.io/istio/tools/istio-iptables/pkg/constants" 62 ) 63 64 var ( 65 runtimeScheme = runtime.NewScheme() 66 codecs = serializer.NewCodecFactory(runtimeScheme) 67 deserializer = codecs.UniversalDeserializer() 68 jsonSerializer = kjson.NewSerializerWithOptions(kjson.DefaultMetaFactory, runtimeScheme, runtimeScheme, kjson.SerializerOptions{}) 69 URLParameterToEnv = map[string]string{ 70 "cluster": "ISTIO_META_CLUSTER_ID", 71 "net": "ISTIO_META_NETWORK", 72 } 73 ) 74 75 func init() { 76 _ = corev1.AddToScheme(runtimeScheme) 77 _ = admissionv1.AddToScheme(runtimeScheme) 78 _ = kubeApiAdmissionv1beta1.AddToScheme(runtimeScheme) 79 } 80 81 const ( 82 // prometheus will convert annotation to this format 83 // `prometheus.io/scrape` `prometheus.io.scrape` `prometheus-io/scrape` have the same meaning in Prometheus 84 // for more details, please checkout [here](https://github.com/prometheus/prometheus/blob/71a0f42331566a8849863d77078083edbb0b3bc4/util/strutil/strconv.go#L40) 85 prometheusScrapeAnnotation = "prometheus_io_scrape" 86 prometheusPortAnnotation = "prometheus_io_port" 87 prometheusPathAnnotation = "prometheus_io_path" 88 89 watchDebounceDelay = 100 * time.Millisecond 90 ) 91 92 const ( 93 // InitContainers is the name of the property in k8s pod spec 94 InitContainers = "initContainers" 95 96 // Containers is the name of the property in k8s pod spec 97 Containers = "containers" 98 ) 99 100 type WebhookConfig struct { 101 Templates Templates 102 Values ValuesConfig 103 MeshConfig *meshconfig.MeshConfig 104 } 105 106 // Webhook implements a mutating webhook for automatic proxy injection. 107 type Webhook struct { 108 mu sync.RWMutex 109 Config *Config 110 meshConfig *meshconfig.MeshConfig 111 valuesConfig ValuesConfig 112 namespaces *multicluster.KclientComponent[*corev1.Namespace] 113 114 // please do not call SetHandler() on this watcher, instead us MultiCast.AddHandler() 115 watcher Watcher 116 MultiCast *WatcherMulticast 117 118 env *model.Environment 119 revision string 120 } 121 122 func (wh *Webhook) GetConfig() WebhookConfig { 123 wh.mu.RLock() 124 defer wh.mu.RUnlock() 125 return WebhookConfig{ 126 Templates: wh.Config.Templates, 127 Values: wh.valuesConfig, 128 MeshConfig: wh.meshConfig, 129 } 130 } 131 132 // ParsedContainers holds the unmarshalled containers and initContainers 133 type ParsedContainers struct { 134 Containers []corev1.Container `json:"containers,omitempty"` 135 InitContainers []corev1.Container `json:"initContainers,omitempty"` 136 } 137 138 func (p ParsedContainers) AllContainers() []corev1.Container { 139 return append(slices.Clone(p.Containers), p.InitContainers...) 140 } 141 142 // nolint directives: interfacer 143 func loadConfig(injectFile, valuesFile string) (*Config, string, error) { 144 data, err := os.ReadFile(injectFile) 145 if err != nil { 146 return nil, "", err 147 } 148 var c *Config 149 if c, err = unmarshalConfig(data); err != nil { 150 log.Warnf("Failed to parse injectFile %s", string(data)) 151 return nil, "", err 152 } 153 154 valuesConfig, err := os.ReadFile(valuesFile) 155 if err != nil { 156 return nil, "", err 157 } 158 return c, string(valuesConfig), nil 159 } 160 161 func unmarshalConfig(data []byte) (*Config, error) { 162 c, err := UnmarshalConfig(data) 163 if err != nil { 164 return nil, err 165 } 166 167 log.Debugf("New inject configuration: sha256sum %x", sha256.Sum256(data)) 168 log.Debugf("Policy: %v", c.Policy) 169 log.Debugf("AlwaysInjectSelector: %v", c.AlwaysInjectSelector) 170 log.Debugf("NeverInjectSelector: %v", c.NeverInjectSelector) 171 log.Debugf("Templates: %v", c.RawTemplates) 172 return &c, nil 173 } 174 175 // WebhookParameters configures parameters for the sidecar injection 176 // webhook. 177 type WebhookParameters struct { 178 // Watcher watches the sidecar injection configuration. 179 Watcher Watcher 180 181 // Port is the webhook port, e.g. typically 443 for https. 182 // This is mainly used for tests. Webhook runs on the port started by Istiod. 183 Port int 184 185 Env *model.Environment 186 187 // Use an existing mux instead of creating our own. 188 Mux *http.ServeMux 189 190 // The istio.io/rev this injector is responsible for 191 Revision string 192 193 // MultiCluster is used to access namespaces across clusters 194 MultiCluster multicluster.ComponentBuilder 195 } 196 197 // NewWebhook creates a new instance of a mutating webhook for automatic sidecar injection. 198 func NewWebhook(p WebhookParameters) (*Webhook, error) { 199 if p.Mux == nil { 200 return nil, errors.New("expected mux to be passed, but was not passed") 201 } 202 203 wh := &Webhook{ 204 watcher: p.Watcher, 205 meshConfig: p.Env.Mesh(), 206 env: p.Env, 207 revision: p.Revision, 208 } 209 210 if p.MultiCluster != nil { 211 if platform.IsOpenShift() { 212 wh.namespaces = multicluster.BuildMultiClusterKclientComponent[*corev1.Namespace](p.MultiCluster, kubetypes.Filter{}) 213 } 214 } 215 216 mc := NewMulticast(p.Watcher, wh.GetConfig) 217 mc.AddHandler(wh.updateConfig) 218 wh.MultiCast = mc 219 sidecarConfig, valuesConfig, err := p.Watcher.Get() 220 if err != nil { 221 return nil, err 222 } 223 if err := wh.updateConfig(sidecarConfig, valuesConfig); err != nil { 224 log.Errorf("failed to process webhook config: %v", err) 225 } 226 227 p.Mux.HandleFunc("/inject", wh.serveInject) 228 p.Mux.HandleFunc("/inject/", wh.serveInject) 229 230 p.Env.Watcher.AddMeshHandler(func() { 231 wh.mu.Lock() 232 wh.meshConfig = p.Env.Mesh() 233 wh.mu.Unlock() 234 }) 235 236 return wh, nil 237 } 238 239 // Run implements the webhook server 240 func (wh *Webhook) Run(stop <-chan struct{}) { 241 go wh.watcher.Run(stop) 242 } 243 244 func (wh *Webhook) updateConfig(sidecarConfig *Config, valuesConfig string) error { 245 wh.mu.Lock() 246 defer wh.mu.Unlock() 247 wh.Config = sidecarConfig 248 vc, err := NewValuesConfig(valuesConfig) 249 if err != nil { 250 return err 251 } 252 wh.valuesConfig = vc 253 return nil 254 } 255 256 type ContainerReorder int 257 258 const ( 259 MoveFirst ContainerReorder = iota 260 MoveLast 261 Remove 262 ) 263 264 func moveContainer(from, to []corev1.Container, name string) ([]corev1.Container, []corev1.Container) { 265 var container *corev1.Container 266 for i, c := range from { 267 c := c 268 if from[i].Name == name { 269 from = slices.Delete(from, i) 270 container = &c 271 break 272 } 273 } 274 if container != nil { 275 to = append(to, *container) 276 } 277 return from, to 278 } 279 280 func modifyContainers(cl []corev1.Container, name string, modifier ContainerReorder) []corev1.Container { 281 containers := []corev1.Container{} 282 var match *corev1.Container 283 for _, c := range cl { 284 c := c 285 if c.Name != name { 286 containers = append(containers, c) 287 } else { 288 match = &c 289 } 290 } 291 if match == nil { 292 return containers 293 } 294 switch modifier { 295 case MoveFirst: 296 return append([]corev1.Container{*match}, containers...) 297 case MoveLast: 298 return append(containers, *match) 299 case Remove: 300 return containers 301 default: 302 return cl 303 } 304 } 305 306 func hasContainer(cl []corev1.Container, name string) bool { 307 for _, c := range cl { 308 if c.Name == name { 309 return true 310 } 311 } 312 return false 313 } 314 315 func enablePrometheusMerge(mesh *meshconfig.MeshConfig, anno map[string]string) bool { 316 // If annotation is present, we look there first 317 if val, f := anno[annotation.PrometheusMergeMetrics.Name]; f { 318 bval, err := strconv.ParseBool(val) 319 if err != nil { 320 // This shouldn't happen since we validate earlier in the code 321 log.Warnf("invalid annotation %v=%v", annotation.PrometheusMergeMetrics.Name, bval) 322 } else { 323 return bval 324 } 325 } 326 // If mesh config setting is present, use that 327 if mesh.GetEnablePrometheusMerge() != nil { 328 return mesh.GetEnablePrometheusMerge().Value 329 } 330 // Otherwise, we default to enable 331 return true 332 } 333 334 func toAdmissionResponse(err error) *kube.AdmissionResponse { 335 return &kube.AdmissionResponse{Result: &metav1.Status{Message: err.Error()}} 336 } 337 338 func ParseTemplates(tmpls RawTemplates) (Templates, error) { 339 ret := make(Templates, len(tmpls)) 340 for k, t := range tmpls { 341 p, err := parseDryTemplate(t, InjectionFuncmap) 342 if err != nil { 343 return nil, err 344 } 345 ret[k] = p 346 } 347 return ret, nil 348 } 349 350 type ValuesConfig struct { 351 raw string 352 asStruct *opconfig.Values 353 asMap map[string]any 354 } 355 356 func (v ValuesConfig) Struct() *opconfig.Values { 357 return v.asStruct 358 } 359 360 func (v ValuesConfig) Map() map[string]any { 361 return v.asMap 362 } 363 364 func NewValuesConfig(v string) (ValuesConfig, error) { 365 c := ValuesConfig{raw: v} 366 valuesStruct := &opconfig.Values{} 367 if err := protomarshal.ApplyYAML(v, valuesStruct); err != nil { 368 return c, fmt.Errorf("could not parse configuration values: %v", err) 369 } 370 c.asStruct = valuesStruct 371 372 values := map[string]any{} 373 if err := yaml.Unmarshal([]byte(v), &values); err != nil { 374 return c, fmt.Errorf("could not parse configuration values: %v", err) 375 } 376 c.asMap = values 377 return c, nil 378 } 379 380 type InjectionParameters struct { 381 pod *corev1.Pod 382 deployMeta types.NamespacedName 383 namespace *corev1.Namespace 384 typeMeta metav1.TypeMeta 385 templates map[string]*template.Template 386 defaultTemplate []string 387 aliases map[string][]string 388 meshConfig *meshconfig.MeshConfig 389 proxyConfig *meshconfig.ProxyConfig 390 valuesConfig ValuesConfig 391 revision string 392 proxyEnvs map[string]string 393 injectedAnnotations map[string]string 394 } 395 396 func checkPreconditions(params InjectionParameters) { 397 spec := params.pod.Spec 398 metadata := params.pod.ObjectMeta 399 // If DNSPolicy is not ClusterFirst, the Envoy sidecar may not able to connect to Istio Pilot. 400 if spec.DNSPolicy != "" && spec.DNSPolicy != corev1.DNSClusterFirst { 401 podName := potentialPodName(metadata) 402 log.Warnf("%q's DNSPolicy is not %q. The Envoy sidecar may not able to connect to Istio Pilot", 403 metadata.Namespace+"/"+podName, corev1.DNSClusterFirst) 404 } 405 } 406 407 func getInjectionStatus(podSpec corev1.PodSpec, revision string) string { 408 stat := &SidecarInjectionStatus{} 409 for _, c := range podSpec.InitContainers { 410 stat.InitContainers = append(stat.InitContainers, c.Name) 411 } 412 for _, c := range podSpec.Containers { 413 stat.Containers = append(stat.Containers, c.Name) 414 } 415 for _, c := range podSpec.Volumes { 416 stat.Volumes = append(stat.Volumes, c.Name) 417 } 418 for _, c := range podSpec.ImagePullSecrets { 419 stat.ImagePullSecrets = append(stat.ImagePullSecrets, c.Name) 420 } 421 // Rather than setting istio.io/rev label on injected pods include them here in status annotation. 422 // This keeps us from overwriting the istio.io/rev label when using revision tags (i.e. istio.io/rev=<tag>). 423 if revision == "" { 424 revision = "default" 425 } 426 stat.Revision = revision 427 statusAnnotationValue, err := json.Marshal(stat) 428 if err != nil { 429 return "{}" 430 } 431 return string(statusAnnotationValue) 432 } 433 434 // injectPod is the core of the injection logic. This takes a pod and injection 435 // template, as well as some inputs to the injection template, and produces a 436 // JSON patch. 437 // 438 // In the webhook, we will receive a Pod directly from Kubernetes, and return the 439 // patch directly; Kubernetes will take care of applying the patch. 440 // 441 // For kube-inject, we will parse out a Pod from YAML (which may involve 442 // extraction from higher level types like Deployment), then apply the patch 443 // locally. 444 // 445 // The injection logic works by first applying the rendered injection template on 446 // top of the input pod This is done using a Strategic Patch Merge 447 // (https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md) 448 // Currently only a single template is supported, although in the future the template to use will be configurable 449 // and multiple templates will be supported by applying them in successive order. 450 // 451 // In addition to the plain templating, there is some post processing done to 452 // handle cases that cannot feasibly be covered in the template, such as 453 // re-ordering pods, rewriting readiness probes, etc. 454 func injectPod(req InjectionParameters) ([]byte, error) { 455 checkPreconditions(req) 456 457 // The patch will be built relative to the initial pod, capture its current state 458 originalPodSpec, err := json.Marshal(req.pod) 459 if err != nil { 460 return nil, err 461 } 462 463 // Run the injection template, giving us a partial pod spec 464 mergedPod, injectedPodData, err := RunTemplate(req) 465 if err != nil { 466 return nil, fmt.Errorf("failed to run injection template: %v", err) 467 } 468 469 mergedPod, err = reapplyOverwrittenContainers(mergedPod, req.pod, injectedPodData, req.proxyConfig) 470 if err != nil { 471 return nil, fmt.Errorf("failed to re apply container: %v", err) 472 } 473 474 // Apply some additional transformations to the pod 475 if err := postProcessPod(mergedPod, *injectedPodData, req); err != nil { 476 return nil, fmt.Errorf("failed to process pod: %v", err) 477 } 478 479 patch, err := createPatch(mergedPod, originalPodSpec) 480 if err != nil { 481 return nil, fmt.Errorf("failed to create patch: %v", err) 482 } 483 484 log.Debugf("AdmissionResponse: patch=%v\n", string(patch)) 485 return patch, nil 486 } 487 488 // reapplyOverwrittenContainers enables users to provide container level overrides for settings in the injection template 489 // * originalPod: the pod before injection. If needed, we will apply some configurations from this pod on top of the final pod 490 // * templatePod: the rendered injection template. This is needed only to see what containers we injected 491 // * finalPod: the current result of injection, roughly equivalent to the merging of originalPod and templatePod 492 // There are essentially three cases we cover here: 493 // 1. There is no overlap in containers in original and template pod. We will do nothing. 494 // 2. There is an overlap (ie, both define istio-proxy), but that is because the pod is being re-injected. 495 // In this case we do nothing, since we want to apply the new settings 496 // 3. There is an overlap. We will re-apply the original container. 497 // 498 // Where "overlap" is a container defined in both the original and template pod. Typically, this would mean 499 // the user has defined an `istio-proxy` container in their own pod spec. 500 func reapplyOverwrittenContainers(finalPod *corev1.Pod, originalPod *corev1.Pod, templatePod *corev1.Pod, 501 proxyConfig *meshconfig.ProxyConfig, 502 ) (*corev1.Pod, error) { 503 overrides := ParsedContainers{} 504 existingOverrides := ParsedContainers{} 505 if annotationOverrides, f := originalPod.Annotations[annotation.ProxyOverrides.Name]; f { 506 if err := json.Unmarshal([]byte(annotationOverrides), &existingOverrides); err != nil { 507 return nil, err 508 } 509 } 510 parsedInjectedStatus := ParsedContainers{} 511 status, alreadyInjected := originalPod.Annotations[annotation.SidecarStatus.Name] 512 if alreadyInjected { 513 parsedInjectedStatus = parseStatus(status) 514 } 515 for _, c := range templatePod.Spec.Containers { 516 // sidecarStatus annotation is added on the pod by webhook. We should use new container template 517 // instead of restoring what may be previously injected. Doing this ensures we are correctly calculating 518 // env variables like ISTIO_META_APP_CONTAINERS and ISTIO_META_POD_PORTS. 519 if match := FindContainer(c.Name, parsedInjectedStatus.Containers); match != nil { 520 continue 521 } 522 match := FindContainer(c.Name, existingOverrides.Containers) 523 if match == nil { 524 match = FindContainer(c.Name, originalPod.Spec.Containers) 525 } 526 if match == nil { 527 continue 528 } 529 overlay := *match.DeepCopy() 530 if overlay.Image == AutoImage { 531 overlay.Image = "" 532 } 533 534 overrides.Containers = append(overrides.Containers, overlay) 535 newMergedPod, err := applyContainer(finalPod, overlay) 536 if err != nil { 537 return nil, fmt.Errorf("failed to apply sidecar container: %v", err) 538 } 539 finalPod = newMergedPod 540 } 541 for _, c := range templatePod.Spec.InitContainers { 542 if match := FindContainer(c.Name, parsedInjectedStatus.InitContainers); match != nil { 543 continue 544 } 545 match := FindContainer(c.Name, existingOverrides.InitContainers) 546 if match == nil { 547 match = FindContainerFromPod(c.Name, originalPod) 548 } 549 if match == nil { 550 continue 551 } 552 overlay := *match.DeepCopy() 553 if overlay.Image == AutoImage { 554 overlay.Image = "" 555 } 556 557 overrides.InitContainers = append(overrides.InitContainers, overlay) 558 newMergedPod, err := applyInitContainer(finalPod, overlay) 559 if err != nil { 560 return nil, fmt.Errorf("failed to apply sidecar init container: %v", err) 561 } 562 finalPod = newMergedPod 563 } 564 565 if !alreadyInjected && (len(overrides.Containers) > 0 || len(overrides.InitContainers) > 0) { 566 // We found any overrides. Put them in the pod annotation so we can re-apply them on re-injection 567 js, err := json.Marshal(overrides) 568 if err != nil { 569 return nil, err 570 } 571 if finalPod.Annotations == nil { 572 finalPod.Annotations = map[string]string{} 573 } 574 finalPod.Annotations[annotation.ProxyOverrides.Name] = string(js) 575 } 576 577 adjustInitContainerUser(finalPod, originalPod, proxyConfig) 578 579 return finalPod, nil 580 } 581 582 // adjustInitContainerUser adjusts the RunAsUser/Group fields and iptables parameter "-u <uid>" 583 // in the init/validation container so that they match the value of SecurityContext.RunAsUser/Group 584 // when it is present in the custom istio-proxy container supplied by the user. 585 func adjustInitContainerUser(finalPod *corev1.Pod, originalPod *corev1.Pod, proxyConfig *meshconfig.ProxyConfig) { 586 userContainer := FindSidecar(originalPod) 587 if userContainer == nil { 588 // if user doesn't override the istio-proxy container, there's nothing to do 589 return 590 } 591 592 if userContainer.SecurityContext == nil || (userContainer.SecurityContext.RunAsUser == nil && userContainer.SecurityContext.RunAsGroup == nil) { 593 // if user doesn't override SecurityContext.RunAsUser/Group, there's nothing to do 594 return 595 } 596 597 // Locate the istio-init or istio-validation container 598 var initContainer *corev1.Container 599 for _, name := range []string{InitContainerName, ValidationContainerName} { 600 if container := FindContainer(name, finalPod.Spec.InitContainers); container != nil { 601 initContainer = container 602 break 603 } 604 } 605 if initContainer == nil { 606 // should not happen 607 log.Warn("Could not find either istio-init or istio-validation container") 608 return 609 } 610 611 // Overriding RunAsUser is now allowed in TPROXY mode, it must always run with uid=0 612 tproxy := false 613 if proxyConfig.InterceptionMode == meshconfig.ProxyConfig_TPROXY { 614 tproxy = true 615 } else if mode, found := finalPod.Annotations[annotation.SidecarInterceptionMode.Name]; found && mode == iptablesconstants.TPROXY { 616 tproxy = true 617 } 618 619 // RunAsUser cannot be overridden (ie, must remain 0) in TPROXY mode 620 if tproxy && userContainer.SecurityContext.RunAsUser != nil { 621 sidecar := FindSidecar(finalPod) 622 if sidecar == nil { 623 // Should not happen 624 log.Warn("Could not find the istio-proxy container") 625 return 626 } 627 *sidecar.SecurityContext.RunAsUser = 0 628 } 629 630 // Make sure the validation container runs with the same uid/gid as the proxy (init container is untouched, it must run with 0) 631 if !tproxy && initContainer.Name == ValidationContainerName { 632 if initContainer.SecurityContext == nil { 633 initContainer.SecurityContext = &corev1.SecurityContext{} 634 } 635 if userContainer.SecurityContext.RunAsUser != nil { 636 initContainer.SecurityContext.RunAsUser = userContainer.SecurityContext.RunAsUser 637 } 638 if userContainer.SecurityContext.RunAsGroup != nil { 639 initContainer.SecurityContext.RunAsGroup = userContainer.SecurityContext.RunAsGroup 640 } 641 } 642 643 // Find the "-u <uid>" parameter in the init container and replace it with the userid from SecurityContext.RunAsUser 644 // but only if it's not 0. iptables --uid-owner argument must not be 0. 645 if userContainer.SecurityContext.RunAsUser == nil || *userContainer.SecurityContext.RunAsUser == 0 { 646 return 647 } 648 for i := range initContainer.Args { 649 if initContainer.Args[i] == "-u" { 650 initContainer.Args[i+1] = fmt.Sprintf("%d", *userContainer.SecurityContext.RunAsUser) 651 return 652 } 653 } 654 } 655 656 // parseStatus extracts containers from injected SidecarStatus annotation 657 func parseStatus(status string) ParsedContainers { 658 parsedContainers := ParsedContainers{} 659 var unMarshalledStatus map[string]interface{} 660 if err := json.Unmarshal([]byte(status), &unMarshalledStatus); err != nil { 661 log.Errorf("Failed to unmarshal %s : %v", annotation.SidecarStatus.Name, err) 662 return parsedContainers 663 } 664 parser := func(key string, obj map[string]interface{}) []corev1.Container { 665 out := make([]corev1.Container, 0) 666 if value, exist := obj[key]; exist && value != nil { 667 for _, v := range value.([]interface{}) { 668 out = append(out, corev1.Container{Name: v.(string)}) 669 } 670 } 671 return out 672 } 673 parsedContainers.Containers = parser(Containers, unMarshalledStatus) 674 parsedContainers.InitContainers = parser(InitContainers, unMarshalledStatus) 675 676 return parsedContainers 677 } 678 679 // reinsertOverrides applies the containers listed in OverrideAnnotation to a pod. This is to achieve 680 // idempotency by handling an edge case where an injection template is modifying a container already 681 // present in the pod spec. In these cases, the logic to strip injected containers would remove the 682 // original injected parts as well, leading to the templating logic being different (for example, 683 // reading the .Spec.Containers field would be empty). 684 func reinsertOverrides(pod *corev1.Pod) (*corev1.Pod, error) { 685 type podOverrides struct { 686 Containers []corev1.Container `json:"containers,omitempty"` 687 InitContainers []corev1.Container `json:"initContainers,omitempty"` 688 } 689 690 existingOverrides := podOverrides{} 691 if annotationOverrides, f := pod.Annotations[annotation.ProxyOverrides.Name]; f { 692 if err := json.Unmarshal([]byte(annotationOverrides), &existingOverrides); err != nil { 693 return nil, err 694 } 695 } 696 697 pod = pod.DeepCopy() 698 for _, c := range existingOverrides.Containers { 699 match := FindContainer(c.Name, pod.Spec.Containers) 700 if match != nil { 701 continue 702 } 703 pod.Spec.Containers = append(pod.Spec.Containers, c) 704 } 705 706 for _, c := range existingOverrides.InitContainers { 707 match := FindContainer(c.Name, pod.Spec.InitContainers) 708 if match != nil { 709 continue 710 } 711 pod.Spec.InitContainers = append(pod.Spec.InitContainers, c) 712 } 713 714 return pod, nil 715 } 716 717 func createPatch(pod *corev1.Pod, original []byte) ([]byte, error) { 718 reinjected, err := json.Marshal(pod) 719 if err != nil { 720 return nil, err 721 } 722 p, err := jsonpatch.CreatePatch(original, reinjected) 723 if err != nil { 724 return nil, err 725 } 726 return json.Marshal(p) 727 } 728 729 // postProcessPod applies additionally transformations to the pod after merging with the injected template 730 // This is generally things that cannot reasonably be added to the template 731 func postProcessPod(pod *corev1.Pod, injectedPod corev1.Pod, req InjectionParameters) error { 732 if pod.Annotations == nil { 733 pod.Annotations = map[string]string{} 734 } 735 if pod.Labels == nil { 736 pod.Labels = map[string]string{} 737 } 738 739 overwriteClusterInfo(pod, req) 740 741 if err := applyPrometheusMerge(pod, req.meshConfig); err != nil { 742 return err 743 } 744 745 if err := applyRewrite(pod, req); err != nil { 746 return err 747 } 748 749 applyMetadata(pod, injectedPod, req) 750 751 if err := reorderPod(pod, req); err != nil { 752 return err 753 } 754 755 return nil 756 } 757 758 func applyMetadata(pod *corev1.Pod, injectedPodData corev1.Pod, req InjectionParameters) { 759 if nw, ok := req.proxyEnvs["ISTIO_META_NETWORK"]; ok { 760 pod.Labels[label.TopologyNetwork.Name] = nw 761 } 762 // Add all additional injected annotations. These are overridden if needed 763 pod.Annotations[annotation.SidecarStatus.Name] = getInjectionStatus(injectedPodData.Spec, req.revision) 764 765 // Deprecated; should be set directly in the template instead 766 for k, v := range req.injectedAnnotations { 767 pod.Annotations[k] = v 768 } 769 } 770 771 // reorderPod ensures containers are properly ordered after merging 772 func reorderPod(pod *corev1.Pod, req InjectionParameters) error { 773 var merr error 774 mc := req.meshConfig 775 // Get copy of pod proxyconfig, to determine container ordering 776 if pca, f := req.pod.ObjectMeta.GetAnnotations()[annotation.ProxyConfig.Name]; f { 777 mc, merr = mesh.ApplyProxyConfig(pca, req.meshConfig) 778 if merr != nil { 779 return merr 780 } 781 } 782 783 // nolint: staticcheck 784 holdPod := mc.GetDefaultConfig().GetHoldApplicationUntilProxyStarts().GetValue() || 785 req.valuesConfig.asStruct.GetGlobal().GetProxy().GetHoldApplicationUntilProxyStarts().GetValue() 786 787 proxyLocation := MoveLast 788 // If HoldApplicationUntilProxyStarts is set, reorder the proxy location 789 if holdPod { 790 proxyLocation = MoveFirst 791 } 792 793 // Proxy container should be last, unless HoldApplicationUntilProxyStarts is set 794 // This is to ensure `kubectl exec` and similar commands continue to default to the user's container 795 pod.Spec.Containers = modifyContainers(pod.Spec.Containers, ProxyContainerName, proxyLocation) 796 797 if hasContainer(pod.Spec.InitContainers, ProxyContainerName) { 798 // This is using native sidecar support in K8s. 799 // We want istio to be first in this case, so init containers are part of the mesh 800 // This is {istio-init/istio-validation} => proxy => rest. 801 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, EnableCoreDumpName, MoveFirst) 802 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, ProxyContainerName, MoveFirst) 803 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, ValidationContainerName, MoveFirst) 804 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, InitContainerName, MoveFirst) 805 } else { 806 // Else, we want iptables setup last so we do not blackhole init containers 807 // This is istio-validation => rest => istio-init (note: only one of istio-init or istio-validation should be present) 808 // Validation container must be first to block any user containers 809 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, ValidationContainerName, MoveFirst) 810 // Init container must be last to allow any traffic to pass before iptables is setup 811 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, InitContainerName, MoveLast) 812 pod.Spec.InitContainers = modifyContainers(pod.Spec.InitContainers, EnableCoreDumpName, MoveLast) 813 } 814 815 return nil 816 } 817 818 func applyRewrite(pod *corev1.Pod, req InjectionParameters) error { 819 sidecar := FindSidecar(pod) 820 if sidecar == nil { 821 return nil 822 } 823 824 rewrite := ShouldRewriteAppHTTPProbers(pod.Annotations, req.valuesConfig.asStruct.GetSidecarInjectorWebhook().GetRewriteAppHTTPProbe().GetValue()) 825 // We don't have to escape json encoding here when using golang libraries. 826 if rewrite { 827 if prober := DumpAppProbers(pod, req.meshConfig.GetDefaultConfig().GetStatusPort()); prober != "" { 828 // If sidecar.istio.io/status is not present then append instead of merge. 829 _, previouslyInjected := pod.Annotations[annotation.SidecarStatus.Name] 830 sidecar.Env = mergeOrAppendProbers(previouslyInjected, sidecar.Env, prober) 831 } 832 patchRewriteProbe(pod.Annotations, pod, req.meshConfig.GetDefaultConfig().GetStatusPort()) 833 } 834 return nil 835 } 836 837 // mergeOrAppendProbers ensures that if sidecar has existing ISTIO_KUBE_APP_PROBERS, 838 // then probers should be merged. 839 func mergeOrAppendProbers(previouslyInjected bool, envVars []corev1.EnvVar, newProbers string) []corev1.EnvVar { 840 if !previouslyInjected { 841 return append(envVars, corev1.EnvVar{Name: status.KubeAppProberEnvName, Value: newProbers}) 842 } 843 for idx, env := range envVars { 844 if env.Name == status.KubeAppProberEnvName { 845 var existingKubeAppProber KubeAppProbers 846 err := json.Unmarshal([]byte(env.Value), &existingKubeAppProber) 847 if err != nil { 848 log.Errorf("failed to unmarshal existing kubeAppProbers %v", err) 849 return envVars 850 } 851 var newKubeAppProber KubeAppProbers 852 err = json.Unmarshal([]byte(newProbers), &newKubeAppProber) 853 if err != nil { 854 log.Errorf("failed to unmarshal new kubeAppProbers %v", err) 855 return envVars 856 } 857 for k, v := range existingKubeAppProber { 858 // merge old and new probers. 859 newKubeAppProber[k] = v 860 } 861 marshalledKubeAppProber, err := json.Marshal(newKubeAppProber) 862 if err != nil { 863 log.Errorf("failed to serialize the merged app prober config %v", err) 864 return envVars 865 } 866 // replace old env var with new value. 867 envVars[idx] = corev1.EnvVar{Name: status.KubeAppProberEnvName, Value: string(marshalledKubeAppProber)} 868 return envVars 869 } 870 } 871 return envVars 872 } 873 874 var emptyScrape = status.PrometheusScrapeConfiguration{} 875 876 // applyPrometheusMerge configures prometheus scraping annotations for the "metrics merge" feature. 877 // This moves the current prometheus.io annotations into an environment variable and replaces them 878 // pointing to the agent. 879 func applyPrometheusMerge(pod *corev1.Pod, mesh *meshconfig.MeshConfig) error { 880 if getPrometheusScrape(pod) && 881 enablePrometheusMerge(mesh, pod.ObjectMeta.Annotations) { 882 targetPort := strconv.Itoa(int(mesh.GetDefaultConfig().GetStatusPort())) 883 if cur, f := getPrometheusPort(pod); f { 884 // We have already set the port, assume user is controlling this or, more likely, re-injected 885 // the pod. 886 if cur == targetPort { 887 return nil 888 } 889 } 890 scrape := getPrometheusScrapeConfiguration(pod) 891 sidecar := FindSidecar(pod) 892 if sidecar != nil && scrape != emptyScrape { 893 by, err := json.Marshal(scrape) 894 if err != nil { 895 return err 896 } 897 sidecar.Env = append(sidecar.Env, corev1.EnvVar{Name: status.PrometheusScrapingConfig.Name, Value: string(by)}) 898 } 899 if pod.Annotations == nil { 900 pod.Annotations = map[string]string{} 901 } 902 // if a user sets `prometheus/io/path: foo`, then we add `prometheus.io/path: /stats/prometheus` 903 // prometheus will pick a random one 904 // need to clear out all variants and then set ours 905 clearPrometheusAnnotations(pod) 906 pod.Annotations["prometheus.io/port"] = targetPort 907 pod.Annotations["prometheus.io/path"] = "/stats/prometheus" 908 pod.Annotations["prometheus.io/scrape"] = "true" 909 return nil 910 } 911 912 return nil 913 } 914 915 // getPrometheusScrape respect prometheus scrape config 916 // not to doing prometheusMerge if this return false 917 func getPrometheusScrape(pod *corev1.Pod) bool { 918 for k, val := range pod.Annotations { 919 if strutil.SanitizeLabelName(k) != prometheusScrapeAnnotation { 920 continue 921 } 922 923 if scrape, err := strconv.ParseBool(val); err == nil { 924 return scrape 925 } 926 } 927 928 return true 929 } 930 931 var prometheusAnnotations = sets.New( 932 prometheusPathAnnotation, 933 prometheusPortAnnotation, 934 prometheusScrapeAnnotation, 935 ) 936 937 func clearPrometheusAnnotations(pod *corev1.Pod) { 938 needRemovedKeys := make([]string, 0, 2) 939 for k := range pod.Annotations { 940 anno := strutil.SanitizeLabelName(k) 941 if prometheusAnnotations.Contains(anno) { 942 needRemovedKeys = append(needRemovedKeys, k) 943 } 944 } 945 946 for _, k := range needRemovedKeys { 947 delete(pod.Annotations, k) 948 } 949 } 950 951 func getPrometheusScrapeConfiguration(pod *corev1.Pod) status.PrometheusScrapeConfiguration { 952 cfg := status.PrometheusScrapeConfiguration{} 953 954 for k, val := range pod.Annotations { 955 anno := strutil.SanitizeLabelName(k) 956 switch anno { 957 case prometheusPortAnnotation: 958 cfg.Port = val 959 case prometheusScrapeAnnotation: 960 cfg.Scrape = val 961 case prometheusPathAnnotation: 962 cfg.Path = val 963 } 964 } 965 966 return cfg 967 } 968 969 func getPrometheusPort(pod *corev1.Pod) (string, bool) { 970 for k, val := range pod.Annotations { 971 if strutil.SanitizeLabelName(k) != prometheusPortAnnotation { 972 continue 973 } 974 975 return val, true 976 } 977 978 return "", false 979 } 980 981 const ( 982 // AutoImage is the special image name to indicate to the injector that we should use the injected image, and NOT override it 983 // This is necessary because image is a required field on container, so if a user defines an istio-proxy container 984 // with customizations they must set an image. 985 AutoImage = "auto" 986 ) 987 988 // applyContainer merges a container spec on top of the provided pod 989 func applyContainer(target *corev1.Pod, container corev1.Container) (*corev1.Pod, error) { 990 overlay := &corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{container}}} 991 992 overlayJSON, err := json.Marshal(overlay) 993 if err != nil { 994 return nil, err 995 } 996 997 return applyOverlay(target, overlayJSON) 998 } 999 1000 // applyInitContainer merges a container spec on top of the provided pod as an init container 1001 func applyInitContainer(target *corev1.Pod, container corev1.Container) (*corev1.Pod, error) { 1002 overlay := &corev1.Pod{Spec: corev1.PodSpec{ 1003 // We need to set containers to empty, otherwise it will marshal as "null" and delete all containers 1004 Containers: []corev1.Container{}, 1005 InitContainers: []corev1.Container{container}, 1006 }} 1007 1008 overlayJSON, err := json.Marshal(overlay) 1009 if err != nil { 1010 return nil, err 1011 } 1012 1013 return applyOverlay(target, overlayJSON) 1014 } 1015 1016 func patchHandleUnmarshal(j []byte, unmarshal func(data []byte, v any) error) (map[string]any, error) { 1017 if j == nil { 1018 j = []byte("{}") 1019 } 1020 1021 m := map[string]any{} 1022 err := unmarshal(j, &m) 1023 if err != nil { 1024 return nil, mergepatch.ErrBadJSONDoc 1025 } 1026 return m, nil 1027 } 1028 1029 // StrategicMergePatchYAML is a small fork of strategicpatch.StrategicMergePatch to allow YAML patches 1030 // This avoids expensive conversion from YAML to JSON 1031 func StrategicMergePatchYAML(originalJSON []byte, patchYAML []byte, dataStruct any) ([]byte, error) { 1032 schema, err := strategicpatch.NewPatchMetaFromStruct(dataStruct) 1033 if err != nil { 1034 return nil, err 1035 } 1036 1037 originalMap, err := patchHandleUnmarshal(originalJSON, json.Unmarshal) 1038 if err != nil { 1039 return nil, err 1040 } 1041 patchMap, err := patchHandleUnmarshal(patchYAML, func(data []byte, v any) error { 1042 return yaml.Unmarshal(data, v) 1043 }) 1044 if err != nil { 1045 return nil, err 1046 } 1047 1048 result, err := strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(originalMap, patchMap, schema) 1049 if err != nil { 1050 return nil, err 1051 } 1052 1053 return json.Marshal(result) 1054 } 1055 1056 // applyContainer merges a pod spec, provided as JSON, on top of the provided pod 1057 func applyOverlayYAML(target *corev1.Pod, overlayYAML []byte) (*corev1.Pod, error) { 1058 currentJSON, err := json.Marshal(target) 1059 if err != nil { 1060 return nil, err 1061 } 1062 1063 pod := corev1.Pod{} 1064 // Overlay the injected template onto the original podSpec 1065 patched, err := StrategicMergePatchYAML(currentJSON, overlayYAML, pod) 1066 if err != nil { 1067 return nil, fmt.Errorf("strategic merge: %v", err) 1068 } 1069 1070 if err := json.Unmarshal(patched, &pod); err != nil { 1071 return nil, fmt.Errorf("unmarshal patched pod: %v", err) 1072 } 1073 return &pod, nil 1074 } 1075 1076 // applyContainer merges a pod spec, provided as JSON, on top of the provided pod 1077 func applyOverlay(target *corev1.Pod, overlayJSON []byte) (*corev1.Pod, error) { 1078 currentJSON, err := json.Marshal(target) 1079 if err != nil { 1080 return nil, err 1081 } 1082 1083 pod := corev1.Pod{} 1084 // Overlay the injected template onto the original podSpec 1085 patched, err := strategicpatch.StrategicMergePatch(currentJSON, overlayJSON, pod) 1086 if err != nil { 1087 return nil, fmt.Errorf("strategic merge: %v", err) 1088 } 1089 1090 if err := json.Unmarshal(patched, &pod); err != nil { 1091 return nil, fmt.Errorf("unmarshal patched pod: %v", err) 1092 } 1093 return &pod, nil 1094 } 1095 1096 func (wh *Webhook) inject(ar *kube.AdmissionReview, path string) *kube.AdmissionResponse { 1097 req := ar.Request 1098 var pod corev1.Pod 1099 if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { 1100 handleError(fmt.Sprintf("Could not unmarshal raw object: %v %s", err, 1101 string(req.Object.Raw))) 1102 return toAdmissionResponse(err) 1103 } 1104 // Managed fields is sometimes extremely large, leading to excessive CPU time on patch generation 1105 // It does not impact the injection output at all, so we can just remove it. 1106 pod.ManagedFields = nil 1107 1108 // Deal with potential empty fields, e.g., when the pod is created by a deployment 1109 podName := potentialPodName(pod.ObjectMeta) 1110 if pod.ObjectMeta.Namespace == "" { 1111 pod.ObjectMeta.Namespace = req.Namespace 1112 } 1113 log.Infof("Sidecar injection request for %v/%v", req.Namespace, podName) 1114 log.Debugf("Object: %v", string(req.Object.Raw)) 1115 log.Debugf("OldObject: %v", string(req.OldObject.Raw)) 1116 1117 wh.mu.RLock() 1118 if !injectRequired(IgnoredNamespaces.UnsortedList(), wh.Config, &pod.Spec, pod.ObjectMeta) { 1119 log.Infof("Skipping %s/%s due to policy check", pod.ObjectMeta.Namespace, podName) 1120 totalSkippedInjections.Increment() 1121 wh.mu.RUnlock() 1122 return &kube.AdmissionResponse{ 1123 Allowed: true, 1124 } 1125 } 1126 1127 proxyConfig := wh.env.GetProxyConfigOrDefault(pod.Namespace, pod.Labels, pod.Annotations, wh.meshConfig) 1128 deploy, typeMeta := kube.GetDeployMetaFromPod(&pod) 1129 1130 params := InjectionParameters{ 1131 pod: &pod, 1132 deployMeta: deploy, 1133 typeMeta: typeMeta, 1134 templates: wh.Config.Templates, 1135 defaultTemplate: wh.Config.DefaultTemplates, 1136 aliases: wh.Config.Aliases, 1137 meshConfig: wh.meshConfig, 1138 proxyConfig: proxyConfig, 1139 valuesConfig: wh.valuesConfig, 1140 revision: wh.revision, 1141 injectedAnnotations: wh.Config.InjectedAnnotations, 1142 proxyEnvs: parseInjectEnvs(path), 1143 } 1144 1145 if platform.IsOpenShift() && wh.namespaces != nil { 1146 clusterID, _ := extractClusterAndNetwork(params) 1147 if clusterID == "" { 1148 clusterID = constants.DefaultClusterName 1149 } 1150 client := wh.namespaces.ForCluster(cluster.ID(clusterID)) 1151 if client != nil { 1152 params.namespace = client.Get(pod.Namespace, "") 1153 } else { 1154 log.Warnf("unable to fetch namespace, failed to get client for %q", clusterID) 1155 } 1156 } 1157 wh.mu.RUnlock() 1158 1159 patchBytes, err := injectPod(params) 1160 if err != nil { 1161 handleError(fmt.Sprintf("Pod injection failed: %v", err)) 1162 return toAdmissionResponse(err) 1163 } 1164 1165 reviewResponse := kube.AdmissionResponse{ 1166 Allowed: true, 1167 Patch: patchBytes, 1168 PatchType: func() *string { 1169 pt := "JSONPatch" 1170 return &pt 1171 }(), 1172 } 1173 totalSuccessfulInjections.Increment() 1174 return &reviewResponse 1175 } 1176 1177 func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) { 1178 totalInjections.Increment() 1179 t0 := time.Now() 1180 defer func() { injectionTime.Record(time.Since(t0).Seconds()) }() 1181 var body []byte 1182 if r.Body != nil { 1183 if data, err := kube.HTTPConfigReader(r); err == nil { 1184 body = data 1185 } else { 1186 http.Error(w, err.Error(), http.StatusBadRequest) 1187 return 1188 } 1189 } 1190 if len(body) == 0 { 1191 handleError("no body found") 1192 http.Error(w, "no body found", http.StatusBadRequest) 1193 return 1194 } 1195 1196 // verify the content type is accurate 1197 contentType := r.Header.Get("Content-Type") 1198 if contentType != "application/json" { 1199 handleError(fmt.Sprintf("contentType=%s, expect application/json", contentType)) 1200 http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType) 1201 return 1202 } 1203 1204 path := "" 1205 if r.URL != nil { 1206 path = r.URL.Path 1207 } 1208 1209 var reviewResponse *kube.AdmissionResponse 1210 var obj runtime.Object 1211 var ar *kube.AdmissionReview 1212 if out, _, err := deserializer.Decode(body, nil, obj); err != nil { 1213 handleError(fmt.Sprintf("Could not decode body: %v", err)) 1214 reviewResponse = toAdmissionResponse(err) 1215 } else { 1216 log.Debugf("AdmissionRequest for path=%s\n", path) 1217 ar, err = kube.AdmissionReviewKubeToAdapter(out) 1218 if err != nil { 1219 handleError(fmt.Sprintf("Could not decode object: %v", err)) 1220 reviewResponse = toAdmissionResponse(err) 1221 } else { 1222 reviewResponse = wh.inject(ar, path) 1223 } 1224 } 1225 1226 response := kube.AdmissionReview{} 1227 response.Response = reviewResponse 1228 var responseKube runtime.Object 1229 var apiVersion string 1230 if ar != nil { 1231 apiVersion = ar.APIVersion 1232 response.TypeMeta = ar.TypeMeta 1233 if response.Response != nil { 1234 if ar.Request != nil { 1235 response.Response.UID = ar.Request.UID 1236 } 1237 } 1238 } 1239 responseKube = kube.AdmissionReviewAdapterToKube(&response, apiVersion) 1240 resp, err := json.Marshal(responseKube) 1241 if err != nil { 1242 log.Errorf("Could not encode response: %v", err) 1243 http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) 1244 return 1245 } 1246 if _, err := w.Write(resp); err != nil { 1247 log.Errorf("Could not write response: %v", err) 1248 http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) 1249 } 1250 } 1251 1252 // parseInjectEnvs parse new envs from inject url path. format: /inject/k1/v1/k2/v2 1253 // slash characters in values must be replaced by --slash-- (e.g. /inject/k1/abc--slash--def/k2/v2). 1254 func parseInjectEnvs(path string) map[string]string { 1255 path = strings.TrimSuffix(path, "/") 1256 res := func(path string) []string { 1257 parts := strings.SplitN(path, "/", 3) 1258 var newRes []string 1259 if len(parts) == 3 { // If length is less than 3, then the path is simply "/inject". 1260 if strings.HasPrefix(parts[2], ":ENV:") { 1261 // Deprecated, not recommended. 1262 // Note that this syntax fails validation when used to set injectionPath (i.e., service.path in mwh). 1263 // It doesn't fail validation when used to set injectionURL, however. K8s bug maybe? 1264 pairs := strings.Split(parts[2], ":ENV:") 1265 for i := 1; i < len(pairs); i++ { // skip the first part, it is a nil 1266 pair := strings.SplitN(pairs[i], "=", 2) 1267 // The first part is the variable name which can not be empty 1268 // the second part is the variable value which can be empty but has to exist 1269 // for example, aaa=bbb, aaa= are valid, but =aaa or = are not valid, the 1270 // invalid ones will be ignored. 1271 if len(pair[0]) > 0 && len(pair) == 2 { 1272 newRes = append(newRes, pair...) 1273 } 1274 } 1275 return newRes 1276 } 1277 newRes = strings.Split(parts[2], "/") 1278 } 1279 for i, value := range newRes { 1280 if i%2 != 0 { 1281 // Replace --slash-- with / in values. 1282 newRes[i] = strings.ReplaceAll(value, "--slash--", "/") 1283 } 1284 } 1285 return newRes 1286 }(path) 1287 newEnvs := make(map[string]string) 1288 1289 for i := 0; i < len(res); i += 2 { 1290 k := res[i] 1291 if i == len(res)-1 { // ignore the last key without value 1292 log.Warnf("Odd number of inject env entries, ignore the last key %s\n", k) 1293 break 1294 } 1295 1296 env, found := URLParameterToEnv[k] 1297 if !found { 1298 env = strings.ToUpper(k) // if not found, use the custom env directly 1299 } 1300 if env != "" { 1301 newEnvs[env] = res[i+1] 1302 } 1303 } 1304 1305 return newEnvs 1306 } 1307 1308 func handleError(message string) { 1309 log.Errorf(message) 1310 totalFailedInjections.Increment() 1311 }