diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9d4d4ac05..ac39ba033 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -113,7 +113,7 @@ jobs: - name: Test common module working-directory: ./common run: | - go test -v ./... + make test test-procdiscovery: runs-on: ubuntu-latest diff --git a/autoscaler/controllers/gateway/root.go b/autoscaler/controllers/gateway/root.go index 44ec81285..0b36003b0 100644 --- a/autoscaler/controllers/gateway/root.go +++ b/autoscaler/controllers/gateway/root.go @@ -60,7 +60,7 @@ func Sync(ctx context.Context, client client.Client, scheme *runtime.Scheme, ima odigosSystemNamespaceName := env.GetCurrentNamespace() var odigosConfig odigosv1.OdigosConfiguration - if err := client.Get(ctx, types.NamespacedName{Namespace: odigosSystemNamespaceName, Name: consts.DefaultOdigosConfigurationName}, &odigosConfig); err != nil { + if err := client.Get(ctx, types.NamespacedName{Namespace: odigosSystemNamespaceName, Name: consts.OdigosConfigurationName}, &odigosConfig); err != nil { logger.Error(err, "failed to get odigos config") return err } diff --git a/cli/cmd/resources/applyresources.go b/cli/cmd/resources/applyresources.go index dc9816248..5137a980e 100644 --- a/cli/cmd/resources/applyresources.go +++ b/cli/cmd/resources/applyresources.go @@ -9,6 +9,7 @@ import ( "github.com/odigos-io/odigos/cli/cmd/resources/resourcemanager" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/cli/pkg/log" + "github.com/odigos-io/odigos/common/consts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -41,5 +42,5 @@ func DeleteOldOdigosSystemObjects(ctx context.Context, client *kube.Client, ns s } func GetCurrentConfig(ctx context.Context, client *kube.Client, ns string) (*v1alpha1.OdigosConfiguration, error) { - return client.OdigosClient.OdigosConfigurations(ns).Get(ctx, OdigosConfigName, metav1.GetOptions{}) + return client.OdigosClient.OdigosConfigurations(ns).Get(ctx, consts.OdigosConfigurationName, metav1.GetOptions{}) } diff --git a/cli/cmd/resources/odigosconfig.go b/cli/cmd/resources/odigosconfig.go index 95594e3c1..d67832785 100644 --- a/cli/cmd/resources/odigosconfig.go +++ b/cli/cmd/resources/odigosconfig.go @@ -7,14 +7,11 @@ import ( "github.com/odigos-io/odigos/cli/cmd/resources/resourcemanager" "github.com/odigos-io/odigos/cli/pkg/kube" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/consts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - OdigosConfigName = "odigos-config" -) - func otelSdkConfigCommunity() (map[common.ProgrammingLanguage]common.OtelSdk, map[common.ProgrammingLanguage][]common.OtelSdk) { return map[common.ProgrammingLanguage]common.OtelSdk{ common.JavaProgrammingLanguage: common.OtelSdkNativeCommunity, @@ -75,7 +72,7 @@ func NewOdigosConfiguration(ns string, config *odigosv1.OdigosConfigurationSpec) APIVersion: "odigos.io/v1alpha1", }, ObjectMeta: metav1.ObjectMeta{ - Name: OdigosConfigName, + Name: consts.OdigosConfigurationName, Namespace: ns, }, Spec: *config, diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go index fe4336af5..111e33ca1 100644 --- a/cli/cmd/uninstall.go +++ b/cli/cmd/uninstall.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/odigos-io/odigos/common/envOverwrite" + "github.com/odigos-io/odigos/k8sutils/pkg/envoverwrite" "github.com/odigos-io/odigos/cli/cmd/resources" "github.com/odigos-io/odigos/cli/pkg/confirm" @@ -202,6 +202,12 @@ func getWorkloadRolloutJsonPatch(obj client.Object, pts *v1.PodTemplateSpec) ([] } // read the original env vars (of the manifest) from the annotation + manifestEnvOriginal, err := envoverwrite.NewOrigWorkloadEnvValues(obj) + if err != nil { + fmt.Println("Failed to get original env vars from annotation: ", err) + manifestEnvOriginal = &envoverwrite.OrigWorkloadEnvValues{} + } + var origManifestEnv map[string]map[string]string if obj.GetAnnotations() != nil { manifestEnvAnnotation, ok := obj.GetAnnotations()[consts.ManifestEnvOriginalValAnnotation] @@ -226,24 +232,20 @@ func getWorkloadRolloutJsonPatch(obj client.Object, pts *v1.PodTemplateSpec) ([] } } - containerOriginalEnv := origManifestEnv[c.Name] - - for iEnv, envVar := range c.Env { - if envOverwrite.ShouldRevert(envVar.Name, envVar.Value) { - if origVal, ok := containerOriginalEnv[envVar.Name]; ok { - // revert the env var to its original value if we have it - patchOperations = append(patchOperations, map[string]interface{}{ - "op": "replace", - "path": fmt.Sprintf("/spec/template/spec/containers/%d/env/%d/value", iContainer, iEnv), - "value": origVal, - }) - } else { - // remove the env var - patchOperations = append(patchOperations, map[string]interface{}{ - "op": "remove", - "path": fmt.Sprintf("/spec/template/spec/containers/%d/env/%d", iContainer, iEnv), - }) - } + for envName, originalEnvValue := range manifestEnvOriginal.GetContainerStoredEnvs(c.Name) { + if origManifestEnv == nil { + // originally the value was absent, so we remove it + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "remove", + "path": fmt.Sprintf("/spec/template/spec/containers/%d/env/%d", iContainer, envName), + }) + } else { + // revert the env var to its original value + patchOperations = append(patchOperations, map[string]interface{}{ + "op": "replace", + "path": fmt.Sprintf("/spec/template/spec/containers/%d/env/%d/value", iContainer, envName), + "value": *originalEnvValue, + }) } } } diff --git a/common/Makefile b/common/Makefile new file mode 100644 index 000000000..06936494f --- /dev/null +++ b/common/Makefile @@ -0,0 +1,4 @@ + +.PHONY: test +test: + go test ./... \ No newline at end of file diff --git a/common/consts/consts.go b/common/consts/consts.go index efff30c0e..7064a2a8e 100644 --- a/common/consts/consts.go +++ b/common/consts/consts.go @@ -5,17 +5,18 @@ import ( ) const ( - CurrentNamespaceEnvVar = "CURRENT_NS" - DefaultOdigosNamespace = "odigos-system" - DefaultOdigosConfigurationName = "odigos-config" - OTLPPort = 4317 - OTLPHttpPort = 4318 - PprofOdigosPort = 6060 - OdigosInstrumentationLabel = "odigos-instrumentation" - InstrumentationEnabled = "enabled" - InstrumentationDisabled = "disabled" - OdigosReportedNameAnnotation = "odigos.io/reported-name" - EbpfInstrumentationAnnotation = "instrumentation.odigos.io/ebpf" // deprecated. + CurrentNamespaceEnvVar = "CURRENT_NS" + DefaultOdigosNamespace = "odigos-system" + OdigosConfigurationName = "odigos-config" + OTLPPort = 4317 + OTLPHttpPort = 4318 + PprofOdigosPort = 6060 + OdigosInstrumentationLabel = "odigos-instrumentation" + InstrumentationEnabled = "enabled" + InstrumentationDisabled = "disabled" + OdigosReportedNameAnnotation = "odigos.io/reported-name" + EbpfInstrumentationAnnotation = "instrumentation.odigos.io/ebpf" // deprecated. + // Used to store the original value of the environment variable in the pod manifest. // This is used to restore the original value when an instrumentation is removed // or odigos is uninstalled. diff --git a/common/envOverwrite/originalenv.go b/common/envOverwrite/originalenv.go new file mode 100644 index 000000000..4fcd5e029 --- /dev/null +++ b/common/envOverwrite/originalenv.go @@ -0,0 +1,14 @@ +package envOverwrite + +// this type is used to store the original values of the environment variables the user has set +// for a specific deployment manifest. +// In k8s we need to store such map for each container in the pod. +// In systemd, this can store the original values of the systemd service. +// +// When we want to rollback the changes we made to the environment variables, we can fetch the original +// values from this map and set them back to the manifest. +// +// The key is the environment variable name. +// The value is either the original value of the environment variable or nil if the environment variable +// was not set by the user. +type OriginalEnv map[string]*string diff --git a/common/envOverwrite/overwriter.go b/common/envOverwrite/overwriter.go index 881aa733b..03eb90088 100644 --- a/common/envOverwrite/overwriter.go +++ b/common/envOverwrite/overwriter.go @@ -7,9 +7,10 @@ import ( ) type envValues struct { - delim string + delim string values map[common.OtelSdk]string } + // EnvValuesMap is a map of environment variables odigos uses for various languages and goals. // The key is the environment variable name and the value is the value to be set or appended // to the environment variable. We need to make sure that in case any of these environment @@ -22,117 +23,94 @@ var EnvValuesMap = map[string]envValues{ delim: " ", values: map[common.OtelSdk]string{ common.OtelSdkNativeCommunity: "--require /var/odigos/nodejs/autoinstrumentation.js", - common.OtelSdkEbpfEnterprise: "--require /var/odigos/nodejs-ebpf/autoinstrumentation.js", + common.OtelSdkEbpfEnterprise: "--require /var/odigos/nodejs-ebpf/autoinstrumentation.js", }, }, "PYTHONPATH": { delim: ":", values: map[common.OtelSdk]string{ common.OtelSdkNativeCommunity: "/var/odigos/python:/var/odigos/python/opentelemetry/instrumentation/auto_instrumentation", - common.OtelSdkEbpfEnterprise: "/var/odigos/python-ebpf:/var/odigos/python/opentelemetry/instrumentation/auto_instrumentation:/var/odigos/python", + common.OtelSdkEbpfEnterprise: "/var/odigos/python-ebpf:/var/odigos/python/opentelemetry/instrumentation/auto_instrumentation:/var/odigos/python", }, }, "JAVA_OPTS": { delim: " ", values: map[common.OtelSdk]string{ common.OtelSdkNativeCommunity: "-javaagent:/var/odigos/java/javaagent.jar", - common.OtelSdkEbpfEnterprise: "-javaagent:/var/odigos/java-ebpf/dtrace-injector.jar", + common.OtelSdkEbpfEnterprise: "-javaagent:/var/odigos/java-ebpf/dtrace-injector.jar", common.OtelSdkNativeEnterprise: "-javaagent:/var/odigos/java-ext-ebpf/javaagent.jar " + - "-Dotel.javaagent.extensions=/var/odigos/java-ext-ebpf/otel_agent_extension.jar", + "-Dotel.javaagent.extensions=/var/odigos/java-ext-ebpf/otel_agent_extension.jar", }, }, "JAVA_TOOL_OPTIONS": { delim: " ", values: map[common.OtelSdk]string{ common.OtelSdkNativeCommunity: "-javaagent:/var/odigos/java/javaagent.jar", - common.OtelSdkEbpfEnterprise: "-javaagent:/var/odigos/java-ebpf/dtrace-injector.jar", + common.OtelSdkEbpfEnterprise: "-javaagent:/var/odigos/java-ebpf/dtrace-injector.jar", common.OtelSdkNativeEnterprise: "-javaagent:/var/odigos/java-ext-ebpf/javaagent.jar " + - "-Dotel.javaagent.extensions=/var/odigos/java-ext-ebpf/otel_agent_extension.jar", + "-Dotel.javaagent.extensions=/var/odigos/java-ext-ebpf/otel_agent_extension.jar", }, }, } -func ShouldPatch(envName string, observedValue string, sdk common.OtelSdk) bool { - env, ok := EnvValuesMap[envName] +// returns the current value that should be populated in a specific environment variable. +// if we should not patch the value, returns nil. +// the are 2 parts to the environment value: odigos part and user part. +// either one can be set or empty. +// so we have 4 cases to handle: +func GetPatchedEnvValue(envName string, observedValue string, currentSdk common.OtelSdk) *string { + envMetadata, ok := EnvValuesMap[envName] if !ok { // Odigos does not manipulate this environment variable, so ignore it - return false + return nil } - desiredValue, ok := env.values[sdk] + desiredOdigosPart, ok := envMetadata.values[currentSdk] if !ok { // No specific overwrite is required for this SDK - return false - } - - if strings.Contains(observedValue, desiredValue) { - // if the observed value contains the value odigos sets for this SDK, - // that means that either: - // 1. the user does not add any additional values and we see here our own value only, - // 2. we already patched the value in the deployment manifest and we see here the patched value, - // so we should not patch it - return false - } - - // if we are moving from one SDK to another, avoid patching the value - // it there is no user defined value (observedValue is equal to the value odigos sets for the previous SDK) - for _, v := range env.values { - if v == observedValue { - return false - } + return nil } - return true -} - -func ShouldRevert(envName string, observedValue string) bool { - env, ok := EnvValuesMap[envName] - if !ok { - // We don't care about this environment variable - return false + // scenario 1: no user defined values and no odigos value + // happens: might be the case right after the source is instrumented, and before the instrumentation is applied. + // action: there are no user defined values, so no need to make any changes. + if observedValue == "" { + return nil } - // If we find any of the values odigos sets for any SDK in the observed value, - // that means that the environment variable is patched by odigos and we need to revert it - for _, val := range env.values { - if strings.Contains(observedValue, val) && observedValue != val { - return true + // scenario 2: no user defined values, only odigos value + // happens: when the user did not set any value to this env (either via manifest or dockerfile) + // action: we don't need to overwrite the value, just let odigos handle it + for _, sdkEnvValue := range envMetadata.values { + if sdkEnvValue == observedValue { + return nil } } - return false -} - -func Patch(envName string, currentVal string, sdk common.OtelSdk) string { - env, exists := EnvValuesMap[envName] - if !exists { - return "" - } - - valToAppend, ok := env.values[sdk] - if !ok { - return "" - } - - if currentVal == "" { - return valToAppend - } - - if strings.Contains(currentVal, valToAppend) { - // The environment variable is already patched with the correct value for this SDK - return currentVal - } - - for sdkKey, val := range env.values { - if sdkKey != sdk && strings.Contains(currentVal, val){ - // The environment variable is patched by another SDK - // but we need to append our value to it, so replace the - // value of the other SDK with ours - return strings.ReplaceAll(currentVal, val, valToAppend) + // Scenario 3: both odigos and user defined values are present + // happens: when the user set some values to this env (either via manifest or dockerfile) and odigos instrumentation is applied. + // action: we want to keep the user defined values and upsert the odigos value. + for _, sdkEnvValue := range envMetadata.values { + if strings.Contains(observedValue, sdkEnvValue) { + if sdkEnvValue == desiredOdigosPart { + // shortcut, the value is already patched + // both the odigos part equals to the new value, and the user part we want to keep + return &observedValue + } else { + // The environment variable is patched by some other odigos sdk. + // replace just the odigos part with the new desired value. + // this can happen when moving between SDKs. + patchedEvnValue := strings.ReplaceAll(observedValue, sdkEnvValue, desiredOdigosPart) + return &patchedEvnValue + } } } - return currentVal + env.delim + valToAppend + // Scenario 4: only user defined values are present + // happens: when the user set some values to this env (either via manifest or dockerfile) and odigos instrumentation not yet applied. + // action: we want to keep the user defined values and append the odigos value. + mergedEnvValue := observedValue + envMetadata.delim + desiredOdigosPart + return &mergedEnvValue } func ValToAppend(envName string, sdk common.OtelSdk) (string, bool) { diff --git a/common/envOverwrite/owerwriter_test.go b/common/envOverwrite/owerwriter_test.go index cef7cc180..6056a4222 100644 --- a/common/envOverwrite/owerwriter_test.go +++ b/common/envOverwrite/owerwriter_test.go @@ -7,122 +7,74 @@ import ( "github.com/stretchr/testify/assert" ) - -func TestShouldPatch(t *testing.T) { +func TestGetPatchedEnvValue(t *testing.T) { nodeOptionsNativeCommunity, _ := ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) nodeOptionsEbpfEnterprise, _ := ValToAppend("NODE_OPTIONS", common.OtelSdkEbpfEnterprise) userVal := "--max-old-space-size=4096" // test different cases tests := []struct { - name string - envName string - observedValue string - sdk common.OtelSdk - shouldPatchExpected bool + name string + envName string + observedValue string + sdk common.OtelSdk patchedValueExpected string }{ { - name: "un-relevant env var", - envName: "PATH", - observedValue: "/usr/local/bin:/usr/bin:/bin", - sdk: common.OtelSdkNativeCommunity, - shouldPatchExpected: false, + name: "un-relevant env var", + envName: "PATH", + observedValue: "/usr/local/bin:/usr/bin:/bin", + sdk: common.OtelSdkNativeCommunity, + patchedValueExpected: "", }, { - name: "only user value", - envName: "NODE_OPTIONS", - observedValue: userVal, - sdk: common.OtelSdkNativeCommunity, - shouldPatchExpected: true, + name: "only user value", + envName: "NODE_OPTIONS", + observedValue: userVal, + sdk: common.OtelSdkNativeCommunity, patchedValueExpected: userVal + " " + nodeOptionsNativeCommunity, }, { - name: "only odigos value", - envName: "NODE_OPTIONS", - observedValue: nodeOptionsNativeCommunity, - sdk: common.OtelSdkNativeCommunity, - shouldPatchExpected: false, + name: "only odigos value", + envName: "NODE_OPTIONS", + observedValue: nodeOptionsNativeCommunity, + sdk: common.OtelSdkNativeCommunity, + patchedValueExpected: "", }, { - name: "user value with odigos value matching SDKs", - envName: "NODE_OPTIONS", - observedValue: userVal + " " + nodeOptionsNativeCommunity, - sdk: common.OtelSdkNativeCommunity, - shouldPatchExpected: false, + name: "user value with odigos value matching SDKs", + envName: "NODE_OPTIONS", + observedValue: userVal + " " + nodeOptionsNativeCommunity, + sdk: common.OtelSdkNativeCommunity, + patchedValueExpected: userVal + " " + nodeOptionsNativeCommunity, }, { - name: "user value with odigos value with different SDKs", - envName: "NODE_OPTIONS", - observedValue: userVal + " " + nodeOptionsNativeCommunity, - sdk: common.OtelSdkEbpfEnterprise, - shouldPatchExpected: true, + name: "user value with odigos value with different SDKs", + envName: "NODE_OPTIONS", + observedValue: userVal + " " + nodeOptionsNativeCommunity, + sdk: common.OtelSdkEbpfEnterprise, patchedValueExpected: userVal + " " + nodeOptionsEbpfEnterprise, }, { // No user values are observed, hence there is not need to patch // even if the observed value is different from the SDK value - name: "observed odigos value different from SDK", - envName: "NODE_OPTIONS", - observedValue: nodeOptionsNativeCommunity, - sdk: common.OtelSdkEbpfEnterprise, - shouldPatchExpected: false, + name: "observed odigos value different from SDK", + envName: "NODE_OPTIONS", + observedValue: nodeOptionsNativeCommunity, + sdk: common.OtelSdkEbpfEnterprise, + patchedValueExpected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotShouldPatch := ShouldPatch(tt.envName, tt.observedValue, tt.sdk) - assert.Equal(t, tt.shouldPatchExpected, gotShouldPatch, "mismatch in ShouldPatch: %s", tt.name) - if gotShouldPatch { - gotPatchedValue := Patch(tt.envName, tt.observedValue, tt.sdk) - assert.Equal(t, tt.patchedValueExpected, gotPatchedValue) + patchedValue := GetPatchedEnvValue(tt.envName, tt.observedValue, tt.sdk) + if patchedValue == nil { + assert.Equal(t, tt.patchedValueExpected, "", "mismatch in GetPatchedEnvValue: %s", tt.name) + } else { + assert.Equal(t, tt.patchedValueExpected, *patchedValue, "mismatch in GetPatchedEnvValue: %s", tt.name) } }) } -} - -func TestShouldRevert(t *testing.T) { - nodeOptionsNativeCommunity, _ := ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) - userVal := "--max-old-space-size=4096" - - // test different cases - tests := []struct { - name string - envName string - observedValue string - wantRevert bool - }{ - { - name: "un-relevant env var", - envName: "PATH", - observedValue: "/usr/local/bin:/usr/bin:/bin", - wantRevert: false, - }, - { - name: "only user value", - envName: "NODE_OPTIONS", - observedValue: userVal, - wantRevert: false, - }, - { - name: "only odigos value", - envName: "NODE_OPTIONS", - observedValue: nodeOptionsNativeCommunity, - wantRevert: false, - }, - { - name: "user value with odigos value matching SDKs", - envName: "NODE_OPTIONS", - observedValue: userVal + " " + nodeOptionsNativeCommunity, - wantRevert: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRevert := ShouldRevert(tt.envName, tt.observedValue) - assert.Equal(t, tt.wantRevert, gotRevert, "mismatch in ShouldRevert: %s", tt.name) - }) - } -} \ No newline at end of file +} diff --git a/frontend/endpoints/namespaces.go b/frontend/endpoints/namespaces.go index 1d4722597..6edd87413 100644 --- a/frontend/endpoints/namespaces.go +++ b/frontend/endpoints/namespaces.go @@ -80,7 +80,7 @@ func getRelevantNameSpaces(ctx context.Context, odigosns string) ([]v1.Namespace g, ctx := errgroup.WithContext(ctx) g.Go(func() error { var err error - odigosConfig, err = kube.DefaultClient.OdigosClient.OdigosConfigurations(odigosns).Get(ctx, consts.DefaultOdigosConfigurationName, metav1.GetOptions{}) + odigosConfig, err = kube.DefaultClient.OdigosClient.OdigosConfigurations(odigosns).Get(ctx, consts.OdigosConfigurationName, metav1.GetOptions{}) return err }) diff --git a/instrumentor/Makefile b/instrumentor/Makefile index 975894d39..f2135298a 100644 --- a/instrumentor/Makefile +++ b/instrumentor/Makefile @@ -36,7 +36,11 @@ vet: ## Run go vet against code. .PHONY: test test: fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -p 1 -coverprofile cover.out + +.PHONY: test-env-overwirte +test-env-overwirte: fmt vet envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./controllers/instrumentationdevice -p 1 -coverprofile cover.out ##@ Build diff --git a/instrumentor/controllers/instrumentationdevice/common.go b/instrumentor/controllers/instrumentationdevice/common.go index f72333ff7..3cd5b4246 100644 --- a/instrumentor/controllers/instrumentationdevice/common.go +++ b/instrumentor/controllers/instrumentationdevice/common.go @@ -72,7 +72,7 @@ func instrument(logger logr.Logger, ctx context.Context, kubeClient client.Clien } var odigosConfig odigosv1.OdigosConfiguration - err = kubeClient.Get(ctx, client.ObjectKey{Namespace: env.GetCurrentNamespace(), Name: "odigos-config"}, &odigosConfig) + err = kubeClient.Get(ctx, client.ObjectKey{Namespace: env.GetCurrentNamespace(), Name: consts.OdigosConfigurationName}, &odigosConfig) if err != nil { return err } @@ -128,7 +128,11 @@ func uninstrument(logger logr.Logger, ctx context.Context, kubeClient client.Cli return err } - instrumentation.Revert(podSpec, obj) + instrumentation.RevertInstrumentationDevices(podSpec) + err = instrumentation.RevertEnvOverwrites(obj, podSpec) + if err != nil { + return err + } return nil }) diff --git a/instrumentor/controllers/instrumentationdevice/envoverwiter_test.go b/instrumentor/controllers/instrumentationdevice/envoverwiter_test.go new file mode 100644 index 000000000..1c2d8375a --- /dev/null +++ b/instrumentor/controllers/instrumentationdevice/envoverwiter_test.go @@ -0,0 +1,239 @@ +package instrumentationdevice_test + +import ( + "context" + + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/consts" + "github.com/odigos-io/odigos/common/envOverwrite" + "github.com/odigos-io/odigos/instrumentor/internal/testutil" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("envoverwrite", func() { + ctx := context.Background() + var namespace *corev1.Namespace + var deployment *appsv1.Deployment + var instrumentedApplication *odigosv1.InstrumentedApplication + + testProgrammingLanguage := common.PythonProgrammingLanguage + deploymentSdk := common.OtelSdkNativeCommunity + testEnvVar := "PYTHONPATH" + // the following is the value that odigos will append to the user's env + var testEnvOdigosValue string + + BeforeEach(func() { + // create a new namespace for each test to prevent conflict + namespace = testutil.NewMockNamespace() + Expect(k8sClient.Create(ctx, namespace)).Should(Succeed()) + + sdkEnvVal, found := envOverwrite.ValToAppend(testEnvVar, deploymentSdk) + Expect(found).Should(BeTrue()) + testEnvOdigosValue = sdkEnvVal + }) + + AfterEach(func() { + // restore odigos config to it's original state + var odigosConfig odigosv1.OdigosConfiguration + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: consts.DefaultOdigosNamespace, Name: consts.OdigosConfigurationName}, &odigosConfig)).Should(Succeed()) + odigosConfig.Spec.DefaultSDKs[testProgrammingLanguage] = common.OtelSdkNativeCommunity + Expect(k8sClient.Update(ctx, &odigosConfig)).Should(Succeed()) + }) + + Describe("User did not set env in manifest or docker image", func() { + + BeforeEach(func() { + // create a deployment with no env + deployment = testutil.SetOdigosInstrumentationEnabled(testutil.NewMockTestDeployment(namespace)) + Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + }) + + It("should not add env vars to deployment", func() { + + // initial state - no env varas in manifest or dockerfile + // and odigos haven't yet injected it's env, so the deployment should have no env vars + instrumentedApplication = testutil.NewMockInstrumentedApplication(deployment) + Expect(k8sClient.Create(ctx, instrumentedApplication)).Should(Succeed()) + + // odigos env is the only one, so no need to inject anything to the manifest + testutil.AssertDepContainerEnvRemainEmpty(ctx, k8sClient, deployment) + + // now the pods restarts, and odigos detects the env var it injected + // via the instrumentation device. + // instrumented application should be updated with the odigos env + k8sClient.Get(ctx, client.ObjectKeyFromObject(instrumentedApplication), instrumentedApplication) + instrumentedApplication = testutil.SetInstrumentedApplicationContainer(instrumentedApplication, &testEnvVar, &testEnvOdigosValue, testProgrammingLanguage) + Expect(k8sClient.Update(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerEnvRemainEmpty(ctx, k8sClient, deployment) + + // uninstrument the deployment, env var in deployment should remain empty + Expect(k8sClient.Delete(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerEnvRemainEmpty(ctx, k8sClient, deployment) + }) + }) + + Describe("User set env var via dockerfile and not in manifest", func() { + userEnvValue := "/from_dockerfile" + var mergedEnvValue string + + BeforeEach(func() { + mergedEnvValue = userEnvValue + ":" + testEnvOdigosValue + // the env var is in dockerfile, thus the manifest should start empty of env vars + deployment = testutil.SetOdigosInstrumentationEnabled(testutil.NewMockTestDeployment(namespace)) + Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + }) + + It("Should add the dockerfile env and odigos env to manifest and successfully revert", func() { + // initial state - should capture the env var from dockerfile only + instrumentedApplication = testutil.SetInstrumentedApplicationContainer(testutil.NewMockInstrumentedApplication(deployment), &testEnvVar, &userEnvValue, testProgrammingLanguage) + Expect(k8sClient.Create(ctx, instrumentedApplication)).Should(Succeed()) + + // odigos should merge the value from dockerfile and odigos env + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // after instrumentation is applied, now the value in the pod should be the merged value + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(instrumentedApplication), instrumentedApplication)).Should(Succeed()) + instrumentedApplication.Spec.RuntimeDetails[0].EnvVars[0].Value = mergedEnvValue + Expect(k8sClient.Update(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnvRemainsSame(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // when uninstrumented, the value should be reverted to the original value which was empty in manifest + Expect(k8sClient.Delete(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnvBecomesEmpty(ctx, k8sClient, deployment) + }) + }) + + Describe("User set env var via manifest and not in dockerfile", func() { + userEnvValue := "/from_manifest" + var mergedEnvValue string + + BeforeEach(func() { + mergedEnvValue = userEnvValue + ":" + testEnvOdigosValue + // the env var is in manifest, thus the deployment should contain it at the start + deployment = testutil.SetDeploymentContainerEnv( + testutil.SetOdigosInstrumentationEnabled( + testutil.NewMockTestDeployment(namespace), + ), + testEnvVar, userEnvValue) + Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + }) + + It("Should update the manifest with merged value, and revet when uninstrumenting", func() { + + // initial state - should capture the env var from manifest only + instrumentedApplication = testutil.SetInstrumentedApplicationContainer(testutil.NewMockInstrumentedApplication(deployment), &testEnvVar, &userEnvValue, testProgrammingLanguage) + Expect(k8sClient.Create(ctx, instrumentedApplication)).Should(Succeed()) + + // odigos should merge the value from manifest and odigos env + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // after instrumentation is applied, now the value in the pod should be the merged value + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(instrumentedApplication), instrumentedApplication)).Should(Succeed()) + instrumentedApplication.Spec.RuntimeDetails[0].EnvVars[0].Value = mergedEnvValue + Expect(k8sClient.Update(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnvRemainsSame(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // when uninstrumented, the value should be reverted to the original value which was in the manifest + Expect(k8sClient.Delete(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, userEnvValue) + }) + }) + + When("Default SDK changes after env var is injected", func() { + + userEnvValue := "/from_manifest" + + BeforeEach(func() { + // the env var is in manifest, thus the deployment should contain it at the start + deployment = testutil.SetDeploymentContainerEnv( + testutil.SetOdigosInstrumentationEnabled( + testutil.NewMockTestDeployment(namespace), + ), + testEnvVar, userEnvValue) + Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + + // initial state - should capture the env var from manifest only + mergedEnvValue := userEnvValue + ":" + testEnvOdigosValue + instrumentedApplication = testutil.SetInstrumentedApplicationContainer(testutil.NewMockInstrumentedApplication(deployment), &testEnvVar, &userEnvValue, testProgrammingLanguage) + Expect(k8sClient.Create(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // after instrumentation is applied, now the value in the pod should be the merged value + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(instrumentedApplication), instrumentedApplication)).Should(Succeed()) + instrumentedApplication.Spec.RuntimeDetails[0].EnvVars[0].Value = mergedEnvValue + Expect(k8sClient.Update(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnvRemainsSame(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + }) + + When("Default SDK changes to another SDK", func() { + + newSdk := common.OtelSdkEbpfEnterprise + + BeforeEach(func() { + var odigosConfig odigosv1.OdigosConfiguration + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: consts.DefaultOdigosNamespace, Name: consts.OdigosConfigurationName}, &odigosConfig)).Should(Succeed()) + odigosConfig.Spec.DefaultSDKs[testProgrammingLanguage] = newSdk + Expect(k8sClient.Update(ctx, &odigosConfig)).Should(Succeed()) + }) + + It("Should update the manifest with new odigos env value", func() { + newOdigosValue, found := envOverwrite.ValToAppend(testEnvVar, newSdk) + Expect(found).Should(BeTrue()) + newMergedEnvValue := userEnvValue + ":" + newOdigosValue + + // after the odigos config is updated, the deployment should be updated with the new odigos value + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, newMergedEnvValue) + + // when uninstrumented, the value should be reverted to the original value which was in the manifest + Expect(k8sClient.Delete(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, userEnvValue) + }) + }) + }) + + When("Apply to workload restores the value to it's original state", func() { + + userEnvValue := "/orig_in_manifest" + var mergedEnvValue string + + BeforeEach(func() { + // the env var is in manifest, thus the deployment should contain it at the start + deployment = testutil.SetDeploymentContainerEnv( + testutil.SetOdigosInstrumentationEnabled( + testutil.NewMockTestDeployment(namespace), + ), + testEnvVar, userEnvValue) + Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + + // initial state - should capture the env var from manifest only + mergedEnvValue = userEnvValue + ":" + testEnvOdigosValue + instrumentedApplication = testutil.SetInstrumentedApplicationContainer(testutil.NewMockInstrumentedApplication(deployment), &testEnvVar, &userEnvValue, testProgrammingLanguage) + Expect(k8sClient.Create(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + + // after instrumentation is applied, now the value in the pod should be the merged value + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(instrumentedApplication), instrumentedApplication)).Should(Succeed()) + instrumentedApplication.Spec.RuntimeDetails[0].EnvVars[0].Value = mergedEnvValue + Expect(k8sClient.Update(ctx, instrumentedApplication)).Should(Succeed()) + testutil.AssertDepContainerSingleEnvRemainsSame(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + }) + + It("Should reapply odigos value to the manifest", func() { + // when the deployment is updated, the odigos value should be reapplied + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment)).Should(Succeed()) + // restore the value to the original state + deployment = testutil.SetDeploymentContainerEnv(deployment, testEnvVar, userEnvValue) + Expect(k8sClient.Update(ctx, deployment)).Should(Succeed()) + + // the odigos value should be reapplied + testutil.AssertDepContainerSingleEnv(ctx, k8sClient, deployment, testEnvVar, mergedEnvValue) + }) + }) + +}) diff --git a/instrumentor/controllers/instrumentationdevice/manager.go b/instrumentor/controllers/instrumentationdevice/manager.go index 25240761c..79c65438b 100644 --- a/instrumentor/controllers/instrumentationdevice/manager.go +++ b/instrumentor/controllers/instrumentationdevice/manager.go @@ -3,10 +3,64 @@ package instrumentationdevice import ( odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" k8sutils "github.com/odigos-io/odigos/k8sutils/pkg/client" + appsv1 "k8s.io/api/apps/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) +type workloadEnvChangePredicate struct { + predicate.Funcs +} + +func (w workloadEnvChangePredicate) Create(e event.CreateEvent) bool { + return false +} + +func (w workloadEnvChangePredicate) Update(e event.UpdateEvent) bool { + + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + oldPodSpec, err := getPodSpecFromObject(e.ObjectOld) + if err != nil { + return false + } + newPodSpec, err := getPodSpecFromObject(e.ObjectNew) + if err != nil { + return false + } + + // only handle workloads if any env changed + if len(oldPodSpec.Spec.Containers) != len(newPodSpec.Spec.Containers) { + return true + } + for i := range oldPodSpec.Spec.Containers { + if len(oldPodSpec.Spec.Containers[i].Env) != len(newPodSpec.Spec.Containers[i].Env) { + return true + } + for j := range oldPodSpec.Spec.Containers[i].Env { + prevEnv := &newPodSpec.Spec.Containers[i].Env[j] + newEnv := &oldPodSpec.Spec.Containers[i].Env[j] + if prevEnv.Name != newEnv.Name || prevEnv.Value != newEnv.Value { + return true + } + } + } + + return false +} + +func (w workloadEnvChangePredicate) Delete(e event.DeleteEvent) bool { + return false +} + +func (w workloadEnvChangePredicate) Generic(e event.GenericEvent) bool { + return false +} + func SetupWithManager(mgr ctrl.Manager) error { // Create a new client with fallback to API server // We are doing this because client-go cache is not supporting dynamic cache rules @@ -46,5 +100,38 @@ func SetupWithManager(mgr ctrl.Manager) error { return err } + err = builder. + ControllerManagedBy(mgr). + For(&appsv1.Deployment{}). + WithEventFilter(workloadEnvChangePredicate{}). + Complete(&DeploymentReconciler{ + Client: mgr.GetClient(), + }) + if err != nil { + return err + } + + err = builder. + ControllerManagedBy(mgr). + For(&appsv1.DaemonSet{}). + WithEventFilter(workloadEnvChangePredicate{}). + Complete(&DaemonSetReconciler{ + Client: mgr.GetClient(), + }) + if err != nil { + return err + } + + err = builder. + ControllerManagedBy(mgr). + For(&appsv1.StatefulSet{}). + WithEventFilter(workloadEnvChangePredicate{}). + Complete(&StatefulSetReconciler{ + Client: mgr.GetClient(), + }) + if err != nil { + return err + } + return nil } diff --git a/instrumentor/controllers/instrumentationdevice/suite_test.go b/instrumentor/controllers/instrumentationdevice/suite_test.go new file mode 100644 index 000000000..4b7681edc --- /dev/null +++ b/instrumentor/controllers/instrumentationdevice/suite_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package instrumentationdevice_test + +import ( + "context" + "path/filepath" + "testing" + + "github.com/odigos-io/odigos/instrumentor/internal/testutil" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/instrumentor/controllers/instrumentationdevice" + //+kubebuilder:scaffold:imports +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + testCtx context.Context + cancel context.CancelFunc +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "InstrumentationDevice Controllers Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + testCtx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "api", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = odigosv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // create the odigos system namespace + odigosSystemNamespace := testutil.NewOdigosSystemNamespace() + Expect(k8sClient.Create(testCtx, odigosSystemNamespace)).Should(Succeed()) + + // report the node collector is ready + datacollection := testutil.NewMockDataCollection() + Expect(k8sClient.Create(testCtx, datacollection)).Should(Succeed()) + k8sClient.Get(testCtx, types.NamespacedName{Name: datacollection.GetName(), Namespace: datacollection.GetNamespace()}, datacollection) + datacollection.Status.Ready = true + Expect(k8sClient.Status().Update(testCtx, datacollection)).Should(Succeed()) + + // create odigos configuration with default sdks + odigosConfiguration := testutil.NewMockOdigosConfig() + Expect(k8sClient.Create(testCtx, odigosConfiguration)).Should(Succeed()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + err = instrumentationdevice.SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(testCtx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() + +}, 60) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/instrumentor/controllers/instrumentationdevice/workload_controllers.go b/instrumentor/controllers/instrumentationdevice/workload_controllers.go new file mode 100644 index 000000000..800db3f0f --- /dev/null +++ b/instrumentor/controllers/instrumentationdevice/workload_controllers.go @@ -0,0 +1,50 @@ +package instrumentationdevice + +import ( + "context" + + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/k8sutils/pkg/workload" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type DeploymentReconciler struct { + client.Client +} + +func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + instrumentedAppName := workload.GetRuntimeObjectName(req.Name, "Deployment") + err := reconcileSingleInstrumentedApplicationByName(ctx, r.Client, instrumentedAppName, req.Namespace) + return ctrl.Result{}, err +} + +type DaemonSetReconciler struct { + client.Client +} + +func (r *DaemonSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + instrumentedAppName := workload.GetRuntimeObjectName(req.Name, "DaemonSet") + err := reconcileSingleInstrumentedApplicationByName(ctx, r.Client, instrumentedAppName, req.Namespace) + return ctrl.Result{}, err +} + +type StatefulSetReconciler struct { + client.Client +} + +func (r *StatefulSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + instrumentedAppName := workload.GetRuntimeObjectName(req.Name, "StatefulSet") + err := reconcileSingleInstrumentedApplicationByName(ctx, r.Client, instrumentedAppName, req.Namespace) + return ctrl.Result{}, err +} + +func reconcileSingleInstrumentedApplicationByName(ctx context.Context, k8sClient client.Client, instrumentedAppName string, namespace string) error { + var instrumentedApplication odigosv1.InstrumentedApplication + err := k8sClient.Get(ctx, types.NamespacedName{Name: instrumentedAppName, Namespace: namespace}, &instrumentedApplication) + if err != nil { + return client.IgnoreNotFound(err) + } + return reconcileSingleInstrumentedApplication(ctx, k8sClient, &instrumentedApplication) +} diff --git a/instrumentor/go.mod b/instrumentor/go.mod index 7452512b4..458683e78 100644 --- a/instrumentor/go.mod +++ b/instrumentor/go.mod @@ -10,7 +10,6 @@ require ( github.com/odigos-io/odigos/k8sutils v0.0.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.33.1 - github.com/stretchr/testify v1.9.0 k8s.io/api v0.30.1 k8s.io/apimachinery v0.30.1 k8s.io/client-go v0.30.1 @@ -28,7 +27,7 @@ require ( github.com/mattn/go-isatty v0.0.12 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/sdk v1.19.0 // indirect diff --git a/instrumentor/instrumentation/instrumentation.go b/instrumentor/instrumentation/instrumentation.go index 8cff1f3e2..ae8a6989b 100644 --- a/instrumentor/instrumentation/instrumentation.go +++ b/instrumentor/instrumentation/instrumentation.go @@ -1,18 +1,17 @@ package instrumentation import ( - "encoding/json" "errors" "fmt" "strings" - "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/common/envOverwrite" "sigs.k8s.io/controller-runtime/pkg/client" odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/common" - v1 "k8s.io/api/core/v1" + "github.com/odigos-io/odigos/k8sutils/pkg/envoverwrite" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) @@ -21,14 +20,19 @@ var ( ErrPatchEnvVars = errors.New("failed to patch env vars") ) -func ApplyInstrumentationDevicesToPodTemplate(original *v1.PodTemplateSpec, runtimeDetails *odigosv1.InstrumentedApplication, defaultSdks map[common.ProgrammingLanguage]common.OtelSdk, targetObj client.Object) error { +func ApplyInstrumentationDevicesToPodTemplate(original *corev1.PodTemplateSpec, runtimeDetails *odigosv1.InstrumentedApplication, defaultSdks map[common.ProgrammingLanguage]common.OtelSdk, targetObj client.Object) error { // delete any existing instrumentation devices. // this is necessary for example when migrating from community to enterprise, // and we need to cleanup the community device before adding the enterprise one. - Revert(original, targetObj) + RevertInstrumentationDevices(original) - var modifiedContainers []v1.Container + manifestEnvOriginal, err := envoverwrite.NewOrigWorkloadEnvValues(targetObj) + if err != nil { + return err + } + + var modifiedContainers []corev1.Container for _, container := range original.Spec.Containers { containerLanguage := getLanguageOfContainer(runtimeDetails, container.Name) if containerLanguage == nil || *containerLanguage == common.UnknownProgrammingLanguage || *containerLanguage == common.IgnoredProgrammingLanguage { @@ -44,11 +48,11 @@ func ApplyInstrumentationDevicesToPodTemplate(original *v1.PodTemplateSpec, runt instrumentationDeviceName := common.InstrumentationDeviceName(*containerLanguage, otelSdk) if container.Resources.Limits == nil { - container.Resources.Limits = make(map[v1.ResourceName]resource.Quantity) + container.Resources.Limits = make(map[corev1.ResourceName]resource.Quantity) } - container.Resources.Limits[v1.ResourceName(instrumentationDeviceName)] = resource.MustParse("1") + container.Resources.Limits[corev1.ResourceName(instrumentationDeviceName)] = resource.MustParse("1") - err := patchEnvVars(runtimeDetails, &container, targetObj, otelSdk) + err = patchEnvVarsForContainer(runtimeDetails, &container, targetObj, otelSdk, manifestEnvOriginal) if err != nil { return fmt.Errorf("%w: %v", ErrPatchEnvVars, err) } @@ -57,24 +61,50 @@ func ApplyInstrumentationDevicesToPodTemplate(original *v1.PodTemplateSpec, runt } original.Spec.Containers = modifiedContainers + + // persist the original values if changed + manifestEnvOriginal.SerializeToAnnotation(targetObj) return nil } -func Revert(original *v1.PodTemplateSpec, targetObj client.Object) { - // read the original env vars (of the manifest) from the annotation - var origManifestEnv map[string]map[string]string - annotations := targetObj.GetAnnotations() - if annotations != nil { - manifestEnvAnnotation, ok := annotations[consts.ManifestEnvOriginalValAnnotation] - if ok { - err := json.Unmarshal([]byte(manifestEnvAnnotation), &origManifestEnv) - if err != nil { - fmt.Printf("failed to unmarshal manifest env original annotation in Revert: %v", err) +// this function restores a workload manifest env vars to their original values. +// it is used when the instrumentation is removed from the workload. +// the original values are read from the annotation which was saved when the instrumentation was applied. +func RevertEnvOverwrites(obj client.Object, podSpec *corev1.PodTemplateSpec) error { + manifestEnvOriginal, err := envoverwrite.NewOrigWorkloadEnvValues(obj) + if err != nil { + return err + } + + for iContainer, c := range podSpec.Spec.Containers { + containerOriginalEnv := manifestEnvOriginal.GetContainerStoredEnvs(c.Name) + newContainerEnvs := make([]corev1.EnvVar, 0, len(c.Env)) + for _, envVar := range c.Env { + if origValue, found := containerOriginalEnv[envVar.Name]; found { + // revert the env var to its original value + if origValue != nil { + newContainerEnvs = append(newContainerEnvs, corev1.EnvVar{ + Name: envVar.Name, + Value: *containerOriginalEnv[envVar.Name], + }) + } else { + // if the value is nil, the env var was not set by the user to begin with. + // we will simply not append it to the new envs to achieve the same effect. + } + } else { + newContainerEnvs = append(newContainerEnvs, envVar) } } + podSpec.Spec.Containers[iContainer].Env = newContainerEnvs } - for iContainer, container := range original.Spec.Containers { + manifestEnvOriginal.DeleteFromObj(obj) + + return nil +} + +func RevertInstrumentationDevices(original *corev1.PodTemplateSpec) { + for _, container := range original.Spec.Containers { for resourceName := range container.Resources.Limits { if strings.HasPrefix(string(resourceName), common.OdigosResourceNamespace) { delete(container.Resources.Limits, resourceName) @@ -86,29 +116,7 @@ func Revert(original *v1.PodTemplateSpec, targetObj client.Object) { delete(container.Resources.Requests, resourceName) } } - - containerOriginalEnv := origManifestEnv[container.Name] - revertedEnvVars := make([]v1.EnvVar, 0, len(container.Env)) - - for _, envVar := range container.Env { - if envOverwrite.ShouldRevert(envVar.Name, envVar.Value) { - if origVal, ok := containerOriginalEnv[envVar.Name]; ok { - // Revert the env var to its original value - revertedEnvVars = append(revertedEnvVars, v1.EnvVar{ - Name: envVar.Name, - Value: origVal, - }) - } - // If the original value is not found, we are not going to add the env var back - } else { - revertedEnvVars = append(revertedEnvVars, envVar) - } - } - original.Spec.Containers[iContainer].Env = revertedEnvVars } - - // Remove the annotation - delete(annotations, consts.ManifestEnvOriginalValAnnotation) } func getLanguageOfContainer(instrumentation *odigosv1.InstrumentedApplication, containerName string) *common.ProgrammingLanguage { @@ -138,66 +146,66 @@ func getEnvVarsOfContainer(instrumentation *odigosv1.InstrumentedApplication, co return envVars } -func patchEnvVars(runtimeDetails *odigosv1.InstrumentedApplication, container *v1.Container, obj client.Object, sdk common.OtelSdk) error { - envs := getEnvVarsOfContainer(runtimeDetails, container.Name) +func patchEnvVarsForContainer(runtimeDetails *odigosv1.InstrumentedApplication, container *corev1.Container, obj client.Object, sdk common.OtelSdk, manifestEnvOriginal *envoverwrite.OrigWorkloadEnvValues) error { - var manifestEnvOriginal map[string]map[string]string + observedEnvs := getEnvVarsOfContainer(runtimeDetails, container.Name) - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } + // Step 1: check existing environment on the manifest and update them if needed + newEnvs := make([]corev1.EnvVar, 0, len(container.Env)) + for _, envVar := range container.Env { - if currentEnvAnnotation, ok := annotations[consts.ManifestEnvOriginalValAnnotation]; ok { - // The annotation is already present, unmarshal it - err := json.Unmarshal([]byte(currentEnvAnnotation), &manifestEnvOriginal) - if err != nil { - return fmt.Errorf("failed to unmarshal manifest env original annotation: %v", err) - } - } else { - manifestEnvOriginal = make(map[string]map[string]string) - } + // extract the observed value for this env var, which might be empty if not currently exists + observedEnvValue := observedEnvs[envVar.Name] - if _, ok := manifestEnvOriginal[container.Name]; !ok { - manifestEnvOriginal[container.Name] = make(map[string]string) - } + desiredEnvValue := envOverwrite.GetPatchedEnvValue(envVar.Name, observedEnvValue, sdk) - savedEnvVar := false - - // Overwrite env var if needed - for i, envVar := range container.Env { - if envOverwrite.ShouldPatch(envVar.Name, envVar.Value, sdk) { - // We are about to patch this env var, check if we need to save the original value - // If the original value is not saved, save it to the annotation. - if _, ok := manifestEnvOriginal[container.Name][envVar.Name]; !ok { - savedEnvVar = true - manifestEnvOriginal[container.Name][envVar.Name] = envVar.Value - container.Env[i].Value = envOverwrite.Patch(envVar.Name, envVar.Value, sdk) + if desiredEnvValue == nil { + // no need to patch this env var, so make sure it is reverted to its original value + origValue, found := manifestEnvOriginal.RemoveOriginalValue(container.Name, envVar.Name) + if !found { + newEnvs = append(newEnvs, envVar) + } else { // found, we need to update the env var to it's original value + if origValue != nil { + // this case reverts back the env var to it's original value + newEnvs = append(newEnvs, corev1.EnvVar{ + Name: envVar.Name, + Value: *origValue, + }) + } else { + // if the original value was nil, then it was not set by the user. + // we will simply not append it to the new envs to achieve the same effect. + } } + } else { // there is a desired value to inject + // if it's the first time we patch this env var, save the original value + manifestEnvOriginal.InsertOriginalValue(container.Name, envVar.Name, &envVar.Value) + // update the env var to it's desired value + newEnvs = append(newEnvs, corev1.EnvVar{ + Name: envVar.Name, + Value: *desiredEnvValue, + }) } + // If an env var is defined both in the container build and in the container spec, the value in the container spec will be used. - delete(envs, envVar.Name) + delete(observedEnvs, envVar.Name) } - // Add the remaining env vars (which are not defined in a manifest) - for envName, envValue := range envs { - if envOverwrite.ShouldPatch(envName, envValue, sdk) { - container.Env = append(container.Env, v1.EnvVar{ + // Step 2: add the new env vars which odigos might patch, but which are not defined in the manifest + for envName, envValue := range observedEnvs { + desiredEnvValue := envOverwrite.GetPatchedEnvValue(envName, envValue, sdk) + if desiredEnvValue != nil { + // store that it was empty to begin with + manifestEnvOriginal.InsertOriginalValue(container.Name, envName, nil) + // and add this new env var to the manifest + newEnvs = append(newEnvs, corev1.EnvVar{ Name: envName, - Value: envOverwrite.Patch(envName, envValue, sdk), + Value: *desiredEnvValue, }) } } - // Update the annotation with the original values from the manifest (if there are any to save) - if savedEnvVar { - updatedAnnotation, err := json.Marshal(manifestEnvOriginal) - if err != nil { - return fmt.Errorf("failed to marshal manifest env original annotation: %v", err) - } - annotations[consts.ManifestEnvOriginalValAnnotation] = string(updatedAnnotation) - obj.SetAnnotations(annotations) - } + // Step 3: update the container with the new env vars + container.Env = newEnvs return nil } diff --git a/instrumentor/instrumentation/instrumentation_test.go b/instrumentor/instrumentation/instrumentation_test.go index aeeb7533c..e4abe3064 100644 --- a/instrumentor/instrumentation/instrumentation_test.go +++ b/instrumentor/instrumentation/instrumentation_test.go @@ -1,982 +1,982 @@ package instrumentation -import ( - "encoding/json" - "testing" - - odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" - "github.com/odigos-io/odigos/common" - "github.com/odigos-io/odigos/common/consts" - "github.com/odigos-io/odigos/common/envOverwrite" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func assertNoAnnotation(t *testing.T, targetObj *appsv1.Deployment, key string) { - if targetObj.GetAnnotations() != nil { - _, hasAnnotation := targetObj.GetAnnotations()[key] - assert.False(t, hasAnnotation) - } -} - -func assertAnnotation(t *testing.T, targetObj *appsv1.Deployment, key, value string) { - assert.NotNil(t, targetObj.GetAnnotations()) - assert.Equal(t, value, targetObj.GetAnnotations()[key]) -} - -func assertContainerNoEnvVar(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, envVarName string) { - if len(podTemplate.Spec.Containers) <= containerIndex { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) - } - - if len(podTemplate.Spec.Containers[containerIndex].Env) == 0 { - return - } - - container := podTemplate.Spec.Containers[containerIndex] - for _, envVar := range container.Env { - assert.NotEqual(t, envVar.Name, envVarName) - } -} - -func assertContainerWithEnvVar(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, envVarName, envVarValue string) { - if len(podTemplate.Spec.Containers) <= containerIndex { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) - } - - container := podTemplate.Spec.Containers[containerIndex] - assert.Contains(t, container.Env, v1.EnvVar{Name: envVarName, Value: envVarValue}) -} - -func assertContainerWithInstrumentationDevice(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, instrumentationDeviceName v1.ResourceName) { - - if len(podTemplate.Spec.Containers) <= containerIndex { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) - } - - container := podTemplate.Spec.Containers[containerIndex] - - if instrumentationDeviceName == "" { - if len(container.Resources.Limits) != 0 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no instrumentation device in resource limits") - } - } else { - if len(container.Resources.Limits) != 1 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing instrumentation device in resource limits") - } - _, hasInstrumentationDevice := container.Resources.Limits[instrumentationDeviceName] - if !hasInstrumentationDevice { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be set. got: %v, wanted: %v", container.Resources.Limits, instrumentationDeviceName) - } - } -} - -func TestApplyInstrumentationDevicesToPodTemplate(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - assertContainerWithInstrumentationDevice(t, podTemplate, 0, v1.ResourceName("instrumentation.odigos.io/go-ebpf-community")) -} - -func TestApplyInstrumentationDevicesToPodTemplate_MissingRuntimeDetails(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{}, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{} - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") -} - -func TestApplyInstrumentationDevicesToPodTemplate_MissingOtelSdk(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{} - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err == nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected error due to missing otel sdk") - } -} - -func TestApplyInstrumentationDevicesToPodTemplate_MultipleContainers(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test1", - }, - { - Language: common.GoProgrammingLanguage, - ContainerName: "test2", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - if len(podTemplate.Spec.Containers) != 2 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") - } - - instrumentationDeviceName := v1.ResourceName("instrumentation.odigos.io/go-ebpf-community") - assertContainerWithInstrumentationDevice(t, podTemplate, 0, instrumentationDeviceName) - assertContainerWithInstrumentationDevice(t, podTemplate, 1, instrumentationDeviceName) -} - -func TestApplyInstrumentationDevicesToPodTemplate_MultipleHeterogeneousContainers(t *testing.T) { - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test1", - }, - { - Language: common.JavaProgrammingLanguage, - ContainerName: "test2", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - common.JavaProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - if len(podTemplate.Spec.Containers) != 2 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") - } - - assertContainerWithInstrumentationDevice(t, podTemplate, 0, v1.ResourceName("instrumentation.odigos.io/go-ebpf-community")) - assertContainerWithInstrumentationDevice(t, podTemplate, 1, v1.ResourceName("instrumentation.odigos.io/java-native-community")) -} - -func TestApplyInstrumentationDevicesToPodTemplate_MultiplePartialContainers(t *testing.T) { - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.JavaProgrammingLanguage, - ContainerName: "test2", - }, - }, - }, - } - - deployment := &appsv1.Deployment{} - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - common.JavaProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - if len(podTemplate.Spec.Containers) != 2 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") - } - - // container 0 should not be modified because it is not in the runtime details - assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") - - assertContainerWithInstrumentationDevice(t, podTemplate, 1, v1.ResourceName("instrumentation.odigos.io/java-native-community")) -} - -func TestApplyInstrumentationDevicesToPodTemplate_AppendExistingLimits(t *testing.T) { - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - Resources: v1.ResourceRequirements{ - Limits: map[v1.ResourceName]resource.Quantity{ - "foo": resource.MustParse("123"), - }, - }, - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - deployment := &appsv1.Deployment{} - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - container := podTemplate.Spec.Containers[0] - - if len(container.Resources.Limits) != 2 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected 2 resource limits") - } - - if container.Resources.Limits["foo"] != resource.MustParse("123") { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected existing resource limit to be preserved") - } - - if container.Resources.Limits["instrumentation.odigos.io/go-ebpf-community"] != resource.MustParse("1") { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be added") - } -} - -func TestApplyInstrumentationDevicesToPodTemplate_RemoveExistingLimits(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - Resources: v1.ResourceRequirements{ - Limits: map[v1.ResourceName]resource.Quantity{ - "instrumentation.odigos.io/go-ebpf-community": resource.MustParse("1"), - }, - }, - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - deployment := &appsv1.Deployment{} - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfEnterprise, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - container := podTemplate.Spec.Containers[0] - - if len(container.Resources.Limits) != 1 { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected 1 resource limits") - } - - if _, ok := container.Resources.Limits["instrumentation.odigos.io/go-ebpf-community"]; ok { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected to remove old existing resource limit for community ") - } - - if container.Resources.Limits["instrumentation.odigos.io/go-ebpf-enterprise"] != resource.MustParse("1") { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be added") - } -} - -func TestRevert(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - Env: []v1.EnvVar{ - { - Name: "PYTHONPATH", - Value: "/very/important/path", - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.PythonProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - // make sure the env var is appended - val, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) - assert.True(t, ok) - - want := "/very/important/path:" + val - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) - - // The original value of the env var should be stored in an annotation - a, _ := json.Marshal(map[string]map[string]string{ - "test": { - "PYTHONPATH": "/very/important/path", - }, - }) - want = string(a) - assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) - - Revert(podTemplate, deployment) - // The env var should be reverted to its original value - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path") - - if len(podTemplate.Spec.Containers) != 1 { - t.Errorf("Revert() expected no change to number of containers") - } - - assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") - assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) -} - -func TestRevert_ExistingResources(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "test", - Resources: v1.ResourceRequirements{ - Limits: map[v1.ResourceName]resource.Quantity{ - "foo": resource.MustParse("123"), - }, - }, - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - Revert(podTemplate, deployment) - - if len(podTemplate.Spec.Containers) != 1 { - t.Errorf("Revert() expected no change to number of containers") - } - - if len(podTemplate.Spec.Containers[0].Resources.Limits) != 1 { - t.Errorf("Revert() expected no change to number of resource limits") - } - - if podTemplate.Spec.Containers[0].Resources.Limits["foo"] != resource.MustParse("123") { - t.Errorf("Revert() expected existing resource limit to be preserved") - } -} - -func TestRevert_MultipleContainers(t *testing.T) { - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "test1"}, - {Name: "test2"}, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.GoProgrammingLanguage, - ContainerName: "test1", - }, - { - Language: common.GoProgrammingLanguage, - ContainerName: "test2", - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - Revert(podTemplate, deployment) - - assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") - assertContainerWithInstrumentationDevice(t, podTemplate, 1, "") -} - -func TestEnvVarAppendMultipleContainers(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "pythonContainer", - Env: []v1.EnvVar{ - { - Name: "PYTHONPATH", - Value: "/very/important/path", - }, - }, - }, - { - Name: "nodeContainer", - Env: []v1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: "--max-old-space-size=8192", - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.PythonProgrammingLanguage, - ContainerName: "pythonContainer", - }, - { - Language: common.JavascriptProgrammingLanguage, - ContainerName: "nodeContainer", - }, - }, - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, - common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want := "/very/important/path:" + pythonpathVal - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) - - nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want = "--max-old-space-size=8192 " + nodeOptionsVal - assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) - - // The original value of the env var should be stored in an annotation - a, _ := json.Marshal(map[string]map[string]string{ - "pythonContainer": { - "PYTHONPATH": "/very/important/path", - }, - "nodeContainer": { - "NODE_OPTIONS": "--max-old-space-size=8192", - }, - }) - want = string(a) - assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) - - Revert(podTemplate, deployment) - - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path") - assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", "--max-old-space-size=8192") - assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) -} - -func TestEnvVarFromRuntimeDetails(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "pythonContainer"}, - {Name: "nodeContainer"}, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.PythonProgrammingLanguage, - ContainerName: "pythonContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "PYTHONPATH", - Value: "/very/important/path", - }, - }, - }, - { - Language: common.JavascriptProgrammingLanguage, - ContainerName: "nodeContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: "--max-old-space-size=8192", - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, - common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want := "/very/important/path:" + pythonpathVal - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) - - nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want = "--max-old-space-size=8192 " + nodeOptionsVal - assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) - - // The env vars originated from the runtime details should not be stored in the annotation - assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) - - Revert(podTemplate, deployment) - // After reverting, the env vars should not be present in the container since they originated from the runtime details - // and were not present in the original pod template - assertContainerNoEnvVar(t, podTemplate, 0, "PYTHONPATH") - assertContainerNoEnvVar(t, podTemplate, 1, "NODE_OPTIONS") -} - -func TestEnvVarAppendFromSpecAndRuntimeDetails(t *testing.T) { - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "pythonContainer", - Env: []v1.EnvVar{ - { - Name: "PYTHONPATH", - Value: "/very/important/path/template", - }, - }, - }, - { - Name: "nodeContainer", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.PythonProgrammingLanguage, - ContainerName: "pythonContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "PYTHONPATH", - Value: "/very/important/path/runtime", - }, - }, - }, - { - Language: common.JavascriptProgrammingLanguage, - ContainerName: "nodeContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: "--max-old-space-size=8192-runtime", - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, - common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - // If env vars are present in both the template and runtime details, the template value should be used (pythonContainer in this case) - pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want := "/very/important/path/template:" + pythonpathVal - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) - - // The env var from the runtime details should be used for nodeContainer since it is not present in the template - nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) - assert.True(t, ok) - want = "--max-old-space-size=8192-runtime " + nodeOptionsVal - assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) - // The original value of the env var should be stored in an annotation - a, _ := json.Marshal(map[string]map[string]string{ - "pythonContainer": { - "PYTHONPATH": "/very/important/path/template", - }, - }) - want = string(a) - assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) - - Revert(podTemplate, deployment) - // After reverting, make sure we are back to the original state - assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path/template") - assertContainerNoEnvVar(t, podTemplate, 1, "NODE_OPTIONS") - assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) -} - -func TestMoveBetweenSDKsWithUserValue(t *testing.T) { - jsOptionsValEbpf, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkEbpfEnterprise) - jsOptionsValNative, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) - userDefinedVal := "--max-old-space-size=8192-runtime" - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "nodeContainer", - Env: []v1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: userDefinedVal + " " + jsOptionsValEbpf, - }, - }, - }, - { - Name: "nodeContainer", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.JavascriptProgrammingLanguage, - ContainerName: "nodeContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: userDefinedVal, - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - want := userDefinedVal + " " + jsOptionsValNative - assertContainerWithEnvVar(t, podTemplate, 0, "NODE_OPTIONS", want) - - Revert(podTemplate, deployment) - assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) -} - -func TestMoveBetweenSDKsWithoutUserValue(t *testing.T) { - jsOptionsValEbpf, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkEbpfEnterprise) - - podTemplate := &v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "nodeContainer", - }, - }, - }, - } - - runtimeDetails := &odigosv1.InstrumentedApplication{ - Spec: odigosv1.InstrumentedApplicationSpec{ - RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ - { - Language: common.JavascriptProgrammingLanguage, - ContainerName: "nodeContainer", - EnvVars: []odigosv1.EnvVar{ - { - Name: "NODE_OPTIONS", - Value: jsOptionsValEbpf, - }, - }, - }, - }, - }, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - }, - } - - defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ - common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, - } - - err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) - if err != nil { - t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) - } - - assertContainerNoEnvVar(t, podTemplate, 0, "NODE_OPTIONS") -} +// import ( +// "encoding/json" +// "testing" + +// odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" +// "github.com/odigos-io/odigos/common" +// "github.com/odigos-io/odigos/common/consts" +// "github.com/odigos-io/odigos/common/envOverwrite" +// "github.com/stretchr/testify/assert" +// appsv1 "k8s.io/api/apps/v1" +// v1 "k8s.io/api/core/v1" +// "k8s.io/apimachinery/pkg/api/resource" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// ) + +// func assertNoAnnotation(t *testing.T, targetObj *appsv1.Deployment, key string) { +// if targetObj.GetAnnotations() != nil { +// _, hasAnnotation := targetObj.GetAnnotations()[key] +// assert.False(t, hasAnnotation) +// } +// } + +// func assertAnnotation(t *testing.T, targetObj *appsv1.Deployment, key, value string) { +// assert.NotNil(t, targetObj.GetAnnotations()) +// assert.Equal(t, value, targetObj.GetAnnotations()[key]) +// } + +// func assertContainerNoEnvVar(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, envVarName string) { +// if len(podTemplate.Spec.Containers) <= containerIndex { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) +// } + +// if len(podTemplate.Spec.Containers[containerIndex].Env) == 0 { +// return +// } + +// container := podTemplate.Spec.Containers[containerIndex] +// for _, envVar := range container.Env { +// assert.NotEqual(t, envVar.Name, envVarName) +// } +// } + +// func assertContainerWithEnvVar(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, envVarName, envVarValue string) { +// if len(podTemplate.Spec.Containers) <= containerIndex { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) +// } + +// container := podTemplate.Spec.Containers[containerIndex] +// assert.Contains(t, container.Env, v1.EnvVar{Name: envVarName, Value: envVarValue}) +// } + +// func assertContainerWithInstrumentationDevice(t *testing.T, podTemplate *v1.PodTemplateSpec, containerIndex int, instrumentationDeviceName v1.ResourceName) { + +// if len(podTemplate.Spec.Containers) <= containerIndex { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing container at index %d", containerIndex) +// } + +// container := podTemplate.Spec.Containers[containerIndex] + +// if instrumentationDeviceName == "" { +// if len(container.Resources.Limits) != 0 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no instrumentation device in resource limits") +// } +// } else { +// if len(container.Resources.Limits) != 1 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() missing instrumentation device in resource limits") +// } +// _, hasInstrumentationDevice := container.Resources.Limits[instrumentationDeviceName] +// if !hasInstrumentationDevice { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be set. got: %v, wanted: %v", container.Resources.Limits, instrumentationDeviceName) +// } +// } +// } + +// func TestApplyInstrumentationDevicesToPodTemplate(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, v1.ResourceName("instrumentation.odigos.io/go-ebpf-community")) +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_MissingRuntimeDetails(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{}, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{} + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_MissingOtelSdk(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{} + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err == nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected error due to missing otel sdk") +// } +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_MultipleContainers(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// {Name: "test1"}, +// {Name: "test2"}, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test1", +// }, +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test2", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// if len(podTemplate.Spec.Containers) != 2 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") +// } + +// instrumentationDeviceName := v1.ResourceName("instrumentation.odigos.io/go-ebpf-community") +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, instrumentationDeviceName) +// assertContainerWithInstrumentationDevice(t, podTemplate, 1, instrumentationDeviceName) +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_MultipleHeterogeneousContainers(t *testing.T) { + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// {Name: "test1"}, +// {Name: "test2"}, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test1", +// }, +// { +// Language: common.JavaProgrammingLanguage, +// ContainerName: "test2", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// common.JavaProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// if len(podTemplate.Spec.Containers) != 2 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") +// } + +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, v1.ResourceName("instrumentation.odigos.io/go-ebpf-community")) +// assertContainerWithInstrumentationDevice(t, podTemplate, 1, v1.ResourceName("instrumentation.odigos.io/java-native-community")) +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_MultiplePartialContainers(t *testing.T) { + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// {Name: "test1"}, +// {Name: "test2"}, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.JavaProgrammingLanguage, +// ContainerName: "test2", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{} + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// common.JavaProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// if len(podTemplate.Spec.Containers) != 2 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected no change to number of containers") +// } + +// // container 0 should not be modified because it is not in the runtime details +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") + +// assertContainerWithInstrumentationDevice(t, podTemplate, 1, v1.ResourceName("instrumentation.odigos.io/java-native-community")) +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_AppendExistingLimits(t *testing.T) { + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// Resources: v1.ResourceRequirements{ +// Limits: map[v1.ResourceName]resource.Quantity{ +// "foo": resource.MustParse("123"), +// }, +// }, +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{} + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// container := podTemplate.Spec.Containers[0] + +// if len(container.Resources.Limits) != 2 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected 2 resource limits") +// } + +// if container.Resources.Limits["foo"] != resource.MustParse("123") { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected existing resource limit to be preserved") +// } + +// if container.Resources.Limits["instrumentation.odigos.io/go-ebpf-community"] != resource.MustParse("1") { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be added") +// } +// } + +// func TestApplyInstrumentationDevicesToPodTemplate_RemoveExistingLimits(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// Resources: v1.ResourceRequirements{ +// Limits: map[v1.ResourceName]resource.Quantity{ +// "instrumentation.odigos.io/go-ebpf-community": resource.MustParse("1"), +// }, +// }, +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{} + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfEnterprise, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// container := podTemplate.Spec.Containers[0] + +// if len(container.Resources.Limits) != 1 { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected 1 resource limits") +// } + +// if _, ok := container.Resources.Limits["instrumentation.odigos.io/go-ebpf-community"]; ok { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected to remove old existing resource limit for community ") +// } + +// if container.Resources.Limits["instrumentation.odigos.io/go-ebpf-enterprise"] != resource.MustParse("1") { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() expected instrumentation device to be added") +// } +// } + +// func TestRevert(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// Env: []v1.EnvVar{ +// { +// Name: "PYTHONPATH", +// Value: "/very/important/path", +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.PythonProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// // make sure the env var is appended +// val, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) +// assert.True(t, ok) + +// want := "/very/important/path:" + val +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) + +// // The original value of the env var should be stored in an annotation +// a, _ := json.Marshal(map[string]map[string]string{ +// "test": { +// "PYTHONPATH": "/very/important/path", +// }, +// }) +// want = string(a) +// assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) + +// RevertInstrumentationDevices(podTemplate, deployment) +// // The env var should be reverted to its original value +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path") + +// if len(podTemplate.Spec.Containers) != 1 { +// t.Errorf("Revert() expected no change to number of containers") +// } + +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") +// assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) +// } + +// func TestRevert_ExistingResources(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "test", +// Resources: v1.ResourceRequirements{ +// Limits: map[v1.ResourceName]resource.Quantity{ +// "foo": resource.MustParse("123"), +// }, +// }, +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// RevertInstrumentationDevices(podTemplate, deployment) + +// if len(podTemplate.Spec.Containers) != 1 { +// t.Errorf("Revert() expected no change to number of containers") +// } + +// if len(podTemplate.Spec.Containers[0].Resources.Limits) != 1 { +// t.Errorf("Revert() expected no change to number of resource limits") +// } + +// if podTemplate.Spec.Containers[0].Resources.Limits["foo"] != resource.MustParse("123") { +// t.Errorf("Revert() expected existing resource limit to be preserved") +// } +// } + +// func TestRevert_MultipleContainers(t *testing.T) { + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// {Name: "test1"}, +// {Name: "test2"}, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test1", +// }, +// { +// Language: common.GoProgrammingLanguage, +// ContainerName: "test2", +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.GoProgrammingLanguage: common.OtelSdkEbpfCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// RevertInstrumentationDevices(podTemplate, deployment) + +// assertContainerWithInstrumentationDevice(t, podTemplate, 0, "") +// assertContainerWithInstrumentationDevice(t, podTemplate, 1, "") +// } + +// func TestEnvVarAppendMultipleContainers(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "pythonContainer", +// Env: []v1.EnvVar{ +// { +// Name: "PYTHONPATH", +// Value: "/very/important/path", +// }, +// }, +// }, +// { +// Name: "nodeContainer", +// Env: []v1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: "--max-old-space-size=8192", +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.PythonProgrammingLanguage, +// ContainerName: "pythonContainer", +// }, +// { +// Language: common.JavascriptProgrammingLanguage, +// ContainerName: "nodeContainer", +// }, +// }, +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, +// common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want := "/very/important/path:" + pythonpathVal +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) + +// nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want = "--max-old-space-size=8192 " + nodeOptionsVal +// assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) + +// // The original value of the env var should be stored in an annotation +// a, _ := json.Marshal(map[string]map[string]string{ +// "pythonContainer": { +// "PYTHONPATH": "/very/important/path", +// }, +// "nodeContainer": { +// "NODE_OPTIONS": "--max-old-space-size=8192", +// }, +// }) +// want = string(a) +// assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) + +// RevertInstrumentationDevices(podTemplate, deployment) + +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path") +// assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", "--max-old-space-size=8192") +// assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) +// } + +// func TestEnvVarFromRuntimeDetails(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// {Name: "pythonContainer"}, +// {Name: "nodeContainer"}, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.PythonProgrammingLanguage, +// ContainerName: "pythonContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "PYTHONPATH", +// Value: "/very/important/path", +// }, +// }, +// }, +// { +// Language: common.JavascriptProgrammingLanguage, +// ContainerName: "nodeContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: "--max-old-space-size=8192", +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, +// common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want := "/very/important/path:" + pythonpathVal +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) + +// nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want = "--max-old-space-size=8192 " + nodeOptionsVal +// assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) + +// // The env vars originated from the runtime details should not be stored in the annotation +// assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) + +// RevertInstrumentationDevices(podTemplate, deployment) +// // After reverting, the env vars should not be present in the container since they originated from the runtime details +// // and were not present in the original pod template +// assertContainerNoEnvVar(t, podTemplate, 0, "PYTHONPATH") +// assertContainerNoEnvVar(t, podTemplate, 1, "NODE_OPTIONS") +// } + +// func TestEnvVarAppendFromSpecAndRuntimeDetails(t *testing.T) { +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "pythonContainer", +// Env: []v1.EnvVar{ +// { +// Name: "PYTHONPATH", +// Value: "/very/important/path/template", +// }, +// }, +// }, +// { +// Name: "nodeContainer", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.PythonProgrammingLanguage, +// ContainerName: "pythonContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "PYTHONPATH", +// Value: "/very/important/path/runtime", +// }, +// }, +// }, +// { +// Language: common.JavascriptProgrammingLanguage, +// ContainerName: "nodeContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: "--max-old-space-size=8192-runtime", +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, +// common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// // If env vars are present in both the template and runtime details, the template value should be used (pythonContainer in this case) +// pythonpathVal, ok := envOverwrite.ValToAppend("PYTHONPATH", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want := "/very/important/path/template:" + pythonpathVal +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", want) + +// // The env var from the runtime details should be used for nodeContainer since it is not present in the template +// nodeOptionsVal, ok := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) +// assert.True(t, ok) +// want = "--max-old-space-size=8192-runtime " + nodeOptionsVal +// assertContainerWithEnvVar(t, podTemplate, 1, "NODE_OPTIONS", want) +// // The original value of the env var should be stored in an annotation +// a, _ := json.Marshal(map[string]map[string]string{ +// "pythonContainer": { +// "PYTHONPATH": "/very/important/path/template", +// }, +// }) +// want = string(a) +// assertAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation, want) + +// RevertInstrumentationDevices(podTemplate, deployment) +// // After reverting, make sure we are back to the original state +// assertContainerWithEnvVar(t, podTemplate, 0, "PYTHONPATH", "/very/important/path/template") +// assertContainerNoEnvVar(t, podTemplate, 1, "NODE_OPTIONS") +// assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) +// } + +// func TestMoveBetweenSDKsWithUserValue(t *testing.T) { +// jsOptionsValEbpf, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkEbpfEnterprise) +// jsOptionsValNative, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkNativeCommunity) +// userDefinedVal := "--max-old-space-size=8192-runtime" + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "nodeContainer", +// Env: []v1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: userDefinedVal + " " + jsOptionsValEbpf, +// }, +// }, +// }, +// { +// Name: "nodeContainer", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.JavascriptProgrammingLanguage, +// ContainerName: "nodeContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: userDefinedVal, +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// want := userDefinedVal + " " + jsOptionsValNative +// assertContainerWithEnvVar(t, podTemplate, 0, "NODE_OPTIONS", want) + +// RevertInstrumentationDevices(podTemplate, deployment) +// assertNoAnnotation(t, deployment, consts.ManifestEnvOriginalValAnnotation) +// } + +// func TestMoveBetweenSDKsWithoutUserValue(t *testing.T) { +// jsOptionsValEbpf, _ := envOverwrite.ValToAppend("NODE_OPTIONS", common.OtelSdkEbpfEnterprise) + +// podTemplate := &v1.PodTemplateSpec{ +// Spec: v1.PodSpec{ +// Containers: []v1.Container{ +// { +// Name: "nodeContainer", +// }, +// }, +// }, +// } + +// runtimeDetails := &odigosv1.InstrumentedApplication{ +// Spec: odigosv1.InstrumentedApplicationSpec{ +// RuntimeDetails: []odigosv1.RuntimeDetailsByContainer{ +// { +// Language: common.JavascriptProgrammingLanguage, +// ContainerName: "nodeContainer", +// EnvVars: []odigosv1.EnvVar{ +// { +// Name: "NODE_OPTIONS", +// Value: jsOptionsValEbpf, +// }, +// }, +// }, +// }, +// }, +// } + +// deployment := &appsv1.Deployment{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: "test", +// Namespace: "test", +// }, +// } + +// defaultSdks := map[common.ProgrammingLanguage]common.OtelSdk{ +// common.JavascriptProgrammingLanguage: common.OtelSdkNativeCommunity, +// } + +// err := ApplyInstrumentationDevicesToPodTemplate(podTemplate, runtimeDetails, defaultSdks, deployment) +// if err != nil { +// t.Errorf("ApplyInstrumentationDevicesToPodTemplate() error = %v", err) +// } + +// assertContainerNoEnvVar(t, podTemplate, 0, "NODE_OPTIONS") +// } diff --git a/instrumentor/internal/testutil/assertions.go b/instrumentor/internal/testutil/assertions.go index 451c0a078..fde46d00c 100644 --- a/instrumentor/internal/testutil/assertions.go +++ b/instrumentor/internal/testutil/assertions.go @@ -83,3 +83,61 @@ func isReportedNameDeleted(obj client.Object, err error) bool { _, found := obj.GetAnnotations()[consts.OdigosReportedNameAnnotation] return found } + +func AssertDepContainerEnvRemainEmpty(ctx context.Context, k8sClient client.Client, dep *appsv1.Deployment) { + key := client.ObjectKey{Namespace: dep.GetNamespace(), Name: dep.GetName()} + Consistently(func() bool { + var currentDeployment appsv1.Deployment + err := k8sClient.Get(ctx, key, ¤tDeployment) + if err != nil { + return false + } + for _, container := range currentDeployment.Spec.Template.Spec.Containers { + if len(container.Env) > 0 { + return false + } + } + return true + }, duration, interval).Should(BeTrue()) +} + +func AssertDepContainerSingleEnvBecomesEmpty(ctx context.Context, k8sClient client.Client, dep *appsv1.Deployment) { + key := client.ObjectKey{Namespace: dep.GetNamespace(), Name: dep.GetName()} + Eventually(func() bool { + var currentDeployment appsv1.Deployment + err := k8sClient.Get(ctx, key, ¤tDeployment) + if err != nil { + return false + } + for _, container := range currentDeployment.Spec.Template.Spec.Containers { + if len(container.Env) > 0 { + return false + } + } + return true + }, duration, interval).Should(BeTrue()) +} + +func AssertDepContainerSingleEnv(ctx context.Context, k8sClient client.Client, dep *appsv1.Deployment, envName string, envValue string) { + key := client.ObjectKey{Namespace: dep.GetNamespace(), Name: dep.GetName()} + Eventually(func() bool { + var currentDeployment appsv1.Deployment + err := k8sClient.Get(ctx, key, ¤tDeployment) + if err != nil { + return false + } + return IsDeploymentSingleContainerSingleEnv(¤tDeployment, envName, envValue) + }, duration, interval).Should(BeTrue()) +} + +func AssertDepContainerSingleEnvRemainsSame(ctx context.Context, k8sClient client.Client, dep *appsv1.Deployment, envName string, envValue string) { + key := client.ObjectKey{Namespace: dep.GetNamespace(), Name: dep.GetName()} + Consistently(func() bool { + var currentDeployment appsv1.Deployment + err := k8sClient.Get(ctx, key, ¤tDeployment) + if err != nil { + return false + } + return IsDeploymentSingleContainerSingleEnv(¤tDeployment, envName, envValue) + }, duration, interval).Should(BeTrue()) +} diff --git a/instrumentor/internal/testutil/helpers.go b/instrumentor/internal/testutil/helpers.go index 80415dc4f..c3bea5664 100644 --- a/instrumentor/internal/testutil/helpers.go +++ b/instrumentor/internal/testutil/helpers.go @@ -1,7 +1,11 @@ package testutil import ( + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/common" "github.com/odigos-io/odigos/common/consts" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -28,3 +32,36 @@ func SetReportedNameAnnotation[W client.Object](obj W, reportedName string) W { copy.SetAnnotations(map[string]string{consts.OdigosReportedNameAnnotation: reportedName}) return copy } + +func SetInstrumentedApplicationContainer(obj *odigosv1.InstrumentedApplication, envName *string, envValue *string, language common.ProgrammingLanguage) *odigosv1.InstrumentedApplication { + copy := obj.DeepCopy() + copy.Spec.RuntimeDetails[0] = odigosv1.RuntimeDetailsByContainer{ + ContainerName: copy.Spec.RuntimeDetails[0].ContainerName, + Language: language, + } + + if envName != nil && envValue != nil { + copy.Spec.RuntimeDetails[0].EnvVars = []odigosv1.EnvVar{{Name: *envName, Value: *envValue}} + } + + return copy +} + +func SetDeploymentContainerEnv(obj *appsv1.Deployment, envName string, envValue string) *appsv1.Deployment { + copy := obj.DeepCopy() + envVar := corev1.EnvVar{Name: envName, Value: envValue} + if len(copy.Spec.Template.Spec.Containers[0].Env) == 0 { + copy.Spec.Template.Spec.Containers[0].Env = append(copy.Spec.Template.Spec.Containers[0].Env, envVar) + } else { + copy.Spec.Template.Spec.Containers[0].Env[0] = envVar + } + + return copy +} + +func IsDeploymentSingleContainerSingleEnv(obj *appsv1.Deployment, envName string, envValue string) bool { + return len(obj.Spec.Template.Spec.Containers) == 1 && + len(obj.Spec.Template.Spec.Containers[0].Env) == 1 && + obj.Spec.Template.Spec.Containers[0].Env[0].Name == envName && + obj.Spec.Template.Spec.Containers[0].Env[0].Value == envValue +} diff --git a/instrumentor/internal/testutil/mocks.go b/instrumentor/internal/testutil/mocks.go index 6312a572b..4ec88508a 100644 --- a/instrumentor/internal/testutil/mocks.go +++ b/instrumentor/internal/testutil/mocks.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/k8sutils/pkg/workload" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -22,6 +23,14 @@ const ( mockStatefulSetName = "test-statefulset" ) +func NewOdigosSystemNamespace() *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "odigos-system", + }, + } +} + func NewMockNamespace() *corev1.Namespace { name := generateUUIDNamespace(mockNamespaceBase) return &corev1.Namespace{ @@ -140,6 +149,33 @@ func NewMockInstrumentedApplication(workloadObject client.Object) *odigosv1.Inst } } +func NewMockDataCollection() *odigosv1.CollectorsGroup { + return &odigosv1.CollectorsGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "odigos-data-collection", + Namespace: consts.DefaultOdigosNamespace, + }, + Spec: odigosv1.CollectorsGroupSpec{ + Role: odigosv1.CollectorsGroupRoleNodeCollector, + }, + } +} + +func NewMockOdigosConfig() *odigosv1.OdigosConfiguration { + return &odigosv1.OdigosConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: consts.OdigosConfigurationName, + Namespace: consts.DefaultOdigosNamespace, + }, + Spec: odigosv1.OdigosConfigurationSpec{ + DefaultSDKs: map[common.ProgrammingLanguage]common.OtelSdk{ + common.PythonProgrammingLanguage: common.OtelSdkNativeCommunity, + common.GoProgrammingLanguage: common.OtelSdkNativeCommunity, + }, + }, + } +} + // this helps to avoid the "already exists" error when creating a new namespace. // it promotes test isolation and avoid conflicts between tests. func generateUUIDNamespace(baseName string) string { diff --git a/k8sutils/pkg/envoverwrite/origenv.go b/k8sutils/pkg/envoverwrite/origenv.go new file mode 100644 index 000000000..923497274 --- /dev/null +++ b/k8sutils/pkg/envoverwrite/origenv.go @@ -0,0 +1,96 @@ +package envoverwrite + +import ( + "encoding/json" + "fmt" + + "github.com/odigos-io/odigos/common/consts" + "github.com/odigos-io/odigos/common/envOverwrite" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// original manifest values for the env vars of a workload +// This is specific to k8s as it assumes there is OriginalEnv per container +type OrigWorkloadEnvValues struct { + origManifestValues map[string]envOverwrite.OriginalEnv + modifiedSinceCreated bool +} + +func NewOrigWorkloadEnvValues(workloadObj client.Object) (*OrigWorkloadEnvValues, error) { + workloadAnnotations := workloadObj.GetAnnotations() + manifestValues := make(map[string]envOverwrite.OriginalEnv) + if workloadAnnotations != nil { + if currentEnvAnnotation, ok := workloadAnnotations[consts.ManifestEnvOriginalValAnnotation]; ok { + err := json.Unmarshal([]byte(currentEnvAnnotation), &manifestValues) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal manifest env original annotation: %v", err) + } + } + } + + return &OrigWorkloadEnvValues{ + origManifestValues: manifestValues, + modifiedSinceCreated: false, + }, nil +} + +func (o *OrigWorkloadEnvValues) GetContainerStoredEnvs(containerName string) envOverwrite.OriginalEnv { + return o.origManifestValues[containerName] +} + +// this function is called when reverting a value back to it's original content. +// it removes the env, if exists, and returns the original value for the caller to populate back into the manifest. +func (o *OrigWorkloadEnvValues) RemoveOriginalValue(containerName string, envName string) (*string, bool) { + if val, ok := o.origManifestValues[containerName][envName]; ok { + delete(o.origManifestValues[containerName], envName) + return val, true + } + return nil, false +} + +func (o *OrigWorkloadEnvValues) InsertOriginalValue(containerName string, envName string, val *string) { + if _, ok := o.origManifestValues[containerName]; !ok { + o.origManifestValues[containerName] = make(envOverwrite.OriginalEnv) + } + if _, alreadyExists := o.origManifestValues[containerName][envName]; alreadyExists { + // we already have the original value for this env, will not update it + // TODO: should we update it if the value is different? + return + } + o.origManifestValues[containerName][envName] = val + o.modifiedSinceCreated = true +} + +// stores the original values back into the manifest annotations +// by modifying the annotations map of the input argument +func (o *OrigWorkloadEnvValues) SerializeToAnnotation(obj client.Object) error { + if !o.modifiedSinceCreated { + return nil + } + + annotationContentBytes, err := json.Marshal(o.origManifestValues) + if err != nil { + // this should never happen, but if it does, we should log it and continue + return fmt.Errorf("failed to marshal original env values: %v", err) + } + annotationEntryContent := string(annotationContentBytes) + + currentAnnotations := obj.GetAnnotations() + if currentAnnotations == nil { + currentAnnotations = make(map[string]string) + } + + // write the original values back to the manifest annotations and update the object + currentAnnotations[consts.ManifestEnvOriginalValAnnotation] = annotationEntryContent + obj.SetAnnotations(currentAnnotations) + return nil +} + +func (o *OrigWorkloadEnvValues) DeleteFromObj(obj client.Object) { + currentAnnotations := obj.GetAnnotations() + if currentAnnotations == nil { + return + } + + delete(currentAnnotations, consts.ManifestEnvOriginalValAnnotation) +} diff --git a/odiglet/pkg/kube/runtime_details/shared.go b/odiglet/pkg/kube/runtime_details/shared.go index 46d554bca..716cfa351 100644 --- a/odiglet/pkg/kube/runtime_details/shared.go +++ b/odiglet/pkg/kube/runtime_details/shared.go @@ -38,7 +38,7 @@ func inspectRuntimesOfRunningPods(ctx context.Context, logger *logr.Logger, labe } odigosConfig := &odigosv1.OdigosConfiguration{} - err = kubeClient.Get(ctx, client.ObjectKey{Namespace: env.GetCurrentNamespace(), Name: consts.DefaultOdigosConfigurationName}, odigosConfig) + err = kubeClient.Get(ctx, client.ObjectKey{Namespace: env.GetCurrentNamespace(), Name: consts.OdigosConfigurationName}, odigosConfig) if err != nil { logger.Error(err, "error fetching odigos configuration") return ctrl.Result{}, err