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: webhook to set agent injection env var with odigos additions #2107

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/odigos/v1alpha1/instrumentationconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package v1alpha1
import (
"github.com/odigos-io/odigos/api/odigos/v1alpha1/instrumentationrules"
"github.com/odigos-io/odigos/common"
"go.opentelemetry.io/otel/attribute"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -62,6 +64,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 {
tamirdavid1 marked this conversation as resolved.
Show resolved Hide resolved
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
22 changes: 21 additions & 1 deletion common/envOverwrite/overwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,25 @@ func ValToAppend(envName string, sdk common.OtelSdk) (string, bool) {
}

func GetPossibleValuesPerEnv(env string) map[common.OtelSdk]string {
return EnvValuesMap[env].values
if envValues, ok := EnvValuesMap[env]; ok {
return envValues.values
}
return nil
}

func AppendOdigosAdditionsToEnvVar(envName string, observedValue 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(observedValue) == "" {
return &desiredOdigosAddition
} else {
// In case observedValue is not empty, we need to append the desiredOdigosAddition with the delim
mergedEnvValue := observedValue + envValues.delim + desiredOdigosAddition
return &mergedEnvValue
}
}
149 changes: 84 additions & 65 deletions instrumentor/controllers/instrumentationdevice/pods_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import (
"fmt"
"strings"

"github.com/odigos-io/odigos/api/k8sconsts"

"github.com/go-logr/logr"
"github.com/odigos-io/odigos/common"
webhookenvinjector "github.com/odigos-io/odigos/instrumentor/internal/webhook_env_injector"
k8sconsts "github.com/odigos-io/odigos/k8sutils/pkg/consts"
"github.com/odigos-io/odigos/api/k8sconsts"
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 +39,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 +50,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)
func (p *PodsWebhook) injectOdigosEnvVars(ctx context.Context, logger logr.Logger, pod *corev1.Pod) error {
// Environment variables that remain consistent across all containers
commonEnvVars := getCommonEnvVars()

podWorkload, err := workload.PodWorkloadObject(ctx, pod)
// 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 extract pod workload details from pod. skipping OTEL_SERVICE_NAME injection")
return nil, nil
return fmt.Errorf("failed to get admission request: %w", err)
}

req, err := admission.RequestFromContext(ctx)
if err != nil {
logger.Error(err, "failed to get admission request from context")
return nil, nil
}
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 +92,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,
}
}
tamirdavid1 marked this conversation as resolved.
Show resolved Hide resolved

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{
tamirdavid1 marked this conversation as resolved.
Show resolved Hide resolved
Name: otelResourceAttributesEnvVarName,
Value: resourceAttributesEnvValue,
})
}
return nil
}

func envVarsExist(containerEnv []corev1.EnvVar, commonEnvVars []corev1.EnvVar) bool {
Expand Down Expand Up @@ -222,3 +209,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