diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25d05a5a..7b877d10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: matrix: # Latest patch version can be found in https://github.com/kubernetes/website/blob/main/content/en/releases/patch-releases.md # Some versions might not be available yet in https://storage.googleapis.com/kubernetes-release/release/v1.X.Y/bin/linux/amd64/kubelet - k8sVersion: [ "v1.34.0", "v1.33.4", "v1.32.8", "v1.31.12", "v1.30.14", "v1.29.15", "v1.28.15", "v1.31.1", "v1.30.5", "v1.29.9", "v1.28.14", "v1.27.16", "v1.26.15" ] + k8sVersion: [ "v1.34.1", "v1.33.5", "v1.32.9", "v1.31.13", "v1.30.14"] steps: - name: Checkout GitHub Repository uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 diff --git a/Makefile b/Makefile index 12f8c0dc..91b0355c 100644 --- a/Makefile +++ b/Makefile @@ -3,15 +3,16 @@ BIN_DIR = ./bin TMP_DIR = $(shell pwd)/tmp LICENSE_KEY ?= fake-abc123 -E2E_K8S_VERSION ?= v1.33.0 -ALL_E2E_K8S_VERSIONS ?= v1.33.0 v1.32.0 v1.31.1 v1.30.5 v1.29.9 v1.28.14 v1.27.16 v1.26.15 +E2E_K8S_VERSION ?= v1.34.1 +ALL_E2E_K8S_VERSIONS ?= v1.34.1 v1.33.5 v1.32.9 v1.31.13 v1.30.14 K8S_AGENTS_OPERATOR_VERSION = "" .DEFAULT_GOAL := help # Go packages to test -TEST_PACKAGES = ./api/v1beta2 \ +TEST_PACKAGES = ./api/v1beta3 \ + ./api/v1beta2 \ ./api/v1beta1 \ ./api/v1alpha2 \ ./internal/apm \ @@ -24,7 +25,8 @@ TEST_PACKAGES = ./api/v1beta2 \ ./internal/migrate/upgrade \ ./internal/version \ ./internal/webhook \ - ./internal/util + ./internal/util \ + ./cmd ## Tool Versions SETUP_ENVTEST ?= $(LOCALBIN)/setup-envtest @@ -39,8 +41,8 @@ HELM_UNITTEST ?= $(LOCALBIN)/helm-unittest GOIMPORTS ?= $(LOCALBIN)/goimports # Kubebuilder variables -SETUP_ENVTEST_K8S_VERSION ?= 1.34.0 -ALL_SETUP_ENVTEST_K8S_VERSIONS ?= 1.34.0 1.33.0 1.32.0 1.31.0 1.30.3 1.29.5 1.28.3 1.27.1 1.26.1 #https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/master/envtest-releases.yaml +SETUP_ENVTEST_K8S_VERSION ?= 1.34.1 +ALL_SETUP_ENVTEST_K8S_VERSIONS ?= 1.34.1 1.33.0 1.32.0 1.31.0 1.30.3 #curl https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/master/envtest-releases.yaml | yq '.releases|keys' | sort # controller-gen crd options CRD_OPTIONS ?= "crd:generateEmbeddedObjectMeta=true" diff --git a/api/current/current.go b/api/current/current.go index 1f4142e4..9d4efe97 100644 --- a/api/current/current.go +++ b/api/current/current.go @@ -1,6 +1,6 @@ package current -import currentapi "github.com/newrelic/k8s-agents-operator/api/v1beta2" +import currentapi "github.com/newrelic/k8s-agents-operator/api/v1beta3" type ( Agent = currentapi.Agent @@ -22,13 +22,46 @@ type ( ImageSelectorOperator = currentapi.ImageSelectorOperator NameSelectorOperator = currentapi.NameSelectorOperator + ImageSelectorKey = currentapi.ImageSelectorKey + NameSelectorKey = currentapi.NameSelectorKey + EnvSelectorRequirement = currentapi.EnvSelectorRequirement ImageSelectorRequirement = currentapi.ImageSelectorRequirement NameSelectorRequirement = currentapi.NameSelectorRequirement ) var ( - AddToScheme = currentapi.AddToScheme - GroupVersion = currentapi.GroupVersion - SchemeBuilder = currentapi.SchemeBuilder + AddToScheme = currentapi.AddToScheme + GroupVersion = currentapi.GroupVersion + SchemeBuilder = currentapi.SchemeBuilder + SetupWebhookWithManager = currentapi.SetupWebhookWithManager + + EnvSelectorOpIn = currentapi.EnvSelectorOpIn + EnvSelectorOpNotIn = currentapi.EnvSelectorOpNotIn + EnvSelectorOpEquals = currentapi.EnvSelectorOpEquals + EnvSelectorOpNotEquals = currentapi.EnvSelectorOpNotEquals + EnvSelectorOpExists = currentapi.EnvSelectorOpExists + EnvSelectorOpDoesNotExist = currentapi.EnvSelectorOpDoesNotExist + + NameSelectorKeyInitContainer = currentapi.NameSelectorKeyInitContainer + NameSelectorKeyContainer = currentapi.NameSelectorKeyContainer + NameSelectorKeyAnyContainer = currentapi.NameSelectorKeyAnyContainer + + NameSelectorOpEquals = currentapi.NameSelectorOpEquals + NameSelectorOpNotEquals = currentapi.NameSelectorOpNotEquals + NameSelectorOpIn = currentapi.NameSelectorOpIn + NameSelectorOpNotIn = currentapi.NameSelectorOpNotIn + + ImageSelectorKeyUrl = currentapi.ImageSelectorKeyUrl + + ImageSelectorOpEquals = currentapi.ImageSelectorOpEquals + ImageSelectorOpNotEquals = currentapi.ImageSelectorOpNotEquals + ImageSelectorOpIn = currentapi.ImageSelectorOpIn + ImageSelectorOpNotIn = currentapi.ImageSelectorOpNotIn + ImageSelectorOpStartsWith = currentapi.ImageSelectorOpStartsWith + ImageSelectorOpNotStartsWith = currentapi.ImageSelectorOpNotStartsWith + ImageSelectorOpEndsWith = currentapi.ImageSelectorOpEndsWith + ImageSelectorOpNotEndsWith = currentapi.ImageSelectorOpNotEndsWith + ImageSelectorOpContains = currentapi.ImageSelectorOpContains + ImageSelectorOpNotContains = currentapi.ImageSelectorOpNotContains ) diff --git a/api/v1beta2/instrumentation_conversion.go b/api/v1beta2/instrumentation_conversion.go index 2cd79b44..bbb4a083 100644 --- a/api/v1beta2/instrumentation_conversion.go +++ b/api/v1beta2/instrumentation_conversion.go @@ -1,4 +1,152 @@ package v1beta2 -// Hub marks this type as a conversion hub. -func (*Instrumentation) Hub() {} +import ( + "github.com/newrelic/k8s-agents-operator/api/current" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this Instrumentation to the Hub version. +func (src *Instrumentation) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*current.Instrumentation) + + // ObjectMeta + dst.ObjectMeta = src.ObjectMeta + + for _, srcExpr := range src.Spec.ContainerSelector.ImageSelector.MatchExpressions { + dst.Spec.ContainerSelector.ImageSelector.MatchExpressions = append(dst.Spec.ContainerSelector.ImageSelector.MatchExpressions, current.ImageSelectorRequirement{ + Key: current.ImageSelectorKey(srcExpr.Key), Operator: current.ImageSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + for _, srcExpr := range src.Spec.ContainerSelector.EnvSelector.MatchExpressions { + dst.Spec.ContainerSelector.EnvSelector.MatchExpressions = append(dst.Spec.ContainerSelector.EnvSelector.MatchExpressions, current.EnvSelectorRequirement{ + Key: srcExpr.Key, Operator: current.EnvSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + for _, srcExpr := range src.Spec.ContainerSelector.NameSelector.MatchExpressions { + dst.Spec.ContainerSelector.NameSelector.MatchExpressions = append(dst.Spec.ContainerSelector.NameSelector.MatchExpressions, current.NameSelectorRequirement{ + Key: current.NameSelectorKey(srcExpr.Key), Operator: current.NameSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + + dst.Spec.ContainerSelector.ImageSelector.MatchImages = src.Spec.ContainerSelector.ImageSelector.MatchImages + dst.Spec.ContainerSelector.EnvSelector.MatchEnvs = src.Spec.ContainerSelector.EnvSelector.MatchEnvs + dst.Spec.ContainerSelector.NameSelector.MatchNames = src.Spec.ContainerSelector.NameSelector.MatchNames + dst.Spec.ContainerSelector.NamesFromPodAnnotation = src.Spec.ContainerSelector.NamesFromPodAnnotation + + dst.Spec.PodLabelSelector = src.Spec.PodLabelSelector + dst.Spec.NamespaceLabelSelector = src.Spec.NamespaceLabelSelector + dst.Spec.LicenseKeySecret = src.Spec.LicenseKeySecret + dst.Spec.AgentConfigMap = src.Spec.AgentConfigMap + dst.Spec.Agent = current.Agent{ + Language: src.Spec.Agent.Language, + Image: src.Spec.Agent.Image, + VolumeSizeLimit: src.Spec.Agent.VolumeSizeLimit, + Env: src.Spec.Agent.Env, + Resources: src.Spec.Agent.Resources, + ImagePullPolicy: src.Spec.Agent.ImagePullPolicy, + SecurityContext: src.Spec.Agent.SecurityContext, + } + dst.Spec.HealthAgent = current.HealthAgent{ + Image: src.Spec.HealthAgent.Image, + Env: src.Spec.HealthAgent.Env, + ImagePullPolicy: src.Spec.HealthAgent.ImagePullPolicy, + SecurityContext: src.Spec.HealthAgent.SecurityContext, + Resources: src.Spec.HealthAgent.Resources, + } + var unhealthyPodErrors []current.UnhealthyPodError + if l := len(src.Status.UnhealthyPodsErrors); l > 0 { + unhealthyPodErrors = make([]current.UnhealthyPodError, l) + for i, e := range src.Status.UnhealthyPodsErrors { + unhealthyPodErrors[i] = current.UnhealthyPodError{ + Pod: e.Pod, + LastError: e.LastError, + } + } + } + dst.Status = current.InstrumentationStatus{ + PodsMatching: src.Status.PodsMatching, + PodsUnhealthy: src.Status.PodsUnhealthy, + PodsHealthy: src.Status.PodsHealthy, + PodsInjected: src.Status.PodsInjected, + PodsOutdated: src.Status.PodsOutdated, + PodsNotReady: src.Status.PodsNotReady, + UnhealthyPodsErrors: unhealthyPodErrors, + LastUpdated: src.Status.LastUpdated, + ObservedVersion: src.Status.ObservedVersion, + } + return nil +} + +// ConvertFrom converts from the Hub version to this version. +func (dst *Instrumentation) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*current.Instrumentation) + + // ObjectMeta + dst.ObjectMeta = src.ObjectMeta + + // Spec + + for _, srcExpr := range src.Spec.ContainerSelector.ImageSelector.MatchExpressions { + dst.Spec.ContainerSelector.ImageSelector.MatchExpressions = append(dst.Spec.ContainerSelector.ImageSelector.MatchExpressions, ImageSelectorRequirement{ + Key: ImageSelectorKey(srcExpr.Key), Operator: ImageSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + for _, srcExpr := range src.Spec.ContainerSelector.EnvSelector.MatchExpressions { + dst.Spec.ContainerSelector.EnvSelector.MatchExpressions = append(dst.Spec.ContainerSelector.EnvSelector.MatchExpressions, EnvSelectorRequirement{ + Key: srcExpr.Key, Operator: EnvSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + for _, srcExpr := range src.Spec.ContainerSelector.NameSelector.MatchExpressions { + dst.Spec.ContainerSelector.NameSelector.MatchExpressions = append(dst.Spec.ContainerSelector.NameSelector.MatchExpressions, NameSelectorRequirement{ + Key: NameSelectorKey(srcExpr.Key), Operator: NameSelectorOperator(srcExpr.Operator), Values: srcExpr.Values, + }) + } + + dst.Spec.ContainerSelector.ImageSelector.MatchImages = src.Spec.ContainerSelector.ImageSelector.MatchImages + dst.Spec.ContainerSelector.EnvSelector.MatchEnvs = src.Spec.ContainerSelector.EnvSelector.MatchEnvs + dst.Spec.ContainerSelector.NameSelector.MatchNames = src.Spec.ContainerSelector.NameSelector.MatchNames + dst.Spec.ContainerSelector.NamesFromPodAnnotation = src.Spec.ContainerSelector.NamesFromPodAnnotation + + dst.Spec.PodLabelSelector = src.Spec.PodLabelSelector + dst.Spec.NamespaceLabelSelector = src.Spec.NamespaceLabelSelector + dst.Spec.LicenseKeySecret = src.Spec.LicenseKeySecret + dst.Spec.Agent = Agent{ + Env: src.Spec.Agent.Env, + Image: src.Spec.Agent.Image, + ImagePullPolicy: src.Spec.Agent.ImagePullPolicy, + Language: src.Spec.Agent.Language, + Resources: src.Spec.Agent.Resources, + SecurityContext: src.Spec.Agent.SecurityContext, + VolumeSizeLimit: src.Spec.Agent.VolumeSizeLimit, + } + dst.Spec.HealthAgent = HealthAgent{ + Env: src.Spec.HealthAgent.Env, + Image: src.Spec.HealthAgent.Image, + ImagePullPolicy: src.Spec.HealthAgent.ImagePullPolicy, + Resources: src.Spec.HealthAgent.Resources, + SecurityContext: src.Spec.HealthAgent.SecurityContext, + } + dst.Spec.AgentConfigMap = src.Spec.AgentConfigMap + var unhealthyPodErrors []UnhealthyPodError + if l := len(src.Status.UnhealthyPodsErrors); l > 0 { + unhealthyPodErrors = make([]UnhealthyPodError, l) + for i, e := range src.Status.UnhealthyPodsErrors { + unhealthyPodErrors[i] = UnhealthyPodError{ + Pod: e.Pod, + LastError: e.LastError, + } + } + } + dst.Status = InstrumentationStatus{ + PodsMatching: src.Status.PodsMatching, + PodsUnhealthy: src.Status.PodsUnhealthy, + PodsHealthy: src.Status.PodsHealthy, + PodsInjected: src.Status.PodsInjected, + PodsOutdated: src.Status.PodsOutdated, + PodsNotReady: src.Status.PodsNotReady, + UnhealthyPodsErrors: unhealthyPodErrors, + LastUpdated: src.Status.LastUpdated, + ObservedVersion: src.Status.ObservedVersion, + } + return nil +} diff --git a/api/v1beta2/instrumentation_conversion_test.go b/api/v1beta2/instrumentation_conversion_test.go new file mode 100644 index 00000000..a746773f --- /dev/null +++ b/api/v1beta2/instrumentation_conversion_test.go @@ -0,0 +1,428 @@ +package v1beta2 + +import ( + "github.com/google/go-cmp/cmp" + "github.com/newrelic/k8s-agents-operator/api/current" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + "time" +) + +func TestConvertTo(t *testing.T) { + src := Instrumentation{ + Spec: InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "a": "b", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "b", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"c"}, + }, + }, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "c": "d", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "d", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"e"}, + }, + }, + }, + ContainerSelector: ContainerSelector{ + NamesFromPodAnnotation: "f", + EnvSelector: EnvSelector{ + MatchEnvs: map[string]string{ + "g": "h", + }, + MatchExpressions: []EnvSelectorRequirement{ + { + Key: "h", + Operator: EnvSelectorOpIn, + Values: []string{"i"}, + }, + }, + }, + ImageSelector: ImageSelector{ + MatchImages: map[string]string{}, + MatchExpressions: []ImageSelectorRequirement{ + { + Key: ImageSelectorKeyUrl, + Operator: ImageSelectorOpIn, + Values: []string{"j"}, + }, + }, + }, + NameSelector: NameSelector{ + MatchNames: map[string]string{ + "k": "l", + }, + MatchExpressions: []NameSelectorRequirement{ + { + Key: "l", + Operator: NameSelectorOpIn, + Values: []string{"m"}, + }, + }, + }, + }, + Agent: Agent{ + Language: "test-language", + Image: "test-image", + ImagePullPolicy: corev1.PullNever, + VolumeSizeLimit: resource.NewQuantity(88, resource.BinarySI), + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + HealthAgent: HealthAgent{ + Image: "test-image2", + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + LicenseKeySecret: "test-license-key", + AgentConfigMap: "test-agent-configmap", + }, + Status: InstrumentationStatus{ + PodsMatching: 1, + PodsHealthy: 2, + PodsOutdated: 3, + PodsInjected: 4, + PodsUnhealthy: 5, + PodsNotReady: 6, + UnhealthyPodsErrors: []UnhealthyPodError{ + { + Pod: "def", + LastError: "ghi", + }, + }, + LastUpdated: metav1.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), + ObservedVersion: "abc", + }, + } + dst := current.Instrumentation{} + expectedInstrumentation := current.Instrumentation{ + Spec: current.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "a": "b", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "b", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"c"}, + }, + }, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "c": "d", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "d", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"e"}, + }, + }, + }, + ContainerSelector: current.ContainerSelector{ + NamesFromPodAnnotation: "f", + EnvSelector: current.EnvSelector{ + MatchEnvs: map[string]string{ + "g": "h", + }, + MatchExpressions: []current.EnvSelectorRequirement{ + { + Key: "h", + Operator: current.EnvSelectorOpIn, + Values: []string{"i"}, + }, + }, + }, + ImageSelector: current.ImageSelector{ + MatchImages: map[string]string{}, + MatchExpressions: []current.ImageSelectorRequirement{ + { + Key: current.ImageSelectorKeyUrl, + Operator: current.ImageSelectorOpIn, + Values: []string{"j"}, + }, + }, + }, + NameSelector: current.NameSelector{ + MatchNames: map[string]string{ + "k": "l", + }, + MatchExpressions: []current.NameSelectorRequirement{ + { + Key: "l", + Operator: current.NameSelectorOpIn, + Values: []string{"m"}, + }, + }, + }, + }, + Agent: current.Agent{ + Language: "test-language", + Image: "test-image", + ImagePullPolicy: corev1.PullNever, + VolumeSizeLimit: resource.NewQuantity(88, resource.BinarySI), + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + HealthAgent: current.HealthAgent{ + Image: "test-image2", + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + LicenseKeySecret: "test-license-key", + AgentConfigMap: "test-agent-configmap", + }, + + Status: current.InstrumentationStatus{ + PodsMatching: 1, + PodsHealthy: 2, + PodsOutdated: 3, + PodsInjected: 4, + PodsUnhealthy: 5, + PodsNotReady: 6, + UnhealthyPodsErrors: []current.UnhealthyPodError{ + { + Pod: "def", + LastError: "ghi", + }, + }, + LastUpdated: metav1.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), + ObservedVersion: "abc", + }, + } + err := src.ConvertTo(&dst) + assert.NoError(t, err) + if diff := cmp.Diff(expectedInstrumentation, dst); diff != "" { + t.Errorf("mismatch (-want, +got):\n%s", diff) + } +} + +func TestConvertFrom(t *testing.T) { + src := current.Instrumentation{ + Spec: current.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "a": "b", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "b", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"c"}, + }, + }, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "c": "d", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "d", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"e"}, + }, + }, + }, + ContainerSelector: current.ContainerSelector{ + NamesFromPodAnnotation: "f", + EnvSelector: current.EnvSelector{ + MatchEnvs: map[string]string{ + "g": "h", + }, + MatchExpressions: []current.EnvSelectorRequirement{ + { + Key: "h", + Operator: current.EnvSelectorOpIn, + Values: []string{"i"}, + }, + }, + }, + ImageSelector: current.ImageSelector{ + MatchImages: map[string]string{}, + MatchExpressions: []current.ImageSelectorRequirement{ + { + Key: current.ImageSelectorKeyUrl, + Operator: current.ImageSelectorOpIn, + Values: []string{"j"}, + }, + }, + }, + NameSelector: current.NameSelector{ + MatchNames: map[string]string{ + "k": "l", + }, + MatchExpressions: []current.NameSelectorRequirement{ + { + Key: "l", + Operator: current.NameSelectorOpIn, + Values: []string{"m"}, + }, + }, + }, + }, + Agent: current.Agent{ + Language: "test-language", + Image: "test-image", + ImagePullPolicy: corev1.PullNever, + VolumeSizeLimit: resource.NewQuantity(88, resource.BinarySI), + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + HealthAgent: current.HealthAgent{ + Image: "test-image2", + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + LicenseKeySecret: "test-license-key", + AgentConfigMap: "test-agent-configmap", + }, + Status: current.InstrumentationStatus{ + PodsMatching: 1, + PodsHealthy: 2, + PodsOutdated: 3, + PodsInjected: 4, + PodsUnhealthy: 5, + PodsNotReady: 6, + UnhealthyPodsErrors: []current.UnhealthyPodError{ + { + Pod: "def", + LastError: "ghi", + }, + }, + LastUpdated: metav1.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), + ObservedVersion: "abc", + EntityGUIDs: []string{"this cant be copied"}, + }, + } + dst := Instrumentation{} + expectedInstrumentation := Instrumentation{ + Spec: InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "a": "b", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "b", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"c"}, + }, + }, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "c": "d", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "d", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"e"}, + }, + }, + }, + ContainerSelector: ContainerSelector{ + NamesFromPodAnnotation: "f", + EnvSelector: EnvSelector{ + MatchEnvs: map[string]string{ + "g": "h", + }, + MatchExpressions: []EnvSelectorRequirement{ + { + Key: "h", + Operator: EnvSelectorOpIn, + Values: []string{"i"}, + }, + }, + }, + ImageSelector: ImageSelector{ + MatchImages: map[string]string{}, + MatchExpressions: []ImageSelectorRequirement{ + { + Key: ImageSelectorKeyUrl, + Operator: ImageSelectorOpIn, + Values: []string{"j"}, + }, + }, + }, + NameSelector: NameSelector{ + MatchNames: map[string]string{ + "k": "l", + }, + MatchExpressions: []NameSelectorRequirement{ + { + Key: "l", + Operator: NameSelectorOpIn, + Values: []string{"m"}, + }, + }, + }, + }, + Agent: Agent{ + Language: "test-language", + Image: "test-image", + ImagePullPolicy: corev1.PullNever, + VolumeSizeLimit: resource.NewQuantity(88, resource.BinarySI), + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + HealthAgent: HealthAgent{ + Image: "test-image2", + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{}, + Resources: corev1.ResourceRequirements{}, + SecurityContext: &corev1.SecurityContext{}, + }, + LicenseKeySecret: "test-license-key", + AgentConfigMap: "test-agent-configmap", + }, + Status: InstrumentationStatus{ + PodsMatching: 1, + PodsHealthy: 2, + PodsOutdated: 3, + PodsInjected: 4, + PodsUnhealthy: 5, + PodsNotReady: 6, + UnhealthyPodsErrors: []UnhealthyPodError{ + { + Pod: "def", + LastError: "ghi", + }, + }, + LastUpdated: metav1.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), + ObservedVersion: "abc", + }, + } + err := dst.ConvertFrom(&src) + assert.NoError(t, err) + if diff := cmp.Diff(expectedInstrumentation, dst); diff != "" { + t.Errorf("mismatch (-want, +got):\n%s", diff) + } +} diff --git a/api/v1beta2/instrumentation_types.go b/api/v1beta2/instrumentation_types.go index 210c9c38..85fbc507 100644 --- a/api/v1beta2/instrumentation_types.go +++ b/api/v1beta2/instrumentation_types.go @@ -172,7 +172,6 @@ type InstrumentationStatus struct { ObservedVersion string `json:"observedVersion,omitempty"` } -// +kubebuilder:storageversion // +kubebuilder:object:root=true // +kubebuilder:resource:shortName=nragent;nragents // +kubebuilder:subresource:status diff --git a/api/v1beta2/instrumentation_webhook.go b/api/v1beta2/instrumentation_webhook.go index a32e05c0..420e4ff7 100644 --- a/api/v1beta2/instrumentation_webhook.go +++ b/api/v1beta2/instrumentation_webhook.go @@ -51,7 +51,7 @@ type InstrumentationDefaulter struct { // Default to set the default values for Instrumentation func (r *InstrumentationDefaulter) Default(ctx context.Context, obj runtime.Object) error { inst := obj.(*Instrumentation) - log.FromContext(ctx).V(1).Info("Setting defaults for v1beta1.Instrumentation", "name", inst.GetName()) + log.FromContext(ctx).V(1).Info("Setting defaults for v1beta2.Instrumentation", "name", inst.GetName()) if inst.Labels == nil { inst.Labels = map[string]string{} } @@ -74,14 +74,18 @@ var validEnvPrefixesStr = strings.Join(validEnvPrefixes, ", ") var _ webhook.CustomValidator = (*InstrumentationValidator)(nil) +// +k8s:deepcopy-gen=false +// InstrumentationSpecValidator is used to validate the instrumentation spec type InstrumentationSpecValidator func(instrumentation *Instrumentation) error +// +k8s:deepcopy-gen=false // InstrumentationValidator is used to validate instrumentations type InstrumentationValidator struct { OperatorNamespace string InstrumentationValidators []InstrumentationSpecValidator } +// NewInstrumentationValidator is used to crate a new validator func NewInstrumentationValidator(operatorNamespace string) *InstrumentationValidator { v := &InstrumentationValidator{ OperatorNamespace: operatorNamespace, diff --git a/api/v1beta2/instrumentation_webhook_test.go b/api/v1beta2/instrumentation_webhook_test.go new file mode 100644 index 00000000..9bece7b6 --- /dev/null +++ b/api/v1beta2/instrumentation_webhook_test.go @@ -0,0 +1,31 @@ +package v1beta2 + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestInstrumentationDefaulter(t *testing.T) { + var expectedObj runtime.Object = &Instrumentation{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "k8s-agents-operator", + }, + }, + Spec: InstrumentationSpec{ + LicenseKeySecret: "newrelic-key-secret", + }, + } + var actualObj runtime.Object = &Instrumentation{} + err := (&InstrumentationDefaulter{}).Default(context.Background(), actualObj) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if diff := cmp.Diff(expectedObj, actualObj); diff != "" { + t.Fatalf("Unexpected diff (-want +got): %v", diff) + } +} diff --git a/api/v1beta2/name_selector.go b/api/v1beta2/name_selector.go index 8805b148..12e94bbc 100644 --- a/api/v1beta2/name_selector.go +++ b/api/v1beta2/name_selector.go @@ -42,7 +42,7 @@ type NameSelectorRequirement struct { // operator represents a key's relationship to a set of values. // Valid operators are In, NotIn, Exists, DoesNotExist. // The list of operators may grow in the future. - Operator EnvSelectorOperator `json:"operator"` + Operator NameSelectorOperator `json:"operator"` // values is an array of string values. // If the operator is In or NotIn, the values array must be non-empty. // If the operator is Exists or DoesNotExist, the values array must be empty. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index c0d55753..cac0388e 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -317,21 +317,6 @@ func (in *InstrumentationStatus) DeepCopy() *InstrumentationStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *InstrumentationValidator) DeepCopyInto(out *InstrumentationValidator) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationValidator. -func (in *InstrumentationValidator) DeepCopy() *InstrumentationValidator { - if in == nil { - return nil - } - out := new(InstrumentationValidator) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NameSelector) DeepCopyInto(out *NameSelector) { *out = *in diff --git a/api/v1beta3/env_selector.go b/api/v1beta3/env_selector.go new file mode 100644 index 00000000..f5a8f9a4 --- /dev/null +++ b/api/v1beta3/env_selector.go @@ -0,0 +1,93 @@ +package v1beta3 + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/newrelic/k8s-agents-operator/internal/selector" +) + +// EnvSelectorOperator A env selector operator is the set of operators that can be used in a selector requirement. +type EnvSelectorOperator string + +const ( + EnvSelectorOpIn EnvSelectorOperator = "In" + EnvSelectorOpNotIn EnvSelectorOperator = "NotIn" + EnvSelectorOpEquals EnvSelectorOperator = "==" + EnvSelectorOpNotEquals EnvSelectorOperator = "!=" + EnvSelectorOpExists EnvSelectorOperator = "Exists" + EnvSelectorOpDoesNotExist EnvSelectorOperator = "DoesNotExist" +) + +var acceptableEnvSelectorOps = map[EnvSelectorOperator]struct{}{ + EnvSelectorOpIn: {}, + EnvSelectorOpNotIn: {}, + EnvSelectorOpExists: {}, + EnvSelectorOpDoesNotExist: {}, + EnvSelectorOpEquals: {}, + EnvSelectorOpNotEquals: {}, +} + +type EnvSelectorRequirement struct { + // key is the field selector key that the requirement applies to. + Key string `json:"key"` + // operator represents a key's relationship to a set of values. + // Valid operators are In, NotIn, Exists, DoesNotExist. + // The list of operators may grow in the future. + Operator EnvSelectorOperator `json:"operator"` + // values is an array of string values. + // If the operator is In or NotIn, the values array must be non-empty. + // If the operator is Exists or DoesNotExist, the values array must be empty. + // +optional + // +listType=atomic + Values []string `json:"values,omitempty"` +} + +type EnvSelector struct { + // matchEnvs is a map of {key,value} pairs. A single {key,value} in the matchEnvs + // map is equivalent to an element of matchExpressions, whose key field is "key", the + // operator is "In", and the values array contains only "value". The requirements are ANDed. + // +optional + MatchEnvs map[string]string `json:"matchEnvs,omitempty"` + // matchExpressions is a list of env selector requirements. The requirements are ANDed. + // +optional + // +listType=atomic + MatchExpressions []EnvSelectorRequirement `json:"matchExpressions,omitempty"` +} + +// IsEmpty is used to check if the container selector is empty +func (s *EnvSelector) IsEmpty() bool { + return len(s.MatchEnvs) == 0 && len(s.MatchExpressions) == 0 +} + +func (s *EnvSelector) AsSelector() (selector.Selector, error) { + sel, err := selector.New(&selector.SimpleSelector{}) + if err != nil { + return nil, err + } + if !s.IsEmpty() { + lsr := make([]selector.SelectorRequirement, len(s.MatchExpressions)) + for i, entry := range s.MatchExpressions { + lsr[i] = selector.SelectorRequirement{ + Key: entry.Key, + Operator: selector.SelectionOperator(entry.Operator), + Values: entry.Values, + } + } + ls := selector.SimpleSelector{ + MatchExact: s.MatchEnvs, + MatchExpressions: lsr, + } + sel, err = selector.New(&ls, selector.WithOperatorValidator(validateEnvOperator)) + if err != nil { + return nil, err + } + } + return sel, nil +} + +func validateEnvOperator(operator string, opts *field.Path) *field.Error { + if _, ok := acceptableEnvSelectorOps[EnvSelectorOperator(operator)]; !ok { + return field.Invalid(opts, operator, "invalid operator") + } + return nil +} diff --git a/api/v1beta3/env_selector_test.go b/api/v1beta3/env_selector_test.go new file mode 100644 index 00000000..c2e34ffe --- /dev/null +++ b/api/v1beta3/env_selector_test.go @@ -0,0 +1 @@ +package v1beta3 diff --git a/api/v1beta3/groupversion_info.go b/api/v1beta3/groupversion_info.go new file mode 100644 index 00000000..47f464d3 --- /dev/null +++ b/api/v1beta3/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +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 v1beta3 contains API Schema definitions for the v1beta3 API group +// +kubebuilder:object:generate=true +// +groupName=newrelic.com +package v1beta3 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "newrelic.com", Version: "v1beta3"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1beta3/image_selector.go b/api/v1beta3/image_selector.go new file mode 100644 index 00000000..f513e87f --- /dev/null +++ b/api/v1beta3/image_selector.go @@ -0,0 +1,115 @@ +package v1beta3 + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/newrelic/k8s-agents-operator/internal/selector" +) + +type ImageSelectorOperator string +type ImageSelectorKey string + +const ( + ImageSelectorOpEquals ImageSelectorOperator = "==" + ImageSelectorOpNotEquals ImageSelectorOperator = "!=" + ImageSelectorOpIn ImageSelectorOperator = "In" + ImageSelectorOpNotIn ImageSelectorOperator = "NotIn" + ImageSelectorOpStartsWith ImageSelectorOperator = "StartsWith" + ImageSelectorOpNotStartsWith ImageSelectorOperator = "NotStartsWith" + ImageSelectorOpEndsWith ImageSelectorOperator = "EndsWith" + ImageSelectorOpNotEndsWith ImageSelectorOperator = "NotEndsWith" + ImageSelectorOpContains ImageSelectorOperator = "Contains" + ImageSelectorOpNotContains ImageSelectorOperator = "NotContains" +) +const ( + ImageSelectorKeyUrl ImageSelectorKey = "url" +) + +var acceptableImageSelectorOps = map[ImageSelectorOperator]struct{}{ + ImageSelectorOpEquals: {}, + ImageSelectorOpNotEquals: {}, + ImageSelectorOpIn: {}, + ImageSelectorOpNotIn: {}, + ImageSelectorOpStartsWith: {}, + ImageSelectorOpNotStartsWith: {}, + ImageSelectorOpEndsWith: {}, + ImageSelectorOpNotEndsWith: {}, + ImageSelectorOpContains: {}, + ImageSelectorOpNotContains: {}, +} + +var acceptableImageSelectorKeys = map[ImageSelectorKey]struct{}{ + ImageSelectorKeyUrl: {}, +} + +type ImageSelectorRequirement struct { + // key is the field selector key that the requirement applies to. + Key ImageSelectorKey `json:"key"` + // operator represents a key's relationship to a set of values. + // Valid operators are In, NotIn, Exists, DoesNotExist. + // The list of operators may grow in the future. + Operator ImageSelectorOperator `json:"operator"` + // values is an array of string values. + // If the operator is In or NotIn, the values array must be non-empty. + // If the operator is Exists or DoesNotExist, the values array must be empty. + // +optional + // +listType=atomic + Values []string `json:"values,omitempty"` +} + +type ImageSelector struct { + // matchImages is a map of {key,value} pairs. A single {key,value} in the matchImages + // map is equivalent to an element of matchExpressions, whose key field is "key", the + // operator is "In", and the values array contains only "value". The requirements are ANDed. + // +optional + MatchImages map[string]string `json:"matchImages,omitempty"` + // matchExpressions is a list of env selector requirements. The requirements are ANDed. + // +optional + // +listType=atomic + MatchExpressions []ImageSelectorRequirement `json:"matchExpressions,omitempty"` +} + +// IsEmpty is used to check if the container selector is empty +func (s *ImageSelector) IsEmpty() bool { + return len(s.MatchImages) == 0 && len(s.MatchExpressions) == 0 +} + +func (s *ImageSelector) AsSelector() (selector.Selector, error) { + sel, err := selector.New(&selector.SimpleSelector{}) + if err != nil { + return nil, err + } + if !s.IsEmpty() { + lsr := make([]selector.SelectorRequirement, len(s.MatchExpressions)) + for i, entry := range s.MatchExpressions { + lsr[i] = selector.SelectorRequirement{ + Key: string(entry.Key), + Operator: selector.SelectionOperator(entry.Operator), + Values: entry.Values, + } + } + ls := selector.SimpleSelector{ + MatchExact: s.MatchImages, + MatchExpressions: lsr, + } + sel, err = selector.New(&ls, selector.WithKeyValidator(validateImageKey), selector.WithOperatorValidator(validateImageOperator)) + if err != nil { + return nil, err + } + } + return sel, nil +} + +func validateImageKey(key string, opts *field.Path) *field.Error { + if _, ok := acceptableImageSelectorKeys[ImageSelectorKey(key)]; !ok { + return field.Invalid(opts, key, "invalid key") + } + return nil +} + +func validateImageOperator(operator string, opts *field.Path) *field.Error { + if _, ok := acceptableImageSelectorOps[ImageSelectorOperator(operator)]; !ok { + return field.Invalid(opts, operator, "invalid operator") + } + return nil +} diff --git a/api/v1beta3/image_selector_test.go b/api/v1beta3/image_selector_test.go new file mode 100644 index 00000000..c2e34ffe --- /dev/null +++ b/api/v1beta3/image_selector_test.go @@ -0,0 +1 @@ +package v1beta3 diff --git a/api/v1beta3/instrumentation_conversion.go b/api/v1beta3/instrumentation_conversion.go new file mode 100644 index 00000000..6729116e --- /dev/null +++ b/api/v1beta3/instrumentation_conversion.go @@ -0,0 +1,4 @@ +package v1beta3 + +// Hub marks this type as a conversion hub. +func (*Instrumentation) Hub() {} diff --git a/api/v1beta3/instrumentation_conversion_test.go b/api/v1beta3/instrumentation_conversion_test.go new file mode 100644 index 00000000..f7f39492 --- /dev/null +++ b/api/v1beta3/instrumentation_conversion_test.go @@ -0,0 +1,8 @@ +package v1beta3 + +import "testing" + +func TestInstrumentationHub(t *testing.T) { + (*Instrumentation)(nil).Hub() + // nothing to test here +} diff --git a/api/v1beta3/instrumentation_types.go b/api/v1beta3/instrumentation_types.go new file mode 100644 index 00000000..38e31ae2 --- /dev/null +++ b/api/v1beta3/instrumentation_types.go @@ -0,0 +1,213 @@ +/* +Copyright 2025. + +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 v1beta3 + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InstrumentationSpec defines the desired state of Instrumentation +type InstrumentationSpec struct { + // PodLabelSelector defines to which pods the config should be applied. + // +optional + PodLabelSelector metav1.LabelSelector `json:"podLabelSelector"` + + // PodLabelSelector defines to which pods the config should be applied. + // +optional + NamespaceLabelSelector metav1.LabelSelector `json:"namespaceLabelSelector"` + + // ContainerSelector defines to which pods the config should be applied. + // +optional + ContainerSelector ContainerSelector `json:"containerSelector"` + + // LicenseKeySecret defines where to take the licenseKeySecret from. + // it should be present in the operator namespace. + // +optional + LicenseKeySecret string `json:"licenseKeySecret,omitempty"` + + // AgentConfigMap defines where to take the agent configuration from. + // it should be present in the operator namespace. + // +optional + AgentConfigMap string `json:"agentConfigMap,omitempty"` + + // Agent defines configuration for agent instrumentation. + Agent Agent `json:"agent,omitempty"` + + // HealthAgent defines configuration for healthAgent instrumentation. + HealthAgent HealthAgent `json:"healthAgent,omitempty"` +} + +type ContainerSelector struct { + NamesFromPodAnnotation string `json:"namesFromPodAnnotation,omitempty"` + EnvSelector EnvSelector `json:"envSelector"` + ImageSelector ImageSelector `json:"imageSelector,omitempty"` + NameSelector NameSelector `json:"nameSelector,omitempty"` +} + +// IsEmpty is used to check if the container selector is empty +func (a *ContainerSelector) IsEmpty() bool { + return a.NamesFromPodAnnotation == "" && a.EnvSelector.IsEmpty() && a.NameSelector.IsEmpty() && a.ImageSelector.IsEmpty() +} + +// Agent is the configuration for the agent +type Agent struct { + // Language is the language that will be instrumented. + Language string `json:"language,omitempty"` + + // Image is a container image with Go SDK and auto-instrumentation. + Image string `json:"image,omitempty"` + + // Image pull policy. + // One of Always, Never, IfNotPresent. + // Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + // Cannot be updated. + // More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + // +optional + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty" protobuf:"bytes,14,opt,name=imagePullPolicy,casttype=PullPolicy"` + + // VolumeSizeLimit defines size limit for volume used for auto-instrumentation. + // The default size is 200Mi. + // +optional + VolumeSizeLimit *resource.Quantity `json:"volumeLimitSize,omitempty"` + + // Env defines Go specific env vars. There are four layers for env vars' definitions and + // the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + // If the former var had been defined, then the other vars would be ignored. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Resources describes the compute resource requirements. + // +optional + Resources corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` + + // SecurityContext defines the security options the container should be run with. + // If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + // More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + // +optional + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + +// IsEmpty is used to check if the agent is empty, excluding `.Language` +func (a *Agent) IsEmpty() bool { + return a.Image == "" && + a.ImagePullPolicy == "" && + len(a.Env) == 0 && + a.VolumeSizeLimit == nil && + len(a.Resources.Limits) == 0 && + len(a.Resources.Requests) == 0 && + len(a.Resources.Claims) == 0 && + a.SecurityContext == nil +} + +// HealthAgent is the configuration for the healthAgent +type HealthAgent struct { + // Image is a container image with Go SDK and auto-instrumentation. + Image string `json:"image,omitempty"` + + // Image pull policy. + // One of Always, Never, IfNotPresent. + // Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + // Cannot be updated. + // More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + // +optional + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty" protobuf:"bytes,14,opt,name=imagePullPolicy,casttype=PullPolicy"` + + // Env defines Go specific env vars. There are four layers for env vars' definitions and + // the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + // If the former var had been defined, then the other vars would be ignored. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Resources describes the compute resource requirements. + // +optional + Resources corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` + + // SecurityContext defines the security options the container should be run with. + // If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + // More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + // +optional + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + +// IsEmpty is used to check if the health agent is empty +func (a *HealthAgent) IsEmpty() bool { + return a.Image == "" && + len(a.Env) == 0 && + a.ImagePullPolicy == "" && + len(a.Resources.Requests) == 0 && len(a.Resources.Limits) == 0 && len(a.Resources.Claims) == 0 && + a.SecurityContext == nil +} + +type UnhealthyPodError struct { + Pod string `json:"pod,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +// InstrumentationStatus defines the observed state of Instrumentation +type InstrumentationStatus struct { + PodsMatching int64 `json:"podsMatching,omitempty"` + PodsInjected int64 `json:"podsInjected,omitempty"` + PodsNotReady int64 `json:"podsNotReady,omitempty"` + PodsOutdated int64 `json:"podsOutdated,omitempty"` + PodsHealthy int64 `json:"podsHealthy,omitempty"` + PodsUnhealthy int64 `json:"podsUnhealthy,omitempty"` + UnhealthyPodsErrors []UnhealthyPodError `json:"unhealthyPodsErrors,omitempty"` + LastUpdated metav1.Time `json:"lastUpdated,omitempty"` + ObservedVersion string `json:"observedVersion,omitempty"` + EntityGUIDs []string `json:"entityGUIDs,omitempty"` +} + +// +kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:resource:shortName=nragent;nragents +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="PodsMatching",type="integer",JSONPath=".status.podsMatching" +// +kubebuilder:printcolumn:name="PodsInjected",type="integer",JSONPath=".status.podsInjected" +// +kubebuilder:printcolumn:name="PodsNotReady",type="integer",JSONPath=".status.podsNotReady" +// +kubebuilder:printcolumn:name="PodsOutdated",type="integer",JSONPath=".status.podsOutdated" +// +kubebuilder:printcolumn:name="PodsHealthy",type="integer",JSONPath=".status.podsHealthy" +// +kubebuilder:printcolumn:name="PodsUnhealthy",type="integer",JSONPath=".status.podsUnhealthy" +// +operator-sdk:csv:customresourcedefinitions:displayName="New Relic Instrumentation" +// +operator-sdk:csv:customresourcedefinitions:resources={{Pod,v1}} +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:object:resource:scope=Namespaced + +// Instrumentation is the Schema for the instrumentations API +type Instrumentation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InstrumentationSpec `json:"spec,omitempty"` + Status InstrumentationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// InstrumentationList contains a list of Instrumentation +type InstrumentationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Instrumentation `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Instrumentation{}, &InstrumentationList{}) +} diff --git a/api/v1beta3/instrumentation_webhook.go b/api/v1beta3/instrumentation_webhook.go new file mode 100644 index 00000000..10272e0e --- /dev/null +++ b/api/v1beta3/instrumentation_webhook.go @@ -0,0 +1,277 @@ +/* +Copyright 2025. + +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 v1beta3 + +import ( + "context" + "fmt" + "slices" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func SetupWebhookWithManager(mgr ctrl.Manager, operatorNamespace string) error { + return ctrl.NewWebhookManagedBy(mgr). + For(&Instrumentation{}). + WithValidator(NewInstrumentationValidator(operatorNamespace)). + WithDefaulter(&InstrumentationDefaulter{}). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-newrelic-com-v1beta3-instrumentation,mutating=true,failurePolicy=fail,sideEffects=None,groups=newrelic.com,resources=instrumentations,verbs=create;update,versions=v1beta3,name=minstrumentation-v1beta3.kb.io,admissionReviewVersions=v1 + +var _ webhook.CustomDefaulter = (*InstrumentationDefaulter)(nil) + +// InstrumentationDefaulter is used to set defaults for instrumentation +type InstrumentationDefaulter struct { +} + +// Default to set the default values for Instrumentation +func (r *InstrumentationDefaulter) Default(ctx context.Context, obj runtime.Object) error { + inst := obj.(*Instrumentation) + log.FromContext(ctx).V(1).Info("Setting defaults for v1beta3.Instrumentation", "name", inst.GetName()) + if inst.Labels == nil { + inst.Labels = map[string]string{} + } + if inst.Labels["app.kubernetes.io/managed-by"] == "" { + inst.Labels["app.kubernetes.io/managed-by"] = "k8s-agents-operator" + } + if inst.Spec.LicenseKeySecret == "" { + inst.Spec.LicenseKeySecret = "newrelic-key-secret" + } + return nil +} + +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:verbs=create;update,path=/validate-newrelic-com-v1beta3-instrumentation,mutating=false,failurePolicy=fail,groups=newrelic.com,resources=instrumentations,versions=v1beta3,name=vinstrumentationcreateupdate-v1beta3.kb.io,sideEffects=none,admissionReviewVersions=v1 +// +kubebuilder:webhook:verbs=delete,path=/validate-newrelic-com-v1beta3-instrumentation,mutating=false,failurePolicy=ignore,groups=newrelic.com,resources=instrumentations,versions=v1beta3,name=vinstrumentationdelete-v1beta3.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var validEnvPrefixes = []string{"NEW_RELIC_", "NEWRELIC_"} +var validEnvPrefixesStr = strings.Join(validEnvPrefixes, ", ") + +var _ webhook.CustomValidator = (*InstrumentationValidator)(nil) + +// +k8s:deepcopy-gen=false +// InstrumentationValidator is used to validate instrumentations +type InstrumentationSpecValidator func(instrumentation *Instrumentation) error + +// +k8s:deepcopy-gen=false +// InstrumentationValidator is used to validate instrumentations +type InstrumentationValidator struct { + OperatorNamespace string + InstrumentationValidators []InstrumentationSpecValidator +} + +// NewInstrumentationValidator is used to crate a new validator +func NewInstrumentationValidator(operatorNamespace string) *InstrumentationValidator { + v := &InstrumentationValidator{ + OperatorNamespace: operatorNamespace, + InstrumentationValidators: defaultInstrumentationSpecValidators, + } + return v +} + +// ValidateCreate to validate the creation operation +func (r *InstrumentationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + inst := obj.(*Instrumentation) + log.FromContext(ctx).V(1).Info("Validating creation of v1beta3.Instrumentation", "name", inst.GetName()) + return r.validate(inst) +} + +// ValidateUpdate to validate the update operation +func (r *InstrumentationValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) (admission.Warnings, error) { + inst := newObj.(*Instrumentation) + log.FromContext(ctx).V(1).Info("Validating update of v1beta3.Instrumentation", "name", inst.GetName()) + return r.validate(inst) +} + +// ValidateDelete to validate the deletion operation +func (r *InstrumentationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + inst := obj.(*Instrumentation) + log.FromContext(ctx).V(1).Info("Validating deletion of v1beta3.Instrumentation", "name", inst.GetName()) + return r.validate(inst) +} + +// validate to validate all the fields +func (r *InstrumentationValidator) validate(inst *Instrumentation) (admission.Warnings, error) { + if r.OperatorNamespace != inst.Namespace { + return nil, fmt.Errorf("instrumentation must be in operator namespace") + } + for _, v := range r.InstrumentationValidators { + if err := v(inst); err != nil { + return nil, err + } + } + return nil, nil +} + +var defaultInstrumentationSpecValidators = []InstrumentationSpecValidator{ + validateLicenseKeySecret, + validateAgentConfigMap, + validateContainerSelector, + validatePodSelector, + validateNamespaceSelector, + validateHealthAgent, + validateAgent, +} + +var validImagePullPolicies = map[corev1.PullPolicy]struct{}{ + corev1.PullNever: {}, + corev1.PullAlways: {}, + corev1.PullIfNotPresent: {}, + corev1.PullPolicy(""): {}, +} + +var acceptableLangs = []string{ + "dotnet", + "dotnet-windows2022", + "dotnet-windows2025", + "go", + "java", + "nodejs", + "php-7.2", "php-7.3", "php-7.4", "php-8.0", "php-8.1", "php-8.2", "php-8.3", "php-8.4", + "python", + "ruby", +} + +func validateAgent(inst *Instrumentation) error { + if inst.Spec.Agent.IsEmpty() { + return fmt.Errorf("instrumentation %q agent is empty", inst.Name) + } + if agentLang := inst.Spec.Agent.Language; !slices.Contains(acceptableLangs, agentLang) { + return fmt.Errorf("instrumentation agent language %q must be one of the accepted languages (%s)", agentLang, strings.Join(acceptableLangs, ", ")) + } + if _, ok := validImagePullPolicies[inst.Spec.Agent.ImagePullPolicy]; !ok { + return fmt.Errorf("instrumentation agent.imagePullPolicy is invalid") + } + if err := validateEnvs(inst.Spec.Agent.Env); err != nil { + return fmt.Errorf("instrumentation agent.env is invalid > %w", err) + } + return nil +} + +func validateHealthAgent(inst *Instrumentation) error { + if inst.Spec.HealthAgent.Image == "" { + err := fmt.Errorf("instrumentation %q healthAgent.image is required if anthing else in the healthAgent is configured", inst.Name) + if len(inst.Spec.HealthAgent.Env) > 0 { + return err + } + if len(inst.Spec.HealthAgent.ImagePullPolicy) > 0 { + return err + } + if len(inst.Spec.HealthAgent.Resources.Requests) > 0 || len(inst.Spec.HealthAgent.Resources.Limits) > 0 || len(inst.Spec.HealthAgent.Resources.Claims) > 0 { + return err + } + if inst.Spec.HealthAgent.SecurityContext != nil { + return err + } + } + if _, ok := validImagePullPolicies[inst.Spec.HealthAgent.ImagePullPolicy]; !ok { + return fmt.Errorf("instrumentation healthAgent.imagePullPolicy is invalid") + } + if err := validateEnvs(inst.Spec.HealthAgent.Env); err != nil { + return fmt.Errorf("instrumentation healthAgent.env is invalid > %w", err) + } + return nil +} + +func validateNamespaceSelector(inst *Instrumentation) error { + if _, err := metav1.LabelSelectorAsSelector(&inst.Spec.NamespaceLabelSelector); err != nil { + return fmt.Errorf("instrumentation namespaceLabelSelector is invalid: %w", err) + } + return nil +} + +func validatePodSelector(inst *Instrumentation) error { + if _, err := metav1.LabelSelectorAsSelector(&inst.Spec.PodLabelSelector); err != nil { + return fmt.Errorf("instrumentation podLabelSelector is invalid: %w", err) + } + return nil +} + +func validateLicenseKeySecret(inst *Instrumentation) error { + for _, entry := range inst.Spec.Agent.Env { + if entry.Name == "NEW_RELIC_LICENSE_KEY" { + return fmt.Errorf("%q is already set by the licenseKeySecret", entry.Name) + } + } + return nil +} + +var acceptLangsForAgentConfigMap = []string{"java"} + +func validateAgentConfigMap(inst *Instrumentation) error { + agentLang := inst.Spec.Agent.Language + + if inst.Spec.AgentConfigMap != "" { + if !slices.Contains(acceptLangsForAgentConfigMap, inst.Spec.Agent.Language) { + return fmt.Errorf("instrumentation agent language %q does not support an agentConfigMap, agentConfigMap can only be configured with one of these languages (%q)", agentLang, strings.Join(acceptLangsForAgentConfigMap, ", ")) + } + } + + if agentLang == "java" && inst.Spec.AgentConfigMap != "" { + for _, entry := range inst.Spec.Agent.Env { + if entry.Name == "NEWRELIC_FILE" { + return fmt.Errorf("%q is already set by the agentConfigMap", entry.Name) + } + } + } + return nil +} + +func validateContainerSelector(inst *Instrumentation) error { + if _, err := inst.Spec.ContainerSelector.NameSelector.AsSelector(); err != nil { + return fmt.Errorf("instrumentation containerSelector.nameSelector is invalid: %w", err) + } + if _, err := inst.Spec.ContainerSelector.ImageSelector.AsSelector(); err != nil { + return fmt.Errorf("instrumentation containerSelector.imageSelector is invalid: %w", err) + } + if _, err := inst.Spec.ContainerSelector.EnvSelector.AsSelector(); err != nil { + return fmt.Errorf("instrumentation containerSelector.envSelector is invalid: %w", err) + } + return nil +} + +// validateEnv to validate the environment variables used all start with the required prefixes +func validateEnvs(env []corev1.EnvVar) error { + var invalidNames []string + for _, env := range env { + var valid bool + for _, validEnvPrefix := range validEnvPrefixes { + if strings.HasPrefix(env.Name, validEnvPrefix) { + valid = true + break + } + } + if !valid { + invalidNames = append(invalidNames, env.Name) + } + } + if len(invalidNames) > 0 { + return fmt.Errorf("env name should start with %s; found these invalid names %s", validEnvPrefixesStr, strings.Join(invalidNames, ", ")) + } + return nil +} diff --git a/api/v1beta3/instrumentation_webhook_test.go b/api/v1beta3/instrumentation_webhook_test.go new file mode 100644 index 00000000..bfa0b639 --- /dev/null +++ b/api/v1beta3/instrumentation_webhook_test.go @@ -0,0 +1,31 @@ +package v1beta3 + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestInstrumentationDefaulter(t *testing.T) { + var expectedObj runtime.Object = &Instrumentation{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "k8s-agents-operator", + }, + }, + Spec: InstrumentationSpec{ + LicenseKeySecret: "newrelic-key-secret", + }, + } + var actualObj runtime.Object = &Instrumentation{} + err := (&InstrumentationDefaulter{}).Default(context.Background(), actualObj) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if diff := cmp.Diff(expectedObj, actualObj); diff != "" { + t.Fatalf("Unexpected diff (-want +got): %v", diff) + } +} diff --git a/api/v1beta3/name_selector.go b/api/v1beta3/name_selector.go new file mode 100644 index 00000000..a72df7c2 --- /dev/null +++ b/api/v1beta3/name_selector.go @@ -0,0 +1,109 @@ +package v1beta3 + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/newrelic/k8s-agents-operator/internal/selector" +) + +// NameSelectorOperator A name selector operator is the set of operators that can be used in a selector requirement. +type NameSelectorOperator string +type NameSelectorKey string + +const ( + NameSelectorOpEquals NameSelectorOperator = "==" + NameSelectorOpNotEquals NameSelectorOperator = "!=" + NameSelectorOpIn NameSelectorOperator = "In" + NameSelectorOpNotIn NameSelectorOperator = "NotIn" +) + +const ( + NameSelectorKeyInitContainer NameSelectorKey = "initContainer" + NameSelectorKeyContainer NameSelectorKey = "container" + NameSelectorKeyAnyContainer NameSelectorKey = "anyContainer" +) + +var acceptableNameSelectorOps = map[NameSelectorOperator]struct{}{ + NameSelectorOpEquals: {}, + NameSelectorOpNotEquals: {}, + NameSelectorOpIn: {}, + NameSelectorOpNotIn: {}, +} + +var acceptableNameSelectorKeys = map[NameSelectorKey]struct{}{ + NameSelectorKeyInitContainer: {}, + NameSelectorKeyContainer: {}, + NameSelectorKeyAnyContainer: {}, +} + +type NameSelectorRequirement struct { + // key is the field selector key that the requirement applies to. + Key NameSelectorKey `json:"key"` + // operator represents a key's relationship to a set of values. + // Valid operators are In, NotIn, Exists, DoesNotExist. + // The list of operators may grow in the future. + Operator NameSelectorOperator `json:"operator"` + // values is an array of string values. + // If the operator is In or NotIn, the values array must be non-empty. + // If the operator is Exists or DoesNotExist, the values array must be empty. + // +optional + // +listType=atomic + Values []string `json:"values,omitempty"` +} + +type NameSelector struct { + // matchNames is a map of {key,value} pairs. A single {key,value} in the matchEnvs + // map is equivalent to an element of matchExpressions, whose key field is "key", the + // operator is "In", and the values array contains only "value". The requirements are ANDed. + // +optional + MatchNames map[string]string `json:"matchNames,omitempty"` + // matchExpressions is a list of env selector requirements. The requirements are ANDed. + // +optional + // +listType=atomic + MatchExpressions []NameSelectorRequirement `json:"matchExpressions,omitempty"` +} + +// IsEmpty is used to check if the container selector is empty +func (s *NameSelector) IsEmpty() bool { + return len(s.MatchNames) == 0 && len(s.MatchExpressions) == 0 +} + +func (s *NameSelector) AsSelector() (selector.Selector, error) { + sel, err := selector.New(&selector.SimpleSelector{}) + if err != nil { + return nil, err + } + if !s.IsEmpty() { + lsr := make([]selector.SelectorRequirement, len(s.MatchExpressions)) + for i, entry := range s.MatchExpressions { + lsr[i] = selector.SelectorRequirement{ + Key: string(entry.Key), + Operator: selector.SelectionOperator(entry.Operator), + Values: entry.Values, + } + } + ls := selector.SimpleSelector{ + MatchExact: s.MatchNames, + MatchExpressions: lsr, + } + sel, err = selector.New(&ls, selector.WithKeyValidator(validateNameKey), selector.WithOperatorValidator(validateNameOperator)) + if err != nil { + return nil, err + } + } + return sel, nil +} + +func validateNameKey(key string, opts *field.Path) *field.Error { + if _, ok := acceptableNameSelectorKeys[NameSelectorKey(key)]; !ok { + return field.Invalid(opts, key, "invalid key") + } + return nil +} + +func validateNameOperator(operator string, opts *field.Path) *field.Error { + if _, ok := acceptableNameSelectorOps[NameSelectorOperator(operator)]; !ok { + return field.Invalid(opts, operator, "invalid operator") + } + return nil +} diff --git a/api/v1beta3/name_selector_test.go b/api/v1beta3/name_selector_test.go new file mode 100644 index 00000000..c2e34ffe --- /dev/null +++ b/api/v1beta3/name_selector_test.go @@ -0,0 +1 @@ +package v1beta3 diff --git a/api/v1beta3/webhook_suite_test.go b/api/v1beta3/webhook_suite_test.go new file mode 100644 index 00000000..649860ff --- /dev/null +++ b/api/v1beta3/webhook_suite_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2025. + +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 v1beta3 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + // +kubebuilder:scaffold:imports + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "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" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = ctrl.NewWebhookManagedBy(mgr).For(&Instrumentation{}).Complete() + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go new file mode 100644 index 00000000..627e176e --- /dev/null +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -0,0 +1,387 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta3 + +import ( + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Agent) DeepCopyInto(out *Agent) { + *out = *in + if in.VolumeSizeLimit != nil { + in, out := &in.VolumeSizeLimit, &out.VolumeSizeLimit + x := (*in).DeepCopy() + *out = &x + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Agent. +func (in *Agent) DeepCopy() *Agent { + if in == nil { + return nil + } + out := new(Agent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerSelector) DeepCopyInto(out *ContainerSelector) { + *out = *in + in.EnvSelector.DeepCopyInto(&out.EnvSelector) + in.ImageSelector.DeepCopyInto(&out.ImageSelector) + in.NameSelector.DeepCopyInto(&out.NameSelector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerSelector. +func (in *ContainerSelector) DeepCopy() *ContainerSelector { + if in == nil { + return nil + } + out := new(ContainerSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvSelector) DeepCopyInto(out *EnvSelector) { + *out = *in + if in.MatchEnvs != nil { + in, out := &in.MatchEnvs, &out.MatchEnvs + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.MatchExpressions != nil { + in, out := &in.MatchExpressions, &out.MatchExpressions + *out = make([]EnvSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvSelector. +func (in *EnvSelector) DeepCopy() *EnvSelector { + if in == nil { + return nil + } + out := new(EnvSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvSelectorRequirement) DeepCopyInto(out *EnvSelectorRequirement) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvSelectorRequirement. +func (in *EnvSelectorRequirement) DeepCopy() *EnvSelectorRequirement { + if in == nil { + return nil + } + out := new(EnvSelectorRequirement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthAgent) DeepCopyInto(out *HealthAgent) { + *out = *in + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthAgent. +func (in *HealthAgent) DeepCopy() *HealthAgent { + if in == nil { + return nil + } + out := new(HealthAgent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageSelector) DeepCopyInto(out *ImageSelector) { + *out = *in + if in.MatchImages != nil { + in, out := &in.MatchImages, &out.MatchImages + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.MatchExpressions != nil { + in, out := &in.MatchExpressions, &out.MatchExpressions + *out = make([]ImageSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSelector. +func (in *ImageSelector) DeepCopy() *ImageSelector { + if in == nil { + return nil + } + out := new(ImageSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageSelectorRequirement) DeepCopyInto(out *ImageSelectorRequirement) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSelectorRequirement. +func (in *ImageSelectorRequirement) DeepCopy() *ImageSelectorRequirement { + if in == nil { + return nil + } + out := new(ImageSelectorRequirement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Instrumentation) DeepCopyInto(out *Instrumentation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instrumentation. +func (in *Instrumentation) DeepCopy() *Instrumentation { + if in == nil { + return nil + } + out := new(Instrumentation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Instrumentation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationDefaulter) DeepCopyInto(out *InstrumentationDefaulter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationDefaulter. +func (in *InstrumentationDefaulter) DeepCopy() *InstrumentationDefaulter { + if in == nil { + return nil + } + out := new(InstrumentationDefaulter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationList) DeepCopyInto(out *InstrumentationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Instrumentation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationList. +func (in *InstrumentationList) DeepCopy() *InstrumentationList { + if in == nil { + return nil + } + out := new(InstrumentationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InstrumentationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationSpec) DeepCopyInto(out *InstrumentationSpec) { + *out = *in + in.PodLabelSelector.DeepCopyInto(&out.PodLabelSelector) + in.NamespaceLabelSelector.DeepCopyInto(&out.NamespaceLabelSelector) + in.ContainerSelector.DeepCopyInto(&out.ContainerSelector) + in.Agent.DeepCopyInto(&out.Agent) + in.HealthAgent.DeepCopyInto(&out.HealthAgent) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationSpec. +func (in *InstrumentationSpec) DeepCopy() *InstrumentationSpec { + if in == nil { + return nil + } + out := new(InstrumentationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationStatus) DeepCopyInto(out *InstrumentationStatus) { + *out = *in + if in.UnhealthyPodsErrors != nil { + in, out := &in.UnhealthyPodsErrors, &out.UnhealthyPodsErrors + *out = make([]UnhealthyPodError, len(*in)) + copy(*out, *in) + } + in.LastUpdated.DeepCopyInto(&out.LastUpdated) + if in.EntityGUIDs != nil { + in, out := &in.EntityGUIDs, &out.EntityGUIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationStatus. +func (in *InstrumentationStatus) DeepCopy() *InstrumentationStatus { + if in == nil { + return nil + } + out := new(InstrumentationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameSelector) DeepCopyInto(out *NameSelector) { + *out = *in + if in.MatchNames != nil { + in, out := &in.MatchNames, &out.MatchNames + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.MatchExpressions != nil { + in, out := &in.MatchExpressions, &out.MatchExpressions + *out = make([]NameSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameSelector. +func (in *NameSelector) DeepCopy() *NameSelector { + if in == nil { + return nil + } + out := new(NameSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameSelectorRequirement) DeepCopyInto(out *NameSelectorRequirement) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameSelectorRequirement. +func (in *NameSelectorRequirement) DeepCopy() *NameSelectorRequirement { + if in == nil { + return nil + } + out := new(NameSelectorRequirement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UnhealthyPodError) DeepCopyInto(out *UnhealthyPodError) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnhealthyPodError. +func (in *UnhealthyPodError) DeepCopy() *UnhealthyPodError { + if in == nil { + return nil + } + out := new(UnhealthyPodError) + in.DeepCopyInto(out) + return out +} diff --git a/charts/k8s-agents-operator/templates/instrumentation-crd.yaml b/charts/k8s-agents-operator/templates/instrumentation-crd.yaml index 6e3dc9a3..3864cb9c 100644 --- a/charts/k8s-agents-operator/templates/instrumentation-crd.yaml +++ b/charts/k8s-agents-operator/templates/instrumentation-crd.yaml @@ -38,14 +38,14 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /validate-newrelic-com-v1beta2-instrumentation + path: /validate-newrelic-com-v1beta3-instrumentation failurePolicy: Fail - name: vinstrumentationcreateupdate-v1beta2.kb.io + name: vinstrumentationcreateupdate-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 operations: - CREATE - UPDATE @@ -61,14 +61,59 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /validate-newrelic-com-v1beta2-instrumentation + path: /validate-newrelic-com-v1beta3-instrumentation failurePolicy: Ignore - name: vinstrumentationdelete-v1beta2.kb.io + name: vinstrumentationdelete-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 + operations: + - DELETE + resources: + - instrumentations + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + {{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + caBundle: {{ $tls.caCert }} + {{- end }} + service: + name: {{ include "k8s-agents-operator.webhook.service.name" . }} + namespace: {{ .Release.Namespace }} + path: /validate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Fail + name: vinstrumentationcreateupdate-v1alpha2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + {{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + caBundle: {{ $tls.caCert }} + {{- end }} + service: + name: {{ include "k8s-agents-operator.webhook.service.name" . }} + namespace: {{ .Release.Namespace }} + path: /validate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Ignore + name: vinstrumentationdelete-v1alpha2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha2 operations: - DELETE resources: @@ -128,14 +173,14 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /validate-newrelic-com-v1alpha2-instrumentation + path: /validate-newrelic-com-v1beta2-instrumentation failurePolicy: Fail - name: vinstrumentationcreateupdate-v1alpha2.kb.io + name: vinstrumentationcreateupdate-v1beta2.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha2 + - v1beta2 operations: - CREATE - UPDATE @@ -151,14 +196,14 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /validate-newrelic-com-v1alpha2-instrumentation + path: /validate-newrelic-com-v1beta2-instrumentation failurePolicy: Ignore - name: vinstrumentationdelete-v1alpha2.kb.io + name: vinstrumentationdelete-v1beta2.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha2 + - v1beta2 operations: - DELETE resources: @@ -185,14 +230,37 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /mutate-newrelic-com-v1beta2-instrumentation + path: /mutate-newrelic-com-v1beta3-instrumentation failurePolicy: Fail - name: minstrumentation-v1beta2.kb.io + name: minstrumentation-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + {{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + caBundle: {{ $tls.caCert }} + {{- end }} + service: + name: {{ include "k8s-agents-operator.webhook.service.name" . }} + namespace: {{ .Release.Namespace }} + path: /mutate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Fail + name: minstrumentation-v1alpha2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha2 operations: - CREATE - UPDATE @@ -231,14 +299,14 @@ webhooks: service: name: {{ include "k8s-agents-operator.webhook.service.name" . }} namespace: {{ .Release.Namespace }} - path: /mutate-newrelic-com-v1alpha2-instrumentation + path: /mutate-newrelic-com-v1beta2-instrumentation failurePolicy: Fail - name: minstrumentation-v1alpha2.kb.io + name: minstrumentation-v1beta2.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha2 + - v1beta2 operations: - CREATE - UPDATE @@ -273,7 +341,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.3 + controller-gen.kubebuilder.io/version: v0.19.0 {{- if .Values.admissionWebhooks.certManager.enabled }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "k8s-agents-operator.cert-manager.certificate.name" . }} {{- end }} @@ -295,6 +363,8 @@ spec: conversionReviewVersions: - v1alpha2 - v1beta1 + - v1beta2 + - v1beta3 group: newrelic.com names: kind: Instrumentation @@ -348,8 +418,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -396,17 +467,54 @@ spec: spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". type: string fieldPath: - description: Path of the field to select in the - specified API version. + description: Path of the field to select in the specified + API version. type: string required: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -420,8 +528,8 @@ spec: anyOf: - type: integer - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" + description: Specifies the output format of the exposed + resources, defaults to "1" pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: @@ -432,8 +540,7 @@ spec: type: object x-kubernetes-map-type: atomic secretKeyRef: - description: Selects a key of a secret in the pod's - namespace + description: Selects a key of a secret in the pod's namespace properties: key: description: The key of the secret to select from. Must @@ -475,7 +582,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -763,8 +870,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -811,17 +919,54 @@ spec: spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". type: string fieldPath: - description: Path of the field to select in the - specified API version. + description: Path of the field to select in the specified + API version. type: string required: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -835,8 +980,8 @@ spec: anyOf: - type: integer - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" + description: Specifies the output format of the exposed + resources, defaults to "1" pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: @@ -847,8 +992,7 @@ spec: type: object x-kubernetes-map-type: atomic secretKeyRef: - description: Selects a key of a secret in the pod's - namespace + description: Selects a key of a secret in the pod's namespace properties: key: description: The key of the secret to select from. Must @@ -890,7 +1034,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -976,8 +1120,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1024,17 +1169,54 @@ spec: spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". type: string fieldPath: - description: Path of the field to select in the - specified API version. + description: Path of the field to select in the specified + API version. type: string required: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1048,8 +1230,8 @@ spec: anyOf: - type: integer - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" + description: Specifies the output format of the exposed + resources, defaults to "1" pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: @@ -1060,8 +1242,7 @@ spec: type: object x-kubernetes-map-type: atomic secretKeyRef: - description: Selects a key of a secret in the pod's - namespace + description: Selects a key of a secret in the pod's namespace properties: key: description: The key of the secret to select from. Must @@ -1344,8 +1525,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1392,17 +1574,54 @@ spec: spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". type: string fieldPath: - description: Path of the field to select in the - specified API version. + description: Path of the field to select in the specified + API version. type: string required: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1416,8 +1635,8 @@ spec: anyOf: - type: integer - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" + description: Specifies the output format of the exposed + resources, defaults to "1" pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: @@ -1428,8 +1647,7 @@ spec: type: object x-kubernetes-map-type: atomic secretKeyRef: - description: Selects a key of a secret in the pod's - namespace + description: Selects a key of a secret in the pod's namespace properties: key: description: The key of the secret to select from. Must @@ -1479,7 +1697,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -1579,16 +1797,14 @@ spec: add: description: Added capabilities items: - description: Capability represent POSIX capabilities - type + description: Capability represent POSIX capabilities type type: string type: array x-kubernetes-list-type: atomic drop: description: Removed capabilities items: - description: Capability represent POSIX capabilities - type + description: Capability represent POSIX capabilities type type: string type: array x-kubernetes-list-type: atomic @@ -1650,20 +1866,20 @@ spec: Note that this field cannot be set when spec.os.name is windows. properties: level: - description: Level is SELinux level label that applies - to the container. + description: Level is SELinux level label that applies to + the container. type: string role: - description: Role is a SELinux role label that applies - to the container. + description: Role is a SELinux role label that applies to + the container. type: string type: - description: Type is a SELinux type label that applies - to the container. + description: Type is a SELinux type label that applies to + the container. type: string user: - description: User is a SELinux user label that applies - to the container. + description: User is a SELinux user label that applies to + the container. type: string type: object seccompProfile: @@ -1706,8 +1922,8 @@ spec: GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. type: string hostProcess: description: |- @@ -1760,8 +1976,8 @@ spec: items: properties: key: - description: key is the field selector key that the - requirement applies to. + description: key is the field selector key that the requirement + applies to. type: string operator: description: |- @@ -1793,8 +2009,8 @@ spec: items: properties: key: - description: key is the field selector key that the - requirement applies to. + description: key is the field selector key that the requirement + applies to. type: string operator: description: |- @@ -1834,8 +2050,8 @@ spec: items: properties: key: - description: key is the field selector key that the - requirement applies to. + description: key is the field selector key that the requirement + applies to. type: string operator: description: |- @@ -1869,8 +2085,6 @@ spec: type: object namesFromPodAnnotation: type: string - namesFromPodLabel: - type: string required: - envSelector type: object @@ -1887,8 +2101,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1935,17 +2150,54 @@ spec: spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". type: string fieldPath: - description: Path of the field to select in the - specified API version. + description: Path of the field to select in the specified + API version. type: string required: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1959,8 +2211,8 @@ spec: anyOf: - type: integer - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" + description: Specifies the output format of the exposed + resources, defaults to "1" pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: @@ -1971,8 +2223,7 @@ spec: type: object x-kubernetes-map-type: atomic secretKeyRef: - description: Selects a key of a secret in the pod's - namespace + description: Selects a key of a secret in the pod's namespace properties: key: description: The key of the secret to select from. Must @@ -2019,7 +2270,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2119,16 +2370,14 @@ spec: add: description: Added capabilities items: - description: Capability represent POSIX capabilities - type + description: Capability represent POSIX capabilities type type: string type: array x-kubernetes-list-type: atomic drop: description: Removed capabilities items: - description: Capability represent POSIX capabilities - type + description: Capability represent POSIX capabilities type type: string type: array x-kubernetes-list-type: atomic @@ -2190,20 +2439,20 @@ spec: Note that this field cannot be set when spec.os.name is windows. properties: level: - description: Level is SELinux level label that applies - to the container. + description: Level is SELinux level label that applies to + the container. type: string role: - description: Role is a SELinux role label that applies - to the container. + description: Role is a SELinux role label that applies to + the container. type: string type: - description: Type is a SELinux type label that applies - to the container. + description: Type is a SELinux type label that applies to + the container. type: string user: - description: User is a SELinux user label that applies - to the container. + description: User is a SELinux user label that applies to + the container. type: string type: object seccompProfile: @@ -2246,8 +2495,8 @@ spec: GMSA credential spec named by the GMSACredentialSpecName field. type: string gmsaCredentialSpecName: - description: GMSACredentialSpecName is the name of the - GMSA credential spec to use. + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. type: string hostProcess: description: |- @@ -2404,7 +2653,1208 @@ spec: type: object type: object served: true - storage: true + storage: false subresources: status: {} -{{- end }} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.podsMatching + name: PodsMatching + type: integer + - jsonPath: .status.podsInjected + name: PodsInjected + type: integer + - jsonPath: .status.podsNotReady + name: PodsNotReady + type: integer + - jsonPath: .status.podsOutdated + name: PodsOutdated + type: integer + - jsonPath: .status.podsHealthy + name: PodsHealthy + type: integer + - jsonPath: .status.podsUnhealthy + name: PodsUnhealthy + type: integer + name: v1beta3 + schema: + openAPIV3Schema: + description: Instrumentation is the Schema for the instrumentations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: InstrumentationSpec defines the desired state of Instrumentation + properties: + agent: + description: Agent defines configuration for agent instrumentation. + properties: + env: + description: |- + Env defines Go specific env vars. There are four layers for env vars' definitions and + the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + If the former var had been defined, then the other vars would be ignored. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + language: + description: Language is the language that will be instrumented. + type: string + resourceRequirements: + description: Resources describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + volumeLimitSize: + anyOf: + - type: integer + - type: string + description: |- + VolumeSizeLimit defines size limit for volume used for auto-instrumentation. + The default size is 200Mi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + agentConfigMap: + description: |- + AgentConfigMap defines where to take the agent configuration from. + it should be present in the operator namespace. + type: string + containerSelector: + description: ContainerSelector defines to which pods the config should + be applied. + properties: + envSelector: + properties: + matchEnvs: + additionalProperties: + type: string + description: |- + matchEnvs is a map of {key,value} pairs. A single {key,value} in the matchEnvs + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the requirement + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + imageSelector: + properties: + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the requirement + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchImages: + additionalProperties: + type: string + description: |- + matchImages is a map of {key,value} pairs. A single {key,value} in the matchImages + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + nameSelector: + properties: + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the requirement + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchNames: + additionalProperties: + type: string + description: |- + matchNames is a map of {key,value} pairs. A single {key,value} in the matchEnvs + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + namesFromPodAnnotation: + type: string + required: + - envSelector + type: object + healthAgent: + description: HealthAgent defines configuration for healthAgent instrumentation. + properties: + env: + description: |- + Env defines Go specific env vars. There are four layers for env vars' definitions and + the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + If the former var had been defined, then the other vars would be ignored. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + resourceRequirements: + description: Resources describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + type: object + licenseKeySecret: + description: |- + LicenseKeySecret defines where to take the licenseKeySecret from. + it should be present in the operator namespace. + type: string + namespaceLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + status: + description: InstrumentationStatus defines the observed state of Instrumentation + properties: + entityGUIDs: + items: + type: string + type: array + lastUpdated: + format: date-time + type: string + observedVersion: + type: string + podsHealthy: + format: int64 + type: integer + podsInjected: + format: int64 + type: integer + podsMatching: + format: int64 + type: integer + podsNotReady: + format: int64 + type: integer + podsOutdated: + format: int64 + type: integer + podsUnhealthy: + format: int64 + type: integer + unhealthyPodsErrors: + items: + properties: + lastError: + type: string + pod: + type: string + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] + {{- end }} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 3fbf6346..3650b017 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -29,10 +29,11 @@ import ( "strings" "time" - routev1 "github.com/openshift/api/route/v1" + "github.com/go-logr/logr" + openshift_routev1 "github.com/openshift/api/route/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" + k8sscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" @@ -47,6 +48,7 @@ import ( newreliccomv1alpha2 "github.com/newrelic/k8s-agents-operator/api/v1alpha2" newreliccomv1beta1 "github.com/newrelic/k8s-agents-operator/api/v1beta1" newreliccomv1beta2 "github.com/newrelic/k8s-agents-operator/api/v1beta2" + newreliccomv1beta3 "github.com/newrelic/k8s-agents-operator/api/v1beta3" "github.com/newrelic/k8s-agents-operator/internal/autodetect" "github.com/newrelic/k8s-agents-operator/internal/config" "github.com/newrelic/k8s-agents-operator/internal/controller" @@ -58,50 +60,41 @@ import ( ) var healthCheckTickInterval = time.Second * 15 - -var ( - scheme = k8sruntime.NewScheme() - setupLog = ctrl.Log.WithName("setup") -) - -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - - utilruntime.Must(routev1.AddToScheme(scheme)) // TODO: Update this to not use a deprecated method - utilruntime.Must(newreliccomv1alpha2.AddToScheme(scheme)) - utilruntime.Must(newreliccomv1beta1.AddToScheme(scheme)) - utilruntime.Must(newreliccomv1beta2.AddToScheme(scheme)) +var setupLog = ctrl.Log.WithName("setup") + +func getScheme() *k8sruntime.Scheme { + var scheme = k8sruntime.NewScheme() + sb := k8sruntime.NewSchemeBuilder( + k8sscheme.AddToScheme, + openshift_routev1.AddToScheme, + newreliccomv1alpha2.AddToScheme, + newreliccomv1beta1.AddToScheme, + newreliccomv1beta2.AddToScheme, + newreliccomv1beta3.AddToScheme, + ) + utilruntime.Must(sb.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme + return scheme } -func main() { - var ( - metricsAddr string - probeAddr string - webhooksvc string - enableLeaderElection bool - secureMetrics bool - enableHTTP2 bool - ) +type managerConfig struct { + leaseDuration time.Duration + renewDeadline time.Duration + retryPeriod time.Duration + webhookBindAddr string + webhookCertDir string + metricsBindAddr string + healthBindAddr string + enableHTTP2 bool + secureMetrics bool + enableLeaderElection bool + leaderElectionID string + leaderElectionReleaseOnCancel bool +} + +func getManagerOptions(scheme *k8sruntime.Scheme, cfg managerConfig) ctrl.Options { var webhookTlsOpts []func(*tls.Config) var metricsTlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ - "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.StringVar(&webhooksvc, "webhook-service-bind-address", ":9443", "The address the webhook service endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, - "Enable leader election for controller manager. "+ - "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&secureMetrics, "metrics-secure", true, - "If set, the\tflag.BoolVar(&enableHTTP2, \"enable-http2\", false,\n\t\t\"If set, HTTP/2 will be enabled for the metrics and webhook servers\")\n metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - opts := zap.Options{} - opts.BindFlags(flag.CommandLine) - flag.Parse() - - logger := zap.New(zap.UseFlagOptions(&opts)) - ctrl.SetLogger(logger) - klog.SetLogger(logger) - // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and @@ -114,25 +107,100 @@ func main() { c.NextProtos = []string{"http/1.1"} } } - - if !enableHTTP2 { + if !cfg.enableHTTP2 { webhookTlsOpts = append(webhookTlsOpts, disableHTTP2("webhook")) metricsTlsOpts = append(webhookTlsOpts, disableHTTP2("metrics")) } - - webhookHost, webhhookPort, err := net.SplitHostPort(webhooksvc) + webhookHost, webhookPort, err := net.SplitHostPort(cfg.webhookBindAddr) if err != nil { setupLog.Error(err, "invalid webhook bind address") os.Exit(1) } - webhooksvcport, err := strconv.Atoi(webhhookPort) + webhooksvcport, err := strconv.Atoi(webhookPort) if err != nil { setupLog.Error(err, "invalid webhook service port") os.Exit(1) } webhookServer := webhookruntime.NewServer(webhookruntime.Options{ - TLSOpts: webhookTlsOpts, Port: webhooksvcport, Host: webhookHost, + TLSOpts: webhookTlsOpts, + Port: webhooksvcport, + Host: webhookHost, + CertDir: cfg.webhookCertDir, }) + metricsServerOptions := metricsserver.Options{ + BindAddress: cfg.metricsBindAddr, + SecureServing: cfg.secureMetrics, + // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are + // not provided, self-signed certificates will be generated by default. This option is not recommended for + // production environments as self-signed certificates do not offer the same level of trust and security + // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing + // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName + // to provide certificates, ensuring the server communicates using trusted and secure certificates. + TLSOpts: metricsTlsOpts, + } + if cfg.secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + mgrOptions := ctrl.Options{ + Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, + HealthProbeBindAddress: cfg.healthBindAddr, + LeaderElection: cfg.enableLeaderElection, + LeaderElectionID: cfg.leaderElectionID, + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + LeaderElectionReleaseOnCancel: cfg.leaderElectionReleaseOnCancel, + LeaseDuration: &cfg.leaseDuration, + RenewDeadline: &cfg.renewDeadline, + RetryPeriod: &cfg.retryPeriod, + } + return mgrOptions +} + +type mainFlags struct { + metricsAddr string + probeAddr string + webhooksvc string + enableLeaderElection bool + secureMetrics bool + enableHTTP2 bool +} + +func parseArgs() (mainFlags, zap.Options) { + var flags mainFlags + flag.StringVar(&flags.metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") + flag.StringVar(&flags.probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.StringVar(&flags.webhooksvc, "webhook-service-bind-address", ":9443", "The address the webhook service endpoint binds to.") + flag.BoolVar(&flags.enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&flags.secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.BoolVar(&flags.enableHTTP2, "enable-http2", true, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + + zapOpts := zap.Options{} + zapOpts.BindFlags(flag.CommandLine) + flag.Parse() + return flags, zapOpts +} + +func main() { + scheme := getScheme() + flags, opts := parseArgs() + + logger := zap.New(zap.UseFlagOptions(&opts)) + ctrl.SetLogger(logger) + klog.SetLogger(logger) operatorNamespace := os.Getenv("OPERATOR_NAMESPACE") if operatorNamespace == "" { @@ -141,7 +209,6 @@ func main() { } v := version.Get() - setupLog.Info("Starting the Kubernetes Agents Operator", "k8s-agents-operator", v.Operator, "running-namespace", operatorNamespace, @@ -151,74 +218,36 @@ func main() { "go-os", runtime.GOOS, ) - // TODO: Start determine usage restConfig := ctrl.GetConfigOrDie() - // builds the operator's configuration ad, err := autodetect.New(restConfig) if err != nil { setupLog.Error(err, "failed to setup auto-detect routine") os.Exit(1) } - cfg := config.New( config.WithLogger(ctrl.Log.WithName("config")), config.WithVersion(v), config.WithAutoDetect(ad), ) - // End determine usage - - // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. - // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server - // - https://book.kubebuilder.io/reference/metrics.html - metricsServerOptions := metricsserver.Options{ - BindAddress: metricsAddr, - SecureServing: secureMetrics, - // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are - // not provided, self-signed certificates will be generated by default. This option is not recommended for - // production environments as self-signed certificates do not offer the same level of trust and security - // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing - // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName - // to provide certificates, ensuring the server communicates using trusted and secure certificates. - TLSOpts: metricsTlsOpts, - } - - if secureMetrics { - // FilterProvider is used to protect the metrics endpoint with authn/authz. - // These configurations ensure that only authorized users and service accounts - // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization - metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization - } // see https://github.com/openshift/library-go/blob/4362aa519714a4b62b00ab8318197ba2bba51cb7/pkg/config/leaderelection/leaderelection.go#L104 leaseDuration := time.Second * 137 renewDeadline := time.Second * 107 retryPeriod := time.Second * 26 - - mgrOptions := ctrl.Options{ - Scheme: scheme, - Metrics: metricsServerOptions, - WebhookServer: webhookServer, - HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, - LeaderElectionID: "9f7554c3.newrelic.com", - // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily - // when the Manager ends. This requires the binary to immediately end when the - // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait - // LeaseDuration time first. - // - // In the default scaffold provided, the program ends immediately after - // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups - // after the manager stops then its usage might be unsafe. - LeaderElectionReleaseOnCancel: true, - LeaseDuration: &leaseDuration, - RenewDeadline: &renewDeadline, - RetryPeriod: &retryPeriod, - } + mgrOptions := getManagerOptions(scheme, managerConfig{ + metricsBindAddr: flags.metricsAddr, + secureMetrics: flags.secureMetrics, + webhookBindAddr: flags.webhooksvc, + healthBindAddr: flags.probeAddr, + enableLeaderElection: flags.enableLeaderElection, + leaderElectionID: "9f7554c3.newrelic.com", + leaderElectionReleaseOnCancel: true, + leaseDuration: leaseDuration, + renewDeadline: renewDeadline, + retryPeriod: retryPeriod, + enableHTTP2: flags.enableHTTP2, + }) watchNamespace, found := os.LookupEnv("WATCH_NAMESPACE") if found { @@ -226,7 +255,6 @@ func main() { } else { setupLog.Info("the env var WATCH_NAMESPACE isn't set, watching all namespaces") } - if watchNamespace != "" { nsDefaults := map[string]cache.Config{} for _, watchNamespaceEntry := range strings.Split(watchNamespace, ",") { @@ -303,19 +331,34 @@ func registerApiHealth(mgr manager.Manager) error { } func setupWebhooks(mgr manager.Manager, operatorNamespace string) error { - var err error - if err = newreliccomv1alpha2.SetupWebhookWithManager(mgr, operatorNamespace); err != nil { - return fmt.Errorf("unable to create v1alpha2 Instrumentation webhook: %w", err) + if err := setupInstrumentationWebhooks(mgr, operatorNamespace); err != nil { + return err } - if err = newreliccomv1beta1.SetupWebhookWithManager(mgr, operatorNamespace); err != nil { - return fmt.Errorf("unable to create v1beta1 Instrumentation webhook: %w", err) + return setupPodMutationWebhook(mgr, operatorNamespace, ctrl.Log.WithName("mutation-webhook")) +} + +func setupInstrumentationWebhooks(mgr manager.Manager, operatorNamespace string) error { + type wehhookSetup struct { + name string + setup func(mgr ctrl.Manager, operatorNamespace string) error + } + webhooks := []wehhookSetup{ + {name: "v1alpha2", setup: newreliccomv1alpha2.SetupWebhookWithManager}, + {name: "v1beta1", setup: newreliccomv1beta1.SetupWebhookWithManager}, + {name: "v1beta2", setup: newreliccomv1beta2.SetupWebhookWithManager}, + {name: "v1beta3", setup: newreliccomv1beta3.SetupWebhookWithManager}, } - if err = newreliccomv1beta2.SetupWebhookWithManager(mgr, operatorNamespace); err != nil { - return fmt.Errorf("unable to create v1beta2 Instrumentation webhook: %w", err) + for _, webhookEntry := range webhooks { + if err := webhookEntry.setup(mgr, operatorNamespace); err != nil { + return fmt.Errorf("unable to register %s.instrumentation webhook: %w", webhookEntry.name, err) + } } + return nil +} +func setupPodMutationWebhook(mgr manager.Manager, operatorNamespace string, logger logr.Logger) error { // Register the Pod mutation webhook - if err = webhook.SetupWebhookWithManager(mgr, operatorNamespace, ctrl.Log.WithName("mutation-webhook")); err != nil { + if err := webhook.SetupWebhookWithManager(mgr, operatorNamespace, logger); err != nil { return fmt.Errorf("unable to register pod mutation webhook: %w", err) } return nil diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 00000000..91c09caf --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,480 @@ +/* +Copyright 2024. + +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 main + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/go-logr/logr" + "io" + "os" + "path/filepath" + stdruntime "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/newrelic/k8s-agents-operator/api/current" + "github.com/newrelic/k8s-agents-operator/internal/apm" + "github.com/newrelic/k8s-agents-operator/internal/instrumentation" + "github.com/newrelic/k8s-agents-operator/internal/version" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + // +kubebuilder:scaffold:imports +) + +var ( + k8sClient client.Client + testEnv *envtest.Environment + //testScheme *runtime.Scheme = clientgoscheme.Scheme + ctx context.Context + cancel context.CancelFunc + err error + cfg *rest.Config +) + +var _ io.Writer = (*fakeWriter)(nil) + +type fakeWriter struct{} + +func (w *fakeWriter) Write(p []byte) (n int, err error) { + return len(p), nil +} + +func TestMain(m *testing.M) { + ctx, cancel = context.WithCancel(context.TODO()) + defer cancel() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + + ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "bin", "k8s", + fmt.Sprintf("1.34.1-%s-%s", stdruntime.GOOS, stdruntime.GOARCH)), + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "config", "webhook")}, + }, + } + cfg, err = testEnv.Start() + if err != nil { + fmt.Printf("failed to start testEnv: %v", err) + os.Exit(1) + } + + testScheme := getScheme() + + if false { + if err = admissionv1.AddToScheme(testScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + } + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + if err != nil { + fmt.Printf("failed to setup a Kubernetes client: %v", err) + os.Exit(1) + } + + whOptions := &testEnv.WebhookInstallOptions + whAddrPort := fmt.Sprintf("%s:%d", whOptions.LocalServingHost, whOptions.LocalServingPort) + mgrOpts := getManagerOptions(testScheme, managerConfig{ + webhookBindAddr: whAddrPort, + webhookCertDir: whOptions.LocalServingCertDir, + metricsBindAddr: "0", + }) + + // start webhook server using Manager + mgr, mgrErr := ctrl.NewManager(cfg, mgrOpts) + if mgrErr != nil { + fmt.Printf("failed to start webhook server: %v", mgrErr) + os.Exit(1) + } + + operatorNamespace := "newrelic" + if err = setupInstrumentationWebhooks(mgr, operatorNamespace); err != nil { + fmt.Printf("%s\n", err.Error()) + os.Exit(1) + } + if err = setupPodMutationWebhook(mgr, operatorNamespace, logr.Logger{}); err != nil { + fmt.Printf("%s\n", err.Error()) + os.Exit(1) + } + + go func() { + if err = mgr.Start(ctx); err != nil { + fmt.Printf("failed to start manager: %v", err) + os.Exit(1) + } + }() + + // wait for the webhook server to get ready + wg := &sync.WaitGroup{} + wg.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + if err = retry.OnError(wait.Backoff{ + Steps: 20, + Duration: 10 * time.Millisecond, + Factor: 1.5, + Jitter: 0.1, + Cap: time.Second * 30, + }, func(error) bool { + return true + }, func() error { + tlsCtx, tlsCtxCancel := context.WithTimeout(ctx, time.Second) + defer tlsCtxCancel() + conn, tlsErr := (&tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true}}).DialContext(tlsCtx, "tcp", whAddrPort) + if tlsErr != nil { + return tlsErr + } + _ = conn.Close() + return nil + }); err != nil { + fmt.Printf("failed to wait for webhook server to be ready: %v", err) + os.Exit(1) + } + }(wg) + wg.Wait() + + code := m.Run() + + cancel() + err = testEnv.Stop() + if err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + + os.Exit(code) +} + +func TestPodMutationHandler_Handle(t *testing.T) { + optionalTrue := true + + tests := []struct { + name string + initNamespaces []corev1.Namespace + initSecrets []corev1.Secret + initInstrumentations []current.Instrumentation + initPod corev1.Pod + expectedPod corev1.Pod + }{ + { + name: "basic", + initNamespaces: []corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "newrelic"}}, + }, + initSecrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{Name: instrumentation.DefaultLicenseKeySecretName, Namespace: "newrelic"}, + Data: map[string][]byte{apm.LicenseKey: []byte("fake-secret-abc123")}, + }, + }, + initInstrumentations: []current.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "instrumentation-python", Namespace: "newrelic"}, + Spec: current.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"inject": "python"}, + }, + Agent: current.Agent{ + Language: "python", + Image: "not-a-real-python-image", + }, + }, + }, + }, + initPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "alpine1", Namespace: "default", Labels: map[string]string{"inject": "python"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "alpine", + Image: "alpine:latest", + Command: []string{"sleep", "300"}, + }, + }, + }, + }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "alpine1", + Namespace: "default", + Labels: map[string]string{ + "inject": "python", + apm.DescK8sAgentOperatorVersionLabelName: version.Get().Operator, + }, + Generation: 1, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "nri-python--alpine", + Image: "not-a-real-python-image", + Command: []string{"cp", "-a", "/instrumentation/.", "/nri-python--alpine/"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "nri-python--alpine", MountPath: "/nri-python--alpine", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "alpine", + Image: "alpine:latest", + Command: []string{"sleep", "300"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "nri-python--alpine", MountPath: "/nri-python--alpine", + }, + }, + Env: []corev1.EnvVar{ + {Name: "PYTHONPATH", Value: "/nri-python--alpine"}, + {Name: "NEW_RELIC_APP_NAME", Value: "alpine1"}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{Key: "new_relic_license_key", Optional: &optionalTrue, LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}}}}, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "nri-python--alpine", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + }, + { + name: "test php", + initNamespaces: []corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "newrelic"}}, + }, + initSecrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{Name: instrumentation.DefaultLicenseKeySecretName, Namespace: "newrelic"}, + Data: map[string][]byte{apm.LicenseKey: []byte("fake-secret-abc123")}, + }, + }, + initInstrumentations: []current.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "instrumentation-php", Namespace: "newrelic"}, + Spec: current.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "inject": "php", + }, + }, + Agent: current.Agent{ + Language: "php-8.3", + Image: "not-a-real-php-image", + }, + }, + }, + }, + initPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "alpine2", Namespace: "default", Labels: map[string]string{"inject": "php"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "alpine", + Image: "alpine:latest", + Command: []string{"sleep", "300"}, + Env: []corev1.EnvVar{ + {Name: "a", Value: "a"}, + {Name: "b", Value: "b"}, + }, + }, + }, + }, + }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "alpine2", Namespace: "default", Labels: map[string]string{ + "inject": "php", + apm.DescK8sAgentOperatorVersionLabelName: version.Get().Operator}, + Generation: 1, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "nri-php--alpine", + Image: "not-a-real-php-image", + Command: []string{"/bin/sh"}, + Args: []string{"-c", strings.Join([]string{ + "cp -a /instrumentation/. /nri-php--alpine/", + "sed -i 's@/newrelic-instrumentation@/nri-php--alpine@g' /nri-php--alpine/php-agent/ini/newrelic.ini", + "sed -i 's@/newrelic-instrumentation@/nri-php--alpine@g' /nri-php--alpine/k8s-php-install.sh", + "sed -i 's@/newrelic-instrumentation@/nri-php--alpine@g' /nri-php--alpine/nr_env_to_ini.sh", + "/nri-php--alpine/k8s-php-install.sh 20230831", + "/nri-php--alpine/nr_env_to_ini.sh", + }, " && ")}, + Env: []corev1.EnvVar{ + {Name: "NEW_RELIC_APP_NAME", Value: "alpine2"}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{Key: "new_relic_license_key", Optional: &optionalTrue, LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}}}}, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "nri-php--alpine", MountPath: "/nri-php--alpine", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "alpine", + Image: "alpine:latest", + Command: []string{"sleep", "300"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "nri-php--alpine", MountPath: "/nri-php--alpine", + }, + }, + Env: []corev1.EnvVar{ + {Name: "a", Value: "a"}, + {Name: "b", Value: "b"}, + {Name: "PHP_INI_SCAN_DIR", Value: ":/nri-php--alpine/php-agent/ini"}, + {Name: "NEW_RELIC_APP_NAME", Value: "alpine2"}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "nri-php--alpine", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + }, + } + + opts := cmp.Options{ + cmpopts.IgnoreFields(corev1.PodSpec{}, + "DNSPolicy", + "EnableServiceLinks", + "PreemptionPolicy", + "Priority", + "RestartPolicy", + "SchedulerName", + "SecurityContext", + "TerminationGracePeriodSeconds", + "Tolerations", + ), + cmpopts.IgnoreFields(metav1.ObjectMeta{}, + "CreationTimestamp", + "ManagedFields", + "ResourceVersion", + "Annotations", + "UID", + ), + cmpopts.IgnoreFields(corev1.Container{}, + "ImagePullPolicy", + "TerminationMessagePath", + "TerminationMessagePolicy", + ), + cmpopts.IgnoreTypes(corev1.PodStatus{}), + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, ns := range tt.initNamespaces { + { + getNs := &corev1.Namespace{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: ns.Name}, getNs) + if err == nil { + continue + } + if !apierrors.IsNotFound(err) { + if err != nil { + t.Fatalf("failed to check for existing namespace: %v", err) + } + } + } + err = k8sClient.Create(context.Background(), &ns) + if err != nil { + t.Fatalf("failed to create namespace: %v", err) + } + } + for _, secret := range tt.initSecrets { + { + getNs := &corev1.Secret{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, getNs) + if err == nil { + continue + } + if !apierrors.IsNotFound(err) { + if err != nil { + t.Fatalf("failed to check for existing secret: %v", err) + } + } + } + err = k8sClient.Create(context.Background(), &secret) + if err != nil { + t.Fatalf("failed to create secret: %v", err) + } + } + for _, instrumentation := range tt.initInstrumentations { + err = k8sClient.Create(context.Background(), &instrumentation) + if err != nil { + t.Fatalf("failed to create instrumentation: %v", err) + } + } + + err = k8sClient.Create(context.Background(), &tt.initPod) + if err != nil { + t.Fatalf("failed to create pod: %v", err) + } + if len(tt.initPod.Spec.InitContainers) != 1 { + t.Errorf("expected 1 init container; got %d", len(tt.initPod.Spec.InitContainers)) + } + diff := cmp.Diff(tt.expectedPod, tt.initPod, opts...) + if diff != "" { + t.Errorf("init pod does not match expected pod (-want +got): %s", diff) + } + }) + } +} diff --git a/codecov.yml b/codecov.yml index 1f790f12..40105b26 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,14 @@ ignore: - "**/zz_generated.deepcopy.go" +coverage: + range: "50...100" + status: + changes: false + patch: false + project: + default: + if_ci_failed: success + enabled: no + target: auto + base: auto + threshold: 10% \ No newline at end of file diff --git a/config/crd/bases/newrelic.com_instrumentations.yaml b/config/crd/bases/newrelic.com_instrumentations.yaml index 36231538..2d80857b 100644 --- a/config/crd/bases/newrelic.com_instrumentations.yaml +++ b/config/crd/bases/newrelic.com_instrumentations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.3 + controller-gen.kubebuilder.io/version: v0.19.0 name: instrumentations.newrelic.com spec: group: newrelic.com @@ -59,8 +59,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -118,6 +119,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -186,7 +224,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -474,8 +512,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -533,6 +572,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -601,7 +677,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -687,8 +763,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -746,6 +823,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1055,8 +1169,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1114,6 +1229,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1190,7 +1342,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -1580,8 +1732,6 @@ spec: type: object namesFromPodAnnotation: type: string - namesFromPodLabel: - type: string required: - envSelector type: object @@ -1598,8 +1748,9 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- @@ -1657,6 +1808,43 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1730,7 +1918,7 @@ spec: Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. - This is an alpha field and requires enabling the + This field depends on the DynamicResourceAllocation feature gate. This field is immutable. It can only be set for containers. @@ -2115,6 +2303,1207 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.podsMatching + name: PodsMatching + type: integer + - jsonPath: .status.podsInjected + name: PodsInjected + type: integer + - jsonPath: .status.podsNotReady + name: PodsNotReady + type: integer + - jsonPath: .status.podsOutdated + name: PodsOutdated + type: integer + - jsonPath: .status.podsHealthy + name: PodsHealthy + type: integer + - jsonPath: .status.podsUnhealthy + name: PodsUnhealthy + type: integer + name: v1beta3 + schema: + openAPIV3Schema: + description: Instrumentation is the Schema for the instrumentations API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: InstrumentationSpec defines the desired state of Instrumentation + properties: + agent: + description: Agent defines configuration for agent instrumentation. + properties: + env: + description: |- + Env defines Go specific env vars. There are four layers for env vars' definitions and + the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + If the former var had been defined, then the other vars would be ignored. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + language: + description: Language is the language that will be instrumented. + type: string + resourceRequirements: + description: Resources describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + volumeLimitSize: + anyOf: + - type: integer + - type: string + description: |- + VolumeSizeLimit defines size limit for volume used for auto-instrumentation. + The default size is 200Mi. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + agentConfigMap: + description: |- + AgentConfigMap defines where to take the agent configuration from. + it should be present in the operator namespace. + type: string + containerSelector: + description: ContainerSelector defines to which pods the config should + be applied. + properties: + envSelector: + properties: + matchEnvs: + additionalProperties: + type: string + description: |- + matchEnvs is a map of {key,value} pairs. A single {key,value} in the matchEnvs + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the + requirement applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + imageSelector: + properties: + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the + requirement applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchImages: + additionalProperties: + type: string + description: |- + matchImages is a map of {key,value} pairs. A single {key,value} in the matchImages + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + nameSelector: + properties: + matchExpressions: + description: matchExpressions is a list of env selector requirements. + The requirements are ANDed. + items: + properties: + key: + description: key is the field selector key that the + requirement applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. + The list of operators may grow in the future. + type: string + values: + description: |- + values is an array of string values. + If the operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values array must be empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchNames: + additionalProperties: + type: string + description: |- + matchNames is a map of {key,value} pairs. A single {key,value} in the matchEnvs + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + namesFromPodAnnotation: + type: string + required: + - envSelector + type: object + healthAgent: + description: HealthAgent defines configuration for healthAgent instrumentation. + properties: + env: + description: |- + Env defines Go specific env vars. There are four layers for env vars' definitions and + the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + If the former var had been defined, then the other vars would be ignored. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + imagePullPolicy: + description: |- + Image pull policy. + One of Always, Never, IfNotPresent. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images + type: string + resourceRequirements: + description: Resources describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext defines the security options the container should be run with. + If set, the fields of SecurityContext override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + type: object + licenseKeySecret: + description: |- + LicenseKeySecret defines where to take the licenseKeySecret from. + it should be present in the operator namespace. + type: string + namespaceLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + status: + description: InstrumentationStatus defines the observed state of Instrumentation + properties: + entityGUIDs: + items: + type: string + type: array + lastUpdated: + format: date-time + type: string + observedVersion: + type: string + podsHealthy: + format: int64 + type: integer + podsInjected: + format: int64 + type: integer + podsMatching: + format: int64 + type: integer + podsNotReady: + format: int64 + type: integer + podsOutdated: + format: int64 + type: integer + podsUnhealthy: + format: int64 + type: integer + unhealthyPodsErrors: + items: + properties: + lastError: + type: string + pod: + type: string + type: object + type: array + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 020b2960..c961300c 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -10,14 +10,14 @@ webhooks: service: name: webhook-service namespace: system - path: /mutate-newrelic-com-v1beta2-instrumentation + path: /mutate-newrelic-com-v1beta3-instrumentation failurePolicy: Fail - name: minstrumentation-v1beta2.kb.io + name: minstrumentation-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 operations: - CREATE - UPDATE @@ -64,6 +64,26 @@ webhooks: resources: - instrumentations sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-newrelic-com-v1beta2-instrumentation + failurePolicy: Fail + name: minstrumentation-v1beta2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -95,14 +115,14 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-newrelic-com-v1beta2-instrumentation + path: /validate-newrelic-com-v1beta3-instrumentation failurePolicy: Fail - name: vinstrumentationcreateupdate-v1beta2.kb.io + name: vinstrumentationcreateupdate-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 operations: - CREATE - UPDATE @@ -115,14 +135,14 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-newrelic-com-v1beta2-instrumentation + path: /validate-newrelic-com-v1beta3-instrumentation failurePolicy: Ignore - name: vinstrumentationdelete-v1beta2.kb.io + name: vinstrumentationdelete-v1beta3.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1beta2 + - v1beta3 operations: - DELETE resources: @@ -206,3 +226,42 @@ webhooks: resources: - instrumentations sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-newrelic-com-v1beta2-instrumentation + failurePolicy: Fail + name: vinstrumentationcreateupdate-v1beta2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-newrelic-com-v1beta2-instrumentation + failurePolicy: Ignore + name: vinstrumentationdelete-v1beta2.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1beta2 + operations: + - DELETE + resources: + - instrumentations + sideEffects: None diff --git a/instrumentation.md b/instrumentation.md index c8444929..82319fa0 100644 --- a/instrumentation.md +++ b/instrumentation.md @@ -1,4 +1,4 @@ -# Instrumentation (v1beta2) +# Instrumentation (v1beta3) The instrumentation is the specification to match specific pods and containers, instrumenting them with the newrelic agent by mutating the pod via admission webhooks. Typically, it means adding an init container to the pod (before the diff --git a/internal/instrumentation/health.go b/internal/instrumentation/health.go index 39a73361..92d02e7c 100644 --- a/internal/instrumentation/health.go +++ b/internal/instrumentation/health.go @@ -3,10 +3,12 @@ package instrumentation import ( "context" "encoding/json" + "errors" "fmt" "reflect" "sort" "strconv" + "strings" "sync" "sync/atomic" "time" @@ -88,6 +90,7 @@ type instrumentationMetric struct { podsHealthy int64 podsUnhealthy int64 unhealthyPods []current.UnhealthyPodError + entityGUIDs []string } // resolve marks the instrumentation metric done. anything waiting via `wait` will continue @@ -111,28 +114,62 @@ func (im *instrumentationMetric) wait(ctx context.Context) error { } } +var ( + errPodsInjectedIsDiff = errors.New("podsInjected is diff") + errPodsOutdatedIsDiff = errors.New("podsOutdated is diff") + errPodsMatchingIsDiff = errors.New("podsMatching is diff") + errPodsHealthyIsDiff = errors.New("podsHealthy is diff") + errPodsUnhealthyIsDiff = errors.New("podsUnhealthy is diff") + errObservedVersionIsDiff = errors.New("observedVersion is diff") + errEntityGUIDIsDiff = errors.New("entityGUIDs is diff") + errUnhealthyPodErrorsIsDiff = errors.New("unhealthyPodErrors is diff") +) + // isDiff to check for differences. used to know if we need to write any data -func (im *instrumentationMetric) isDiff() bool { +func (im *instrumentationMetric) isDiff() error { if im.instrumentation.Status.PodsInjected != im.podsInjected { - return true + return errPodsInjectedIsDiff } if im.instrumentation.Status.PodsOutdated != im.podsOutdated { - return true + return errPodsOutdatedIsDiff } if im.instrumentation.Status.PodsMatching != im.podsMatching { - return true + return errPodsMatchingIsDiff } if im.instrumentation.Status.PodsHealthy != im.podsHealthy { - return true + return errPodsHealthyIsDiff } if im.instrumentation.Status.PodsUnhealthy != im.podsUnhealthy { - return true + return errPodsUnhealthyIsDiff } if im.instrumentation.Status.ObservedVersion != im.instrumentation.ResourceVersion { - return true + return errObservedVersionIsDiff } sort.Slice(im.unhealthyPods, func(i, j int) bool { return im.unhealthyPods[i].Pod < im.unhealthyPods[j].Pod }) - return !reflect.DeepEqual(im.unhealthyPods, im.instrumentation.Status.UnhealthyPodsErrors) + im.entityGUIDs = uniqueSlices(im.entityGUIDs) + sort.Slice(im.entityGUIDs, func(i, j int) bool { return strings.Compare(im.entityGUIDs[i], im.entityGUIDs[j]) < 0 }) + if !reflect.DeepEqual(im.entityGUIDs, im.instrumentation.Status.EntityGUIDs) { + return errEntityGUIDIsDiff + } + if !reflect.DeepEqual(im.unhealthyPods, im.instrumentation.Status.UnhealthyPodsErrors) { + return errUnhealthyPodErrorsIsDiff + } + return nil +} + +func uniqueSlices(in []string) []string { + if len(in) == 0 { + return nil + } + out := make([]string, 0, len(in)) + u := map[string]struct{}{} + for _, item := range in { + if _, ok := u[item]; !ok { + u[item] = struct{}{} + out = append(out, item) + } + } + return out } // syncStatus to copy the data from the instrumentation metric to the instrumentation @@ -146,6 +183,7 @@ func (im *instrumentationMetric) syncStatus() { im.instrumentation.Status.PodsUnhealthy = im.podsUnhealthy im.instrumentation.Status.UnhealthyPodsErrors = im.unhealthyPods im.instrumentation.Status.ObservedVersion = im.instrumentation.ResourceVersion + im.instrumentation.Status.EntityGUIDs = im.entityGUIDs } // podMetric contains the pod, it's id (used for logging), health (empty by default), and doneCh - which is closed once health has been retrieved @@ -402,6 +440,10 @@ func (m *HealthMonitor) instrumentationMetricQueueEvent(ctx context.Context, eve continue } + if eventPodMetrics.health.EntityGUID != "" { + event.entityGUIDs = append(event.entityGUIDs, eventPodMetrics.health.EntityGUID) + } + if eventPodMetrics.health.Healthy { event.podsHealthy++ } else { @@ -430,7 +472,7 @@ func (m *HealthMonitor) instrumentationMetricPersistQueueEvent(ctx context.Conte "injected": event.podsInjected, }, ) - if event.isDiff() { + if err := event.isDiff(); err != nil { event.syncStatus() event.instrumentation.Status.LastUpdated = metav1.Now() if err := m.instrumentationStatusUpdater.UpdateInstrumentationStatus(ctx, event.instrumentation); err != nil { diff --git a/internal/instrumentation/health_check.go b/internal/instrumentation/health_check.go index 682871ad..fb855d69 100644 --- a/internal/instrumentation/health_check.go +++ b/internal/instrumentation/health_check.go @@ -11,6 +11,7 @@ import ( // Health in the opamp format type Health struct { + EntityGUID string `json:"entity_guid" yaml:"entity_guid"` Healthy bool `json:"healthy" yaml:"healthy"` Status string `json:"status" yaml:"status"` StartTime int64 `json:"start_time_unix_nano" yaml:"start_time_unix_nano"` diff --git a/internal/instrumentation/health_check_test.go b/internal/instrumentation/health_check_test.go index 4df24ca1..65e514f5 100644 --- a/internal/instrumentation/health_check_test.go +++ b/internal/instrumentation/health_check_test.go @@ -28,3 +28,24 @@ last_error: "some error" t.Fatal(diff) } } + +func TestHealthCheckApi_GetHealthWithEntityGUID(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(` +healthy: true +status: "not ready" +status_time_unix_nano: 1734559668210947000 +start_time_unix_nano: 1734559614000000000 +last_error: "some error" +entity_guid: 1bad-f00d +`)) + })) + defer testServer.Close() + healthChecker := NewHealthCheckApi(nil) + health, _ := healthChecker.GetHealth(context.Background(), testServer.URL) + diff := cmp.Diff(health, Health{LastError: "some error", Status: "not ready", Healthy: true, StatusTime: 1734559668210947000, StartTime: 1734559614000000000, EntityGUID: "1bad-f00d"}) + if diff != "" { + t.Fatal(diff) + } +} diff --git a/internal/instrumentation/health_test.go b/internal/instrumentation/health_test.go index 94272579..067c86b1 100644 --- a/internal/instrumentation/health_test.go +++ b/internal/instrumentation/health_test.go @@ -2,6 +2,7 @@ package instrumentation import ( "context" + "errors" "fmt" "testing" "time" @@ -369,6 +370,60 @@ func TestHealthMonitor(t *testing.T) { UnhealthyPodsErrors: []current.UnhealthyPodError{{Pod: "default/pod0", LastError: "failed while retrieving health > fake health check error, url: \"http://127.0.0.1:5678/healthz\""}}, }, }, + { + name: "matching, injected and ready, has the health sidecar with only 1 exposed port", + fnHealthCheck: fakeHealthCheck(func(ctx context.Context, url string) (health Health, err error) { + logger := log.FromContext(ctx) + logger.Info("fake health check") + return Health{EntityGUID: "1bad-f00d", Healthy: true}, nil + }), + fnInstrumentationStatusUpdater: fakeUpdateInstrumentationStatus(func(ctx context.Context, instrumentation *current.Instrumentation) error { + logger := log.FromContext(ctx) + logger.Info("fake instrumentation status updater") + return nil + }), + namespaces: map[string]*corev1.Namespace{ + "default": {ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + "newrelic": {ObjectMeta: metav1.ObjectMeta{Name: "newrelic"}}, + }, + pods: map[string]*corev1.Pod{ + "default/pod0": { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod0", Namespace: "default", Annotations: map[string]string{ + "newrelic.com/apm-health": "true", + instrumentationVersionAnnotation: `{"newrelic/instrumentation0":"01234567-89ab-cdef-0123-456789abcdef/55"}`, + }, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + RestartPolicy: &containerRestartPolicyAlways, + Name: healthSidecarContainerName, + Ports: []corev1.ContainerPort{ + {ContainerPort: 5678}, + }, + }, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + PodIP: "127.0.0.1", + }, + }, + }, + instrumentations: map[string]*current.Instrumentation{ + "newrelic/instrumentation0": { + ObjectMeta: metav1.ObjectMeta{Name: "instrumentation0", Namespace: "newrelic", UID: "01234567-89ab-cdef-0123-456789abcdef", Generation: 55}, + Spec: current.InstrumentationSpec{HealthAgent: current.HealthAgent{Image: "health"}}, + }, + }, + expectedInstrumentationStatus: current.InstrumentationStatus{ + PodsMatching: 1, + PodsInjected: 1, + PodsHealthy: 1, + EntityGUIDs: []string{"1bad-f00d"}, + }, + }, } for _, test := range tests { @@ -412,3 +467,95 @@ func TestHealthMonitor(t *testing.T) { }) } } + +func TestIsDiff(t *testing.T) { + tests := []struct { + name string + metric instrumentationMetric + expected error + }{ + { + name: "no diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{}}, + }, + }, + { + name: "pods injected is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{PodsInjected: 6}}, + podsInjected: 5, + }, + expected: errPodsInjectedIsDiff, + }, + { + name: "pods outdated is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{PodsOutdated: 5}}, + podsOutdated: 4, + }, + expected: errPodsOutdatedIsDiff, + }, + { + name: "pods matching is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{PodsMatching: 4}}, + podsMatching: 3, + }, + expected: errPodsMatchingIsDiff, + }, + { + name: "pods healthy is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{PodsHealthy: 3}}, + podsHealthy: 2, + }, + expected: errPodsHealthyIsDiff, + }, + { + name: "pods unhealthy is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{PodsUnhealthy: 2}}, + podsUnhealthy: 1, + }, + expected: errPodsUnhealthyIsDiff, + }, + { + name: "observed version is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "abc"}, Spec: current.InstrumentationSpec{}, Status: current.InstrumentationStatus{}}, + }, + expected: errObservedVersionIsDiff, + }, + { + name: "entity ids is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{EntityGUIDs: []string{"1bad-f00d"}}}, + entityGUIDs: []string{"6ood-f00d"}, + }, + expected: errEntityGUIDIsDiff, + }, + { + name: "unhealthy pod errors is diff", + metric: instrumentationMetric{ + instrumentation: ¤t.Instrumentation{Status: current.InstrumentationStatus{UnhealthyPodsErrors: []current.UnhealthyPodError{{Pod: "b"}}}}, + unhealthyPods: []current.UnhealthyPodError{{Pod: "a"}}, + }, + expected: errUnhealthyPodErrorsIsDiff, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := tc.metric.isDiff() + if tc.expected != nil && actual != nil && !errors.Is(tc.expected, actual) { + t.Errorf("expected %v, got %v", tc.expected, actual) + } + if tc.expected == nil && actual != nil { + t.Errorf("expected nil, got %v", actual) + } + if tc.expected != nil && actual == nil { + t.Errorf("expected %v, got nil", tc.expected) + } + }) + } +} diff --git a/internal/webhook/podmutationhandler_suite_test.go b/internal/webhook/podmutationhandler_suite_test.go index 5cfeb9c7..b3ac4272 100644 --- a/internal/webhook/podmutationhandler_suite_test.go +++ b/internal/webhook/podmutationhandler_suite_test.go @@ -50,6 +50,8 @@ import ( "github.com/newrelic/k8s-agents-operator/api/current" "github.com/newrelic/k8s-agents-operator/api/v1alpha2" "github.com/newrelic/k8s-agents-operator/api/v1beta1" + "github.com/newrelic/k8s-agents-operator/api/v1beta2" + "github.com/newrelic/k8s-agents-operator/internal/apm" "github.com/newrelic/k8s-agents-operator/internal/instrumentation" "github.com/newrelic/k8s-agents-operator/internal/version" @@ -90,7 +92,7 @@ func TestMain(m *testing.M) { // Note that you must have the required binaries setup under the bin directory to perform // the tests directly. When we run make test it will be setup and used automatically. BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", - fmt.Sprintf("1.29.0-%s-%s", stdruntime.GOOS, stdruntime.GOARCH)), + fmt.Sprintf("1.34.1-%s-%s", stdruntime.GOOS, stdruntime.GOARCH)), WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "config", "webhook")}, @@ -112,6 +114,11 @@ func TestMain(m *testing.M) { os.Exit(1) } + if err = v1beta2.AddToScheme(testScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + if err = current.AddToScheme(testScheme); err != nil { fmt.Printf("failed to register scheme: %v", err) os.Exit(1) @@ -178,6 +185,20 @@ func TestMain(m *testing.M) { os.Exit(1) } + v1beta2InstDefaulter := &v1beta2.InstrumentationDefaulter{} + v1beta2InstValidator := &v1beta2.InstrumentationValidator{ + OperatorNamespace: operatorNamespace, + } + err = ctrl.NewWebhookManagedBy(mgr). + For(&v1beta2.Instrumentation{}). + WithValidator(v1beta2InstValidator). + WithDefaulter(v1beta2InstDefaulter). + Complete() + if err != nil { + fmt.Printf("failed to register v1beta2.instrumentation webhook: %v", err) + os.Exit(1) + } + currentInstDefaulter := ¤t.InstrumentationDefaulter{} currentInstValidator := ¤t.InstrumentationValidator{ OperatorNamespace: operatorNamespace, diff --git a/tests/e2e/v1alpha2/e2e-tests.sh b/tests/e2e/v1alpha2/e2e-tests.sh index 51877b45..e0b28af5 100755 --- a/tests/e2e/v1alpha2/e2e-tests.sh +++ b/tests/e2e/v1alpha2/e2e-tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail # Test cluster CLUSTER_NAME="" @@ -97,7 +98,7 @@ function create_cluster() { helm upgrade --install k8s-agents-operator ${REPO_ROOT}/charts/k8s-agents-operator \ --namespace k8s-agents-operator \ --create-namespace \ - --values ${SCRIPT_PATH}/e2e-values.yml \ + --set controllerManager.manager.image.version=e2e,controllerManager.manager.image.pullPolicy=Never,controllerManager.manager.image.repository=e2e/k8s-agents-operator \ --set licenseKey=${LICENSE_KEY} echo "🔄 Waiting for operator to settle" @@ -118,7 +119,7 @@ function create_cluster() { echo "🔄 Waiting for apps to settle" for label in $(find ${SCRIPT_PATH}/apps -type f -name '*.yaml' -exec yq '. | select(.kind == "Deployment") | .metadata.name' {} \;); do - kubectl wait --timeout=120s --for=jsonpath='{.status.phase}'=Running --namespace e2e-namespace -l="app=$label" pod + kubectl wait --timeout=600s --for=jsonpath='{.status.phase}'=Running --namespace e2e-namespace -l="app=$label" pod done } diff --git a/tests/e2e/v1alpha2/e2e-values.yml b/tests/e2e/v1alpha2/e2e-values.yml deleted file mode 100644 index 5dd76f3a..00000000 --- a/tests/e2e/v1alpha2/e2e-values.yml +++ /dev/null @@ -1,6 +0,0 @@ -controllerManager: - manager: - image: - repository: e2e/k8s-agents-operator - version: e2e - pullPolicy: Never diff --git a/tests/e2e/v1beta1/e2e-tests.sh b/tests/e2e/v1beta1/e2e-tests.sh index 908c1799..b5d46447 100755 --- a/tests/e2e/v1beta1/e2e-tests.sh +++ b/tests/e2e/v1beta1/e2e-tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail # Test cluster CLUSTER_NAME="" @@ -115,7 +116,7 @@ function create_cluster() { helm upgrade --install k8s-agents-operator ${REPO_ROOT}/charts/k8s-agents-operator \ --namespace $operator_ns \ --create-namespace \ - --values ${SCRIPT_PATH}/e2e-values.yml \ + --set controllerManager.manager.image.version=e2e,controllerManager.manager.image.pullPolicy=Never,controllerManager.manager.image.repository=e2e/k8s-agents-operator \ --set licenseKey=${LICENSE_KEY} echo "🔄 Waiting for operator namespace" @@ -146,7 +147,7 @@ function create_cluster() { echo "🔄 Waiting for apps to settle" for label in $(find ${SCRIPT_PATH}/apps -type f -name '*.yaml' -exec yq '. | select(.kind == "Deployment") | .metadata.name' {} \;); do - kubectl wait --timeout=120s --for=jsonpath='{.status.phase}'=Running --namespace $app_ns -l="app=$label" pod + kubectl wait --timeout=600s --for=jsonpath='{.status.phase}'=Running --namespace $app_ns -l="app=$label" pod done } diff --git a/tests/e2e/v1beta1/e2e-values.yml b/tests/e2e/v1beta1/e2e-values.yml deleted file mode 100644 index 5dd76f3a..00000000 --- a/tests/e2e/v1beta1/e2e-values.yml +++ /dev/null @@ -1,6 +0,0 @@ -controllerManager: - manager: - image: - repository: e2e/k8s-agents-operator - version: e2e - pullPolicy: Never diff --git a/tests/e2e/v1beta2/e2e-tests.sh b/tests/e2e/v1beta2/e2e-tests.sh index b8274559..e4d89aaa 100755 --- a/tests/e2e/v1beta2/e2e-tests.sh +++ b/tests/e2e/v1beta2/e2e-tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail # Test cluster CLUSTER_NAME="" @@ -118,7 +119,7 @@ function create_cluster() { helm upgrade --install k8s-agents-operator ${REPO_ROOT}/charts/k8s-agents-operator \ --namespace k8s-agents-operator \ --create-namespace \ - --values ${SCRIPT_PATH}/e2e-values.yml \ + --set controllerManager.manager.image.version=e2e,controllerManager.manager.image.pullPolicy=Never,controllerManager.manager.image.repository=e2e/k8s-agents-operator \ --set licenseKey=${LICENSE_KEY} echo "🔄 Waiting for operator to settle" @@ -139,7 +140,7 @@ function create_cluster() { echo "🔄 Waiting for apps to settle" for label in $(find ${SCRIPT_PATH}/apps -type f -name '*.yaml' -exec yq '. | select(.kind == "Deployment") | .metadata.name' {} \;); do - kubectl wait --timeout=120s --for=jsonpath='{.status.phase}'=Running --namespace e2e-namespace -l="app=$label" pod + kubectl wait --timeout=600s --for=jsonpath='{.status.phase}'=Running --namespace e2e-namespace -l="app=$label" pod done } diff --git a/tests/e2e/v1beta2/e2e-values.yml b/tests/e2e/v1beta2/e2e-values.yml deleted file mode 100644 index 5dd76f3a..00000000 --- a/tests/e2e/v1beta2/e2e-values.yml +++ /dev/null @@ -1,6 +0,0 @@ -controllerManager: - manager: - image: - repository: e2e/k8s-agents-operator - version: e2e - pullPolicy: Never