Skip to content

Instantly share code, notes, and snippets.

@algo7
Created October 10, 2024 02:59
Show Gist options
  • Save algo7/451023460573c1704f4185b3d3953477 to your computer and use it in GitHub Desktop.
Save algo7/451023460573c1704f4185b3d3953477 to your computer and use it in GitHub Desktop.
Go Pointer Issues
// 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...)
}
}
}
// 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...)
}
}
}
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