Created
October 10, 2024 02:59
-
-
Save algo7/451023460573c1704f4185b3d3953477 to your computer and use it in GitHub Desktop.
Go Pointer Issues
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// injectEnvVars fails the test | |
package v1 | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"github.com/morphean-sa/grafana-apm-yielder/internal/config" | |
corev1 "k8s.io/api/core/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
ctrl "sigs.k8s.io/controller-runtime" | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | |
) | |
var ( | |
executionLog = ctrl.Log.WithName("execution") | |
// mergeableEnvVars is a list of environment variables that should be merged and the merge strategy | |
// otherwise by default the environment variable will be replaced | |
mergeableEnvVars = map[string]string{ | |
"NODE_OPTIONS": "space", | |
"JAVA_OPTS": "space", | |
"PYTHONPATH": "colon", | |
} | |
) | |
// keep the injection status annotation | |
// the grafana agent url should be configurable in a configmap | |
// env var injection should be configurable in a configmap | |
// Ref: https://book.kubebuilder.io/reference/markers/webhook | |
// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=ignore,groups="",resources=pods,verbs=create;update,versions=v1,sideEffects=None,name=grafana-apm-yielder.morphean.io,admissionReviewVersions=v1,matchPolicy=Exact | |
// PodApmConfigInjector is an implementation of admission.Handler | |
type PodApmConfigInjector struct { | |
client client.Client | |
decoder admission.Decoder | |
config *config.InjectionSettings // Configurations for the injection | |
} | |
// NewPodApmConfigInjector returns a new instance of PodApmConfigInjector | |
// Takes client.Client and *runtime.Scheme as arguments which are provided by the manager | |
func NewPodApmConfigInjector(client client.Client, scheme *runtime.Scheme, config *config.InjectionSettings) *PodApmConfigInjector { | |
return &PodApmConfigInjector{ | |
client: client, | |
decoder: admission.NewDecoder(scheme), | |
config: config, | |
} | |
} | |
// generateAdmissionErrorMessage generates an error message for the admission response | |
func generateAdmissionErrorMessage(reason string) string { | |
return fmt.Sprintf("not instrumenting the pod due to %s, proceeding with the normal pod creation process", reason) | |
} | |
// Handle is the function to handle pod admission requests | |
func (a *PodApmConfigInjector) Handle(ctx context.Context, req admission.Request) admission.Response { | |
/** | |
There is no admission.Errored() even if error occurs, the response will be admission.Allowed() | |
This is because the pod creation should not be blocked due to instrumentation failure. | |
**/ | |
pod := &corev1.Pod{} | |
err := a.decoder.Decode(req, pod) | |
if err != nil { | |
executionLog.Error(err, "failed to decode the pod") | |
return admission.Allowed(generateAdmissionErrorMessage("failure to decode the pod")) | |
} | |
// Log request information | |
podName := pod.Name | |
if req.Operation == "CREATE" { | |
podName = pod.GenerateName | |
} | |
executionLog.Info("admission request", "operation", req.Operation, "namespace", req.Namespace, "pod", podName) | |
// Check if the OTEL endpoint is available | |
if a.config.CheckEndpointAvailability { | |
err := a.config.CheckEndpoint() | |
if err != nil { | |
executionLog.Error(err, "not instrumenting the pod due to endpoint availability check failure", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("endpoint availability check failure")) | |
} | |
} | |
/*** | |
Check if the pod requires a mutation | |
Even though we have objectSelector in the webhook configuration, it is a good practice to check if the pod requires a mutation | |
***/ | |
podLabels := pod.GetLabels() | |
mutationRequired := isMutationRequired(podLabels) | |
executionLog.Info("checking if the pod requires a mutation", "pod", podName) | |
executionLog.Info("mutation required?", "required", mutationRequired, "pod", podName) | |
if !mutationRequired { | |
// If the pod does not require a mutation, return Allowed | |
return admission.Allowed("no mutation required") | |
} | |
// Detect the language of the pod | |
language := detectLanguage(podLabels) | |
// Create environment variables based on the language | |
envVarsToInject, err := newAutoInstrumentationEnvVars(language, a.config) | |
if err != nil { | |
executionLog.Error(err, "failed to create environment variables, not instrumenting the pod, proceed with the normal pod creation process", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("failure to create environment variables")) | |
} | |
// Inject the environment variables | |
executionLog.Info("injecting environment variables", "pod", podName) | |
injectEnvVars(ctx, pod, envVarsToInject, a.client) | |
// Create the init container based on the language | |
initContainerToInject, err := newAutoInstrumentationContainer(language, a.config) | |
if err != nil { | |
executionLog.Error(err, "failed to create init container, not instrumenting the pod", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("failure to create the initcontainer")) | |
} | |
// Inject the init container | |
executionLog.Info("injecting init container", "pod", podName) | |
injectInitContainer(pod, initContainerToInject) | |
// Add annotation to the pod to indicate that it has been injected | |
if pod.Annotations == nil { | |
pod.Annotations = make(map[string]string) | |
} | |
pod.Annotations["apm.morphean.io/injected"] = "true" | |
// Marshal the pod to JSON | |
marshaledPod, err := json.Marshal(pod) | |
if err != nil { | |
executionLog.Error(err, "failed to marshal pod") | |
return admission.Allowed(generateAdmissionErrorMessage("failure to marshal the pod")) | |
} | |
// Return the patched response | |
executionLog.Info("pod mutation completed", "pod", podName) | |
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) | |
} | |
// isMutationRequired checks if the pod requires a mutation | |
// The conidtion is if the label "apm.morphean.io/inject" is set to true | |
func isMutationRequired(labels map[string]string) bool { | |
value, ok := labels["apm.morphean.io/inject"] | |
if ok { | |
return value == "true" | |
} | |
return false | |
} | |
// detectLanguage detects the language of the pod based on the pod labels | |
func detectLanguage(podLabels map[string]string) string { | |
switch podLabels["apm.morphean.io/language"] { | |
case "java": | |
return "java" | |
case "nodejs": | |
return "nodejs" | |
case "python": | |
return "python" | |
default: | |
return "" | |
} | |
} | |
// newAutoInstrumentationEnvVars creates a list of environment variables for autoinstrumentation based on the language of the pod | |
func newAutoInstrumentationEnvVars(language string, config *config.InjectionSettings) ([]corev1.EnvVar, error) { | |
switch language { | |
case "java": | |
return config.Java.ToEnvVars(), nil | |
case "python": | |
return config.Python.ToEnvVars(), nil | |
case "nodejs": | |
return config.NodeJS.ToEnvVars(), nil | |
default: | |
return nil, fmt.Errorf("unsupported language: %s", language) | |
} | |
} | |
// appendEnVar appends the environment variable to the original environment variable based on the append strategy | |
// if no supported strategy is provided, the original environment variable is returned | |
func appendEnvVar(originalEnvVar string, envVarToInject string, appendStrategy string) string { | |
switch appendStrategy { | |
case "space": | |
return fmt.Sprintf("%s %s", originalEnvVar, envVarToInject) | |
case "comma": | |
return fmt.Sprintf("%s,%s", originalEnvVar, envVarToInject) | |
case "colon": | |
return fmt.Sprintf("%s:%s", originalEnvVar, envVarToInject) | |
default: | |
return originalEnvVar | |
} | |
} | |
// injectEnvVars injects the monitoring environment variables into the pod | |
// Note that this function modifies the pod in place | |
func injectEnvVars(ctx context.Context, pod *corev1.Pod, envVarsToInject []corev1.EnvVar, c client.Client) { | |
// Step 1: Cache ConfigMaps and collect environment variables | |
configMapCache := make(map[string]*corev1.ConfigMap) | |
envVarsFromConfigMap := make(map[string]string) // Stores all env vars from ConfigMap | |
// Loop through each container in the pod | |
for i := range pod.Spec.Containers { | |
// Check if the container uses environment variables from external sources | |
if pod.Spec.Containers[i].EnvFrom != nil { | |
// Loop through each environment variable source | |
for _, cm := range pod.Spec.Containers[i].EnvFrom { | |
// Check if the environment variables are sourced from ConfigMap | |
if cm.ConfigMapRef != nil { | |
configMapName := cm.ConfigMapRef.Name | |
// Fetch and cache ConfigMap if not already present | |
_, exists := configMapCache[configMapName] | |
if !exists { | |
configMap := &corev1.ConfigMap{} | |
err := c.Get(ctx, client.ObjectKey{Namespace: pod.Namespace, Name: configMapName}, configMap) | |
if err != nil { | |
executionLog.Error(err, "failed to fetch ConfigMap", "configmap", configMapName) | |
continue | |
} | |
// Cache the ConfigMap | |
configMapCache[configMapName] = configMap | |
} | |
// Collect all environment variables from the ConfigMap | |
for key, value := range configMapCache[configMapName].Data { | |
envVarsFromConfigMap[key] = value | |
} | |
} | |
} | |
} | |
// Step 2: Construct a map of existing container Env variables for lookup | |
envVarsFromEnv := make(map[string]*corev1.EnvVar, len(pod.Spec.Containers[i].Env)) | |
for e := range pod.Spec.Containers[i].Env { | |
envVarsFromEnv[pod.Spec.Containers[i].Env[e].Name] = &pod.Spec.Containers[i].Env[e] | |
} | |
// Step 3: Handle merging and injection of variables based on conditions | |
for _, envVarToInject := range envVarsToInject { | |
// Check if the environment variable is in the ConfigMap | |
configMapValue, inConfigMap := envVarsFromConfigMap[envVarToInject.Name] | |
// Check if the environment variable is in the Env | |
existingEnvVar, inEnv := envVarsFromEnv[envVarToInject.Name] | |
// Check if the environment variable is mergeable | |
_, isMergeable := mergeableEnvVars[envVarToInject.Name] | |
switch isMergeable { | |
// Mergeable env var | |
case true: | |
// Scenario 1: Exists in both Env and ConfigMap | |
// merge the injected value with the value from Env, ignoring the value from ConfigMap | |
if inEnv && inConfigMap { | |
existingEnvVar.Value = appendEnvVar(existingEnvVar.Value, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
} | |
// Scenario 2: Exists in ConfigMap but not in Env | |
// merge the injected value with the value from ConfigMap | |
if !inEnv && inConfigMap { | |
envVarToInject.Value = appendEnvVar(configMapValue, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, envVarToInject) | |
} | |
// Scenario 3: Exists in Env but not in ConfigMap | |
// merge the injected value with the value from Env | |
if inEnv && !inConfigMap { | |
existingEnvVar.Value = appendEnvVar(existingEnvVar.Value, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
} | |
// Non-mergeable env var | |
case false: | |
// Scenario 4: Exists in both Env, ConfigMap, or both | |
// leave the value as is for Kubernetes to handle | |
if inEnv || inConfigMap { | |
continue | |
} | |
// Scenario 5: Does not exist in Env or ConfigMap | |
// inject the value | |
if !inEnv && !inConfigMap { | |
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, envVarToInject) | |
} | |
} | |
} | |
} | |
} | |
// newAutoInstrumentationContainer creates an initContainer for autoinstrumentation based on the language of the pod | |
func newAutoInstrumentationContainer(language string, config *config.InjectionSettings) (corev1.Container, error) { | |
switch language { | |
case "java": | |
return config.Java.GenerateInitContainer(), nil | |
case "python": | |
return config.Python.GenerateInitContainer(), nil | |
case "nodejs": | |
return config.NodeJS.GenerateInitContainer(), nil | |
default: | |
return corev1.Container{}, fmt.Errorf("unsupported language: %s", language) | |
} | |
} | |
// injectInitContainer injects the init container, volume, and volume mounts to the pod | |
func injectInitContainer(pod *corev1.Pod, initContainer corev1.Container) { | |
// Check for duplicate init container | |
for _, existingContainer := range pod.Spec.InitContainers { | |
if existingContainer.Name == initContainer.Name { | |
return // Do not add if init container already exists | |
} | |
} | |
pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) | |
// Ensure the volume exists | |
volumeExists := false | |
for _, volume := range pod.Spec.Volumes { | |
if volume.Name == initContainer.VolumeMounts[0].Name { | |
volumeExists = true | |
break | |
} | |
} | |
// If the volume does not exist, add it | |
if !volumeExists { | |
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ | |
Name: initContainer.VolumeMounts[0].Name, | |
VolumeSource: corev1.VolumeSource{ | |
EmptyDir: &corev1.EmptyDirVolumeSource{}, | |
}, | |
}) | |
} | |
// Ensure the volume mount exists for each container | |
for i := range pod.Spec.Containers { | |
volumeMountExists := false | |
for _, volumeMount := range pod.Spec.Containers[i].VolumeMounts { | |
if volumeMount.Name == initContainer.VolumeMounts[0].Name { | |
volumeMountExists = true | |
break | |
} | |
} | |
// If the volume mount does not exist, add it | |
if !volumeMountExists { | |
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, initContainer.VolumeMounts...) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// injectEnvVars passes the test | |
package v1 | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"github.com/morphean-sa/grafana-apm-yielder/internal/config" | |
corev1 "k8s.io/api/core/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
ctrl "sigs.k8s.io/controller-runtime" | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | |
) | |
var ( | |
executionLog = ctrl.Log.WithName("execution") | |
// mergeableEnvVars is a list of environment variables that should be merged and the merge strategy | |
// otherwise by default the environment variable will be replaced | |
mergeableEnvVars = map[string]string{ | |
"NODE_OPTIONS": "space", | |
"JAVA_OPTS": "space", | |
"PYTHONPATH": "colon", | |
} | |
) | |
// keep the injection status annotation | |
// the grafana agent url should be configurable in a configmap | |
// env var injection should be configurable in a configmap | |
// Ref: https://book.kubebuilder.io/reference/markers/webhook | |
// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=ignore,groups="",resources=pods,verbs=create;update,versions=v1,sideEffects=None,name=grafana-apm-yielder.morphean.io,admissionReviewVersions=v1,matchPolicy=Exact | |
// PodApmConfigInjector is an implementation of admission.Handler | |
type PodApmConfigInjector struct { | |
client client.Client | |
decoder admission.Decoder | |
config *config.InjectionSettings // Configurations for the injection | |
} | |
// NewPodApmConfigInjector returns a new instance of PodApmConfigInjector | |
// Takes client.Client and *runtime.Scheme as arguments which are provided by the manager | |
func NewPodApmConfigInjector(client client.Client, scheme *runtime.Scheme, config *config.InjectionSettings) *PodApmConfigInjector { | |
return &PodApmConfigInjector{ | |
client: client, | |
decoder: admission.NewDecoder(scheme), | |
config: config, | |
} | |
} | |
// generateAdmissionErrorMessage generates an error message for the admission response | |
func generateAdmissionErrorMessage(reason string) string { | |
return fmt.Sprintf("not instrumenting the pod due to %s, proceeding with the normal pod creation process", reason) | |
} | |
// Handle is the function to handle pod admission requests | |
func (a *PodApmConfigInjector) Handle(ctx context.Context, req admission.Request) admission.Response { | |
/** | |
There is no admission.Errored() even if error occurs, the response will be admission.Allowed() | |
This is because the pod creation should not be blocked due to instrumentation failure. | |
**/ | |
pod := &corev1.Pod{} | |
err := a.decoder.Decode(req, pod) | |
if err != nil { | |
executionLog.Error(err, "failed to decode the pod") | |
return admission.Allowed(generateAdmissionErrorMessage("failure to decode the pod")) | |
} | |
// Log request information | |
podName := pod.Name | |
if req.Operation == "CREATE" { | |
podName = pod.GenerateName | |
} | |
executionLog.Info("admission request", "operation", req.Operation, "namespace", req.Namespace, "pod", podName) | |
// Check if the OTEL endpoint is available | |
if a.config.CheckEndpointAvailability { | |
err := a.config.CheckEndpoint() | |
if err != nil { | |
executionLog.Error(err, "not instrumenting the pod due to endpoint availability check failure", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("endpoint availability check failure")) | |
} | |
} | |
/*** | |
Check if the pod requires a mutation | |
Even though we have objectSelector in the webhook configuration, it is a good practice to check if the pod requires a mutation | |
***/ | |
podLabels := pod.GetLabels() | |
mutationRequired := isMutationRequired(podLabels) | |
executionLog.Info("checking if the pod requires a mutation", "pod", podName) | |
executionLog.Info("mutation required?", "required", mutationRequired, "pod", podName) | |
if !mutationRequired { | |
// If the pod does not require a mutation, return Allowed | |
return admission.Allowed("no mutation required") | |
} | |
// Detect the language of the pod | |
language := detectLanguage(podLabels) | |
// Create environment variables based on the language | |
envVarsToInject, err := newAutoInstrumentationEnvVars(language, a.config) | |
if err != nil { | |
executionLog.Error(err, "failed to create environment variables, not instrumenting the pod, proceed with the normal pod creation process", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("failure to create environment variables")) | |
} | |
// Inject the environment variables | |
executionLog.Info("injecting environment variables", "pod", podName) | |
injectEnvVars(ctx, pod, envVarsToInject, a.client) | |
// Create the init container based on the language | |
initContainerToInject, err := newAutoInstrumentationContainer(language, a.config) | |
if err != nil { | |
executionLog.Error(err, "failed to create init container, not instrumenting the pod", "pod", podName) | |
return admission.Allowed(generateAdmissionErrorMessage("failure to create the initcontainer")) | |
} | |
// Inject the init container | |
executionLog.Info("injecting init container", "pod", podName) | |
injectInitContainer(pod, initContainerToInject) | |
// Add annotation to the pod to indicate that it has been injected | |
if pod.Annotations == nil { | |
pod.Annotations = make(map[string]string) | |
} | |
pod.Annotations["apm.morphean.io/injected"] = "true" | |
// Marshal the pod to JSON | |
marshaledPod, err := json.Marshal(pod) | |
if err != nil { | |
executionLog.Error(err, "failed to marshal pod") | |
return admission.Allowed(generateAdmissionErrorMessage("failure to marshal the pod")) | |
} | |
// Return the patched response | |
executionLog.Info("pod mutation completed", "pod", podName) | |
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) | |
} | |
// isMutationRequired checks if the pod requires a mutation | |
// The conidtion is if the label "apm.morphean.io/inject" is set to true | |
func isMutationRequired(labels map[string]string) bool { | |
value, ok := labels["apm.morphean.io/inject"] | |
if ok { | |
return value == "true" | |
} | |
return false | |
} | |
// detectLanguage detects the language of the pod based on the pod labels | |
func detectLanguage(podLabels map[string]string) string { | |
switch podLabels["apm.morphean.io/language"] { | |
case "java": | |
return "java" | |
case "nodejs": | |
return "nodejs" | |
case "python": | |
return "python" | |
default: | |
return "" | |
} | |
} | |
// newAutoInstrumentationEnvVars creates a list of environment variables for autoinstrumentation based on the language of the pod | |
func newAutoInstrumentationEnvVars(language string, config *config.InjectionSettings) ([]corev1.EnvVar, error) { | |
switch language { | |
case "java": | |
return config.Java.ToEnvVars(), nil | |
case "python": | |
return config.Python.ToEnvVars(), nil | |
case "nodejs": | |
return config.NodeJS.ToEnvVars(), nil | |
default: | |
return nil, fmt.Errorf("unsupported language: %s", language) | |
} | |
} | |
// appendEnVar appends the environment variable to the original environment variable based on the append strategy | |
// if no supported strategy is provided, the original environment variable is returned | |
func appendEnvVar(originalEnvVar string, envVarToInject string, appendStrategy string) string { | |
switch appendStrategy { | |
case "space": | |
return fmt.Sprintf("%s %s", originalEnvVar, envVarToInject) | |
case "comma": | |
return fmt.Sprintf("%s,%s", originalEnvVar, envVarToInject) | |
case "colon": | |
return fmt.Sprintf("%s:%s", originalEnvVar, envVarToInject) | |
default: | |
return originalEnvVar | |
} | |
} | |
// injectEnvVars injects the monitoring environment variables into the pod | |
// Note that this function modifies the pod in place | |
func injectEnvVars(ctx context.Context, pod *corev1.Pod, envVarsToInject []corev1.EnvVar, c client.Client) { | |
// Step 1: Cache ConfigMaps and collect environment variables | |
configMapCache := make(map[string]*corev1.ConfigMap) | |
envVarsFromConfigMap := make(map[string]string) // Stores all env vars from ConfigMap | |
// Loop through each container in the pod | |
for i := range pod.Spec.Containers { | |
// Check if the container uses environment variables from external sources | |
if pod.Spec.Containers[i].EnvFrom != nil { | |
// Loop through each environment variable source | |
for _, cm := range pod.Spec.Containers[i].EnvFrom { | |
// Check if the environment variables are sourced from ConfigMap | |
if cm.ConfigMapRef != nil { | |
configMapName := cm.ConfigMapRef.Name | |
// Fetch and cache ConfigMap if not already present | |
_, exists := configMapCache[configMapName] | |
if !exists { | |
configMap := &corev1.ConfigMap{} | |
err := c.Get(ctx, client.ObjectKey{Namespace: pod.Namespace, Name: configMapName}, configMap) | |
if err != nil { | |
executionLog.Error(err, "failed to fetch ConfigMap", "configmap", configMapName) | |
continue | |
} | |
// Cache the ConfigMap | |
configMapCache[configMapName] = configMap | |
} | |
// Collect all environment variables from the ConfigMap | |
for key, value := range configMapCache[configMapName].Data { | |
envVarsFromConfigMap[key] = value | |
} | |
} | |
} | |
} | |
// Step 2: Construct a map of existing container Env variables for lookup | |
envVarsFromEnv := make(map[string]int, len(pod.Spec.Containers[i].Env)) | |
for e := range pod.Spec.Containers[i].Env { | |
envVarsFromEnv[pod.Spec.Containers[i].Env[e].Name] = e | |
} | |
// Step 3: Handle merging and injection of variables based on conditions | |
for _, envVarToInject := range envVarsToInject { | |
// Check if the environment variable is in the ConfigMap | |
configMapValue, inConfigMap := envVarsFromConfigMap[envVarToInject.Name] | |
// Check if the environment variable is in the Env | |
envIndex, inEnv := envVarsFromEnv[envVarToInject.Name] | |
// Check if the environment variable is mergeable | |
_, isMergeable := mergeableEnvVars[envVarToInject.Name] | |
switch isMergeable { | |
// Mergeable env var | |
case true: | |
// Scenario 1: Exists in both Env and ConfigMap | |
// merge the injected value with the value from Env, ignoring the value from ConfigMap | |
if inEnv && inConfigMap { | |
pod.Spec.Containers[i].Env[envIndex].Value = appendEnvVar(pod.Spec.Containers[i].Env[envIndex].Value, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
} | |
// Scenario 2: Exists in ConfigMap but not in Env | |
// merge the injected value with the value from ConfigMap | |
if !inEnv && inConfigMap { | |
envVarToInject.Value = appendEnvVar(configMapValue, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, envVarToInject) | |
} | |
// Scenario 3: Exists in Env but not in ConfigMap | |
// merge the injected value with the value from Env | |
if inEnv && !inConfigMap { | |
pod.Spec.Containers[i].Env[envIndex].Value = appendEnvVar(pod.Spec.Containers[i].Env[envIndex].Value, envVarToInject.Value, mergeableEnvVars[envVarToInject.Name]) | |
} | |
// Non-mergeable env var | |
case false: | |
// Scenario 4: Exists in both Env, ConfigMap, or both | |
// leave the value as is for Kubernetes to handle | |
if inEnv || inConfigMap { | |
continue | |
} | |
// Scenario 5: Does not exist in Env or ConfigMap | |
// inject the value | |
if !inEnv && !inConfigMap { | |
pod.Spec.Containers[i].Env = append(pod.Spec.Containers[i].Env, envVarToInject) | |
} | |
} | |
} | |
} | |
} | |
// newAutoInstrumentationContainer creates an initContainer for autoinstrumentation based on the language of the pod | |
func newAutoInstrumentationContainer(language string, config *config.InjectionSettings) (corev1.Container, error) { | |
switch language { | |
case "java": | |
return config.Java.GenerateInitContainer(), nil | |
case "python": | |
return config.Python.GenerateInitContainer(), nil | |
case "nodejs": | |
return config.NodeJS.GenerateInitContainer(), nil | |
default: | |
return corev1.Container{}, fmt.Errorf("unsupported language: %s", language) | |
} | |
} | |
// injectInitContainer injects the init container, volume, and volume mounts to the pod | |
func injectInitContainer(pod *corev1.Pod, initContainer corev1.Container) { | |
// Check for duplicate init container | |
for _, existingContainer := range pod.Spec.InitContainers { | |
if existingContainer.Name == initContainer.Name { | |
return // Do not add if init container already exists | |
} | |
} | |
pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) | |
// Ensure the volume exists | |
volumeExists := false | |
for _, volume := range pod.Spec.Volumes { | |
if volume.Name == initContainer.VolumeMounts[0].Name { | |
volumeExists = true | |
break | |
} | |
} | |
// If the volume does not exist, add it | |
if !volumeExists { | |
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ | |
Name: initContainer.VolumeMounts[0].Name, | |
VolumeSource: corev1.VolumeSource{ | |
EmptyDir: &corev1.EmptyDirVolumeSource{}, | |
}, | |
}) | |
} | |
// Ensure the volume mount exists for each container | |
for i := range pod.Spec.Containers { | |
volumeMountExists := false | |
for _, volumeMount := range pod.Spec.Containers[i].VolumeMounts { | |
if volumeMount.Name == initContainer.VolumeMounts[0].Name { | |
volumeMountExists = true | |
break | |
} | |
} | |
// If the volume mount does not exist, add it | |
if !volumeMountExists { | |
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, initContainer.VolumeMounts...) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package v1 | |
import ( | |
"context" | |
"encoding/json" | |
"testing" | |
"github.com/morphean-sa/grafana-apm-yielder/internal/config" | |
"github.com/stretchr/testify/assert" | |
"gomodules.xyz/jsonpatch/v2" | |
admissionv1 "k8s.io/api/admission/v1" | |
corev1 "k8s.io/api/core/v1" | |
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
"k8s.io/apimachinery/pkg/runtime" | |
"k8s.io/apimachinery/pkg/types" | |
kfake "k8s.io/client-go/kubernetes/fake" | |
"sigs.k8s.io/controller-runtime/pkg/client" | |
"sigs.k8s.io/controller-runtime/pkg/client/fake" | |
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | |
) | |
/** | |
The following tests uses Java as the language to test the injection as other languages follow the same pattern | |
**/ | |
func TestHandle(t *testing.T) { | |
// Mock data | |
injectorConfig := loadTestConfig(t) | |
// Create a new scheme and add v1 to it | |
scheme := runtime.NewScheme() | |
if err := corev1.AddToScheme(scheme); err != nil { | |
t.Fatalf("failed to add v1 to scheme: %v", err) | |
} | |
// Create a fake controller runtime client with the scheme | |
ctrlClient := fake.NewClientBuilder().WithScheme(scheme).Build() | |
// Create a new decoder with the scheme | |
decoder := admission.NewDecoder(scheme) | |
// Create a new PodApmConfigInjector handler | |
handler := &PodApmConfigInjector{ | |
client: ctrlClient, | |
decoder: decoder, | |
config: injectorConfig, | |
} | |
// Define test cases | |
testCases := []struct { | |
name string | |
labels map[string]string | |
pod *corev1.Pod | |
allowed bool // Expected value of resp.Allowed | |
verifyPatches bool // Whether to verify patches (only true when label is present) | |
}{ | |
{ | |
name: "Label Present - Allowed", | |
labels: map[string]string{"apm.morphean.io/inject": "true", "apm.morphean.io/language": "java"}, | |
allowed: true, | |
verifyPatches: true, | |
}, | |
// { | |
// name: "Label Missing - Allowed", | |
// labels: map[string]string{}, | |
// allowed: true, | |
// verifyPatches: false, | |
// }, | |
} | |
// Run the test cases | |
for _, tc := range testCases { | |
t.Run(tc.name, func(t *testing.T) { | |
// Create a simple Alpine pod with the required label | |
pod := &corev1.Pod{ | |
ObjectMeta: metav1.ObjectMeta{ | |
GenerateName: "alpine-", | |
Namespace: "default", | |
Labels: tc.labels, | |
}, | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "alpine", | |
Image: "alpine", | |
}, | |
}, | |
}, | |
} | |
// Marshal the pod into JSON | |
rawPod, err := json.Marshal(pod) | |
if err != nil { | |
t.Fatalf("failed to marshal pod: %v", err) | |
} | |
// Create a mock admission request | |
req := admission.Request{ | |
AdmissionRequest: admissionv1.AdmissionRequest{ | |
UID: types.UID("1234"), | |
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, | |
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, | |
Name: "alpine-1234", | |
Namespace: "default", | |
Operation: "CREATE", | |
Object: runtime.RawExtension{ | |
Raw: rawPod, | |
}, | |
}, | |
} | |
// Create a context and handle the request | |
ctx := context.Background() | |
resp := handler.Handle(ctx, req) | |
// Verify the response | |
if !resp.Allowed { | |
t.Errorf("expected request to be allowed, but it was denied: %v", resp.Result.Message) | |
} | |
/** | |
Below is to test if the generated patches are as desired | |
We create a fake Kubernetes client (not the controller runtime client) to create the original pod | |
and then apply the patches to the pod to verify the mutation | |
**/ | |
if tc.verifyPatches { | |
// Create a fake Kubernetes client | |
k8sClient := kfake.NewSimpleClientset() | |
// Create the original pod using the fake Kubernetes client | |
_, err = k8sClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{}) | |
if err != nil { | |
t.Fatalf("failed to create original pod: %v", err) | |
} | |
// Convert the patches (in the form of AdmissionResponse.Patches) to the format expected by the Kubernetes client | |
patches := make([]jsonpatch.Operation, len(resp.Patches)) | |
for i, patch := range resp.Patches { | |
patches[i] = jsonpatch.Operation{ | |
Operation: patch.Operation, | |
Path: patch.Path, | |
Value: patch.Value, | |
} | |
} | |
// Marshal the patches into JSON | |
patchBytes, err := json.Marshal(patches) | |
if err != nil { | |
t.Fatalf("failed to marshal patches: %v", err) | |
} | |
// Apply the patches to the pod | |
mutatedPod, err := k8sClient.CoreV1().Pods(pod.Namespace).Patch(ctx, pod.Name, types.JSONPatchType, patchBytes, metav1.PatchOptions{}) | |
if err != nil { | |
t.Fatalf("failed to apply patch: %v", err) | |
} | |
// Verify the annotation | |
_, ok := mutatedPod.Annotations["apm.morphean.io/injected"] | |
if !ok { | |
t.Errorf("expected annotation 'apm.morphean.io/injected' to be present, but it was not found") | |
} | |
// Verify the presence of the init container | |
initContainerFound := false | |
for _, initContainer := range mutatedPod.Spec.InitContainers { | |
if initContainer.Name == "javaagent-download" { | |
initContainerFound = true | |
if initContainer.Image != injectorConfig.Java.InitContainerImage { | |
t.Errorf("expected init container 'javaagent-download' to have image %v, but got %v", injectorConfig.Java.InitContainerImage, initContainer.Image) | |
} | |
break | |
} | |
} | |
if !initContainerFound { | |
t.Errorf("expected init container 'javaagent-download' to be present, but it was not found") | |
} | |
// Verify the environment variables | |
for _, expectedEnvVar := range injectorConfig.Java.ToEnvVars() { | |
envVarFound := false | |
for _, envVar := range mutatedPod.Spec.Containers[0].Env { | |
if envVar.Name == expectedEnvVar.Name && envVar.Value == expectedEnvVar.Value { | |
envVarFound = true | |
break | |
} | |
} | |
if !envVarFound { | |
t.Errorf("expected env var %v=%v to be present, but it was not found", expectedEnvVar.Name, expectedEnvVar.Value) | |
} | |
} | |
} | |
}) | |
} | |
} | |
func TestIsMutationRequired(t *testing.T) { | |
assert := assert.New(t) | |
testCases := []struct { | |
name string | |
labels map[string]string | |
expected bool | |
}{ | |
{ | |
name: "inject label is true", | |
labels: map[string]string{ | |
"apm.morphean.io/inject": "true", | |
}, | |
expected: true, | |
}, | |
{ | |
name: "inject label is false", | |
labels: map[string]string{ | |
"apm.morphean.io/inject": "false", | |
}, | |
expected: false, | |
}, | |
{ | |
name: "inject label is not set", | |
labels: map[string]string{}, | |
expected: false, | |
}, | |
} | |
// Run the test cases | |
for _, tc := range testCases { | |
t.Run(tc.name, func(t *testing.T) { | |
assert.Equal(tc.expected, isMutationRequired(tc.labels)) | |
}) | |
} | |
} | |
func TestInjectInitContainer(t *testing.T) { | |
assert := assert.New(t) | |
// Mock data | |
javaSettings := config.Java{ | |
JavaToolOptions: "-javaagent:/javaagent/grafana-opentelemetry-java.jar -Dotel.jmx.target.system=jvm", | |
InitContainerImage: "appropriate/curl", | |
InitContainerArgs: []string{"-o", "/javaagent/grafana-opentelemetry-java.jar", "-L", "https://github.com/grafana/grafana-opentelemetry-java/releases/download/v2.4.0-beta.1/grafana-opentelemetry-java.jar"}, | |
} | |
// Test cases | |
testCases := []struct { | |
name string | |
pod *corev1.Pod | |
config config.Java | |
expectedInitContainer corev1.Container | |
expectedVolumes []corev1.Volume | |
expectedMounts []corev1.VolumeMount | |
}{ | |
{ | |
name: "inject init container, volume, and mounts", | |
pod: &corev1.Pod{ | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
}, | |
}, | |
}, | |
}, | |
config: javaSettings, | |
expectedInitContainer: corev1.Container{ | |
Name: "javaagent-download", | |
Image: "appropriate/curl", | |
Args: []string{ | |
"-o", | |
"/javaagent/grafana-opentelemetry-java.jar", | |
"-L", | |
"https://github.com/grafana/grafana-opentelemetry-java/releases/download/v2.4.0-beta.1/grafana-opentelemetry-java.jar", | |
}, | |
VolumeMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
MountPath: "/javaagent", | |
}, | |
}, | |
}, | |
expectedVolumes: []corev1.Volume{ | |
{ | |
Name: "java-agent", | |
VolumeSource: corev1.VolumeSource{ | |
EmptyDir: &corev1.EmptyDirVolumeSource{}, | |
}, | |
}, | |
}, | |
expectedMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
MountPath: "/javaagent", | |
}, | |
}, | |
}, | |
{ | |
name: "init container already exists", | |
pod: &corev1.Pod{ | |
Spec: corev1.PodSpec{ | |
InitContainers: []corev1.Container{ | |
{ | |
Name: "javaagent-download", | |
}, | |
}, | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
}, | |
}, | |
}, | |
}, | |
config: javaSettings, | |
expectedInitContainer: corev1.Container{ | |
Name: "javaagent-download", | |
}, | |
expectedVolumes: []corev1.Volume{}, | |
expectedMounts: []corev1.VolumeMount{}, | |
}, | |
{ | |
name: "volume already exists", | |
pod: &corev1.Pod{ | |
Spec: corev1.PodSpec{ | |
Volumes: []corev1.Volume{ | |
{ | |
Name: "java-agent", | |
}, | |
}, | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
}, | |
}, | |
}, | |
}, | |
config: javaSettings, | |
expectedInitContainer: corev1.Container{ | |
Name: "javaagent-download", | |
Image: "appropriate/curl", | |
Args: []string{ | |
"-o", | |
"/javaagent/grafana-opentelemetry-java.jar", | |
"-L", | |
"https://github.com/grafana/grafana-opentelemetry-java/releases/download/v2.4.0-beta.1/grafana-opentelemetry-java.jar", | |
}, | |
VolumeMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
MountPath: "/javaagent", | |
}, | |
}, | |
}, | |
expectedVolumes: []corev1.Volume{ | |
{ | |
Name: "java-agent", | |
}, | |
}, | |
expectedMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
MountPath: "/javaagent", | |
}, | |
}, | |
}, | |
{ | |
name: "volume mount already exists", | |
pod: &corev1.Pod{ | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
VolumeMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
config: javaSettings, | |
expectedInitContainer: corev1.Container{ | |
Name: "javaagent-download", | |
Image: "appropriate/curl", | |
Args: []string{ | |
"-o", | |
"/javaagent/grafana-opentelemetry-java.jar", | |
"-L", | |
"https://github.com/grafana/grafana-opentelemetry-java/releases/download/v2.4.0-beta.1/grafana-opentelemetry-java.jar", | |
}, | |
VolumeMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
MountPath: "/javaagent", | |
}, | |
}, | |
}, | |
expectedVolumes: []corev1.Volume{}, | |
expectedMounts: []corev1.VolumeMount{ | |
{ | |
Name: "java-agent", | |
}, | |
}, | |
}, | |
} | |
// Run the test cases | |
for _, tc := range testCases { | |
t.Run(tc.name, func(t *testing.T) { | |
injectInitContainer(tc.pod, tc.config.GenerateInitContainer()) | |
// Check init containers | |
if len(tc.expectedInitContainer.Name) > 0 { | |
found := false | |
for _, initContainer := range tc.pod.Spec.InitContainers { | |
if initContainer.Name == tc.expectedInitContainer.Name { | |
found = true | |
assert.Equal(tc.expectedInitContainer, initContainer) | |
break | |
} | |
} | |
assert.True(found, "expected init container not found") | |
} | |
// Check volumes | |
for _, expectedVolume := range tc.expectedVolumes { | |
found := false | |
for _, volume := range tc.pod.Spec.Volumes { | |
if volume.Name == expectedVolume.Name { | |
found = true | |
assert.Equal(expectedVolume, volume) | |
break | |
} | |
} | |
assert.True(found, "expected volume not found") | |
} | |
// Check volume mounts | |
for i, container := range tc.pod.Spec.Containers { | |
for _, expectedMount := range tc.expectedMounts { | |
found := false | |
for _, volumeMount := range container.VolumeMounts { | |
if volumeMount.Name == expectedMount.Name { | |
found = true | |
assert.Equal(expectedMount, volumeMount) | |
break | |
} | |
} | |
assert.True(found, "expected volume mount not found in container %d", i) | |
} | |
} | |
}) | |
} | |
} | |
func TestAppendEnvVars(t *testing.T) { | |
assert := assert.New(t) | |
originalEnvVar := "a" | |
envVarToInject := "b" | |
testCases := []struct { | |
name string | |
mergeStrategy string | |
}{ | |
{ | |
name: "space", | |
mergeStrategy: "space", | |
}, | |
{ | |
name: "comma", | |
mergeStrategy: "comma", | |
}, | |
{ | |
name: "colon", | |
mergeStrategy: "colon", | |
}, | |
{ | |
name: "invalid merge strategy", | |
mergeStrategy: "", | |
}, | |
} | |
// Run the test cases | |
for _, tc := range testCases { | |
t.Run(tc.name, func(t *testing.T) { | |
switch tc.mergeStrategy { | |
case "space": | |
result := appendEnvVar(originalEnvVar, envVarToInject, "space") | |
assert.Equal("a b", result, "expected 'a b', but got %s", result) | |
case "comma": | |
result := appendEnvVar(originalEnvVar, envVarToInject, "comma") | |
assert.Equal("a,b", result, "expected 'a,b', but got %s", result) | |
case "colon": | |
result := appendEnvVar(originalEnvVar, envVarToInject, "colon") | |
assert.Equal("a:b", result, "expected 'a:b', but got %s", result) | |
default: | |
result := appendEnvVar(originalEnvVar, envVarToInject, "") | |
assert.Equal("a", result, "expected 'a', but got %s", result) | |
} | |
}) | |
} | |
} | |
func TestInjectEnvVars(t *testing.T) { | |
assert := assert.New(t) | |
// Mock data | |
injectorConfig := loadTestConfig(t) | |
// Test cases | |
testCases := []struct { | |
name string | |
pod *corev1.Pod | |
expectedEnvVars []corev1.EnvVar | |
ctx context.Context | |
createConfigMap bool | |
}{ | |
{ | |
name: "inject env vars", | |
pod: &corev1.Pod{ | |
ObjectMeta: metav1.ObjectMeta{ | |
Name: "test-pod", | |
Namespace: "default", | |
}, | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
Env: []corev1.EnvVar{ | |
{ | |
Name: "EXISTING_ENV_VAR", | |
Value: "existing-value", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
expectedEnvVars: append(injectorConfig.Java.ToEnvVars(), corev1.EnvVar{ | |
Name: "EXISTING_ENV_VAR", | |
Value: "existing-value", | |
}), | |
ctx: context.Background(), | |
createConfigMap: false, | |
}, | |
{ | |
name: "inject env vars with existing env vars", | |
pod: &corev1.Pod{ | |
ObjectMeta: metav1.ObjectMeta{ | |
Name: "test-pod", | |
Namespace: "default", | |
}, | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
Env: []corev1.EnvVar{ | |
{ | |
Name: "OTEL_RESOURCE_PROVIDERS_AWS_ENABLED", | |
Value: "false", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
expectedEnvVars: []corev1.EnvVar{ | |
{ | |
Name: "OTEL_JAVAAGENT_ENABLED", | |
Value: "true", | |
}, | |
{ | |
Name: "JAVA_TOOL_OPTIONS", | |
Value: "-javaagent:/javaagent/grafana-opentelemetry-java.jar -Dotel.jmx.target.system=jvm", | |
}, | |
{ | |
Name: "OTEL_RESOURCE_PROVIDERS_AWS_ENABLED", | |
Value: "false", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_ENDPOINT", | |
Value: "http://alloy.monitoring:4318", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/traces", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/metrics", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/logs", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_PROTOCOL", | |
Value: "http/protobuf", | |
}, | |
{ | |
Name: "OTEL_METRICS_EXPORTER", | |
Value: "otlp", | |
}, | |
{ | |
Name: "OTEL_TRACES_EXPORTER", | |
Value: "otlp", | |
}, | |
{ | |
Name: "OTEL_TRACES_SAMPLER", | |
Value: "parentbased_always_on", | |
}, | |
{ | |
Name: "OTEL_LOGS_EXPORTER", | |
Value: "none", | |
}, | |
{ | |
Name: "OTEL_INSTRUMENTATION_MICROMETER_ENABLED", | |
Value: "true", | |
}, | |
}, | |
ctx: context.Background(), | |
createConfigMap: false, | |
}, | |
{ | |
name: "inject env vars with existing env vars in configmap", | |
pod: &corev1.Pod{ | |
ObjectMeta: metav1.ObjectMeta{ | |
Name: "test-pod", | |
Namespace: "default", | |
}, | |
Spec: corev1.PodSpec{ | |
Containers: []corev1.Container{ | |
{ | |
Name: "test-container", | |
EnvFrom: []corev1.EnvFromSource{ | |
{ | |
ConfigMapRef: &corev1.ConfigMapEnvSource{ | |
LocalObjectReference: corev1.LocalObjectReference{ | |
Name: "test-config", | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
}, | |
expectedEnvVars: []corev1.EnvVar{ | |
{ | |
Name: "NODE_OPTIONS", | |
Value: "brrooooo --require /otel-auto-instrumentation-nodejs/build/src/register.js", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_ENDPOINT", | |
Value: "http://alloy.monitoring:4318", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/traces", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/metrics", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", | |
Value: "http://alloy.monitoring:4318/v1/logs", | |
}, | |
{ | |
Name: "OTEL_EXPORTER_OTLP_PROTOCOL", | |
Value: "http/protobuf", | |
}, | |
{ | |
Name: "OTEL_METRICS_EXPORTER", | |
Value: "otlp", | |
}, | |
{ | |
Name: "OTEL_TRACES_EXPORTER", | |
Value: "otlp", | |
}, | |
{ | |
Name: "OTEL_TRACES_SAMPLER", | |
Value: "parentbased_always_on", | |
}, | |
{ | |
Name: "OTEL_LOGS_EXPORTER", | |
Value: "none", | |
}, | |
}, | |
createConfigMap: true, | |
ctx: context.Background(), | |
}, | |
} | |
// Run the test cases | |
for _, tc := range testCases { | |
t.Run(tc.name, func(t *testing.T) { | |
client := setupTestControllerClient(t, tc.createConfigMap, *tc.pod) | |
if tc.createConfigMap { | |
injectEnvVars(tc.ctx, tc.pod, injectorConfig.NodeJS.ToEnvVars(), client) | |
} else { | |
injectEnvVars(tc.ctx, tc.pod, injectorConfig.Java.ToEnvVars(), client) | |
} | |
for _, container := range tc.pod.Spec.Containers { | |
assert.ElementsMatch(tc.expectedEnvVars, container.Env) | |
} | |
}) | |
} | |
} | |
func loadTestConfig(t *testing.T) *config.InjectionSettings { | |
t.Helper() // Marks the function as a test helper function | |
// Load the configuration from the testdata directory | |
cfg, err := config.Load([]byte(`checkEndpointAvailability: false | |
otelSettings: | |
otelExporterOtlpEndpoint: "http://alloy.monitoring:4318" | |
otelExporterOtlpProtocol: "http/protobuf" | |
otelMetricsExporter: "otlp" | |
otelTracesExporter: "otlp" | |
otelTracesSampler: "parentbased_always_on" | |
otelLogsExporter: "none" | |
java: | |
javaToolOptions: "-javaagent:/javaagent/grafana-opentelemetry-java.jar -Dotel.jmx.target.system=jvm" | |
otelResourceProvidersAwsEnabled: "true" | |
otelJavaAgentEnabled: "true" | |
otelInstrumentationMicrometerEnabled: "true" | |
initContainerImage: "otel/java-agent-init:latest" | |
initContainerArgs: | |
- "install" | |
- "opentelemetry-java-instrumentation" | |
python: | |
initContainerImage: "otel/python-agent-init:latest" | |
initContainerArgs: | |
- "pip" | |
- "install" | |
- "opentelemetry-instrumentation" | |
- "-t" | |
- "/otel-auto-instrumentation-python" | |
nodejs: | |
nodeOptions: "--require /otel-auto-instrumentation-nodejs/build/src/register.js" | |
initContainerImage: "otel/nodejs-agent-init:latest" | |
initContainerArgs: | |
- "npm" | |
- "install" | |
- "@opentelemetry/auto-instrumentations-node" | |
- "-g" | |
- "--prefix" | |
- "/otel-auto-instrumentation-nodejs" | |
`)) | |
if err != nil { | |
t.Fatalf("failed to load configuration: %v", err) | |
} | |
return cfg | |
} | |
func setupTestControllerClient(t *testing.T, createConfigMap bool, pod corev1.Pod) client.WithWatch { | |
t.Helper() | |
ctrlClient := fake.NewFakeClient() | |
if createConfigMap { | |
// create a test configmap | |
err := ctrlClient.Create(context.Background(), &corev1.ConfigMap{ | |
ObjectMeta: metav1.ObjectMeta{ | |
Name: "test-config", | |
Namespace: "default", | |
}, | |
Data: map[string]string{ | |
"NODE_OPTIONS": "brrooooo", | |
}, | |
}) | |
if err != nil { | |
t.Fatalf("failed to create test configmap: %v", err) | |
} | |
} | |
// Create a test pod | |
err := ctrlClient.Create(context.Background(), &pod) | |
if err != nil { | |
t.Fatalf("failed to create test pod: %v", err) | |
} | |
return ctrlClient | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment