| // 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 ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strings" |
| |
| corev1 "k8s.io/api/core/v1" |
| "k8s.io/apimachinery/pkg/api/errors" |
| "sigs.k8s.io/controller-runtime/pkg/client" |
| 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 has the optional plugin , |
| // the container will panic |
| optionsAnnotation = "optional.skywalking.apache.org" |
| // If user want to use optional-reporter-plugins , the annotation must match a optinal-reporter plugin |
| // such as optional-exporter.skywalking.apache.org: "kafka" |
| optionsReporterAnnotation = "optional-reporter.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 |
| // configmapVolume is volume that provide user with configmap |
| ConfigmapVolume corev1.Volume |
| // configmapVolumeMount is the configmap's mountpath for user |
| // Notice : the mount path will overwrite the original agent/config/agent.config |
| // So the mount path must match the path of agent.config in the image |
| ConfigmapVolumeMount 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) |
| if len(s.ConfigmapVolume.Name) > 0 && len(s.ConfigmapVolume.ConfigMap.Name) > 0 { |
| pod.Spec.Volumes = append(pod.Spec.Volumes, s.ConfigmapVolume) |
| } |
| |
| // 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 len(s.ConfigmapVolumeMount.Name) > 0 && len(s.ConfigmapVolumeMount.MountPath) > 0 { |
| (*targetContainers[i]).VolumeMounts = append((*targetContainers[i]).VolumeMounts, s.ConfigmapVolumeMount) |
| } |
| |
| 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(a Annotations, 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 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 |
| } else { |
| *config = a.DefaultValue |
| return true |
| } |
| } |
| return false |
| } |
| |
| func (s *SidecarInjectField) OverlaySwAgentCR(swAgentL *v1alpha1.SwAgentList, pod *corev1.Pod) bool { |
| s.ConfigmapVolume.ConfigMap = new(corev1.ConfigMapVolumeSource) |
| |
| // 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 |
| |
| // agent configmap |
| if swAgent.Spec.SwConfigMapVolume != nil { |
| if len(swAgent.Spec.SwConfigMapVolume.Name) > 0 && |
| len(swAgent.Spec.SwConfigMapVolume.ConfigMapName) > 0 && |
| len(swAgent.Spec.SwConfigMapVolume.ConfigMapMountFile) > 0 { |
| //s.ConfigmapVolume = corev1.Volume{} |
| s.ConfigmapVolume.Name = swAgent.Spec.SwConfigMapVolume.Name |
| s.ConfigmapVolume.ConfigMap = new(corev1.ConfigMapVolumeSource) |
| s.ConfigmapVolume.ConfigMap.Name = swAgent.Spec.SwConfigMapVolume.ConfigMapName |
| //s.ConfigmapVolumeMount = corev1.VolumeMount{} |
| s.ConfigmapVolumeMount.Name = swAgent.Spec.SwConfigMapVolume.Name |
| s.ConfigmapVolumeMount.MountPath = "/sky/agent/config/" + swAgent.Spec.SwConfigMapVolume.ConfigMapMountFile |
| s.ConfigmapVolumeMount.SubPath = swAgent.Spec.SwConfigMapVolume.ConfigMapMountFile |
| } |
| } |
| |
| // 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) |
| if nil == s.ConfigmapVolume.ConfigMap { |
| s.ConfigmapVolume.ConfigMap = new(corev1.ConfigMapVolumeSource) |
| } |
| |
| 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, |
| "configmapVolume.ConfigMap.Name": &s.ConfigmapVolume.ConfigMap.Name, |
| "configmapVolume.Name": &s.ConfigmapVolume.Name, |
| "configmapVolumeMount.MountPath": &s.ConfigmapVolumeMount.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.ConfigmapVolumeMount.Name = s.ConfigmapVolume.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/"}, "/") |
| targetPath := strings.Join([]string{s.SidecarVolumeMount.MountPath, "plugins/"}, "/") |
| |
| command := "" |
| optionalPlugin := "" |
| optinalReporterPlugins := "" |
| |
| // 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 { |
| optinalReporterPlugins = strings.Join(swAgent.Spec.OptionalReporterPlugins, "|") |
| } |
| } |
| |
| for k, v := range *annotation { |
| if strings.EqualFold(k, optionsAnnotation) { |
| optionalPlugin = v |
| } else if strings.EqualFold(k, optionsReporterAnnotation) { |
| optinalReporterPlugins = 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(optinalReporterPlugins) > 0 { |
| command = "cd " + sourceOptionalReporterPath + " && ls | grep -E \"" + optinalReporterPlugins + "\" | 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 |
| } |
| } |
| } |
| } |
| |
| // ValidateConfigmap will validate a configmap(if exists) to set java agent config. |
| func (s *SidecarInjectField) ValidateConfigmap(ctx context.Context, kubeclient client.Client, namespace string, |
| annotation *map[string]string) bool { |
| if len(s.ConfigmapVolume.Name) == 0 || len(s.ConfigmapVolume.ConfigMap.Name) == 0 { |
| return true |
| } |
| configmap := &corev1.ConfigMap{} |
| configmapName := s.ConfigmapVolume.VolumeSource.ConfigMap.LocalObjectReference.Name |
| // check whether the configmap is existed |
| err := kubeclient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: configmapName}, configmap) |
| if err != nil && !errors.IsNotFound(err) { |
| log.Error(err, "Get Configmap failed", "configmapName", configmapName, "namespace", namespace) |
| return false |
| } |
| // if configmap exist , validate it |
| if !errors.IsNotFound(err) { |
| ok, errinfo := ValidateConfigmap(configmap) |
| if ok { |
| log.Info("the configmap validate true", "configmapName", configmapName) |
| return true |
| } |
| log.Error(errinfo, "the configmap validate false", "configmapName", configmapName) |
| } |
| return true |
| } |
| |
| 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) |
| } |
| } |
| } |