diff --git a/api/instrumentation/v1alpha1/instrumentation_types.go b/api/instrumentation/v1alpha1/instrumentation_types.go index 2e20111b2e..82c8bb6c74 100644 --- a/api/instrumentation/v1alpha1/instrumentation_types.go +++ b/api/instrumentation/v1alpha1/instrumentation_types.go @@ -27,6 +27,11 @@ type InstrumentationSpec struct { // +optional // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true Java JavaSpec `json:"java,omitempty"` + + // NodeJS defines configuration for nodejs auto-instrumentation. + // +optional + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true + NodeJS NodeJSSpec `json:"nodejs,omitempty"` } // JavaSpec defines Java SDK and instrumentation configuration. @@ -37,6 +42,14 @@ type JavaSpec struct { Image string `json:"image,omitempty"` } +// NodeJSSpec defines NodeJS SDK and instrumentation configuration. +type NodeJSSpec struct { + // Image is a container image with NodeJS SDK and autoinstrumentation. + // +optional + // +operator-sdk:gen-csv:customresourcedefinitions.specDescriptors=true + Image string `json:"image,omitempty"` +} + // Exporter defines OTLP exporter configuration. type Exporter struct { // Endpoint is address of the collector with OTLP endpoint. diff --git a/api/instrumentation/v1alpha1/zz_generated.deepcopy.go b/api/instrumentation/v1alpha1/zz_generated.deepcopy.go index 23ecbabcf1..77fd44daa2 100644 --- a/api/instrumentation/v1alpha1/zz_generated.deepcopy.go +++ b/api/instrumentation/v1alpha1/zz_generated.deepcopy.go @@ -102,6 +102,7 @@ func (in *InstrumentationSpec) DeepCopyInto(out *InstrumentationSpec) { *out = *in out.Exporter = in.Exporter out.Java = in.Java + out.NodeJS = in.NodeJS } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationSpec. @@ -143,3 +144,18 @@ func (in *JavaSpec) DeepCopy() *JavaSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeJSSpec) DeepCopyInto(out *NodeJSSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeJSSpec. +func (in *NodeJSSpec) DeepCopy() *NodeJSSpec { + if in == nil { + return nil + } + out := new(NodeJSSpec) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/opentelemetry-operator.clusterserviceversion.yaml b/bundle/manifests/opentelemetry-operator.clusterserviceversion.yaml index df3c816131..40ee72dcb6 100644 --- a/bundle/manifests/opentelemetry-operator.clusterserviceversion.yaml +++ b/bundle/manifests/opentelemetry-operator.clusterserviceversion.yaml @@ -21,7 +21,7 @@ metadata: containerImage: quay.io/opentelemetry/opentelemetry-operator createdAt: "2020-12-16T13:37:00+00:00" description: Provides the OpenTelemetry components, including the Collector - operators.operatorframework.io/builder: operator-sdk-v1.13.0+git + operators.operatorframework.io/builder: operator-sdk-v1.14.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v2 repository: github.com/open-telemetry/opentelemetry-operator support: OpenTelemetry Community diff --git a/bundle/manifests/opentelemetry.io_instrumentations.yaml b/bundle/manifests/opentelemetry.io_instrumentations.yaml index 9cf9e406ed..fc71751741 100644 --- a/bundle/manifests/opentelemetry.io_instrumentations.yaml +++ b/bundle/manifests/opentelemetry.io_instrumentations.yaml @@ -57,6 +57,13 @@ spec: description: Image is a container image with javaagent JAR. type: string type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + image: + description: Image is a container image with NodeJS SDK and autoinstrumentation. + type: string + type: object type: object status: description: InstrumentationStatus defines status of the instrumentation. diff --git a/config/crd/bases/opentelemetry.io_instrumentations.yaml b/config/crd/bases/opentelemetry.io_instrumentations.yaml index ade599959b..aea7021987 100644 --- a/config/crd/bases/opentelemetry.io_instrumentations.yaml +++ b/config/crd/bases/opentelemetry.io_instrumentations.yaml @@ -59,6 +59,13 @@ spec: description: Image is a container image with javaagent JAR. type: string type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + image: + description: Image is a container image with NodeJS SDK and autoinstrumentation. + type: string + type: object type: object status: description: InstrumentationStatus defines status of the instrumentation. diff --git a/pkg/instrumentation/annotation.go b/pkg/instrumentation/annotation.go index fd063f8a3f..76884a5dcb 100644 --- a/pkg/instrumentation/annotation.go +++ b/pkg/instrumentation/annotation.go @@ -23,15 +23,16 @@ import ( const ( // annotationInjectJava indicates whether java auto-instrumentation should be injected or not. // Possible values are "true", "false" or "" name. - annotationInjectJava = "instrumentation.opentelemetry.io/inject-java" + annotationInject = "instrumentation.opentelemetry.io/inject" + annotationLanguage = "instrumentation.opentelemetry.io/language" ) // annotationValue returns the effective annotationInjectJava value, based on the annotations from the pod and namespace. -func annotationValue(ns metav1.ObjectMeta, pod metav1.ObjectMeta) string { +func annotationValue(ns metav1.ObjectMeta, pod metav1.ObjectMeta, annotation string) string { // is the pod annotated with instructions to inject sidecars? is the namespace annotated? // if any of those is true, a sidecar might be desired. - podAnnValue := pod.Annotations[annotationInjectJava] - nsAnnValue := ns.Annotations[annotationInjectJava] + podAnnValue := pod.Annotations[annotation] + nsAnnValue := ns.Annotations[annotation] // if the namespace value is empty, the pod annotation should be used, whatever it is if len(nsAnnValue) == 0 { diff --git a/pkg/instrumentation/annotation_test.go b/pkg/instrumentation/annotation_test.go index d8f2ac2b52..35835982ff 100644 --- a/pkg/instrumentation/annotation_test.go +++ b/pkg/instrumentation/annotation_test.go @@ -35,14 +35,14 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "true", + annotationInject: "true", }, }, }, corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "false", + annotationInject: "false", }, }, }, @@ -54,14 +54,14 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "true", + annotationInject: "true", }, }, }, corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "some-instance", + annotationInject: "some-instance", }, }, }, @@ -73,14 +73,14 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "some-instance-from-pod", + annotationInject: "some-instance-from-pod", }, }, }, corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "some-instance", + annotationInject: "some-instance", }, }, }, @@ -92,14 +92,14 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "false", + annotationInject: "false", }, }, }, corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "some-instance", + annotationInject: "some-instance", }, }, }, @@ -112,7 +112,7 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "some-instance", + annotationInject: "some-instance", }, }, }, @@ -124,7 +124,7 @@ func TestEffectiveAnnotationValue(t *testing.T) { corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "true", + annotationInject: "true", }, }, }, @@ -133,7 +133,7 @@ func TestEffectiveAnnotationValue(t *testing.T) { } { t.Run(tt.desc, func(t *testing.T) { // test - annValue := annotationValue(tt.ns.ObjectMeta, tt.pod.ObjectMeta) + annValue := annotationValue(tt.ns.ObjectMeta, tt.pod.ObjectMeta, annotationInject) // verify assert.Equal(t, tt.expected, annValue) diff --git a/pkg/instrumentation/nodejs.go b/pkg/instrumentation/nodejs.go new file mode 100644 index 0000000000..308fc73344 --- /dev/null +++ b/pkg/instrumentation/nodejs.go @@ -0,0 +1,68 @@ +// Copyright The OpenTelemetry Authors +// +// 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 instrumentation + +import ( + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + "github.com/open-telemetry/opentelemetry-operator/api/instrumentation/v1alpha1" +) + +const ( + envNodeOptions = "NODE_OPTIONS" + nodeRequireArgument = " --require /otel-auto-instrumentation/autoinstrumentation.js" +) + +func injectNodeJSSDK(logger logr.Logger, nodeJSSpec v1alpha1.NodeJSSpec, pod corev1.Pod) corev1.Pod { + // caller checks if there is at least one container + container := &pod.Spec.Containers[0] + idx := getIndexOfEnv(container.Env, envNodeOptions) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envNodeOptions, + Value: nodeRequireArgument, + }) + } else if idx > -1 { + if container.Env[idx].ValueFrom != nil { + // TODO add to status object or submit it as an event + logger.Info("Skipping NodeJS SDK injection, the container defines NODE_OPTIONS env var value via ValueFrom", "container", container.Name) + return pod + } + container.Env[idx].Value = container.Env[idx].Value + nodeRequireArgument + } + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }) + + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: nodeJSSpec.Image, + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }) + + return pod +} diff --git a/pkg/instrumentation/nodejs_test.go b/pkg/instrumentation/nodejs_test.go new file mode 100644 index 0000000000..86b6302985 --- /dev/null +++ b/pkg/instrumentation/nodejs_test.go @@ -0,0 +1,181 @@ +// Copyright The OpenTelemetry Authors +// +// 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 instrumentation + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + + "github.com/open-telemetry/opentelemetry-operator/api/instrumentation/v1alpha1" +) + +func TestInjectNodeJSSDK(t *testing.T) { + tests := []struct { + name string + v1alpha1.NodeJSSpec + pod corev1.Pod + expected corev1.Pod + }{ + { + name: "NODE_OPTIONS not defined", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {}, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "foo/bar:1", + Command: []string{"cp", "-R", "/autoinstrumentation/*", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: javaJVMArgument, + }, + }, + }, + }, + }, + }, + }, + { + name: "NODE_OPTIONS defined", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: "-Dbaz=bar", + }, + }, + }, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "foo/bar:1", + Command: []string{"cp", "-R", "/autoinstrumentation/*", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + Value: "-Dbaz=bar" + javaJVMArgument, + }, + }, + }, + }, + }, + }, + }, + { + name: "NODE_OPTIONS defined as ValueFrom", + NodeJSSpec: v1alpha1.NodeJSSpec{Image: "foo/bar:1"}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + ValueFrom: &corev1.EnvVarSource{}, + }, + }, + }, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "NODE_OPTIONS", + ValueFrom: &corev1.EnvVarSource{}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pod := injectNodeJSSDK(logr.Discard(), test.NodeJSSpec, test.pod) + assert.Equal(t, test.expected, pod) + }) + } +} diff --git a/pkg/instrumentation/podmutator.go b/pkg/instrumentation/podmutator.go index cc358249bb..fb07dc9e85 100644 --- a/pkg/instrumentation/podmutator.go +++ b/pkg/instrumentation/podmutator.go @@ -17,6 +17,7 @@ package instrumentation import ( "context" "errors" + "fmt" "strings" "github.com/go-logr/logr" @@ -31,6 +32,8 @@ import ( var ( errMultipleInstancesPossible = errors.New("multiple OpenTelemetry Instrumentation instances available, cannot determine which one to select") errNoInstancesAvailable = errors.New("no OpenTelemetry Instrumentation instances available") + errNoLanguageSpecified = errors.New(fmt.Sprintf("%s must be set to the desired SDK language", annotationLanguage)) + errUnsupportedLanguage = errors.New(fmt.Sprintf("SDK language not supported, supported languages are (java, nodejs)")) ) type instPodMutator struct { @@ -51,20 +54,22 @@ func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod c logger := pm.Logger.WithValues("namespace", pod.Namespace, "name", pod.Name) // if no annotations are found at all, just return the same pod - annValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta) - if len(annValue) == 0 { + instValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta, annotationInject) + if len(instValue) == 0 { logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection") return pod, nil } // is the annotation value 'false'? if so, we need a pod without the instrumentation - if strings.EqualFold(annValue, "false") { + if strings.EqualFold(instValue, "false") { logger.V(1).Info("pod explicitly refuses instrumentation injection, attempting to remove instrumentation if it exists") return pod, nil } + langValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta, annotationLanguage) + // which instance should it talk to? - otelinst, err := pm.getInstrumentationInstance(ctx, ns, annValue) + otelinst, err := pm.getInstrumentationInstance(ctx, ns, instValue, langValue) if err != nil { if err == errNoInstancesAvailable || err == errMultipleInstancesPossible { // we still allow the pod to be created, but we log a message to the operator's logs @@ -79,16 +84,24 @@ func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod c // once it's been determined that instrumentation is desired, none exists yet, and we know which instance it should talk to, // we should inject the instrumentation. logger.V(1).Info("injecting instrumentation into pod", "otelinst-namespace", otelinst.Namespace, "otelinst-name", otelinst.Name) - return inject(pm.Logger, otelinst, pod), nil + return inject(pm.Logger, otelinst, pod, langValue), nil } -func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns corev1.Namespace, ann string) (v1alpha1.Instrumentation, error) { - if strings.EqualFold(ann, "true") { +func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns corev1.Namespace, instValue string, langValue string) (v1alpha1.Instrumentation, error) { + otelInst := v1alpha1.Instrumentation{} + + if len(langValue) == 0 { + return otelInst, errNoLanguageSpecified + } + if langValue != "java" && langValue != "nodejs" { + return otelInst, errUnsupportedLanguage + } + + if strings.EqualFold(instValue, "true") { return pm.selectInstrumentationInstanceFromNamespace(ctx, ns) } - otelInst := v1alpha1.Instrumentation{} - err := pm.Client.Get(ctx, types.NamespacedName{Name: ann, Namespace: ns.Name}, &otelInst) + err := pm.Client.Get(ctx, types.NamespacedName{Name: instValue, Namespace: ns.Name}, &otelInst) if err != nil { return otelInst, err } diff --git a/pkg/instrumentation/podmutator_test.go b/pkg/instrumentation/podmutator_test.go index e16a9ffc72..e80b94d49b 100644 --- a/pkg/instrumentation/podmutator_test.go +++ b/pkg/instrumentation/podmutator_test.go @@ -26,7 +26,6 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -36,7 +35,7 @@ import ( var k8sClient client.Client var testEnv *envtest.Environment -var testScheme *runtime.Scheme = scheme.Scheme +var testScheme = scheme.Scheme func TestMain(m *testing.M) { testEnv = &envtest.Environment{ @@ -107,7 +106,8 @@ func TestMutatePod(t *testing.T) { pod: corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "true", + annotationInject: "true", + annotationLanguage: "java", }, }, Spec: corev1.PodSpec{ @@ -121,7 +121,8 @@ func TestMutatePod(t *testing.T) { expected: corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "true", + annotationInject: "true", + annotationLanguage: "java", }, }, Spec: corev1.PodSpec{ @@ -172,6 +173,142 @@ func TestMutatePod(t *testing.T) { }, }, }, + { + name: "nodejs injection, true", + ns: corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nodejs", + }, + }, + inst: v1alpha1.Instrumentation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-inst", + Namespace: "nodejs", + }, + Spec: v1alpha1.InstrumentationSpec{ + Java: v1alpha1.JavaSpec{ + Image: "otel/nodejs:1", + }, + Exporter: v1alpha1.Exporter{ + Endpoint: "http://collector:12345", + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationInject: "true", + annotationLanguage: "nodejs", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }, + expected: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationInject: "true", + annotationLanguage: "nodejs", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "opentelemetry-auto-instrumentation", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "otel/java:1", + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + Name: "app", + Env: []corev1.EnvVar{ + { + Name: "OTEL_SERVICE_NAME", + Value: "app", + }, + { + Name: "OTEL_EXPORTER_OTLP_ENDPOINT", + Value: "http://collector:12345", + }, + { + Name: "NODE_OPTIONS", + Value: nodeRequireArgument, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "opentelemetry-auto-instrumentation", + MountPath: "/otel-auto-instrumentation", + }, + }, + }, + }, + }, + }, + }, + { + name: "missing language", + ns: corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "missing-language", + }, + }, + inst: v1alpha1.Instrumentation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-inst", + Namespace: "missing-language", + }, + Spec: v1alpha1.InstrumentationSpec{ + Java: v1alpha1.JavaSpec{ + Image: "otel/java:1", + }, + Exporter: v1alpha1.Exporter{ + Endpoint: "http://collector:12345", + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotationInject: "true", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }, + expected: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }, + }, { name: "missing annotation", ns: corev1.Namespace{ @@ -236,7 +373,7 @@ func TestMutatePod(t *testing.T) { pod: corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "false", + annotationInject: "false", }, }, Spec: corev1.PodSpec{ @@ -250,7 +387,7 @@ func TestMutatePod(t *testing.T) { expected: corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "false", + annotationInject: "false", }, }, Spec: corev1.PodSpec{ @@ -286,7 +423,7 @@ func TestMutatePod(t *testing.T) { pod: corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - annotationInjectJava: "doesnotexists", + annotationInject: "doesnotexists", }, }, Spec: corev1.PodSpec{ diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go index 013a676502..08a617738d 100644 --- a/pkg/instrumentation/sdk.go +++ b/pkg/instrumentation/sdk.go @@ -30,7 +30,7 @@ const ( ) // inject a new sidecar container to the given pod, based on the given OpenTelemetryCollector. -func inject(logger logr.Logger, otelinst v1alpha1.Instrumentation, pod corev1.Pod) corev1.Pod { +func inject(logger logr.Logger, otelinst v1alpha1.Instrumentation, pod corev1.Pod, language string) corev1.Pod { if len(pod.Spec.Containers) < 1 { return pod } @@ -38,7 +38,12 @@ func inject(logger logr.Logger, otelinst v1alpha1.Instrumentation, pod corev1.Po // inject only to the first container for now // in the future we can define an annotation to configure this pod = injectCommonSDKConfig(otelinst, pod) - pod = injectJavaagent(logger, otelinst.Spec.Java, pod) + if language == "java" { + pod = injectJavaagent(logger, otelinst.Spec.Java, pod) + } + if language == "nodejs" { + pod = injectNodeJSSDK(logger, otelinst.Spec.NodeJS, pod) + } return pod } diff --git a/pkg/instrumentation/sdk_test.go b/pkg/instrumentation/sdk_test.go index a71d381744..156f54b59d 100644 --- a/pkg/instrumentation/sdk_test.go +++ b/pkg/instrumentation/sdk_test.go @@ -125,7 +125,7 @@ func TestSDKInjection(t *testing.T) { } } -func TestInjection(t *testing.T) { +func TestInjectJava(t *testing.T) { inst := v1alpha1.Instrumentation{ Spec: v1alpha1.InstrumentationSpec{ Java: v1alpha1.JavaSpec{ @@ -144,7 +144,7 @@ func TestInjection(t *testing.T) { }, }, }, - }) + }, "java") assert.Equal(t, corev1.Pod{ Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ @@ -194,3 +194,73 @@ func TestInjection(t *testing.T) { }, }, pod) } + +func TestInjectNodeJS(t *testing.T) { + inst := v1alpha1.Instrumentation{ + Spec: v1alpha1.InstrumentationSpec{ + NodeJS: v1alpha1.NodeJSSpec{ + Image: "img:1", + }, + Exporter: v1alpha1.Exporter{ + Endpoint: "https://collector:4318", + }, + }, + } + pod := inject(logr.Discard(), inst, corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + }, + }, + }, + }, "nodejs") + assert.Equal(t, corev1.Pod{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: initContainerName, + Image: "img:1", + Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }}, + }, + }, + Containers: []corev1.Container{ + { + Name: "app", + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: "/otel-auto-instrumentation", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "OTEL_SERVICE_NAME", + Value: "app", + }, + { + Name: "OTEL_EXPORTER_OTLP_ENDPOINT", + Value: "https://collector:4318", + }, + { + Name: "NODE_OPTIONS", + Value: nodeRequireArgument, + }, + }, + }, + }, + }, + }, pod) +}