operator/pkg/operator/injector/injector.go (338 lines of code) (raw):

// Licensed to Apache Software Foundation (ASF) under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Apache Software Foundation (ASF) licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package injector import ( "encoding/json" "fmt" "regexp" "strings" corev1 "k8s.io/api/core/v1" logf "sigs.k8s.io/controller-runtime/pkg/log" "github.com/apache/skywalking-swck/operator/apis/operator/v1alpha1" ) const ( // the label means whether to enbale injection , "true" or "false". ActiveInjectorLabel = "swck-java-agent-injected" // SidecarInjectSucceedAnno represents injection succeed SidecarInjectSucceedAnno = "sidecar.skywalking.apache.org/succeed" // the annotation means which container to inject sidecarInjectContainerAnno = "strategy.skywalking.apache.org/inject.Container" // the annotation that specify the reason for injection failure sidecarInjectErrorAnno = "sidecar.skywalking.apache.org/error" // those annotations with the following prefixes represent sidecar information sidecarAnnotationPrefix = "sidecar.skywalking.apache.org/" // those annotations with the following prefixes represent agent information agentAnnotationPrefix = "agent.skywalking.apache.org/" // If user want to use other Plugins' config ,the annotation must have the following form // plugins.skywalking.apache.org/${config.name} = ${config.value} // for example , if user want to enable plugin.mongodb.trace_param // the annotation is plugins.skywalking.apache.org/plugin.mongodb.trace_param: "true" pluginsAnnotationPrefix = "plugins.skywalking.apache.org/" // If user want to use optional-plugins , the annotation must match a optinal plugin // such as optional.skywalking.apache.org: "trace|webflux|cloud-gateway-2.1.x" // Notice , If the injected container's image don't hava the optional plugin , // the container will panic optionsAnnotation = "optional.skywalking.apache.org" // If user want to use optional-reporter-plugins , the annotation must match an optional-reporter plugin // such as optional-reporter.skywalking.apache.org: "kafka" optionsReporterAnnotation = "optional-reporter.skywalking.apache.org" // If user want to use bootstrap-plugins , the annotation must match a bootstrap plugin // such as bootstrap.skywalking.apache.org: "jdk-threading" bootstrapAnnotation = "bootstrap.skywalking.apache.org" // the mount path of empty volume, which shared by init container and target container. mountPath = "/sky/agent" ) // log is for logging in this package. var log = logf.Log.WithName("injector") // SidecarInjectField contains all info that will be injected type SidecarInjectField struct { // determine whether to inject , default is not to inject NeedInject bool // Initcontainer is a container that has the agent folder Initcontainer corev1.Container // sidecarVolume is a shared directory between App's container and initcontainer SidecarVolume corev1.Volume // sidecarVolumeMount is a path that specifies a shared directory SidecarVolumeMount corev1.VolumeMount // env is used to set java agent’s parameters Env corev1.EnvVar // envs is the envs pass to target containers Envs []corev1.EnvVar // the string is used to set jvm agent ,just like following // -javaagent: /sky/agent/skywalking-agent,jar=jvmAgentConfigStr JvmAgentConfigStr string // determine which container to inject , default is to inject all containers InjectContainer string } // NewSidecarInjectField will create a new SidecarInjectField func NewSidecarInjectField() *SidecarInjectField { return new(SidecarInjectField) } // Inject will do real injection func (s *SidecarInjectField) Inject(pod *corev1.Pod) { log.Info(fmt.Sprintf("inject pod : %s", pod.GenerateName)) // add initcontrainers to spec if pod.Spec.InitContainers != nil { pod.Spec.InitContainers = append(pod.Spec.InitContainers, s.Initcontainer) } else { pod.Spec.InitContainers = []corev1.Container{s.Initcontainer} } // add volume to spec if pod.Spec.Volumes == nil { pod.Spec.Volumes = []corev1.Volume{} } pod.Spec.Volumes = append(pod.Spec.Volumes, s.SidecarVolume) // choose a specific container to inject targetContainers := s.findInjectContainer(pod.Spec.Containers) // add volumemount and env to container for i := range targetContainers { log.Info(fmt.Sprintf("inject container : %s", targetContainers[i].Name)) if (*targetContainers[i]).VolumeMounts == nil { (*targetContainers[i]).VolumeMounts = []corev1.VolumeMount{} } (*targetContainers[i]).VolumeMounts = append((*targetContainers[i]).VolumeMounts, s.SidecarVolumeMount) if (*targetContainers[i]).Env != nil { (*targetContainers[i]).Env = append((*targetContainers[i]).Env, s.Env) } else { (*targetContainers[i]).Env = []corev1.EnvVar{s.Env} } // envs to be append var envsTBA []corev1.EnvVar for j, envInject := range s.Envs { isExists := false for _, envExists := range targetContainers[i].Env { if strings.EqualFold(envExists.Name, envInject.Name) { isExists = true break } } if !isExists { envsTBA = append(envsTBA, s.Envs[j]) } } if len(s.Envs) > 0 { (*targetContainers[i]).Env = append((*targetContainers[i]).Env, envsTBA...) } } } // GetInjectStrategy gets user's injection strategy func (s *SidecarInjectField) GetInjectStrategy(labels, annotation *map[string]string) { // set default value s.NeedInject = false // set NeedInject's value, if the pod has the label "swck-java-agent-injected=true", means need inject if *labels == nil { return } // if the pod is injected, then return if strings.EqualFold((*annotation)[SidecarInjectSucceedAnno], "true") { return } if strings.EqualFold((*labels)[ActiveInjectorLabel], "true") { s.NeedInject = true } if *annotation == nil { return } // set injectContainer's value if v, ok := (*annotation)[sidecarInjectContainerAnno]; ok { s.InjectContainer = v } } func (s *SidecarInjectField) findInjectContainer(containers []corev1.Container) []*corev1.Container { var targetContainers []*corev1.Container if len(containers) == 0 { return targetContainers } // validate the container is or not exist if len(s.InjectContainer) == 0 { for i := range containers { targetContainers = append(targetContainers, &containers[i]) } return targetContainers } containerMatcher, err := regexp.Compile(s.InjectContainer) if err != nil { log.Error(err, fmt.Sprintf("failed to compile regular expression: %s", s.InjectContainer)) targetContainers = append(targetContainers, &containers[0]) return targetContainers } for i := range containers { if containerMatcher.MatchString(containers[i].Name) { log.Info(fmt.Sprintf("container %s matched. ", containers[i].Name)) targetContainers = append(targetContainers, &containers[i]) } } return targetContainers } func (s *SidecarInjectField) injectErrorAnnotation(annotation *map[string]string, errorInfo string) { (*annotation)[sidecarInjectErrorAnno] = errorInfo } func (s *SidecarInjectField) injectSucceedAnnotation(annotation *map[string]string) { (*annotation)[SidecarInjectSucceedAnno] = "true" (*annotation)[ActiveInjectorLabel] = "true" } // SidecarOverlayandGetValue get final value of sidecar func (s *SidecarInjectField) SidecarOverlayandGetValue(ao *AnnotationOverlay, annotation *map[string]string, a Annotation, ) (string, bool) { if _, ok := (*annotation)[a.Name]; ok { err := ao.SetOverlay(annotation, a) if err != nil { s.injectErrorAnnotation(annotation, err.Error()) return "", false } } return (*ao)[a], true } // annotation > swAgent > default func (s *SidecarInjectField) setValue(config *string, ao *AnnotationOverlay, annotation *map[string]string, a Annotation, ) bool { if v, ok := s.SidecarOverlayandGetValue(ao, annotation, a); ok { if len(v) > 0 { *config = v return true } else if len(*config) > 0 { return true } *config = a.DefaultValue return true } return false } func (s *SidecarInjectField) OverlaySwAgentCR(swAgentL *v1alpha1.SwAgentList) bool { // chose the last matched SwAgent if len(swAgentL.Items) > 0 { swAgent := swAgentL.Items[len(swAgentL.Items)-1] log.Info(fmt.Sprintf("agent %s loaded.", swAgent.Name)) // shared volume, mount path is fixed s.SidecarVolume.Name = swAgent.Spec.SharedVolumeName s.SidecarVolume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{} s.SidecarVolumeMount.Name = swAgent.Spec.SharedVolumeName s.SidecarVolumeMount.MountPath = mountPath // init container s.Initcontainer.Name = swAgent.Spec.JavaSidecar.Name s.Initcontainer.Image = swAgent.Spec.JavaSidecar.Image s.Initcontainer.Args = swAgent.Spec.JavaSidecar.Args s.Initcontainer.Command = swAgent.Spec.JavaSidecar.Command s.Initcontainer.VolumeMounts = append(s.Initcontainer.VolumeMounts, corev1.VolumeMount{ Name: swAgent.Spec.SharedVolumeName, MountPath: mountPath, }) // target container s.Envs = swAgent.Spec.JavaSidecar.Env s.InjectContainer = swAgent.Spec.ContainerMatcher } return true } // OverlaySidecar overlays default config func (s *SidecarInjectField) OverlaySidecar(a Annotations, ao *AnnotationOverlay, annotation *map[string]string) bool { s.Initcontainer.Command = make([]string, 1) s.Initcontainer.Args = make([]string, 2) limitsStr := "" requestStr := "" // every annotation map a pointer to the field of SidecarInjectField annoField := map[string]*string{ "initcontainer.Name": &s.Initcontainer.Name, "initcontainer.Image": &s.Initcontainer.Image, "initcontainer.Command": &s.Initcontainer.Command[0], "initcontainer.args.Option": &s.Initcontainer.Args[0], "initcontainer.args.Command": &s.Initcontainer.Args[1], "initcontainer.resources.limits": &limitsStr, "initcontainer.resources.requests": &requestStr, "sidecarVolume.Name": &s.SidecarVolume.Name, "sidecarVolumeMount.MountPath": &s.SidecarVolumeMount.MountPath, "env.Name": &s.Env.Name, "env.Value": &s.Env.Value, } anno := GetAnnotationsByPrefix(a, sidecarAnnotationPrefix) for _, v := range anno.Annotations { fieldName := strings.TrimPrefix(v.Name, sidecarAnnotationPrefix) if pointer, ok := annoField[fieldName]; ok { if !s.setValue(pointer, ao, annotation, v) { return false } } } s.SidecarVolumeMount.Name = s.SidecarVolume.Name s.Initcontainer.VolumeMounts = []corev1.VolumeMount{s.SidecarVolumeMount} // add requests and limits to initcontainer if limitsStr != "nil" { limits := make(corev1.ResourceList) err := json.Unmarshal([]byte(limitsStr), &limits) if err != nil { log.Error(err, "unmarshal limitsStr error") return false } s.Initcontainer.Resources.Limits = limits } if requestStr != "nil" { requests := make(corev1.ResourceList) err := json.Unmarshal([]byte(requestStr), &requests) if err != nil { log.Error(err, "unmarshal requestStr error") return false } s.Initcontainer.Resources.Requests = requests } // the sidecar volume's type is determined s.SidecarVolume.VolumeSource.EmptyDir = nil return true } // AgentOverlayandGetValue will do real annotation overlay func (s *SidecarInjectField) AgentOverlayandGetValue(ao *AnnotationOverlay, annotation *map[string]string, a Annotation, ) bool { if _, ok := (*annotation)[a.Name]; ok { err := ao.SetOverlay(annotation, a) if err != nil { s.injectErrorAnnotation(annotation, err.Error()) return false } } return true } // OverlayAgent overlays agent func (s *SidecarInjectField) OverlayAgent(a Annotations, ao *AnnotationOverlay, annotation *map[string]string) bool { // jvmAgentConfigStr init s.JvmAgentConfigStr = "" anno := GetAnnotationsByPrefix(a, agentAnnotationPrefix) for k, v := range *annotation { if strings.HasPrefix(k, agentAnnotationPrefix) { for _, an := range anno.Annotations { if strings.EqualFold(k, an.Name) { if !s.AgentOverlayandGetValue(ao, annotation, an) { return false } } } configName := strings.TrimPrefix(k, agentAnnotationPrefix) config := strings.Join([]string{configName, v}, "=") // add to jvmAgentConfigStr if s.JvmAgentConfigStr != "" { s.JvmAgentConfigStr = strings.Join([]string{s.JvmAgentConfigStr, config}, ",") } else { s.JvmAgentConfigStr = config } } } return true } // OverlayOptional overlays optional plugins and move optional plugins to the directory(/plugins) // user must ensure that the optional plugins are in the injected container's image // Notice , user must specify the correctness of the regular value // such as optional.skywalking.apache.org: "trace|webflux|cloud-gateway-2.1.x" or // optional-reporter.skywalking.apache.org: "kafka" // the final command will be "cd /optional-plugins && ls | grep -E "trace|webflux|cloud-gateway-2.1.x" | xargs -i cp {} /plugins // or "cd /optional-reporter-plugins && ls | grep -E "kafka" | xargs -i cp {} /plugins" func (s *SidecarInjectField) OverlayOptional(swAgentL []v1alpha1.SwAgent, annotation *map[string]string) { sourceOptionalPath := strings.Join([]string{s.SidecarVolumeMount.MountPath, "optional-plugins/"}, "/") sourceOptionalReporterPath := strings.Join([]string{s.SidecarVolumeMount.MountPath, "optional-reporter-plugins/"}, "/") sourceBootstrapPath := strings.Join([]string{s.SidecarVolumeMount.MountPath, "bootstrap-plugins/"}, "/") targetPath := strings.Join([]string{s.SidecarVolumeMount.MountPath, "plugins/"}, "/") command := "" optionalPlugin := "" optionalReporterPlugins := "" bootstrapPlugins := "" // chose the last matched SwAgent if len(swAgentL) > 0 { swAgent := swAgentL[len(swAgentL)-1] log.Info(fmt.Sprintf("agent %s loaded.", swAgent.Name)) if len(swAgent.Spec.OptionalPlugins) > 0 { optionalPlugin = strings.Join(swAgent.Spec.OptionalPlugins, "|") } if len(swAgent.Spec.OptionalReporterPlugins) > 0 { optionalReporterPlugins = strings.Join(swAgent.Spec.OptionalReporterPlugins, "|") } if len(swAgent.Spec.BootstrapPlugins) > 0 { bootstrapPlugins = strings.Join(swAgent.Spec.BootstrapPlugins, "|") } } for k, v := range *annotation { if strings.EqualFold(k, optionsAnnotation) { optionalPlugin = v } else if strings.EqualFold(k, optionsReporterAnnotation) { optionalReporterPlugins = v } else if strings.EqualFold(k, bootstrapAnnotation) { bootstrapPlugins = v } if command != "" { s.Initcontainer.Args[1] = strings.Join([]string{s.Initcontainer.Args[1], command}, " && ") } } if len(optionalPlugin) > 0 { command = "cd " + sourceOptionalPath + " && ls | grep -E \"" + optionalPlugin + "\" | xargs -i cp {} " + targetPath s.Initcontainer.Args[1] = strings.Join([]string{s.Initcontainer.Args[1], command}, " && ") } if len(optionalReporterPlugins) > 0 { command = "cd " + sourceOptionalReporterPath + " && ls | grep -E \"" + optionalReporterPlugins + "\" | xargs -i cp {} " + targetPath s.Initcontainer.Args[1] = strings.Join([]string{s.Initcontainer.Args[1], command}, " && ") } if len(bootstrapPlugins) > 0 { command = "cd " + sourceBootstrapPath + " && ls | grep -E \"" + bootstrapPlugins + "\" | xargs -i cp {} " + targetPath s.Initcontainer.Args[1] = strings.Join([]string{s.Initcontainer.Args[1], command}, " && ") } } // OverlayPlugins will add Plugins' config to JvmAgentStr without verification // Notice, if a config is not in agent.config, it will be seen as a plugin config // user must ensure the accuracy of configuration. // Otherwides,If a separator(, or =) in the option or value, it should be wrapped in quotes. func (s *SidecarInjectField) OverlayPlugins(annotation *map[string]string) { for k, v := range *annotation { if strings.HasPrefix(k, pluginsAnnotationPrefix) { configName := strings.TrimPrefix(k, pluginsAnnotationPrefix) config := strings.Join([]string{configName, v}, "=") // add to jvmAgentConfigStr if s.JvmAgentConfigStr != "" { s.JvmAgentConfigStr = strings.Join([]string{s.JvmAgentConfigStr, config}, ",") } else { s.JvmAgentConfigStr = config } } } } func GetInjectedAgentConfig(annotation *map[string]string, configuration *map[string]string) { for k, v := range *annotation { if strings.HasPrefix(k, agentAnnotationPrefix) { option := strings.TrimPrefix(k, agentAnnotationPrefix) (*configuration)[option] = strings.Join([]string{"\"", "\""}, v) } else if strings.HasPrefix(k, pluginsAnnotationPrefix) { option := strings.TrimPrefix(k, pluginsAnnotationPrefix) (*configuration)[option] = strings.Join([]string{"\"", "\""}, v) } else if strings.EqualFold(k, optionsAnnotation) { option := "optional-plugin" (*configuration)[option] = strings.Join([]string{"\"", "\""}, v) } else if strings.EqualFold(k, optionsReporterAnnotation) { option := "optional-reporter-plugin" (*configuration)[option] = strings.Join([]string{"\"", "\""}, v) } } }