Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: inject instrumentation relevant env vars using webhook #2304

Merged
10 changes: 10 additions & 0 deletions api/odigos/v1alpha1/instrumentationconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v1alpha1
import (
"github.com/odigos-io/odigos/api/odigos/v1alpha1/instrumentationrules"
"github.com/odigos-io/odigos/common"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -62,6 +63,15 @@ type InstrumentationConfigStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" protobuf:"bytes,1,rep,name=conditions"`
}

func (in *InstrumentationConfigStatus) GetRuntimeDetailsForContainer(container v1.Container) *RuntimeDetailsByContainer {
for _, runtimeDetails := range in.RuntimeDetailsByContainer {
if runtimeDetails.ContainerName == container.Name {
return &runtimeDetails
}
}
return nil
}

// Config for the OpenTelemeetry SDKs that should be applied to a workload.
// The workload is identified by the owner reference
type InstrumentationConfigSpec struct {
Expand Down
21 changes: 21 additions & 0 deletions common/envOverwrite/overwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ func ValToAppend(envName string, sdk common.OtelSdk) (string, bool) {
return valToAppend, true
}

func GetPossibleValuesPerEnv(env string) map[common.OtelSdk]string {
return EnvValuesMap[env].values
}

// due to a bug we had with the env overwriter logic,
// some patched values were recorded incorrectly into the workload annotation for original value.
// they include odigos values (/var/odigos/...) as if they were the original value in the manifest,
Expand Down Expand Up @@ -214,3 +218,20 @@ func CleanupEnvValueFromOdigosAdditions(envVarName string, envVarValue string) s
sanitizedEnvValue := strings.Join(cleanParts, overwriteMetadata.delim)
return sanitizedEnvValue
}

func AppendOdigosAdditionsToEnvVar(envName string, envFromContainerRuntimeValue string, desiredOdigosAddition string) *string {
envValues, ok := EnvValuesMap[envName]
if !ok {
// Odigos does not manipulate this environment variable, so ignore it
return nil
}

// In case observedValue is exists but empty, we just need to set the desiredOdigosAddition without delim before
if strings.TrimSpace(envFromContainerRuntimeValue) == "" {
return &desiredOdigosAddition
} else {
// In case observedValue is not empty, we need to append the desiredOdigosAddition with the delim
mergedEnvValue := envFromContainerRuntimeValue + envValues.delim + desiredOdigosAddition
return &mergedEnvValue
}
}
145 changes: 81 additions & 64 deletions instrumentor/controllers/instrumentationdevice/pods_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"strings"

"github.com/go-logr/logr"
"github.com/odigos-io/odigos/api/k8sconsts"
"github.com/odigos-io/odigos/common"
webhookenvinjector "github.com/odigos-io/odigos/instrumentor/internal/webhook_env_injector"
containerutils "github.com/odigos-io/odigos/k8sutils/pkg/container"
"github.com/odigos-io/odigos/k8sutils/pkg/workload"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -35,6 +37,8 @@ type PodsWebhook struct {
var _ webhook.CustomDefaulter = &PodsWebhook{}

func (p *PodsWebhook) Default(ctx context.Context, obj runtime.Object) error {
logger := log.FromContext(ctx)

pod, ok := obj.(*corev1.Pod)
if !ok {
return fmt.Errorf("expected a Pod but got a %T", obj)
Expand All @@ -44,70 +48,39 @@ func (p *PodsWebhook) Default(ctx context.Context, obj runtime.Object) error {
pod.Annotations = map[string]string{}
}

serviceName, podWorkload := p.getServiceNameForEnv(ctx, pod)

// Inject ODIGOS environment variables into all containers
injectOdigosEnvVars(pod, podWorkload, serviceName)

return nil
return p.injectOdigosEnvVars(ctx, logger, pod)
}

// checks for the service name on the annotation, or fallback to the workload name
func (p *PodsWebhook) getServiceNameForEnv(ctx context.Context, pod *corev1.Pod) (*string, *workload.PodWorkload) {

logger := log.FromContext(ctx)

podWorkload, err := workload.PodWorkloadObject(ctx, pod)
if err != nil {
logger.Error(err, "failed to extract pod workload details from pod. skipping OTEL_SERVICE_NAME injection")
return nil, nil
}
func (p *PodsWebhook) injectOdigosEnvVars(ctx context.Context, logger logr.Logger, pod *corev1.Pod) error {
// Environment variables that remain consistent across all containers
commonEnvVars := getCommonEnvVars()

req, err := admission.RequestFromContext(ctx)
// In certain scenarios, the raw request can be utilized to retrieve missing details, like the namespace.
// For example, prior to Kubernetes version 1.24 (see https://github.com/kubernetes/kubernetes/pull/94637),
// namespaced objects could be sent to admission webhooks with empty namespaces during their creation.
admissionRequest, err := admission.RequestFromContext(ctx)
if err != nil {
logger.Error(err, "failed to get admission request from context")
return nil, nil
return fmt.Errorf("failed to get admission request: %w", err)
}
podWorkload.Namespace = req.Namespace

workloadObj, err := workload.GetWorkloadObject(ctx, client.ObjectKey{Namespace: podWorkload.Namespace, Name: podWorkload.Name}, podWorkload.Kind, p.Client)
podWorkload, err := workload.PodWorkloadObject(ctx, pod)
if err != nil {
logger.Error(err, "failed to get workload object from cache. cannot check for workload annotation. using workload name as OTEL_SERVICE_NAME")
return &podWorkload.Name, podWorkload
return fmt.Errorf("failed to extract pod workload details from pod: %w", err)
}
resolvedServiceName := workload.ExtractServiceNameFromAnnotations(workloadObj.GetAnnotations(), podWorkload.Name)
return &resolvedServiceName, podWorkload
}

func injectOdigosEnvVars(pod *corev1.Pod, podWorkload *workload.PodWorkload, serviceName *string) {

// Common environment variables that do not change across containers
commonEnvVars := []corev1.EnvVar{
{
Name: k8sconsts.OdigosEnvVarNamespace,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
{
Name: k8sconsts.OdigosEnvVarPodName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
},
if podWorkload.Namespace == "" {
if admissionRequest.Namespace != "" {
// If the namespace is available in the admission request, set it in the podWorkload.Namespace.
podWorkload.Namespace = admissionRequest.Namespace
} else {
// It is a case that not supposed to happen, but if it does, return an error.
return fmt.Errorf("namespace is empty for pod %s/%s, Skipping Injection of ODIGOS environment variables", pod.Namespace, pod.Name)
}
}

var serviceName *string
var serviceNameEnv *corev1.EnvVar
if serviceName != nil {
serviceNameEnv = &corev1.EnvVar{
Name: otelServiceNameEnvVarName,
Value: *serviceName,
}
}

for i := range pod.Spec.Containers {
container := &pod.Spec.Containers[i]
Expand All @@ -117,31 +90,43 @@ func injectOdigosEnvVars(pod *corev1.Pod, podWorkload *workload.PodWorkload, ser
continue
}

webhookenvinjector.InjectOdigosAgentEnvVars(ctx, p.Client, logger, *podWorkload, container, pl, otelsdk)

// Check if the environment variables are already present, if so skip inject them again.
if envVarsExist(container.Env, commonEnvVars) {
continue
}

containerNameEnv := corev1.EnvVar{
Name: k8sconsts.OdigosEnvVarContainerName,
Value: container.Name,
}

resourceAttributes := getResourceAttributes(podWorkload, container.Name)
resourceAttributesEnvValue := getResourceAttributesEnvVarValue(resourceAttributes)

containerNameEnv := corev1.EnvVar{Name: k8sconsts.OdigosEnvVarContainerName, Value: container.Name}
container.Env = append(container.Env, append(commonEnvVars, containerNameEnv)...)

if serviceNameEnv != nil && shouldInjectServiceName(pl, otelsdk) {
if shouldInjectServiceName(pl, otelsdk) {
// Ensure the serviceName is fetched only once per pod
if serviceName == nil {
serviceName = p.getServiceNameForEnv(ctx, logger, podWorkload)
}
// Initialize serviceNameEnv only once per pod if serviceName is valid
if serviceName != nil && serviceNameEnv == nil {
serviceNameEnv = &corev1.EnvVar{
Name: otelServiceNameEnvVarName,
Value: *serviceName,
}
}

if !otelNameExists(container.Env) {
container.Env = append(container.Env, *serviceNameEnv)
}
container.Env = append(container.Env, corev1.EnvVar{
Name: otelResourceAttributesEnvVarName,
Value: resourceAttributesEnvValue,
})
}

resourceAttributes := getResourceAttributes(podWorkload, container.Name)
resourceAttributesEnvValue := getResourceAttributesEnvVarValue(resourceAttributes)

container.Env = append(container.Env, corev1.EnvVar{
Name: otelResourceAttributesEnvVarName,
Value: resourceAttributesEnvValue,
})
}
return nil
}

func envVarsExist(containerEnv []corev1.EnvVar, commonEnvVars []corev1.EnvVar) bool {
Expand Down Expand Up @@ -222,3 +207,35 @@ func shouldInjectServiceName(pl common.ProgrammingLanguage, otelsdk common.OtelS
}
return false
}

func getCommonEnvVars() []corev1.EnvVar {
return []corev1.EnvVar{
{
Name: k8sconsts.OdigosEnvVarNamespace,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
{
Name: k8sconsts.OdigosEnvVarPodName,
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
},
}
}

// checks for the service name on the annotation, or fallback to the workload name
func (p *PodsWebhook) getServiceNameForEnv(ctx context.Context, logger logr.Logger, podWorkload *workload.PodWorkload) *string {
workloadObj, err := workload.GetWorkloadObject(ctx, client.ObjectKey{Namespace: podWorkload.Namespace, Name: podWorkload.Name}, podWorkload.Kind, p.Client)
if err != nil {
logger.Error(err, "failed to get workload object from cache. cannot check for workload annotation. using workload name as OTEL_SERVICE_NAME")
return &podWorkload.Name
}
resolvedServiceName := workload.ExtractServiceNameFromAnnotations(workloadObj.GetAnnotations(), podWorkload.Name)
return &resolvedServiceName
}
Loading
Loading