From a2e8321d50ef54b6000f91c18717918b5ec1ac6f Mon Sep 17 00:00:00 2001 From: Daniel Stokes Date: Wed, 9 Oct 2024 08:48:36 -0500 Subject: [PATCH 1/6] feat: selector based apm injector --- .dockerignore | 6 + .github/.golangci.yml | 3 +- .github/CODEOWNERS | 8 +- .github/ct.yaml | 10 + .github/workflows/ci.yml | 30 +- .gitignore | 2 + Dockerfile | 3 +- Makefile | 198 +- Tiltfile | 61 + boilerplate.txt | 15 + charts/k8s-agents-operator/Chart.yaml | 4 +- charts/k8s-agents-operator/README.md | 245 ++- charts/k8s-agents-operator/README.md.gotmpl | 231 ++- .../k8s-agents-operator/templates/NOTES.txt | 2 +- .../templates/_helpers.tpl | 41 + .../templates/certmanager.yaml | 17 +- .../templates/deployment.yaml | 20 +- .../templates/instrumentation-crd.yaml | 1103 ++-------- .../templates/leader-election-rbac.yaml | 4 +- .../templates/manager-rbac.yaml | 14 +- .../mutating-webhook-configuration.yaml | 49 - .../templates/newrelic_license_secret.yaml | 2 +- .../templates/proxy-rbac.yaml | 3 +- .../templates/reader-rbac.yaml | 2 +- .../templates/selfsigned-issuer.yaml | 8 - .../templates/service.yaml | 4 +- .../validating-webhook-configuration.yaml | 48 - .../templates/webhook-configuration.yaml | 134 ++ .../templates/webhook-service.yaml | 5 +- .../tests/cert_manager_test.yaml | 85 + .../tests/webhook_ssl_test.yaml | 176 ++ charts/k8s-agents-operator/values.yaml | 35 +- go.mod | 15 +- go.sum | 44 +- local/example-cr-matching-all.yml | 9 + local/example-cr.yml | 32 + ...pod-include-label-and-namespace-test2.yaml | 14 + local/pod-include-label.yaml | 13 + local/pod-include-namespace-test2.yaml | 12 + local/pod-not-matching.yaml | 11 + local/super-agent-tilt.yml | 1 + src/api/v1alpha1/instrumentation_webhook.go | 163 -- .../groupversion_info.go | 6 +- .../instrumentation_types.go | 139 +- src/api/v1alpha2/instrumentation_webhook.go | 113 ++ .../instrumentation_webhook_test.go | 30 +- src/api/{v1alpha1 => v1alpha2}/propagators.go | 2 +- src/api/{v1alpha1 => v1alpha2}/samplers.go | 2 +- .../upgrade_strategy.go | 2 +- .../webhook_suite_test.go | 4 +- .../zz_generated.deepcopy.go | 172 +- src/apm/dotnet.go | 128 +- src/apm/dotnet_test.go | 107 + src/apm/golang.go | 264 ++- src/apm/golang_test.go | 107 + src/apm/helper.go | 346 +++- src/apm/helper_test.go | 23 + src/apm/{javaagent.go => java.go} | 76 +- src/apm/java_test.go | 104 + src/apm/nodejs.go | 74 +- src/apm/nodejs_test.go | 104 + src/apm/php.go | 150 +- src/apm/php_test.go | 111 + src/apm/python.go | 73 +- src/apm/python_test.go | 104 + src/apm/ruby.go | 74 +- src/apm/ruby_test.go | 104 + src/constants/env.go | 35 - src/instrumentation/annotation.go | 81 - src/instrumentation/annotation_test.go | 128 -- .../instrumentation_defaulter.go | 23 + .../instrumentation_suite_test.go | 20 +- .../instrumentation_validator.go | 42 + src/instrumentation/podmutator.go | 317 ++- src/instrumentation/podmutator_test.go | 1782 ++++++++++------- src/instrumentation/sdk.go | 520 +---- src/instrumentation/sdk_test.go | 967 ++------- src/instrumentation/upgrade/upgrade.go | 73 +- .../upgrade/upgrade_suite_test.go | 4 +- src/instrumentation/upgrade/upgrade_test.go | 48 +- src/internal/config/main.go | 77 +- src/internal/config/options.go | 65 +- src/internal/version/main.go | 95 +- src/internal/version/main_test.go | 34 - src/internal/webhookhandler/webhookhandler.go | 11 +- .../webhookhandler_suite_test.go | 6 +- src/main.go | 102 +- tests/e2e/apps/dotnet_deployment.yaml | 3 +- tests/e2e/apps/java_deployment.yaml | 3 +- tests/e2e/apps/nodejs_deployment.yaml | 3 +- tests/e2e/apps/python_deployment.yaml | 3 +- tests/e2e/apps/ruby_deployment.yaml | 74 + tests/e2e/e2e-instrumentation-dotnet.yml | 16 + tests/e2e/e2e-instrumentation-java.yml | 16 + tests/e2e/e2e-instrumentation-nodejs.yml | 16 + tests/e2e/e2e-instrumentation-python.yml | 16 + tests/e2e/e2e-instrumentation-ruby.yml | 16 + tests/e2e/e2e-instrumentation.yml | 18 - tests/e2e/e2e-tests.sh | 31 +- tests/kustomize/certmanager/certificate.yaml | 1 + .../bases/newrelic.com_instrumentations.yaml | 953 ++------- tests/kustomize/default/kustomization.yaml | 20 +- .../default/mutatingwebhook_patch.yaml | 9 + ...atch.yaml => validatingwebhook_patch.yaml} | 7 - tests/kustomize/manager/manager.yaml | 1 + ...-agent-operator.clusterserviceversion.yaml | 4 +- tests/kustomize/rbac/service_account.yaml | 2 +- ...trumentation_v1alpha1_instrumentation.yaml | 15 - ...trumentation_v1alpha2_instrumentation.yaml | 68 + tests/kustomize/samples/kustomization.yaml | 2 +- tests/kustomize/webhook/manifests.yaml | 15 +- 111 files changed, 5396 insertions(+), 5647 deletions(-) create mode 100644 .github/ct.yaml create mode 100644 Tiltfile create mode 100644 boilerplate.txt delete mode 100644 charts/k8s-agents-operator/templates/mutating-webhook-configuration.yaml delete mode 100644 charts/k8s-agents-operator/templates/selfsigned-issuer.yaml delete mode 100644 charts/k8s-agents-operator/templates/validating-webhook-configuration.yaml create mode 100644 charts/k8s-agents-operator/templates/webhook-configuration.yaml create mode 100644 charts/k8s-agents-operator/tests/cert_manager_test.yaml create mode 100644 charts/k8s-agents-operator/tests/webhook_ssl_test.yaml create mode 100644 local/example-cr-matching-all.yml create mode 100644 local/example-cr.yml create mode 100644 local/pod-include-label-and-namespace-test2.yaml create mode 100644 local/pod-include-label.yaml create mode 100644 local/pod-include-namespace-test2.yaml create mode 100644 local/pod-not-matching.yaml create mode 100644 local/super-agent-tilt.yml delete mode 100644 src/api/v1alpha1/instrumentation_webhook.go rename src/api/{v1alpha1 => v1alpha2}/groupversion_info.go (90%) rename src/api/{v1alpha1 => v1alpha2}/instrumentation_types.go (56%) create mode 100644 src/api/v1alpha2/instrumentation_webhook.go rename src/api/{v1alpha1 => v1alpha2}/instrumentation_webhook_test.go (57%) rename src/api/{v1alpha1 => v1alpha2}/propagators.go (94%) rename src/api/{v1alpha1 => v1alpha2}/samplers.go (98%) rename src/api/{v1alpha1 => v1alpha2}/upgrade_strategy.go (98%) rename src/api/{v1alpha1 => v1alpha2}/webhook_suite_test.go (97%) rename src/api/{v1alpha1 => v1alpha2}/zz_generated.deepcopy.go (59%) create mode 100644 src/apm/dotnet_test.go create mode 100644 src/apm/golang_test.go create mode 100644 src/apm/helper_test.go rename src/apm/{javaagent.go => java.go} (53%) create mode 100644 src/apm/java_test.go create mode 100644 src/apm/nodejs_test.go create mode 100644 src/apm/php_test.go create mode 100644 src/apm/python_test.go create mode 100644 src/apm/ruby_test.go delete mode 100644 src/constants/env.go delete mode 100644 src/instrumentation/annotation.go delete mode 100644 src/instrumentation/annotation_test.go create mode 100644 src/instrumentation/instrumentation_defaulter.go create mode 100644 src/instrumentation/instrumentation_validator.go create mode 100644 tests/e2e/apps/ruby_deployment.yaml create mode 100644 tests/e2e/e2e-instrumentation-dotnet.yml create mode 100644 tests/e2e/e2e-instrumentation-java.yml create mode 100644 tests/e2e/e2e-instrumentation-nodejs.yml create mode 100644 tests/e2e/e2e-instrumentation-python.yml create mode 100644 tests/e2e/e2e-instrumentation-ruby.yml delete mode 100644 tests/e2e/e2e-instrumentation.yml create mode 100644 tests/kustomize/default/mutatingwebhook_patch.yaml rename tests/kustomize/default/{webhookcainjection_patch.yaml => validatingwebhook_patch.yaml} (62%) delete mode 100644 tests/kustomize/samples/instrumentation_v1alpha1_instrumentation.yaml create mode 100644 tests/kustomize/samples/instrumentation_v1alpha2_instrumentation.yaml diff --git a/.dockerignore b/.dockerignore index 0f046820..e44a35c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,10 @@ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore build and test binaries. +.github/ bin/ +charts/ +config/ +local/ testbin/ +tests/ +tmp/ diff --git a/.github/.golangci.yml b/.github/.golangci.yml index cec5079d..eedaaabc 100644 --- a/.github/.golangci.yml +++ b/.github/.golangci.yml @@ -30,7 +30,8 @@ linters: - errcheck - errorlint - exhaustive - - exportloopref + #- exportloopref Since Go1.22 (loopvar) this linter is no longer relevant. + - copyloopvar - gocyclo - goprintffuncname - gosimple diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fc389c9a..0ca33cc0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,4 +7,10 @@ * @newrelic/k8s-agents # APM team owned directories -/src/apm/ @newrelic/dotnet @newrelic/go-agent @newrelic/java-agent @newrelic/node-js-agent @newrelic/php-agent @newrelic/python @newrelic/ruby-agent +/src/apm/dotnet*.go @newrelic/dotnet +/src/apm/golang*.go @newrelic/go +/src/apm/java*.go @newrelic/java +/src/apm/nodejs*.go @newrelic/node +/src/apm/php*.go @newrelic/php-agent +/src/apm/python*.go @newrelic/python +/src/apm/ruby*.go @newrelic/ruby diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 00000000..9045650e --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,10 @@ +# Chart linter defaults to `master` branch, so we need to specify this as the default branch +# or `cl` will fail with a not-so-helpful error that says: +# "Error linting charts: Error identifying charts to process: Error running process: exit status 128" +target-branch: main + +chart-repos: + - newrelic=https://helm-charts.newrelic.com + +# Charts will be released manually. +check-version-increment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbbb2247..a7439e62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,17 +48,39 @@ jobs: steps: - name: Checkout GitHub Repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4.2.0 + with: + version: v3.14.4 + + - name: Set up helm-unittest + run: helm plugin install https://github.com/helm-unittest/helm-unittest + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.1 + + - name: Lint charts + run: ct --config .github/ct.yaml lint --debug - - name: Helm lint + - name: Run unit tests run: | - helm lint charts/** + for chart in $(ct list-changed --config .github/ct-lint.yaml); do + if [ -d "$chart/tests/" ]; then + helm unittest $chart + else + echo "No unit tests found for $chart" + fi + done - name: Install Helm Docs # Use syntax ${version} instead of $version # In certain contexts, only the less ambiguous ${version} form works # Source: https://tldp.org/LDP/abs/html/parameter-substitution.html run: | - version="v1.13.1" + version="v1.14.2" stripped=$( echo "${version}" | sed s'/v//' ) wget https://github.com/norwoodj/helm-docs/releases/download/${version}/helm-docs_${stripped}_Linux_x86_64.tar.gz tar --extract --verbose --file="helm-docs_${stripped}_Linux_x86_64.tar.gz" helm-docs @@ -106,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.30.0", "v1.29.5", "v1.28.3", "v1.27.5", "v1.26.8" ] + k8sVersion: [ "v1.31.1", "v1.30.5", "v1.29.9", "v1.28.14", "v1.27.16", "v1.26.15" ] steps: - name: Checkout GitHub Repository uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 diff --git a/.gitignore b/.gitignore index ec905b5e..fc0ea493 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ bin/ tmp/ + +config diff --git a/Dockerfile b/Dockerfile index 65287ece..9039468d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ RUN go mod download COPY ./src/ ./src/ COPY Makefile . -ARG TARGETOS TARGETARCH +ARG TARGETOS +ARG TARGETARCH ARG GOOS=$TARGETOS ARG GOARCH=$TARGETARCH diff --git a/Makefile b/Makefile index c8e208fd..36291500 100644 --- a/Makefile +++ b/Makefile @@ -3,27 +3,57 @@ GO_DIR = ./src BIN_DIR = ./bin TMP_DIR = $(shell pwd)/tmp +LICENSE_KEY ?= fake-abc123 +E2E_K8S_VERSION ?= v1.31.1 + +.DEFAULT_GOAL := help + # Go packages to test TEST_PACKAGES = ./src/internal/config \ - ./src/api/v1alpha1 \ + ./src/api/v1alpha2 \ ./src/autodetect \ ./src/instrumentation/ \ ./src/instrumentation/upgrade \ - ./src/internal/version + ./src/internal/version \ + ./src/apm # Kubebuilder variables -SETUP_ENVTEST = sigs.k8s.io/controller-runtime/tools/setup-envtest -ENVTEST_VERSION = release-0.18 -ENVTEST_BIN = $(TMP_DIR)/setup-envtest -ENVTEST_K8S_VERSION = 1.29.0 +SETUP_ENVTEST = $(TMP_DIR)/setup-envtest +SETUP_ENVTEST_VERSION ?= release-0.18 +SETUP_ENVTEST_K8S_VERSION ?= 1.29.0 + +## Tool Versions +KUSTOMIZE ?= $(LOCALBIN)/kustomize +KUSTOMIZE_VERSION ?= v5.4.3 +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +CONTROLLER_TOOLS_VERSION ?= v0.14.0 +HELMIFY ?= $(LOCALBIN)/helmify +HELMIFY_VERSION ?= v0.3.34 +GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.61.0 +HELM ?= $(LOCALBIN)/helm +HELM_VERSION ?= v3.16.1 +HELM_DOCS ?= $(LOCALBIN)/helm-docs +HELM_DOCS_VERSION ?= v1.14.2 +CT ?= $(LOCALBIN)/ct +CT_VERSION ?= v3.11.0 +HELM_UNITTEST ?= $(LOCALBIN)/helm-unittest +HELM_UNITTEST_VERSION ?= v0.6.2 + +CRD_OPTIONS ?= "crd:generateEmbeddedObjectMeta=true" + +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) # Temp location to install dependencies $(TMP_DIR): mkdir $(TMP_DIR) -# Install setup-envtest -$(ENVTEST_BIN): $(TMP_DIR) - GOBIN="$(realpath $(TMP_DIR))" go install $(SETUP_ENVTEST)@$(ENVTEST_VERSION) + +.PHONY: help +help: ## Show help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-17s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } END{printf "\n"}' $(MAKEFILE_LIST) .PHONY: all all: clean format modules test build @@ -32,29 +62,149 @@ all: clean format modules test build clean: rm -rf $(BIN_DIR) $(TMP_DIR) -.PHONY: format -format: - go fmt ./... - go vet ./... - .PHONY: modules -modules: +modules: ## Download go dependencies @# Add any missing modules and remove unused modules in go.mod and go.sum go mod tidy @# Verify dependencies have not been modified since being downloaded go mod verify -.PHONY: test -test: $(ENVTEST_BIN) - @chmod -R 755 $(TMP_DIR)/k8s - KUBEBUILDER_ASSETS="$(shell $(TMP_DIR)/setup-envtest use $(ENVTEST_K8S_VERSION) --bin-dir $(TMP_DIR) -p path)" \ - go test -cover -covermode=count -coverprofile=$(TMP_DIR)/cover.out $(TEST_PACKAGES) +##@ Testing -.PHONY: build -build: - CGO_ENABLED=0 go build -o $(BIN_DIR)/operator $(GO_DIR) +$(TMP_DIR)/cover.out: test .PHONY: coverprofile -coverprofile: +coverprofile: $(TMP_DIR)/cover.out ## Generate coverage report go tool cover -html=$(TMP_DIR)/cover.out go tool cover -func=$(TMP_DIR)/cover.out + +.PHONY: go-test +go-test: $(SETUP_ENVTEST) ## Run Go tests + @chmod -R 755 $(TMP_DIR)/k8s + KUBEBUILDER_ASSETS="$(shell $(TMP_DIR)/setup-envtest use $(SETUP_ENVTEST_K8S_VERSION) --bin-dir $(TMP_DIR) -p path)" \ + go test -v -cover -covermode=count -coverprofile=$(TMP_DIR)/cover.out $(TEST_PACKAGES) + +e2e-tests: + @for cmd in docker minikube helm kubectl yq; do \ + if ! command -v $$cmd > /dev/null; then \ + echo "$$cmd required" >&2; \ + exit 1; \ + fi; \ + done + cd tests/e2e && ./e2e-tests.sh --k8s_version $(E2E_K8S_VERSION) --license_key $(LICENSE_KEY) --run_tests + +.PHONY: run-helm-unittest +run-helm-unittest: $(CT) ## Run helm unit tests based on changes + @if ! test -f ./.github/ct-lint.yaml; then echo "missing .github/ct-lint.yaml" >&2; exit 1; fi + @for chart in $$($(CT) list-changed --config ./.github/ct-lint.yaml); do \ + if test -d "$$chart/tests/"; then \ + $(HELM_UNITTEST) $$chart; \ + else \ + echo "No unit tests found for $$chart"; \ + fi; \ + done; + +.PHONY: test +test: go-test # run-helm-unittest ## Run all tests + +##@ Linting + +.PHONY: go-lint +go-lint: golangci-lint ## Lint all go files + $(GOLANGCI_LINT) run --config=./.github/.golangci.yml + +.PHONY: lint +lint: go-lint run-helm-lint ## Lint everything + +.PHONY: run-helm-lint +run-helm-lint: ## Lint all the helm charts + @helm lint charts/** + +##@ Formatting + +.PHONY: format +format: go-format ## Format all files + +.PHONY: go-format +go-format: ## Format all go files + go fmt ./... + go vet ./... + +##@ Builds + +.PHONY: build +build: ## Build the go binary + CGO_ENABLED=0 go build -o $(BIN_DIR)/operator $(GO_DIR) + +.PHONY: dockerbuild +dockerbuild: ## Build the docker image + DOCKER_BUILDKIT=1 docker build -t k8s-agent-operator:latest \ + --platform=linux/amd64,linux/arm64,linux/arm \ + . + +##@ Tools + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(CONTROLLER_GEN) || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: ct +ct: $(CT) ## Download ct (Chart Testing) +$(CT): $(LOCALBIN) + test -s $(CT) || GOBIN=$(LOCALBIN) go install github.com/helm/chart-testing/v3/ct@$(CT_VERSION) + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint +$(GOLANGCI_LINT): $(LOCALBIN) + test -s $(GOLANGCI_LINT) || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + +.PHONY: helm +helm: $(HELM) ## Download helm +$(HELM): $(LOCALBIN) + test -s $(HELM) || GOBIN=$(LOCALBIN) go install helm.sh/helm/v3/cmd/helm@$(HELM_VERSION) + +.PHONY: helm-docs +helm-docs: $(HELM_DOCS) ## Download helm-docs +$(HELM_DOCS): $(LOCALBIN) + test -s $(HELM_DOCS) || GOBIN=$(LOCALBIN) go install github.com/norwoodj/helm-docs/cmd/helm-docs@$(HELM_DOCS_VERSION) + +.PHONY: helm-unittest +helm-unittest: $(HELM_UNITTEST) ## Download helm-unittest +$(HELM_UNITTEST): $(LOCALBIN) + test -s $(HELM_UNITTEST) || GOBIN=$(LOCALBIN) go install github.com/helm-unittest/helm-unittest/cmd/helm-unittest@$(HELM_UNITTEST_VERSION) + +.PHONY: helmify +helmify: $(HELMIFY) ## Download helmify +$(HELMIFY): $(LOCALBIN) + test -s $(HELMIFY) || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@$(HELMIFY_VERSION) + +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize +$(KUSTOMIZE): $(LOCALBIN) + test -s $(KUSTOMIZE) || GOBIN=$(LOCALBIN) go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) + +.PHONY: setup-envtest +setup-envtest: $(SETUP_ENVTEST) ## Download setup-envtest +$(SETUP_ENVTEST): $(TMP_DIR) + GOBIN="$(realpath $(TMP_DIR))" go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(SETUP_ENVTEST_VERSION) + +##@ Generate manifests e.g. CRD, RBAC etc. + +.PHONY: gen-helm-docs +gen-helm-docs: helm-docs ## Generate Helm Docs from templates + cd ./charts && $(HELM_DOCS) + +.PHONY: generate +generate: controller-gen ## Generate stuff + $(CONTROLLER_GEN) object:headerFile="boilerplate.txt" paths="./..." + +.PHONY: manifests +manifests: generate controller-gen + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=tests/kustomize/crd/bases + +.PHONY: run-helmify +run-helmify: manifests helmify kustomize ## Generate the CRD with kustomize and helmify from the manifests + @# could we do more here? + $(KUSTOMIZE) build tests/kustomize/default | $(HELMIFY) tmp/k8s-agents-operator + cp ./tmp/k8s-agents-operator/templates/instrumentation-crd.yaml ./charts/k8s-agents-operator/templates/instrumentation-crd.yaml diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 00000000..db3a1c73 --- /dev/null +++ b/Tiltfile @@ -0,0 +1,61 @@ +# -*- mode: Python -*- + +#### Config +# This env var is automatically added by the e2e action. +namespace = os.getenv('NAMESPACE','newrelic') +chart_values_file = 'local/super-agent-tilt.yml' + + +#### Build the final Docker image with the binary. +docker_build( + 'tilt.local/operator-dev', + context='.', + dockerfile='./Dockerfile' +) + + +#### Set-up charts +load('ext://helm_resource', 'helm_repo','helm_resource') +load('ext://git_resource', 'git_checkout') + +update_dependencies = True +chart = 'charts/k8s-agents-operator' +deps=[chart] + + +flags_helm = ['--create-namespace','--version=>=0.0.0-beta','--set=super-agent-deployment.image.imagePullPolicy=Always','--values=' + chart_values_file] + + +#### Installs charts +helm_repo( + 'jetstack', + 'https://charts.jetstack.io', + resource_name='jetstack-helm-repo', +) + +helm_resource( + 'cert-manager', + 'jetstack/cert-manager', + namespace='cert-manager', + release_name='cert-manager', + update_dependencies=False, + flags=['--create-namespace', '--set=crds.enabled=true'], + resource_deps=['jetstack-helm-repo'] +) + + +helm_resource( + 'operator', + chart, + deps=deps, # re-deploy chart if modified locally + namespace=namespace, + release_name='operator', + update_dependencies=False, + flags=flags_helm, + image_deps=['tilt.local/operator-dev'], + image_keys=[('controllerManager.manager.image.repository', 'controllerManager.manager.image.tag')], + resource_deps=['cert-manager'] +) + +update_settings(k8s_upsert_timeout_secs=150) + diff --git a/boilerplate.txt b/boilerplate.txt new file mode 100644 index 00000000..06a460ef --- /dev/null +++ b/boilerplate.txt @@ -0,0 +1,15 @@ +/* +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. +*/ diff --git a/charts/k8s-agents-operator/Chart.yaml b/charts/k8s-agents-operator/Chart.yaml index 4f93f582..8a16a703 100644 --- a/charts/k8s-agents-operator/Chart.yaml +++ b/charts/k8s-agents-operator/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: k8s-agents-operator description: A Helm chart for the Kubernetes Agents Operator type: application -version: '0.10.0' -appVersion: '0.10.0' +version: '0.12.0' +appVersion: '0.12.0' home: https://github.com/newrelic/k8s-agents-operator/blob/main/charts/k8s-agents-operator/README.md sources: - https://github.com/newrelic/k8s-agents-operator diff --git a/charts/k8s-agents-operator/README.md b/charts/k8s-agents-operator/README.md index d344832d..935d4f2c 100644 --- a/charts/k8s-agents-operator/README.md +++ b/charts/k8s-agents-operator/README.md @@ -1,6 +1,6 @@ # k8s-agents-operator -![Version: 0.10.0](https://img.shields.io/badge/Version-0.10.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.10.0](https://img.shields.io/badge/AppVersion-0.10.0-informational?style=flat-square) +![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.12.0](https://img.shields.io/badge/AppVersion-0.12.0-informational?style=flat-square) A Helm chart for the Kubernetes Agents Operator @@ -14,127 +14,200 @@ A Helm chart for the Kubernetes Agents Operator ### Requirements -Add the `jetstack` and `k8s-agents-operator` Helm chart repositories: +Add the `k8s-agents-operator` Helm chart repository: ```shell -helm repo add jetstack https://charts.jetstack.io helm repo add k8s-agents-operator https://newrelic.github.io/k8s-agents-operator ``` -Install the [`cert-manager`](https://github.com/cert-manager/cert-manager) Helm chart: -```shell -helm install cert-manager jetstack/cert-manager \ - --namespace cert-manager \ - --create-namespace \ - --set crds.enabled=true -``` - ### Instrumentation Install the [`k8s-agents-operator`](https://github.com/newrelic/k8s-agents-operator) Helm chart: ```shell helm upgrade --install k8s-agents-operator k8s-agents-operator/k8s-agents-operator \ - --namespace k8s-agents-operator \ + --namespace newrelic \ --create-namespace \ --values your-custom-values.yaml ``` ### Monitored namespaces -For each namespace you want the operator to be instrumented, create a secret containing a valid New Relic ingest license key: -```shell -kubectl create secret generic newrelic-key-secret \ - --namespace my-monitored-namespace \ - --from-literal=new_relic_license_key= -``` +For each namespace you want the operator to be instrumented, a secret will be replicated from the newrelic operator namespace. + +For each `Instrumentation` custom resource created, specifying which APM agent you want to instrument for each language. All available APM + agent docker images and corresponding tags are listed on DockerHub: -Similarly, for each namespace you need to instrument create the `Instrumentation` custom resource, specifying which APM agents you want to instrument. All available APM agent docker images and corresponding tags are listed on DockerHub: +* [.NET](https://hub.docker.com/repository/docker/newrelic/newrelic-dotnet-init/general) * [Java](https://hub.docker.com/repository/docker/newrelic/newrelic-java-init/general) * [Node](https://hub.docker.com/repository/docker/newrelic/newrelic-node-init/general) * [Python](https://hub.docker.com/repository/docker/newrelic/newrelic-python-init/general) -* [.NET](https://hub.docker.com/repository/docker/newrelic/newrelic-dotnet-init/general) * [Ruby](https://hub.docker.com/repository/docker/newrelic/newrelic-ruby-init/general) +For .NET + ```yaml -apiVersion: newrelic.com/v1alpha1 +apiVersion: newrelic.com/v1alpha2 kind: Instrumentation metadata: - labels: - app.kubernetes.io/name: instrumentation - app.kubernetes.io/created-by: k8s-agents-operator - name: newrelic-instrumentation + name: newrelic-instrumentation-dotnet spec: - java: + agent: + language: dotnet + image: newrelic/newrelic-dotnet-init:latest + # env: ... +``` + +For Java + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-java + namespace: newrelic +spec: + agent: + language: java image: newrelic/newrelic-java-init:latest - # env: - # Example New Relic agent supported environment variables - # - name: NEW_RELIC_LABELS - # value: "environment:auto-injection" - # Example overriding the appName configuration - # - name: NEW_RELIC_POD_NAME - # valueFrom: - # fieldRef: - # fieldPath: metadata.name - # - name: NEW_RELIC_APP_NAME - # value: "$(NEW_RELIC_LABELS)-$(NEW_RELIC_POD_NAME)" - nodejs: + # env: ... +``` + +For NodeJS + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-nodejs + namespace: newrelic +spec: + agent: + language: nodejs image: newrelic/newrelic-node-init:latest - python: + # env: ... +``` + +For Python + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-python + namespace: newrelic +spec: + agent: + language: python image: newrelic/newrelic-python-init:latest - dotnet: - image: newrelic/newrelic-dotnet-init:latest - ruby: + # env: ... +``` + +For Ruby + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-ruby + namespace: newrelic +spec: + agent: + language: ruby image: newrelic/newrelic-ruby-init:latest + # env: ... +``` + +For environment specific configurations + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + agent: + env: + # Example New Relic agent supported environment variables + - name: NEW_RELIC_LABELS + value: "environment:auto-injection" + # Example setting the pod name based on the metadata + - name: NEW_RELIC_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # Example overriding the appName configuration + - name: NEW_RELIC_APP_NAME + value: "$(NEW_RELIC_LABELS)-$(NEW_RELIC_POD_NAME)" ``` -In the example above, we show how you can configure the agent settings globally using environment variables. See each agent's configuration documentation for available configuration options: -* [Java](https://docs.newrelic.com/docs/apm/agents/java-agent/configuration/java-agent-configuration-config-file/) -* [Node](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/) -* [Python](https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/) -* [.NET](https://docs.newrelic.com/docs/apm/agents/net-agent/configuration/net-agent-configuration/) -* [Ruby](https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/) -Global agent settings can be overridden in your deployment manifest if a different configuration is required. +Targeting everything in a specific namespace with a label -### Annotations +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + #agent: ... + namespaceLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["java"] +``` -The `k8s-agents-operator` looks for language-specific annotations when your pods are being scheduled to know which applications you want to monitor. +Targeting a pod with a specific label -Below are the currently supported annotations: ```yaml -instrumentation.newrelic.com/inject-java: "true" -instrumentation.newrelic.com/inject-nodejs: "true" -instrumentation.newrelic.com/inject-python: "true" -instrumentation.newrelic.com/inject-dotnet: "true" -instrumentation.newrelic.com/inject-ruby: "true" +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + # agent: ... + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["dotnet"] ``` -Example deployment with annotation to instrument the Java agent: +Using a secret with a non-default name + ```yaml -apiVersion: apps/v1 -kind: Deployment +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation metadata: - name: spring-petclinic + name: newrelic-instrumentation-lang + namespace: newrelic spec: - selector: - matchLabels: - app: spring-petclinic - replicas: 1 - template: - metadata: - labels: - app: spring-petclinic - annotations: - instrumentation.newrelic.com/inject-java: "true" - spec: - containers: - - name: spring-petclinic - image: ghcr.io/pavolloffay/spring-petclinic:latest - ports: - - containerPort: 8080 - env: - - name: NEW_RELIC_APP_NAME - value: spring-petclinic-demo + # agent: ... + licenseKeySecret: the-name-of-the-custom-secret +``` + +In the example above, we show how you can configure the agent settings globally using environment variables. See each agent's configuration documentation for available configuration options: +* [Java](https://docs.newrelic.com/docs/apm/agents/java-agent/configuration/java-agent-configuration-config-file/) +* [Node](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/) +* [Python](https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/) +* [.NET](https://docs.newrelic.com/docs/apm/agents/net-agent/configuration/net-agent-configuration/) +* [Ruby](https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/) + +### cert-manager + +The K8s Agents Operator supports the use of [`cert-manager`](https://github.com/cert-manager/cert-manager) if preferred. + +Install the [`cert-manager`](https://github.com/cert-manager/cert-manager) Helm chart: +```shell +helm install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true ``` +In your `values.yaml` file, set `admissionWebhooks.autoGenerateCert.enabled: false` and `admissionWebhooks.certManager.enabled: true`. Then install the chart as normal. + ## Available Chart Releases To see the available charts: @@ -152,7 +225,14 @@ If you want to see a list of all available charts and releases, check [index.yam | Key | Type | Default | Description | |-----|------|---------|-------------| -| admissionWebhooks | object | `{"create":true}` | Admission webhooks make sure only requests with correctly formatted rules will get into the Operator | +| admissionWebhooks | object | `{"autoGenerateCert":{"certPeriodDays":365,"enabled":true,"recreate":true},"caFile":"","certFile":"","certManager":{"enabled":false},"create":true,"keyFile":""}` | Admission webhooks make sure only requests with correctly formatted rules will get into the Operator | +| admissionWebhooks.autoGenerateCert.certPeriodDays | int | `365` | Cert validity period time in days. | +| admissionWebhooks.autoGenerateCert.enabled | bool | `true` | If true and certManager.enabled is false, Helm will automatically create a self-signed cert and secret for you. | +| admissionWebhooks.autoGenerateCert.recreate | bool | `true` | If set to true, new webhook key/certificate is generated on helm upgrade. | +| admissionWebhooks.caFile | string | `""` | Path to the CA cert. | +| admissionWebhooks.certFile | string | `""` | Path to your own PEM-encoded certificate. | +| admissionWebhooks.certManager.enabled | bool | `false` | If true and autoGenerateCert.enabled is false, cert-manager will create a self-signed cert and secret for you. | +| admissionWebhooks.keyFile | string | `""` | Path to your own PEM-encoded private key. | | controllerManager.kubeRbacProxy.image.repository | string | `"gcr.io/kubebuilder/kube-rbac-proxy"` | | | controllerManager.kubeRbacProxy.image.tag | string | `"v0.14.0"` | | | controllerManager.kubeRbacProxy.resources.limits.cpu | string | `"500m"` | | @@ -168,6 +248,7 @@ If you want to see a list of all available charts and releases, check [index.yam | controllerManager.manager.serviceAccount.create | bool | `true` | | | controllerManager.replicas | int | `1` | | | kubernetesClusterDomain | string | `"cluster.local"` | | +| licenseKey | string | `""` | This set this license key to use. Can be configured also with `global.licenseKey` | | metricsService.ports[0].name | string | `"https"` | | | metricsService.ports[0].port | int | `8443` | | | metricsService.ports[0].protocol | string | `"TCP"` | | @@ -187,5 +268,3 @@ If you want to see a list of all available charts and releases, check [index.yam | csongnr | | | | dbudziwojskiNR | | | ----------------------------------------------- -Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1) diff --git a/charts/k8s-agents-operator/README.md.gotmpl b/charts/k8s-agents-operator/README.md.gotmpl index fadd3b97..be451d31 100644 --- a/charts/k8s-agents-operator/README.md.gotmpl +++ b/charts/k8s-agents-operator/README.md.gotmpl @@ -16,127 +16,200 @@ ### Requirements -Add the `jetstack` and `k8s-agents-operator` Helm chart repositories: +Add the `k8s-agents-operator` Helm chart repository: ```shell -helm repo add jetstack https://charts.jetstack.io helm repo add k8s-agents-operator https://newrelic.github.io/k8s-agents-operator ``` -Install the [`cert-manager`](https://github.com/cert-manager/cert-manager) Helm chart: -```shell -helm install cert-manager jetstack/cert-manager \ - --namespace cert-manager \ - --create-namespace \ - --set crds.enabled=true -``` - ### Instrumentation Install the [`k8s-agents-operator`](https://github.com/newrelic/k8s-agents-operator) Helm chart: ```shell helm upgrade --install k8s-agents-operator k8s-agents-operator/k8s-agents-operator \ - --namespace k8s-agents-operator \ + --namespace newrelic \ --create-namespace \ --values your-custom-values.yaml ``` ### Monitored namespaces -For each namespace you want the operator to be instrumented, create a secret containing a valid New Relic ingest license key: -```shell -kubectl create secret generic newrelic-key-secret \ - --namespace my-monitored-namespace \ - --from-literal=new_relic_license_key= -``` +For each namespace you want the operator to be instrumented, a secret will be replicated from the newrelic operator namespace. + +For each `Instrumentation` custom resource created, specifying which APM agent you want to instrument for each language. All available APM + agent docker images and corresponding tags are listed on DockerHub: -Similarly, for each namespace you need to instrument create the `Instrumentation` custom resource, specifying which APM agents you want to instrument. All available APM agent docker images and corresponding tags are listed on DockerHub: +* [.NET](https://hub.docker.com/repository/docker/newrelic/newrelic-dotnet-init/general) * [Java](https://hub.docker.com/repository/docker/newrelic/newrelic-java-init/general) * [Node](https://hub.docker.com/repository/docker/newrelic/newrelic-node-init/general) * [Python](https://hub.docker.com/repository/docker/newrelic/newrelic-python-init/general) -* [.NET](https://hub.docker.com/repository/docker/newrelic/newrelic-dotnet-init/general) * [Ruby](https://hub.docker.com/repository/docker/newrelic/newrelic-ruby-init/general) +For .NET + ```yaml -apiVersion: newrelic.com/v1alpha1 +apiVersion: newrelic.com/v1alpha2 kind: Instrumentation metadata: - labels: - app.kubernetes.io/name: instrumentation - app.kubernetes.io/created-by: k8s-agents-operator - name: newrelic-instrumentation + name: newrelic-instrumentation-dotnet spec: - java: + agent: + language: dotnet + image: newrelic/newrelic-dotnet-init:latest + # env: ... +``` + +For Java + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-java + namespace: newrelic +spec: + agent: + language: java image: newrelic/newrelic-java-init:latest - # env: - # Example New Relic agent supported environment variables - # - name: NEW_RELIC_LABELS - # value: "environment:auto-injection" - # Example overriding the appName configuration - # - name: NEW_RELIC_POD_NAME - # valueFrom: - # fieldRef: - # fieldPath: metadata.name - # - name: NEW_RELIC_APP_NAME - # value: "$(NEW_RELIC_LABELS)-$(NEW_RELIC_POD_NAME)" - nodejs: + # env: ... +``` + +For NodeJS + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-nodejs + namespace: newrelic +spec: + agent: + language: nodejs image: newrelic/newrelic-node-init:latest - python: + # env: ... +``` + +For Python + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-python + namespace: newrelic +spec: + agent: + language: python image: newrelic/newrelic-python-init:latest - dotnet: - image: newrelic/newrelic-dotnet-init:latest - ruby: + # env: ... +``` + +For Ruby + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-ruby + namespace: newrelic +spec: + agent: + language: ruby image: newrelic/newrelic-ruby-init:latest + # env: ... +``` + +For environment specific configurations + +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + agent: + env: + # Example New Relic agent supported environment variables + - name: NEW_RELIC_LABELS + value: "environment:auto-injection" + # Example setting the pod name based on the metadata + - name: NEW_RELIC_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # Example overriding the appName configuration + - name: NEW_RELIC_APP_NAME + value: "$(NEW_RELIC_LABELS)-$(NEW_RELIC_POD_NAME)" ``` -In the example above, we show how you can configure the agent settings globally using environment variables. See each agent's configuration documentation for available configuration options: -* [Java](https://docs.newrelic.com/docs/apm/agents/java-agent/configuration/java-agent-configuration-config-file/) -* [Node](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/) -* [Python](https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/) -* [.NET](https://docs.newrelic.com/docs/apm/agents/net-agent/configuration/net-agent-configuration/) -* [Ruby](https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/) -Global agent settings can be overridden in your deployment manifest if a different configuration is required. +Targeting everything in a specific namespace with a label -### Annotations +```yaml +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + #agent: ... + namespaceLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["java"] +``` -The `k8s-agents-operator` looks for language-specific annotations when your pods are being scheduled to know which applications you want to monitor. +Targeting a pod with a specific label -Below are the currently supported annotations: ```yaml -instrumentation.newrelic.com/inject-java: "true" -instrumentation.newrelic.com/inject-nodejs: "true" -instrumentation.newrelic.com/inject-python: "true" -instrumentation.newrelic.com/inject-dotnet: "true" -instrumentation.newrelic.com/inject-ruby: "true" +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-lang + namespace: newrelic +spec: + # agent: ... + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["dotnet"] ``` -Example deployment with annotation to instrument the Java agent: +Using a secret with a non-default name + ```yaml -apiVersion: apps/v1 -kind: Deployment +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation metadata: - name: spring-petclinic + name: newrelic-instrumentation-lang + namespace: newrelic spec: - selector: - matchLabels: - app: spring-petclinic - replicas: 1 - template: - metadata: - labels: - app: spring-petclinic - annotations: - instrumentation.newrelic.com/inject-java: "true" - spec: - containers: - - name: spring-petclinic - image: ghcr.io/pavolloffay/spring-petclinic:latest - ports: - - containerPort: 8080 - env: - - name: NEW_RELIC_APP_NAME - value: spring-petclinic-demo + # agent: ... + licenseKeySecret: the-name-of-the-custom-secret +``` + +In the example above, we show how you can configure the agent settings globally using environment variables. See each agent's configuration documentation for available configuration options: +* [Java](https://docs.newrelic.com/docs/apm/agents/java-agent/configuration/java-agent-configuration-config-file/) +* [Node](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/) +* [Python](https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/) +* [.NET](https://docs.newrelic.com/docs/apm/agents/net-agent/configuration/net-agent-configuration/) +* [Ruby](https://docs.newrelic.com/docs/apm/agents/ruby-agent/configuration/ruby-agent-configuration/) + +### cert-manager + +The K8s Agents Operator supports the use of [`cert-manager`](https://github.com/cert-manager/cert-manager) if preferred. + +Install the [`cert-manager`](https://github.com/cert-manager/cert-manager) Helm chart: +```shell +helm install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true ``` +In your `values.yaml` file, set `admissionWebhooks.autoGenerateCert.enabled: false` and `admissionWebhooks.certManager.enabled: true`. Then install the chart as normal. + ## Available Chart Releases To see the available charts: diff --git a/charts/k8s-agents-operator/templates/NOTES.txt b/charts/k8s-agents-operator/templates/NOTES.txt index e3fb9176..b330f475 100644 --- a/charts/k8s-agents-operator/templates/NOTES.txt +++ b/charts/k8s-agents-operator/templates/NOTES.txt @@ -1,4 +1,4 @@ -This project is currently in experimental phases and is provided AS-IS WITHOUT WARRANTY OR DEDICATED SUPPORT. +This project is currently in preview. Issues and contributions should be reported to the project's GitHub. {{- if (include "k8s-agents-operator.areValuesValid" .) }} ===================================== diff --git a/charts/k8s-agents-operator/templates/_helpers.tpl b/charts/k8s-agents-operator/templates/_helpers.tpl index 43b57a4d..f6ed6d04 100644 --- a/charts/k8s-agents-operator/templates/_helpers.tpl +++ b/charts/k8s-agents-operator/templates/_helpers.tpl @@ -78,3 +78,44 @@ Controller manager service certificate's secret. {{- define "k8s-agents-operator.certificateSecret" -}} {{- printf "%s-controller-manager-service-cert" (include "k8s-agents-operator.fullname" .) | trunc 63 | trimSuffix "-" -}} {{- end }} + +{{/* +Return certificate and CA for Webhooks. +It handles variants when a cert has to be generated by Helm, +a cert is loaded from an existing secret or is provided via `.Values` +*/}} +{{- define "k8s-agents-operator.webhookCert" -}} +{{- $caCert := "" }} +{{- $clientCert := "" }} +{{- $clientKey := "" }} +{{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + {{- $prevSecret := (lookup "v1" "Secret" .Release.Namespace (include "k8s-agents-operator.certificateSecret" . )) }} + {{- if and (not .Values.admissionWebhooks.autoGenerateCert.recreate) $prevSecret }} + {{- $clientCert = index $prevSecret "data" "tls.crt" }} + {{- $clientKey = index $prevSecret "data" "tls.key" }} + {{- $caCert = index $prevSecret "data" "ca.crt" }} + {{- if not $caCert }} + {{- $prevHook := (lookup "admissionregistration.k8s.io/v1" "MutatingWebhookConfiguration" .Release.Namespace (print (include "k8s-agents-operator.fullname" . ) "-mutation")) }} + {{- if not (eq (toString $prevHook) "") }} + {{- $caCert = (first $prevHook.webhooks).clientConfig.caBundle }} + {{- end }} + {{- end }} + {{- else }} + {{- $certValidity := int .Values.admissionWebhooks.autoGenerateCert.certPeriodDays | default 365 }} + {{- $ca := genCA "k8s-agents-operator-operator-ca" $certValidity }} + {{- $domain1 := printf "%s-webhook-service.%s.svc" (include "k8s-agents-operator.fullname" .) $.Release.Namespace }} + {{- $domain2 := printf "%s-webhook-service.%s.svc.%s" (include "k8s-agents-operator.fullname" .) $.Release.Namespace $.Values.kubernetesClusterDomain }} + {{- $domains := list $domain1 $domain2 }} + {{- $cert := genSignedCert (include "k8s-agents-operator.fullname" .) nil $domains $certValidity $ca }} + {{- $clientCert = b64enc $cert.Cert }} + {{- $clientKey = b64enc $cert.Key }} + {{- $caCert = b64enc $ca.Cert }} + {{- end }} +{{- else }} + {{- $clientCert = .Files.Get .Values.admissionWebhooks.certFile | b64enc }} + {{- $clientKey = .Files.Get .Values.admissionWebhooks.keyFile | b64enc }} + {{- $caCert = .Files.Get .Values.admissionWebhooks.caFile | b64enc }} +{{- end }} +{{- $result := dict "clientCert" $clientCert "clientKey" $clientKey "caCert" $caCert }} +{{- $result | toYaml }} +{{- end }} diff --git a/charts/k8s-agents-operator/templates/certmanager.yaml b/charts/k8s-agents-operator/templates/certmanager.yaml index 54509f67..ccef9f25 100644 --- a/charts/k8s-agents-operator/templates/certmanager.yaml +++ b/charts/k8s-agents-operator/templates/certmanager.yaml @@ -1,9 +1,11 @@ +{{- if and .Values.admissionWebhooks.create .Values.admissionWebhooks.certManager.enabled }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: {{ template "k8s-agents-operator.fullname" . }}-serving-cert + namespace: {{ .Release.Namespace }} labels: - {{- include "k8s-agents-operator.labels" . | nindent 4 }} + {{- include "k8s-agents-operator.labels" . | nindent 4 }} spec: dnsNames: - '{{ template "k8s-agents-operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc' @@ -14,4 +16,15 @@ spec: secretName: {{ template "k8s-agents-operator.certificateSecret" . }} subject: organizationalUnits: - - k8s-agents-operator \ No newline at end of file + - k8s-agents-operator +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ template "k8s-agents-operator.fullname" . }}-selfsigned-issuer + namespace: {{ .Release.Namespace }} + labels: + {{- include "k8s-agents-operator.labels" . | nindent 4 }} +spec: + selfSigned: {} +{{- end }} diff --git a/charts/k8s-agents-operator/templates/deployment.yaml b/charts/k8s-agents-operator/templates/deployment.yaml index bf19d4e1..56784aae 100644 --- a/charts/k8s-agents-operator/templates/deployment.yaml +++ b/charts/k8s-agents-operator/templates/deployment.yaml @@ -2,29 +2,29 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ template "k8s-agents-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} labels: - {{- include "k8s-agents-operator.labels" . | nindent 4 }} + {{- include "k8s-agents-operator.labels" . | nindent 4 }} --- apiVersion: apps/v1 kind: Deployment metadata: name: {{ template "k8s-agents-operator.fullname" . }} + namespace: {{ .Release.Namespace }} labels: control-plane: controller-manager - {{- include "k8s-agents-operator.labels" . | nindent 4 }} + {{- include "k8s-agents-operator.labels" . | nindent 4 }} spec: replicas: {{ .Values.controllerManager.replicas }} selector: matchLabels: - app.kubernetes.io/name: k8s-agents-operator control-plane: controller-manager - {{- include "k8s-agents-operator.labels" . | nindent 6 }} + {{- include "k8s-agents-operator.labels" . | nindent 6 }} template: metadata: labels: - app.kubernetes.io/name: k8s-agents-operator control-plane: controller-manager - {{- include "k8s-agents-operator.labels" . | nindent 8 }} + {{- include "k8s-agents-operator.labels" . | nindent 8 }} spec: containers: - args: @@ -35,6 +35,10 @@ spec: - --zap-log-level=info - --zap-time-encoding=rfc3339nano env: + - name: OPERATOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} - name: ENABLE_WEBHOOKS @@ -80,7 +84,7 @@ spec: resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent 10 }} serviceAccountName: {{ template "k8s-agents-operator.serviceAccountName" . }} terminationGracePeriodSeconds: 10 - {{- if or .Values.admissionWebhooks.create .Values.admissionWebhooks.secretName }} + {{- if or .Values.admissionWebhooks.create (include "k8s-agents-operator.certificateSecret" . ) }} volumes: - name: cert secret: @@ -88,4 +92,4 @@ spec: secretName: {{ template "k8s-agents-operator.certificateSecret" . }} {{- end }} securityContext: -{{ toYaml .Values.securityContext | indent 8 }} + {{- toYaml .Values.securityContext | nindent 8 }} diff --git a/charts/k8s-agents-operator/templates/instrumentation-crd.yaml b/charts/k8s-agents-operator/templates/instrumentation-crd.yaml index ae81414f..6aaecae9 100644 --- a/charts/k8s-agents-operator/templates/instrumentation-crd.yaml +++ b/charts/k8s-agents-operator/templates/instrumentation-crd.yaml @@ -3,7 +3,7 @@ kind: CustomResourceDefinition metadata: name: instrumentations.newrelic.com annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.14.0 labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} spec: @@ -22,50 +22,57 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date - name: v1alpha1 + name: v1alpha2 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' + 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' + 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: - dotnet: - description: DotNet defines configuration for dotnet auto-instrumentation. + agent: + description: Agent defines configuration for agent instrumentation. properties: env: - description: Env defines DotNet specific env vars. If the former - var had been defined, then the other vars would be ignored. + 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. Must be a - C_IDENTIFIER. + description: Name of the environment variable. Must be a C_IDENTIFIER. 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. + 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 "".' + 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. @@ -78,9 +85,10 @@ spec: description: The key to select. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? type: string optional: description: Specify whether the ConfigMap or its @@ -91,29 +99,26 @@ spec: 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.' + 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". + 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 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.' + 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, @@ -123,8 +128,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: @@ -135,268 +140,17 @@ 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 be a valid secret key. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 DotNet agent and - auto-instrumentation. - type: string - type: object - env: - description: 'Env defines common 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. Must be a C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - 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 - exporter: - description: Exporter defines exporter configuration. - properties: - endpoint: - description: Endpoint is address of the collector with OTLP endpoint. - type: string - type: object - go: - description: Go defines configuration for Go auto-instrumentation. - When using Go auto-instrumentation you must provide a value for - the OTEL_GO_AUTO_TARGET_EXE env var via the Instrumentation env - vars or via the instrumentation.opentelemetry.io/otel-go-auto-target-exe - pod annotation. Failure to set this value causes instrumentation - injection to abort, leaving the original pod unchanged. - 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? type: string optional: description: Specify whether the Secret or its key @@ -414,23 +168,31 @@ spec: image: description: Image is a container image with Go SDK and auto-instrumentation. 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. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." + description: |- + 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 + 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. + 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 required: - name @@ -446,8 +208,9 @@ spec: - 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/' + 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: @@ -456,393 +219,130 @@ spec: - 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. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + 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. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 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. + 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 - java: - description: Java defines configuration for java auto-instrumentation. + exporter: + description: Exporter defines exporter configuration. properties: - env: - description: Env defines java specific env vars. If the former - var had been defined, then the other vars would be ignored. + endpoint: + description: Endpoint is address of the collector with OTLP endpoint. + type: string + type: object + licenseKeySecret: + description: |- + LicenseKeySecret defines where to take the licenseKeySecret. + 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: EnvVar represents an environment variable present - in a Container. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + key: + description: key is the label key that the selector applies + to. 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 "".' + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 + 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 required: - - name + - key + - operator type: object type: array - image: - description: Image is a container image with javaagent auto-instrumentation - JAR. - type: string + 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 - nodejs: - description: NodeJS defines configuration for nodejs auto-instrumentation. + x-kubernetes-map-type: atomic + podLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. properties: - env: - description: Env defines nodejs specific env vars. If the former - var had been defined, then the other vars would be ignored. + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. items: - description: EnvVar represents an environment variable present - in a Container. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + key: + description: key is the label key that the selector applies + to. 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 "".' + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 NodeJS agent and - auto-instrumentation. - type: string - type: object - php: - description: Php defines configuration for php auto-instrumentation. - properties: - env: - description: Env defines Php specific env 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 + 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 required: - - name + - key + - operator type: object type: array - image: - description: Image is a container image with Php agent and auto-instrumentation. - type: string + 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 propagators: - description: Propagators defines inter-process context propagation - configuration. Values in this list will be set in the OTEL_PROPAGATORS - env var. Enum=tracecontext;none + description: |- + Propagators defines inter-process context propagation configuration. + Values in this list will be set in the OTEL_PROPAGATORS env var. + Enum=tracecontext;none items: description: Propagator represents the propagation type. enum: @@ -850,252 +350,6 @@ spec: - none type: string type: array - python: - description: Python defines configuration for python auto-instrumentation. - properties: - env: - description: Env defines python specific env 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 Python agent and - auto-instrumentation. - type: string - type: object - ruby: - description: Ruby defines configuration for ruby auto-instrumentation. - properties: - env: - description: Env defines Ruby specific env 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 Ruby agent and - auto-instrumentation. - type: string - type: object resource: description: Resource defines the configuration for the resource attributes, as defined by the OpenTelemetry specification. @@ -1107,23 +361,26 @@ spec: resourceAttributes: additionalProperties: type: string - description: 'Attributes defines attributes that are added to - the resource. For example environment: dev' + description: |- + Attributes defines attributes that are added to the resource. + For example environment: dev type: object type: object sampler: description: Sampler defines sampling configuration. properties: argument: - description: Argument defines sampler argument. The value depends - on the sampler type. For instance for parentbased_traceidratio - sampler type it is a number in range [0..1] e.g. 0.25. The value - will be set in the OTEL_TRACES_SAMPLER_ARG env var. + description: |- + Argument defines sampler argument. + The value depends on the sampler type. + For instance for parentbased_traceidratio sampler type it is a number in range [0..1] e.g. 0.25. + The value will be set in the OTEL_TRACES_SAMPLER_ARG env var. type: string type: - description: Type defines sampler type. The value will be set - in the OTEL_TRACES_SAMPLER env var. The value can be for instance - parentbased_always_on, parentbased_always_off, parentbased_traceidratio... + description: |- + Type defines sampler type. + The value will be set in the OTEL_TRACES_SAMPLER env var. + The value can be for instance parentbased_always_on, parentbased_always_off, parentbased_traceidratio... enum: - always_on - always_off @@ -1146,5 +403,5 @@ status: acceptedNames: kind: "" plural: "" - conditions: null - storedVersions: null \ No newline at end of file + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/charts/k8s-agents-operator/templates/leader-election-rbac.yaml b/charts/k8s-agents-operator/templates/leader-election-rbac.yaml index 57a5be3a..5a421492 100644 --- a/charts/k8s-agents-operator/templates/leader-election-rbac.yaml +++ b/charts/k8s-agents-operator/templates/leader-election-rbac.yaml @@ -2,6 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ template "k8s-agents-operator.fullname" . }}-leader-election-role + namespace: {{ .Release.Namespace }} labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} rules: @@ -37,6 +38,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ template "k8s-agents-operator.fullname" . }}-leader-election-rolebinding + namespace: {{ .Release.Namespace }} labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} roleRef: @@ -46,4 +48,4 @@ roleRef: subjects: - kind: ServiceAccount name: '{{ template "k8s-agents-operator.serviceAccountName" . }}' - namespace: '{{ .Release.Namespace }}' \ No newline at end of file + namespace: '{{ .Release.Namespace }}' diff --git a/charts/k8s-agents-operator/templates/manager-rbac.yaml b/charts/k8s-agents-operator/templates/manager-rbac.yaml index 7a1d9d3b..7578de52 100644 --- a/charts/k8s-agents-operator/templates/manager-rbac.yaml +++ b/charts/k8s-agents-operator/templates/manager-rbac.yaml @@ -12,6 +12,17 @@ rules: verbs: - create - patch +- apiGroups: [ "" ] + resources: ["secrets"] + verbs: + - get + - list + - create + - delete + - deletecollection + - patch + - update + - watch - apiGroups: - "" resources: @@ -64,6 +75,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ template "k8s-agents-operator.fullname" . }}-manager-rolebinding + namespace: {{ .Release.Namespace }} labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} roleRef: @@ -73,4 +85,4 @@ roleRef: subjects: - kind: ServiceAccount name: '{{ template "k8s-agents-operator.serviceAccountName" . }}' - namespace: '{{ .Release.Namespace }}' \ No newline at end of file + namespace: '{{ .Release.Namespace }}' diff --git a/charts/k8s-agents-operator/templates/mutating-webhook-configuration.yaml b/charts/k8s-agents-operator/templates/mutating-webhook-configuration.yaml deleted file mode 100644 index f37ad6a7..00000000 --- a/charts/k8s-agents-operator/templates/mutating-webhook-configuration.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: {{ template "k8s-agents-operator.fullname" . }}-mutating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "k8s-agents-operator.fullname" . }}-serving-cert - labels: - {{- include "k8s-agents-operator.labels" . | nindent 4 }} -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' - namespace: '{{ .Release.Namespace }}' - path: /mutate-newrelic-com-v1alpha1-instrumentation - failurePolicy: Fail - name: instrumentation.kb.io - rules: - - apiGroups: - - newrelic.com - apiVersions: - - v1alpha1 - operations: - - CREATE - - UPDATE - resources: - - instrumentations - sideEffects: None -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' - namespace: '{{ .Release.Namespace }}' - path: /mutate-v1-pod - failurePolicy: Ignore - name: mpod.kb.io - rules: - - apiGroups: - - "" - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - pods - sideEffects: None \ No newline at end of file diff --git a/charts/k8s-agents-operator/templates/newrelic_license_secret.yaml b/charts/k8s-agents-operator/templates/newrelic_license_secret.yaml index db2c35f7..00734c4c 100644 --- a/charts/k8s-agents-operator/templates/newrelic_license_secret.yaml +++ b/charts/k8s-agents-operator/templates/newrelic_license_secret.yaml @@ -11,4 +11,4 @@ metadata: type: Opaque data: new_relic_license_key: {{ $licenseKey | b64enc }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/k8s-agents-operator/templates/proxy-rbac.yaml b/charts/k8s-agents-operator/templates/proxy-rbac.yaml index af583f59..2a9ea866 100644 --- a/charts/k8s-agents-operator/templates/proxy-rbac.yaml +++ b/charts/k8s-agents-operator/templates/proxy-rbac.yaml @@ -22,6 +22,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ template "k8s-agents-operator.fullname" . }}-proxy-rolebinding + namespace: {{ .Release.Namespace }} labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} roleRef: @@ -31,4 +32,4 @@ roleRef: subjects: - kind: ServiceAccount name: '{{ template "k8s-agents-operator.serviceAccountName" . }}' - namespace: '{{ .Release.Namespace }}' \ No newline at end of file + namespace: '{{ .Release.Namespace }}' diff --git a/charts/k8s-agents-operator/templates/reader-rbac.yaml b/charts/k8s-agents-operator/templates/reader-rbac.yaml index 6482ff0d..4aa1d446 100644 --- a/charts/k8s-agents-operator/templates/reader-rbac.yaml +++ b/charts/k8s-agents-operator/templates/reader-rbac.yaml @@ -8,4 +8,4 @@ rules: - nonResourceURLs: - /metrics verbs: - - get \ No newline at end of file + - get diff --git a/charts/k8s-agents-operator/templates/selfsigned-issuer.yaml b/charts/k8s-agents-operator/templates/selfsigned-issuer.yaml deleted file mode 100644 index 31c0cc79..00000000 --- a/charts/k8s-agents-operator/templates/selfsigned-issuer.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: {{ template "k8s-agents-operator.fullname" . }}-selfsigned-issuer - labels: - {{- include "k8s-agents-operator.labels" . | nindent 4 }} -spec: - selfSigned: {} \ No newline at end of file diff --git a/charts/k8s-agents-operator/templates/service.yaml b/charts/k8s-agents-operator/templates/service.yaml index 892b1b3e..cfdc9d2b 100644 --- a/charts/k8s-agents-operator/templates/service.yaml +++ b/charts/k8s-agents-operator/templates/service.yaml @@ -2,14 +2,14 @@ apiVersion: v1 kind: Service metadata: name: {{ template "k8s-agents-operator.fullname" . }} + namespace: {{ .Release.Namespace }} labels: control-plane: controller-manager {{- include "k8s-agents-operator.labels" . | nindent 4 }} spec: type: {{ .Values.metricsService.type }} selector: - app.kubernetes.io/name: {{ include "k8s-agents-operator.chart" . }} control-plane: controller-manager {{- include "k8s-agents-operator.labels" . | nindent 4 }} ports: - {{- .Values.metricsService.ports | toYaml | nindent 2 -}} \ No newline at end of file + {{- .Values.metricsService.ports | toYaml | nindent 2 -}} diff --git a/charts/k8s-agents-operator/templates/validating-webhook-configuration.yaml b/charts/k8s-agents-operator/templates/validating-webhook-configuration.yaml deleted file mode 100644 index f98608b7..00000000 --- a/charts/k8s-agents-operator/templates/validating-webhook-configuration.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: admissionregistration.k8s.io/v1 -kind: ValidatingWebhookConfiguration -metadata: - name: {{ template "k8s-agents-operator.fullname" . }}-validating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "k8s-agents-operator.fullname" . }}-serving-cert - labels: - {{- include "k8s-agents-operator.labels" . | nindent 4 }} -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' - namespace: '{{ .Release.Namespace }}' - path: /validate-newrelic-com-v1alpha1-instrumentation - failurePolicy: Fail - name: vinstrumentationcreateupdate.kb.io - rules: - - apiGroups: - - newrelic.com - apiVersions: - - v1alpha1 - operations: - - CREATE - - UPDATE - resources: - - instrumentations - sideEffects: None -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' - namespace: '{{ .Release.Namespace }}' - path: /validate-newrelic-com-v1alpha1-instrumentation - failurePolicy: Ignore - name: vinstrumentationdelete.kb.io - rules: - - apiGroups: - - newrelic.com - apiVersions: - - v1alpha1 - operations: - - DELETE - resources: - - instrumentations - sideEffects: None \ No newline at end of file diff --git a/charts/k8s-agents-operator/templates/webhook-configuration.yaml b/charts/k8s-agents-operator/templates/webhook-configuration.yaml new file mode 100644 index 00000000..d979e40a --- /dev/null +++ b/charts/k8s-agents-operator/templates/webhook-configuration.yaml @@ -0,0 +1,134 @@ +{{- $tls := fromYaml (include "k8s-agents-operator.webhookCert" .) }} +{{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} +apiVersion: v1 +kind: Secret +type: kubernetes.io/tls +metadata: + name: {{ template "k8s-agents-operator.certificateSecret" . }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-delete-policy": "before-hook-creation" + labels: + {{- include "k8s-agents-operator.labels" . | nindent 4 }} + app.kubernetes.io/component: webhook + namespace: {{ .Release.Namespace }} +data: + tls.crt: {{ $tls.clientCert }} + tls.key: {{ $tls.clientKey }} + ca.crt: {{ $tls.caCert }} +{{- end }} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ template "k8s-agents-operator.fullname" . }}-mutation + {{- if .Values.admissionWebhooks.certManager.enabled }} + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "k8s-agents-operator.fullname" . }}-serving-cert + {{- end }} + labels: + {{- include "k8s-agents-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + {{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + caBundle: {{ $tls.caCert }} + {{- end }} + service: + name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Fail + name: minstrumentation.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: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-v1-pod + failurePolicy: Ignore + name: mpod.kb.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - pods + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ template "k8s-agents-operator.fullname" . }}-validation + {{- if .Values.admissionWebhooks.certManager.enabled }} + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "k8s-agents-operator.fullname" . }}-serving-cert + {{- end }} + labels: + {{- include "k8s-agents-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + {{- if .Values.admissionWebhooks.autoGenerateCert.enabled }} + caBundle: {{ $tls.caCert }} + {{- end }} + service: + name: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Fail + name: vinstrumentationcreateupdate.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: '{{ template "k8s-agents-operator.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-newrelic-com-v1alpha2-instrumentation + failurePolicy: Ignore + name: vinstrumentationdelete.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha2 + operations: + - DELETE + resources: + - instrumentations + sideEffects: None diff --git a/charts/k8s-agents-operator/templates/webhook-service.yaml b/charts/k8s-agents-operator/templates/webhook-service.yaml index d2197c67..e5598e8f 100644 --- a/charts/k8s-agents-operator/templates/webhook-service.yaml +++ b/charts/k8s-agents-operator/templates/webhook-service.yaml @@ -2,14 +2,13 @@ apiVersion: v1 kind: Service metadata: name: {{ template "k8s-agents-operator.fullname" . }}-webhook-service + namespace: {{ .Release.Namespace }} labels: {{- include "k8s-agents-operator.labels" . | nindent 4 }} spec: type: {{ .Values.webhookService.type }} selector: - app.kubernetes.io/name: {{ include "k8s-agents-operator.chart" . }} - app.kubernetes.io/name: k8s-agents-operator control-plane: controller-manager {{- include "k8s-agents-operator.labels" . | nindent 4 }} ports: - {{- .Values.webhookService.ports | toYaml | nindent 2 -}} \ No newline at end of file + {{- .Values.webhookService.ports | toYaml | nindent 2 -}} diff --git a/charts/k8s-agents-operator/tests/cert_manager_test.yaml b/charts/k8s-agents-operator/tests/cert_manager_test.yaml new file mode 100644 index 00000000..1de20192 --- /dev/null +++ b/charts/k8s-agents-operator/tests/cert_manager_test.yaml @@ -0,0 +1,85 @@ +suite: cert-manager +templates: + - templates/certmanager.yaml +release: + name: my-release + namespace: my-namespace +tests: + - it: creates cert-manager resources if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - hasDocuments: + count: 2 + - it: creates Issuer if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - equal: + path: kind + value: Issuer + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-selfsigned-issuer + - exists: + path: spec.selfSigned + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-selfsigned-issuer + - it: creates Certificate in default domain if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - equal: + path: kind + value: Certificate + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-serving-cert + - equal: + path: spec.dnsNames + value: + - my-release-k8s-agents-operator-webhook-service.my-namespace.svc + - my-release-k8s-agents-operator-webhook-service.my-namespace.svc.cluster.local + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-serving-cert + - it: creates Certificate in custom domain if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + kubernetesClusterDomain: kubey.test + asserts: + - equal: + path: kind + value: Certificate + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-serving-cert + - equal: + path: spec.dnsNames + value: + - my-release-k8s-agents-operator-webhook-service.my-namespace.svc + - my-release-k8s-agents-operator-webhook-service.my-namespace.svc.kubey.test + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-serving-cert diff --git a/charts/k8s-agents-operator/tests/webhook_ssl_test.yaml b/charts/k8s-agents-operator/tests/webhook_ssl_test.yaml new file mode 100644 index 00000000..9343a43a --- /dev/null +++ b/charts/k8s-agents-operator/tests/webhook_ssl_test.yaml @@ -0,0 +1,176 @@ +suite: webhook ssl +templates: + - templates/webhook-configuration.yaml +release: + name: my-release + namespace: my-namespace +tests: + - it: creates ssl certificate secret by default + set: + licenseKey: us-whatever + asserts: + - hasDocuments: + count: 3 + - containsDocument: + kind: Secret + apiVersion: v1 + name: my-release-k8s-agents-operator-controller-manager-service-cert + namespace: my-namespace + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-controller-manager-service-cert + - exists: + path: data["tls.crt"] + template: templates/webhook-configuration.yaml + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-controller-manager-service-cert + - exists: + path: data["tls.key"] + template: templates/webhook-configuration.yaml + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-controller-manager-service-cert + - exists: + path: data["ca.crt"] + template: templates/webhook-configuration.yaml + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-controller-manager-service-cert + - it: does not inject cert-manager annotations into MutatingWebhook by default + set: + licenseKey: us-whatever + asserts: + - notExists: + path: metadata.annotations["cert-manager.io/inject-ca-from"] + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - it: does not inject cert-manager annotations into ValidatingWebhook by default + set: + licenseKey: us-whatever + asserts: + - notExists: + path: metadata.annotations["cert-manager.io/inject-ca-from"] + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-validation + - it: does inject caBundle into MutatingWebhook clientConfigs by default + set: + licenseKey: us-whatever + asserts: + - lengthEqual: + path: webhooks + count: 2 + - exists: + path: webhooks[0].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - exists: + path: webhooks[1].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - it: does inject caBundle into ValidatingWebhook clientConfigs by default + set: + licenseKey: us-whatever + asserts: + - lengthEqual: + path: webhooks + count: 2 + - exists: + path: webhooks[0].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - exists: + path: webhooks[1].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-validation + - it: does not creates ssl certificate secret if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - hasDocuments: + count: 2 + - it: injects cert-manager annotations into MutatingWebhook if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - equal: + path: metadata.annotations["cert-manager.io/inject-ca-from"] + value: my-namespace/my-release-k8s-agents-operator-serving-cert + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - it: injects cert-manager annotations into ValidatingWebhook if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - equal: + path: metadata.annotations["cert-manager.io/inject-ca-from"] + value: my-namespace/my-release-k8s-agents-operator-serving-cert + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-validation + - it: does not inject caBundle into MutatingWebhook clientConfigs if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - lengthEqual: + path: webhooks + count: 2 + - notExists: + path: webhooks[0].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - notExists: + path: webhooks[1].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - it: does not inject caBundle into ValidatingWebhook clientConfigs if cert-manager enabled and auto cert disabled + set: + licenseKey: us-whatever + admissionWebhooks: + autoGenerateCert: + enabled: false + certManager: + enabled: true + asserts: + - lengthEqual: + path: webhooks + count: 2 + - notExists: + path: webhooks[0].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-mutation + - notExists: + path: webhooks[1].clientConfig.caBundle + documentSelector: + path: metadata.name + value: my-release-k8s-agents-operator-validation diff --git a/charts/k8s-agents-operator/values.yaml b/charts/k8s-agents-operator/values.yaml index 7cae82fb..f2897977 100644 --- a/charts/k8s-agents-operator/values.yaml +++ b/charts/k8s-agents-operator/values.yaml @@ -1,5 +1,9 @@ -# -- Ingest license key to use -# licenseKey: +# Default values for k8s-agents-operator. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# -- This set this license key to use. Can be configured also with `global.licenseKey` +licenseKey: "" controllerManager: replicas: 1 @@ -60,3 +64,30 @@ securityContext: # -- Admission webhooks make sure only requests with correctly formatted rules will get into the Operator admissionWebhooks: create: true + + ## TLS Certificate Option 1: Use Helm to automatically generate self-signed certificate. + ## certManager must be disabled and autoGenerateCert must be enabled. + autoGenerateCert: + # -- If true and certManager.enabled is false, Helm will automatically create a self-signed cert and secret for you. + enabled: true + # -- If set to true, new webhook key/certificate is generated on helm upgrade. + recreate: true + # -- Cert validity period time in days. + certPeriodDays: 365 + + ## TLS Certificate Option 2: Use certManager to generate self-signed certificate. + certManager: + # -- If true and autoGenerateCert.enabled is false, cert-manager will create a self-signed cert and secret for you. + enabled: false + + ## TLS Certificate Option 3: Use your own self-signed certificate. + ## certManager and autoGenerateCert must be disabled and certFile, keyFile, and caFile must be set. + ## The chart reads the contents of the file paths with the helm .Files.Get function. + ## Refer to this doc https://helm.sh/docs/chart_template_guide/accessing_files/ to understand + ## limitations of file paths accessible to the chart. + # -- Path to your own PEM-encoded certificate. + certFile: "" + # -- Path to your own PEM-encoded private key. + keyFile: "" + # -- Path to the CA cert. + caFile: "" diff --git a/go.mod b/go.mod index 5ab266bc..d3852bc3 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/newrelic/k8s-agents-operator go 1.22 require ( - github.com/go-logr/logr v1.2.3 + github.com/go-logr/logr v1.4.2 + github.com/google/go-cmp v0.5.9 github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 github.com/openshift/api v3.9.0+incompatible github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.11.2 k8s.io/api v0.26.3 k8s.io/apimachinery v0.26.3 @@ -24,7 +25,7 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/zapr v1.2.3 // indirect + github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.15 // indirect @@ -32,7 +33,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.2.0 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -52,9 +52,8 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/spf13/cobra v1.6.0 // indirect go.opentelemetry.io/otel/trace v1.11.2 // indirect - go.uber.org/atomic v1.8.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.5.0 // indirect @@ -68,7 +67,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.26.3 // indirect - k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect diff --git a/go.sum b/go.sum index c84faac3..e58e2711 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -83,12 +81,10 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -264,18 +260,13 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -289,17 +280,12 @@ go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0= go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.8.0 h1:CUhrE4N1rqSE6FM9ecihEjRkLQu8cDfgDyoOs83mEY4= -go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -463,7 +449,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -596,7 +581,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -616,8 +600,8 @@ k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s= k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ= k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g= k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= diff --git a/local/example-cr-matching-all.yml b/local/example-cr-matching-all.yml new file mode 100644 index 00000000..fb41caaf --- /dev/null +++ b/local/example-cr-matching-all.yml @@ -0,0 +1,9 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: matching-whole-cluster +spec: + agent: + language: java + image: newrelic/newrelic-java-init:latest + licenseKeySecret: "newrelic-key-secret" diff --git a/local/example-cr.yml b/local/example-cr.yml new file mode 100644 index 00000000..a0d1836d --- /dev/null +++ b/local/example-cr.yml @@ -0,0 +1,32 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: matching-include +spec: + podLabelSelector: + matchExpressions: + - key: "app.kubernetes.io/name" + operator: "In" + values: ["test"] + agent: + language: java + image: newrelic/newrelic-java-init:latest + licenseKeySecret: "newrelic-key-secret" +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: matching-whole-test2-namespace +spec: + namespaceLabelSelector: + matchExpressions: + - key: "kubernetes.io/metadata.name" + operator: "In" + values: ["test2"] + agent: + language: java + image: newrelic/newrelic-java-init:8.14.0 + env: + - name: NEW_RELIC_LABELS + value: "whatever" + licenseKeySecret: "newrelic-key-secret" diff --git a/local/pod-include-label-and-namespace-test2.yaml b/local/pod-include-label-and-namespace-test2.yaml new file mode 100644 index 00000000..0293a3b1 --- /dev/null +++ b/local/pod-include-label-and-namespace-test2.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: test + name: include-label-and-namespace-test2 + namespace: test2 +spec: + containers: + - image: busybox + name: a + command: + - sleep + - "9999" diff --git a/local/pod-include-label.yaml b/local/pod-include-label.yaml new file mode 100644 index 00000000..aa388105 --- /dev/null +++ b/local/pod-include-label.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/name: test + name: include-label +spec: + containers: + - image: busybox + name: a + command: + - sleep + - "9999" diff --git a/local/pod-include-namespace-test2.yaml b/local/pod-include-namespace-test2.yaml new file mode 100644 index 00000000..a53c21f3 --- /dev/null +++ b/local/pod-include-namespace-test2.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: include-namespace-test2 + namespace: test2 +spec: + containers: + - image: busybox + name: a + command: + - sleep + - "9999" diff --git a/local/pod-not-matching.yaml b/local/pod-not-matching.yaml new file mode 100644 index 00000000..c4312a1e --- /dev/null +++ b/local/pod-not-matching.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: not-matching +spec: + containers: + - image: busybox + name: a + command: + - sleep + - "9999" diff --git a/local/super-agent-tilt.yml b/local/super-agent-tilt.yml new file mode 100644 index 00000000..caa31a2d --- /dev/null +++ b/local/super-agent-tilt.yml @@ -0,0 +1 @@ +licenseKey: test123 diff --git a/src/api/v1alpha1/instrumentation_webhook.go b/src/api/v1alpha1/instrumentation_webhook.go deleted file mode 100644 index c5fdc500..00000000 --- a/src/api/v1alpha1/instrumentation_webhook.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -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 v1alpha1 - -import ( - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook" -) - -const ( - AnnotationDefaultAutoInstrumentationJava = "instrumentation.newrelic.com/default-auto-instrumentation-java-image" - AnnotationDefaultAutoInstrumentationNodeJS = "instrumentation.newrelic.com/default-auto-instrumentation-nodejs-image" - AnnotationDefaultAutoInstrumentationPython = "instrumentation.newrelic.com/default-auto-instrumentation-python-image" - AnnotationDefaultAutoInstrumentationDotNet = "instrumentation.newrelic.com/default-auto-instrumentation-dotnet-image" - AnnotationDefaultAutoInstrumentationPhp = "instrumentation.newrelic.com/default-auto-instrumentation-php-image" - AnnotationDefaultAutoInstrumentationRuby = "instrumentation.newrelic.com/default-auto-instrumentation-ruby-image" - AnnotationDefaultAutoInstrumentationGo = "instrumentation.newrelic.com/default-auto-instrumentation-go-image" - envNewRelicPrefix = "NEW_RELIC_" - envOtelPrefix = "OTEL_" -) - -// log is for logging in this package. -var instrumentationlog = logf.Log.WithName("instrumentation-resource") - -func (r *Instrumentation) SetupWebhookWithManager(mgr ctrl.Manager) error { - return ctrl.NewWebhookManagedBy(mgr). - For(r). - Complete() -} - -// +kubebuilder:webhook:path=/mutate-newrelic-com-v1alpha1-instrumentation,mutating=true,failurePolicy=fail,sideEffects=None,groups=newrelic.com,resources=instrumentations,verbs=create;update,versions=v1alpha1,name=instrumentation.kb.io,admissionReviewVersions=v1 - -var _ webhook.Defaulter = &Instrumentation{} - -// Default implements webhook.Defaulter so a webhook will be registered for the type. -func (r *Instrumentation) Default() { - instrumentationlog.Info("default", "name", r.Name) - if r.Labels == nil { - r.Labels = map[string]string{} - } - if r.Labels["app.kubernetes.io/managed-by"] == "" { - r.Labels["app.kubernetes.io/managed-by"] = "k8s-agents-operator" - } - - if r.Spec.Java.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationJava]; ok { - r.Spec.Java.Image = val - } - } - if r.Spec.NodeJS.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationNodeJS]; ok { - r.Spec.NodeJS.Image = val - } - } - if r.Spec.Python.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationPython]; ok { - r.Spec.Python.Image = val - } - } - if r.Spec.DotNet.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationDotNet]; ok { - r.Spec.DotNet.Image = val - } - } - if r.Spec.Php.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationPhp]; ok { - r.Spec.Php.Image = val - } - } - if r.Spec.Ruby.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationRuby]; ok { - r.Spec.Ruby.Image = val - } - } - if r.Spec.Go.Image == "" { - if val, ok := r.Annotations[AnnotationDefaultAutoInstrumentationGo]; ok { - r.Spec.Go.Image = val - } - } -} - -// +kubebuilder:webhook:verbs=create;update,path=/validate-newrelic-com-v1alpha1-instrumentation,mutating=false,failurePolicy=fail,groups=newrelic.com,resources=instrumentations,versions=v1alpha1,name=vinstrumentationcreateupdate.kb.io,sideEffects=none,admissionReviewVersions=v1 -// +kubebuilder:webhook:verbs=delete,path=/validate-newrelic-com-v1alpha1-instrumentation,mutating=false,failurePolicy=ignore,groups=newrelic.com,resources=instrumentations,versions=v1alpha1,name=vinstrumentationdelete.kb.io,sideEffects=none,admissionReviewVersions=v1 - -var _ webhook.Validator = &Instrumentation{} - -// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. -func (r *Instrumentation) ValidateCreate() error { - instrumentationlog.Info("validate create", "name", r.Name) - return r.validate() -} - -// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. -func (r *Instrumentation) ValidateUpdate(old runtime.Object) error { - instrumentationlog.Info("validate update", "name", r.Name) - return r.validate() -} - -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. -func (r *Instrumentation) ValidateDelete() error { - instrumentationlog.Info("validate delete", "name", r.Name) - return nil -} - -func (r *Instrumentation) validate() error { - - // validate env vars - if err := r.validateEnv(r.Spec.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.Java.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.NodeJS.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.Python.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.DotNet.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.Php.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.Ruby.Env); err != nil { - return err - } - if err := r.validateEnv(r.Spec.Go.Env); err != nil { - return err - } - - return nil -} - -func (r *Instrumentation) validateEnv(envs []corev1.EnvVar) error { - for _, env := range envs { - if !strings.HasPrefix(env.Name, envNewRelicPrefix) && !strings.HasPrefix(env.Name, envOtelPrefix) { - return fmt.Errorf("env name should start with \"NEW_RELIC_\" or \"OTEL_\": %s", env.Name) - } - } - return nil -} diff --git a/src/api/v1alpha1/groupversion_info.go b/src/api/v1alpha2/groupversion_info.go similarity index 90% rename from src/api/v1alpha1/groupversion_info.go rename to src/api/v1alpha2/groupversion_info.go index b9525d61..280365a1 100644 --- a/src/api/v1alpha1/groupversion_info.go +++ b/src/api/v1alpha2/groupversion_info.go @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group +// Package v1alpha2 contains API Schema definitions for the v1alpha2 API group // +kubebuilder:object:generate=true // +groupName=newrelic.com -package v1alpha1 +package v1alpha2 import ( "k8s.io/apimachinery/pkg/runtime/schema" @@ -26,7 +26,7 @@ import ( var ( // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "newrelic.com", Version: "v1alpha1"} + GroupVersion = schema.GroupVersion{Group: "newrelic.com", Version: "v1alpha2"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} diff --git a/src/api/v1alpha1/instrumentation_types.go b/src/api/v1alpha2/instrumentation_types.go similarity index 56% rename from src/api/v1alpha1/instrumentation_types.go rename to src/api/v1alpha2/instrumentation_types.go index 3778dcbd..e90626b9 100644 --- a/src/api/v1alpha1/instrumentation_types.go +++ b/src/api/v1alpha2/instrumentation_types.go @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package v1alpha2 import ( + "reflect" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -45,44 +47,24 @@ type InstrumentationSpec struct { // +optional Sampler `json:"sampler,omitempty"` - // Env defines common 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. + // PodLabelSelector defines to which pods the config should be applied. // +optional - Env []corev1.EnvVar `json:"env,omitempty"` + PodLabelSelector metav1.LabelSelector `json:"podLabelSelector"` - // Java defines configuration for java auto-instrumentation. + // PodLabelSelector defines to which pods the config should be applied. // +optional - Java Java `json:"java,omitempty"` + NamespaceLabelSelector metav1.LabelSelector `json:"namespaceLabelSelector"` - // NodeJS defines configuration for nodejs auto-instrumentation. + // LicenseKeySecret defines where to take the licenseKeySecret. + // it should be present in the operator namespace. // +optional - NodeJS NodeJS `json:"nodejs,omitempty"` + LicenseKeySecret string `json:"licenseKeySecret,omitempty"` - // Python defines configuration for python auto-instrumentation. - // +optional - Python Python `json:"python,omitempty"` - - // DotNet defines configuration for dotnet auto-instrumentation. - // +optional - DotNet DotNet `json:"dotnet,omitempty"` - - // Php defines configuration for php auto-instrumentation. - // +optional - Php Php `json:"php,omitempty"` - - // Ruby defines configuration for ruby auto-instrumentation. - // +optional - Ruby Ruby `json:"ruby,omitempty"` - - // Go defines configuration for Go auto-instrumentation. - // When using Go auto-instrumentation you must provide a value for the OTEL_GO_AUTO_TARGET_EXE env var via the - // Instrumentation env vars or via the instrumentation.opentelemetry.io/otel-go-auto-target-exe pod annotation. - // Failure to set this value causes instrumentation injection to abort, leaving the original pod unchanged. - // +optional - Go Go `json:"go,omitempty"` + // Agent defines configuration for agent instrumentation. + Agent Agent `json:"agent,omitempty"` } +// Resource is the attributes that are added to the resource type Resource struct { // Attributes defines attributes that are added to the resource. // For example environment: dev @@ -94,6 +76,11 @@ type Resource struct { AddK8sUIDAttributes bool `json:"addK8sUIDAttributes,omitempty"` } +// IsEmpty is used to check if the resource is empty +func (r Resource) IsEmpty() bool { + return !r.AddK8sUIDAttributes && len(r.Attributes) == 0 +} + // Exporter defines OTLP exporter configuration. type Exporter struct { // Endpoint is address of the collector with OTLP endpoint. @@ -117,82 +104,17 @@ type Sampler struct { Argument string `json:"argument,omitempty"` } -// Java defines Java agent and instrumentation configuration. -type Java struct { - // Image is a container image with javaagent auto-instrumentation JAR. - // +optional - Image string `json:"image,omitempty"` - - // Env defines java specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -// NodeJS defines NodeJS agent and instrumentation configuration. -type NodeJS struct { - // Image is a container image with NodeJS agent and auto-instrumentation. - // +optional - Image string `json:"image,omitempty"` - - // Env defines nodejs specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -// Python defines Python agent and instrumentation configuration. -type Python struct { - // Image is a container image with Python agent and auto-instrumentation. - // +optional - Image string `json:"image,omitempty"` - - // Env defines python specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -type DotNet struct { - // Image is a container image with DotNet agent and auto-instrumentation. - // +optional - Image string `json:"image,omitempty"` - - // Env defines DotNet specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -type Php struct { - // Image is a container image with Php agent and auto-instrumentation. - // +optional - Image string `json:"image,omitempty"` - - // Env defines Php specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -type Ruby struct { - // Image is a container image with Ruby agent and auto-instrumentation. - // +optional - Image string `json:"image,omitempty"` +// Agent is the configuration for the agent +type Agent struct { + // Language is the language that will be instrumented. + Language string `json:"language,omitempty"` - // Env defines ruby specific env vars. - // If the former var had been defined, then the other vars would be ignored. - // +optional - Env []corev1.EnvVar `json:"env,omitempty"` -} - -type Go struct { // Image is a container image with Go SDK and auto-instrumentation. - // +optional Image string `json:"image,omitempty"` // 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 @@ -206,6 +128,21 @@ type Go struct { Resources corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` } +// IsEmpty is used to check if the agent is empty, excluding `.Language` +func (a *Agent) IsEmpty() bool { + return a.Image == "" && + len(a.Env) == 0 && + a.VolumeSizeLimit == nil && + len(a.Resources.Limits) == 0 && + len(a.Resources.Requests) == 0 && + len(a.Resources.Claims) == 0 +} + +// IsEqual is used to compare if an agent is equal to another, excluding `.Language` +func (a *Agent) IsEqual(b Agent) bool { + return a.Image == b.Image && reflect.DeepEqual(a.Env, b.Env) && reflect.DeepEqual(a.VolumeSizeLimit, b.VolumeSizeLimit) && reflect.DeepEqual(a.Resources, b.Resources) +} + // InstrumentationStatus defines the observed state of Instrumentation type InstrumentationStatus struct { } diff --git a/src/api/v1alpha2/instrumentation_webhook.go b/src/api/v1alpha2/instrumentation_webhook.go new file mode 100644 index 00000000..e20c18ce --- /dev/null +++ b/src/api/v1alpha2/instrumentation_webhook.go @@ -0,0 +1,113 @@ +/* +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 v1alpha2 + +import ( + "fmt" + 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" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "strings" +) + +const ( + envNewRelicPrefix = "NEW_RELIC_" + envOtelPrefix = "OTEL_" +) + +// log is for logging in this package. +var instrumentationlog = logf.Log.WithName("instrumentation-resource") + +func (r *Instrumentation) SetupWebhookWithManager(mgr ctrl.Manager, defaulter webhook.CustomDefaulter, validator webhook.CustomValidator) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(validator). + WithDefaulter(defaulter). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-newrelic-com-v1alpha2-instrumentation,mutating=true,failurePolicy=fail,sideEffects=None,groups=newrelic.com,resources=instrumentations,verbs=create;update,versions=v1alpha2,name=minstrumentation.kb.io,admissionReviewVersions=v1 +var _ webhook.Defaulter = (*Instrumentation)(nil) + +// Default implements webhook.Defaulter so a webhook will be registered for the type. +func (r *Instrumentation) Default() { + instrumentationlog.V(1).Info("default", "name", r.Name) + if r.Labels == nil { + r.Labels = map[string]string{} + } + if r.Labels["app.kubernetes.io/managed-by"] == "" { + r.Labels["app.kubernetes.io/managed-by"] = "k8s-agents-operator" + } + + if r.Spec.LicenseKeySecret == "" { + r.Spec.LicenseKeySecret = "newrelic-key-secret" + } +} + +//+kubebuilder:webhook:verbs=create;update,path=/validate-newrelic-com-v1alpha2-instrumentation,mutating=false,failurePolicy=fail,groups=newrelic.com,resources=instrumentations,versions=v1alpha2,name=vinstrumentationcreateupdate.kb.io,sideEffects=none,admissionReviewVersions=v1 +//+kubebuilder:webhook:verbs=delete,path=/validate-newrelic-com-v1alpha2-instrumentation,mutating=false,failurePolicy=ignore,groups=newrelic.com,resources=instrumentations,versions=v1alpha2,name=vinstrumentationdelete.kb.io,sideEffects=none,admissionReviewVersions=v1 + +var _ webhook.Validator = (*Instrumentation)(nil) + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (r *Instrumentation) ValidateCreate() error { + instrumentationlog.V(1).Info("validate_create", "name", r.Name) + return r.validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (r *Instrumentation) ValidateUpdate(oldObj runtime.Object) error { + instrumentationlog.V(1).Info("validate_update", "name", r.Name) + return r.validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (r *Instrumentation) ValidateDelete() error { + instrumentationlog.V(1).Info("validate_delete", "name", r.Name) + return nil +} + +func (r *Instrumentation) validate() error { + if err := r.validateEnv(r.Spec.Agent.Env); err != nil { + return err + } + + if r.Spec.Agent.IsEmpty() { + return fmt.Errorf("instrumentation %q agent is empty", r.Name) + } + + if _, err := metav1.LabelSelectorAsSelector(&r.Spec.PodLabelSelector); err != nil { + return err + } + if _, err := metav1.LabelSelectorAsSelector(&r.Spec.NamespaceLabelSelector); err != nil { + return err + } + + return nil +} + +func (r *Instrumentation) validateEnv(envs []corev1.EnvVar) error { + for _, env := range envs { + if !strings.HasPrefix(env.Name, envNewRelicPrefix) && !strings.HasPrefix(env.Name, envOtelPrefix) { + return fmt.Errorf("env name should start with \"NEW_RELIC_\" or \"OTEL_\": %s", env.Name) + } + } + return nil +} diff --git a/src/api/v1alpha1/instrumentation_webhook_test.go b/src/api/v1alpha2/instrumentation_webhook_test.go similarity index 57% rename from src/api/v1alpha1/instrumentation_webhook_test.go rename to src/api/v1alpha2/instrumentation_webhook_test.go index acd33f2f..f05b2a84 100644 --- a/src/api/v1alpha1/instrumentation_webhook_test.go +++ b/src/api/v1alpha2/instrumentation_webhook_test.go @@ -1,30 +1,12 @@ -package v1alpha1 +package v1alpha2 import ( "testing" "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/stretchr/testify/require" ) -func TestInstrumentationDefaultingWebhook(t *testing.T) { - inst := &Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - AnnotationDefaultAutoInstrumentationJava: "java-img:1", - AnnotationDefaultAutoInstrumentationNodeJS: "nodejs-img:1", - AnnotationDefaultAutoInstrumentationPython: "python-img:1", - AnnotationDefaultAutoInstrumentationDotNet: "dotnet-img:1", - }, - }, - } - inst.Default() - assert.Equal(t, "java-img:1", inst.Spec.Java.Image) - assert.Equal(t, "nodejs-img:1", inst.Spec.NodeJS.Image) - assert.Equal(t, "python-img:1", inst.Spec.Python.Image) - assert.Equal(t, "dotnet-img:1", inst.Spec.DotNet.Image) -} - func TestInstrumentationValidatingWebhook(t *testing.T) { tests := []struct { name string @@ -39,6 +21,7 @@ func TestInstrumentationValidatingWebhook(t *testing.T) { Type: ParentBasedTraceIDRatio, Argument: "0.99", }, + Agent: Agent{Language: "java", Image: "java"}, }, }, }, @@ -49,12 +32,17 @@ func TestInstrumentationValidatingWebhook(t *testing.T) { Sampler: Sampler{ Type: ParentBasedTraceIDRatio, }, + Agent: Agent{Language: "java", Image: "java"}, }, }, }, } for _, test := range tests { + test.inst.Default() + err := test.inst.ValidateCreate() + require.NoError(t, err) t.Run(test.name, func(t *testing.T) { + test.inst.Default() if test.err == "" { assert.Nil(t, test.inst.ValidateCreate()) assert.Nil(t, test.inst.ValidateUpdate(nil)) @@ -66,4 +54,4 @@ func TestInstrumentationValidatingWebhook(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/src/api/v1alpha1/propagators.go b/src/api/v1alpha2/propagators.go similarity index 94% rename from src/api/v1alpha1/propagators.go rename to src/api/v1alpha2/propagators.go index edb6a848..18571fc5 100644 --- a/src/api/v1alpha1/propagators.go +++ b/src/api/v1alpha2/propagators.go @@ -1,4 +1,4 @@ -package v1alpha1 +package v1alpha2 type ( // Propagator represents the propagation type. diff --git a/src/api/v1alpha1/samplers.go b/src/api/v1alpha2/samplers.go similarity index 98% rename from src/api/v1alpha1/samplers.go rename to src/api/v1alpha2/samplers.go index 758bc8ac..493b3584 100644 --- a/src/api/v1alpha1/samplers.go +++ b/src/api/v1alpha2/samplers.go @@ -1,4 +1,4 @@ -package v1alpha1 +package v1alpha2 type ( // SamplerType represents sampler type. diff --git a/src/api/v1alpha1/upgrade_strategy.go b/src/api/v1alpha2/upgrade_strategy.go similarity index 98% rename from src/api/v1alpha1/upgrade_strategy.go rename to src/api/v1alpha2/upgrade_strategy.go index 89a42af7..a7ded958 100644 --- a/src/api/v1alpha1/upgrade_strategy.go +++ b/src/api/v1alpha2/upgrade_strategy.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package v1alpha2 type ( // UpgradeStrategy represents how the operator will handle upgrades to the CR when a newer version of the operator is deployed diff --git a/src/api/v1alpha1/webhook_suite_test.go b/src/api/v1alpha2/webhook_suite_test.go similarity index 97% rename from src/api/v1alpha1/webhook_suite_test.go rename to src/api/v1alpha2/webhook_suite_test.go index 0a03b76a..a380afb5 100644 --- a/src/api/v1alpha1/webhook_suite_test.go +++ b/src/api/v1alpha2/webhook_suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package v1alpha2 import ( "context" @@ -99,7 +99,7 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) - err = (&Instrumentation{}).SetupWebhookWithManager(mgr) + err = ctrl.NewWebhookManagedBy(mgr).For(&Instrumentation{}).Complete() Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:webhook diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha2/zz_generated.deepcopy.go similarity index 59% rename from src/api/v1alpha1/zz_generated.deepcopy.go rename to src/api/v1alpha2/zz_generated.deepcopy.go index 30452f7b..7d96769a 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha2/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2024. @@ -19,7 +18,7 @@ limitations under the License. // Code generated by controller-gen. DO NOT EDIT. -package v1alpha1 +package v1alpha2 import ( "k8s.io/api/core/v1" @@ -27,8 +26,13 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DotNet) DeepCopyInto(out *DotNet) { +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)) @@ -36,14 +40,15 @@ func (in *DotNet) DeepCopyInto(out *DotNet) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.Resources.DeepCopyInto(&out.Resources) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DotNet. -func (in *DotNet) DeepCopy() *DotNet { +// 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(DotNet) + out := new(Agent) in.DeepCopyInto(out) return out } @@ -63,34 +68,6 @@ func (in *Exporter) DeepCopy() *Exporter { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Go) DeepCopyInto(out *Go) { - *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) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Go. -func (in *Go) DeepCopy() *Go { - if in == nil { - return nil - } - out := new(Go) - 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 @@ -161,20 +138,9 @@ func (in *InstrumentationSpec) DeepCopyInto(out *InstrumentationSpec) { copy(*out, *in) } out.Sampler = in.Sampler - 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.Java.DeepCopyInto(&out.Java) - in.NodeJS.DeepCopyInto(&out.NodeJS) - in.Python.DeepCopyInto(&out.Python) - in.DotNet.DeepCopyInto(&out.DotNet) - in.Php.DeepCopyInto(&out.Php) - in.Ruby.DeepCopyInto(&out.Ruby) - in.Go.DeepCopyInto(&out.Go) + in.PodLabelSelector.DeepCopyInto(&out.PodLabelSelector) + in.NamespaceLabelSelector.DeepCopyInto(&out.NamespaceLabelSelector) + in.Agent.DeepCopyInto(&out.Agent) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationSpec. @@ -202,116 +168,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 *Java) DeepCopyInto(out *Java) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Java. -func (in *Java) DeepCopy() *Java { - if in == nil { - return nil - } - out := new(Java) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NodeJS) DeepCopyInto(out *NodeJS) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeJS. -func (in *NodeJS) DeepCopy() *NodeJS { - if in == nil { - return nil - } - out := new(NodeJS) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Php) DeepCopyInto(out *Php) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Php. -func (in *Php) DeepCopy() *Php { - if in == nil { - return nil - } - out := new(Php) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Python) DeepCopyInto(out *Python) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Python. -func (in *Python) DeepCopy() *Python { - if in == nil { - return nil - } - out := new(Python) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Ruby) DeepCopyInto(out *Ruby) { - *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]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ruby. -func (in *Ruby) DeepCopy() *Ruby { - if in == nil { - return nil - } - out := new(Ruby) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Resource) DeepCopyInto(out *Resource) { *out = *in diff --git a/src/apm/dotnet.go b/src/apm/dotnet.go index 72b2c999..1297b954 100644 --- a/src/apm/dotnet.go +++ b/src/apm/dotnet.go @@ -16,81 +16,107 @@ limitations under the License. package apm import ( + "context" "errors" - "fmt" corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( - envDotNetCoreClrEnableProfiling = "CORECLR_ENABLE_PROFILING" - envDotNetCoreClrProfiler = "CORECLR_PROFILER" - envDotNetCoreClrProfilerPath = "CORECLR_PROFILER_PATH" - envDotNetNewrelicHome = "CORECLR_NEWRELIC_HOME" - dotNetCoreClrEnableProfilingEnabled = "1" - dotNetCoreClrProfilerID = "{36032161-FFC0-4B61-B559-F6C5D41BAE5A}" - dotNetCoreClrProfilerPath = "/newrelic-instrumentation/libNewRelicProfiler.so" - dotNetNewrelicHomePath = "/newrelic-instrumentation" - dotnetVolumeName = volumeName + "-dotnet" + envDotnetCoreClrEnableProfiling = "CORECLR_ENABLE_PROFILING" + envDotnetCoreClrProfiler = "CORECLR_PROFILER" + envDotnetCoreClrProfilerPath = "CORECLR_PROFILER_PATH" + envDotnetNewrelicHome = "CORECLR_NEWRELIC_HOME" + dotnetCoreClrEnableProfilingEnabled = "1" + dotnetCoreClrProfilerID = "{36032161-FFC0-4B61-B559-F6C5D41BAE5A}" + dotnetCoreClrProfilerPath = "/newrelic-instrumentation/libNewRelicProfiler.so" + dotnetNewrelicHomePath = "/newrelic-instrumentation" dotnetInitContainerName = initContainerName + "-dotnet" ) -func InjectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*DotnetInjector)(nil) +func init() { + DefaultInjectorRegistry.MustRegister(&DotnetInjector{}) +} + +type DotnetInjector struct { + baseInjector +} + +func (i *DotnetInjector) Language() string { + return "dotnet" +} + +func (i *DotnetInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +func (i DotnetInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] // check if CORECLR_NEWRELIC_HOME env var is already set in the container // if it is already set, then we assume that .NET newrelic-instrumentation is already configured for this container - if getIndexOfEnv(container.Env, envDotNetNewrelicHome) > -1 { + if getIndexOfEnv(container.Env, envDotnetNewrelicHome) > -1 { return pod, errors.New("CORECLR_NEWRELIC_HOME environment variable is already set in the container") } - // check if CORECLR_NEWRELIC_HOME env var is already set in the .NET instrumentatiom spec + // check if CORECLR_NEWRELIC_HOME env var is already set in the .NET instrumentation spec // if it is already set, then we assume that .NET newrelic-instrumentation is already configured for this container - if getIndexOfEnv(dotNetSpec.Env, envDotNetNewrelicHome) > -1 { + if getIndexOfEnv(inst.Spec.Agent.Env, envDotnetNewrelicHome) > -1 { return pod, errors.New("CORECLR_NEWRELIC_HOME environment variable is already set in the .NET instrumentation spec") } // inject .NET instrumentation spec env vars. - for _, env := range dotNetSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) } } - const ( - doNotConcatEnvValues = false - concatEnvValues = true - ) - - setDotNetEnvVar(container, envDotNetCoreClrEnableProfiling, dotNetCoreClrEnableProfilingEnabled, doNotConcatEnvValues) + setEnvVar(container, envDotnetCoreClrEnableProfiling, dotnetCoreClrEnableProfilingEnabled, false) + setEnvVar(container, envDotnetCoreClrProfiler, dotnetCoreClrProfilerID, false) + setEnvVar(container, envDotnetCoreClrProfilerPath, dotnetCoreClrProfilerPath, false) + setEnvVar(container, envDotnetNewrelicHome, dotnetNewrelicHomePath, false) - setDotNetEnvVar(container, envDotNetCoreClrProfiler, dotNetCoreClrProfilerID, doNotConcatEnvValues) - - setDotNetEnvVar(container, envDotNetCoreClrProfilerPath, dotNetCoreClrProfilerPath, doNotConcatEnvValues) - - setDotNetEnvVar(container, envDotNetNewrelicHome, dotNetNewrelicHomePath, doNotConcatEnvValues) - - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container. - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, dotnetInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: dotNetSpec.Image, + Name: dotnetInitContainerName, + Image: inst.Spec.Agent.Image, Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, @@ -98,22 +124,8 @@ func InjectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, index int) (cor }}, }) } - return pod, nil -} -// setDotNetEnvVar function sets env var to the container if not exist already. -// value of concatValues should be set to true if the env var supports multiple values separated by :. -// If it is set to false, the original container's env var value has priority. -func setDotNetEnvVar(container *corev1.Container, envVarName string, envVarValue string, concatValues bool) { - idx := getIndexOfEnv(container.Env, envVarName) - if idx < 0 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: envVarName, - Value: envVarValue, - }) - return - } - if concatValues { - container.Env[idx].Value = fmt.Sprintf("%s:%s", container.Env[idx].Value, envVarValue) - } + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) + + return pod, nil } diff --git a/src/apm/dotnet_test.go b/src/apm/dotnet_test.go new file mode 100644 index 00000000..f40cf9b5 --- /dev/null +++ b/src/apm/dotnet_test.go @@ -0,0 +1,107 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestDotnetInjector_Language(t *testing.T) { + require.Equal(t, "dotnet", (&DotnetInjector{}).Language()) +} + +func TestDotnetInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "dotnet"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "CORECLR_ENABLE_PROFILING", Value: "1"}, + {Name: "CORECLR_PROFILER", Value: "{36032161-FFC0-4B61-B559-F6C5D41BAE5A}"}, + {Name: "CORECLR_PROFILER_PATH", Value: "/newrelic-instrumentation/libNewRelicProfiler.so"}, + {Name: "CORECLR_NEWRELIC_HOME", Value: "/newrelic-instrumentation"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-dotnet", + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "dotnet"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &DotnetInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/golang.go b/src/apm/golang.go index cd278008..6bc165f2 100644 --- a/src/apm/golang.go +++ b/src/apm/golang.go @@ -1,47 +1,87 @@ package apm import ( + "context" "fmt" + "sort" "strings" + "unsafe" + semconv "go.opentelemetry.io/otel/semconv/v1.5.0" corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( - envOtelTargetExe = "OTEL_GO_AUTO_TARGET_EXE" - + annotationGoExecPath = "instrumentation.opentelemetry.io/otel-go-auto-target-exe" + envOtelTargetExe = "OTEL_GO_AUTO_TARGET_EXE" kernelDebugVolumeName = "kernel-debug" kernelDebugVolumePath = "/sys/kernel/debug" + sideCarName = "opentelemetry-auto-instrumentation" ) -func InjectGoSDK(goSpec v1alpha1.Go, pod corev1.Pod) (corev1.Pod, error) { - // skip instrumentation if share process namespaces is explicitly disabled - if pod.Spec.ShareProcessNamespace != nil && !*pod.Spec.ShareProcessNamespace { - return pod, fmt.Errorf("shared process namespace has been explicitly disabled") +const ( + EnvOTELServiceName = "OTEL_SERVICE_NAME" + EnvOTELExporterOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT" + EnvOTELResourceAttrs = "OTEL_RESOURCE_ATTRIBUTES" + EnvOTELPropagators = "OTEL_PROPAGATORS" + EnvOTELTracesSampler = "OTEL_TRACES_SAMPLER" + EnvOTELTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG" + + EnvPodName = "OTEL_RESOURCE_ATTRIBUTES_POD_NAME" + EnvPodUID = "OTEL_RESOURCE_ATTRIBUTES_POD_UID" + EnvNodeName = "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME" +) + +var _ Injector = (*GoInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&GoInjector{}) +} + +type GoInjector struct { + baseInjector +} + +func (i *GoInjector) Language() string { + return "go" +} + +func (i *GoInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} - // skip instrumentation when more than one containers provided - containerNames := "" - ok := false - containerNames, ok = pod.Annotations[annotationInjectContainerName] +func (i *GoInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } - if ok && len(strings.Split(containerNames, ",")) > 1 { - return pod, fmt.Errorf("go instrumentation cannot be injected into a pod, multiple containers configured") + // skip instrumentation if share process namespaces is explicitly disabled + if pod.Spec.ShareProcessNamespace != nil && !*pod.Spec.ShareProcessNamespace { + return pod, fmt.Errorf("shared process namespace has been explicitly disabled") } - true := true - zero := int64(0) - pod.Spec.ShareProcessNamespace = &true + vtrue := true + vzero := int64(0) + pod.Spec.ShareProcessNamespace = &vtrue goAgent := corev1.Container{ Name: sideCarName, - Image: goSpec.Image, - Resources: goSpec.Resources, + Image: inst.Spec.Agent.Image, + Resources: inst.Spec.Agent.Resources, SecurityContext: &corev1.SecurityContext{ - RunAsUser: &zero, - Privileged: &true, + RunAsUser: &vzero, + Privileged: &vtrue, }, VolumeMounts: []corev1.VolumeMount{ { @@ -62,7 +102,7 @@ func InjectGoSDK(goSpec v1alpha1.Go, pod corev1.Pod) (corev1.Pod, error) { // Inject Go instrumentation spec env vars. // For Go, env vars must be added to the agent contain - for _, env := range goSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(goAgent.Env, env.Name) if idx == -1 { goAgent.Env = append(goAgent.Env, env) @@ -78,5 +118,187 @@ func InjectGoSDK(goSpec v1alpha1.Go, pod corev1.Pod) (corev1.Pod, error) { }, }, }) + + lastIndex := len(pod.Spec.Containers) - 1 + pod = i.injectEnvVar(inst, pod, lastIndex) + pod = i.injectCommonSDKConfig(ctx, inst, ns, pod, lastIndex, 0) + return pod, nil } + +func (i *GoInjector) injectEnvVar(newrelic v1alpha2.Instrumentation, pod corev1.Pod, index int) corev1.Pod { + container := &pod.Spec.Containers[index] + for _, env := range newrelic.Spec.Agent.Env { + idx := getIndexOfEnv(container.Env, env.Name) + if idx == -1 { + container.Env = append(container.Env, env) + } + //@todo: should we update this if it exists? + } + return pod +} + +// injectCommonSDKConfig adds common SDK configuration environment variables to the necessary pod +// agentIndex represents the index of the pod the needs the env vars to instrument the application. +// appIndex represents the index of the pod the will produce the telemetry. +// When the pod handling the instrumentation is the same as the pod producing the telemetry agentIndex +// and appIndex should be the same value. This is true for dotnet, java, nodejs, python, and ruby instrumentations. +// Go requires the agent to be a different container in the pod, so the agentIndex should represent this new sidecar +// and appIndex should represent the application being instrumented. +func (i *GoInjector) injectCommonSDKConfig(ctx context.Context, newrelic v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod, agentIndex int, appIndex int) corev1.Pod { + container := &pod.Spec.Containers[agentIndex] + resourceMap := i.createResourceMap(ctx, newrelic.Spec.Resource, ns, pod, appIndex) + idx := getIndexOfEnv(container.Env, EnvOTELServiceName) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELServiceName, + Value: chooseServiceName(pod, resourceMap, appIndex), + }) + } + if newrelic.Spec.Exporter.Endpoint != "" { + idx = getIndexOfEnv(container.Env, EnvOTELExporterOTLPEndpoint) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELExporterOTLPEndpoint, + Value: newrelic.Spec.Endpoint, + }) + } + } + + // Some attributes might be empty, we should get them via k8s downward API + if resourceMap[string(semconv.K8SPodNameKey)] == "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvPodName, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }) + resourceMap[string(semconv.K8SPodNameKey)] = fmt.Sprintf("$(%s)", EnvPodName) + } + if newrelic.Spec.Resource.AddK8sUIDAttributes { + if resourceMap[string(semconv.K8SPodUIDKey)] == "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvPodUID, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.uid", + }, + }, + }) + resourceMap[string(semconv.K8SPodUIDKey)] = fmt.Sprintf("$(%s)", EnvPodUID) + } + } + + idx = getIndexOfEnv(container.Env, EnvOTELResourceAttrs) + if idx == -1 || !strings.Contains(container.Env[idx].Value, string(semconv.ServiceVersionKey)) { + vsn := chooseServiceVersion(pod, appIndex) + if vsn != "" { + resourceMap[string(semconv.ServiceVersionKey)] = vsn + } + } + + if resourceMap[string(semconv.K8SNodeNameKey)] == "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvNodeName, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "spec.nodeName", + }, + }, + }) + resourceMap[string(semconv.K8SNodeNameKey)] = fmt.Sprintf("$(%s)", EnvNodeName) + } + + idx = getIndexOfEnv(container.Env, EnvOTELResourceAttrs) + resStr := resourceMapToStr(resourceMap) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELResourceAttrs, + Value: resStr, + }) + } else { + if !strings.HasSuffix(container.Env[idx].Value, ",") { + resStr = "," + resStr + } + container.Env[idx].Value += resStr + } + + idx = getIndexOfEnv(container.Env, EnvOTELPropagators) + if idx == -1 && len(newrelic.Spec.Propagators) > 0 { + propagators := *(*[]string)((unsafe.Pointer(&newrelic.Spec.Propagators))) + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELPropagators, + Value: strings.Join(propagators, ","), + }) + } + + idx = getIndexOfEnv(container.Env, EnvOTELTracesSampler) + // configure sampler only if it is configured in the CR + if idx == -1 && newrelic.Spec.Sampler.Type != "" { + idxSamplerArg := getIndexOfEnv(container.Env, EnvOTELTracesSamplerArg) + if idxSamplerArg == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELTracesSampler, + Value: string(newrelic.Spec.Sampler.Type), + }) + if newrelic.Spec.Sampler.Argument != "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvOTELTracesSamplerArg, + Value: newrelic.Spec.Sampler.Argument, + }) + } + } + } + + // Move OTEL_RESOURCE_ATTRIBUTES to last position on env list. + // When OTEL_RESOURCE_ATTRIBUTES environment variable uses other env vars + // as attributes value they have to be configured before. + // It is mandatory to set right order to avoid attributes with value + // pointing to the name of used environment variable instead of its value. + idx = getIndexOfEnv(container.Env, EnvOTELResourceAttrs) + envs := moveEnvToListEnd(container.Env, idx) + container.Env = envs + + return pod +} + +func moveEnvToListEnd(envs []corev1.EnvVar, idx int) []corev1.EnvVar { + if idx >= 0 && idx < len(envs) { + envToMove := envs[idx] + envs = append(envs[:idx], envs[idx+1:]...) + envs = append(envs, envToMove) + } + + return envs +} + +func resourceMapToStr(res map[string]string) string { + keys := make([]string, 0, len(res)) + for k := range res { + keys = append(keys, k) + } + sort.Strings(keys) + + var str = "" + for _, k := range keys { + if str != "" { + str += "," + } + str += fmt.Sprintf("%s=%s", k, res[k]) + } + + return str +} + +// obtains version by splitting image string on ":" and extracting final element from resulting array. +func chooseServiceVersion(pod corev1.Pod, index int) string { + parts := strings.Split(pod.Spec.Containers[index].Image, ":") + tag := parts[len(parts)-1] + //guard statement to handle case where image name has a port number + if strings.Contains(tag, "/") { + return "" + } + return tag +} diff --git a/src/apm/golang_test.go b/src/apm/golang_test.go new file mode 100644 index 00000000..2e865bcc --- /dev/null +++ b/src/apm/golang_test.go @@ -0,0 +1,107 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestGoInjector_Language(t *testing.T) { + require.Equal(t, "go", (&GoInjector{}).Language()) +} + +func TestGoInjector_Inject(t *testing.T) { + vtrue := true + var vzero int64 + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "go"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "test"}}}}, + expectedPod: corev1.Pod{ + Spec: corev1.PodSpec{ + ShareProcessNamespace: &vtrue, + Containers: []corev1.Container{ + {Name: "test"}, + { + Name: "opentelemetry-auto-instrumentation", + Image: "", + Env: []corev1.EnvVar{ + {Name: "OTEL_SERVICE_NAME", Value: "test"}, + {Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}, + {Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "spec.nodeName"}}}, + {Name: "OTEL_RESOURCE_ATTRIBUTES", Value: "k8s.container.name=test,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "kernel-debug", MountPath: "/sys/kernel/debug"}}, + SecurityContext: &corev1.SecurityContext{Privileged: &vtrue, RunAsUser: &vzero}, + }, + }, + Volumes: []corev1.Volume{ + {Name: "kernel-debug", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/sys/kernel/debug"}}}, + }, + }, + }, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "go"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &GoInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/helper.go b/src/apm/helper.go index 1ad78baf..f5b3cb74 100644 --- a/src/apm/helper.go +++ b/src/apm/helper.go @@ -16,39 +16,116 @@ limitations under the License. package apm import ( + "context" + "errors" "fmt" + "strings" + "sync" + "time" + "github.com/go-logr/logr" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.5.0" + appsv1 "k8s.io/api/apps/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/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) +const LicenseKey = "new_relic_license_key" + const ( volumeName = "newrelic-instrumentation" initContainerName = "newrelic-instrumentation" - sideCarName = "opentelemetry-auto-instrumentation" - - // indicates whether newrelic agents should be injected or not. - // Possible values are "true", "false" or "" name. - annotationInjectJava = "instrumentation.newrelic.com/inject-java" - annotationInjectJavaContainersName = "instrumentation.newrelic.com/java-container-names" - annotationInjectNodeJS = "instrumentation.newrelic.com/inject-nodejs" - annotationInjectNodeJSContainersName = "instrumentation.newrelic.com/nodejs-container-names" - annotationInjectPython = "instrumentation.newrelic.com/inject-python" - annotationInjectPythonContainersName = "instrumentation.newrelic.com/python-container-names" - annotationInjectDotNet = "instrumentation.newrelic.com/inject-dotnet" - annotationInjectDotnetContainersName = "instrumentation.newrelic.com/dotnet-container-names" - annotationInjectPhp = "instrumentation.newrelic.com/inject-php" - annotationInjectPhpContainersName = "instrumentation.newrelic.com/php-container-names" - annotationInjectRuby = "instrumentation.newrelic.com/inject-ruby" - annotationInjectRubyContainersName = "instrumentation.newrelic.com/ruby-container-names" - annotationPhpExecCmd = "instrumentation.newrelic.com/php-exec-command" - annotationInjectContainerName = "instrumentation.newrelic.com/container-name" - annotationInjectGo = "instrumentation.opentelemetry.io/inject-go" - annotationGoExecPath = "instrumentation.opentelemetry.io/otel-go-auto-target-exe" - annotationInjectGoContainerName = "instrumentation.opentelemetry.io/go-container-name" ) +const ( + EnvNewRelicAppName = "NEW_RELIC_APP_NAME" + EnvNewRelicK8sOperatorEnabled = "NEW_RELIC_K8S_OPERATOR_ENABLED" + EnvNewRelicLabels = "NEW_RELIC_LABELS" + EnvNewRelicLicenseKey = "NEW_RELIC_LICENSE_KEY" +) + +var ErrInjectorAlreadyRegistered = errors.New("injector already registered in registry") + +type Injector interface { + Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) + Language() string + ConfigureClient(client client.Client) + ConfigureLogger(logger logr.Logger) +} + +type Injectors []Injector + +func (i Injectors) Names() []string { + injectorNames := make([]string, len(i)) + for j, injector := range i { + injectorNames[j] = injector.Language() + } + return injectorNames +} + +type InjectorRegistery struct { + injectors []Injector + injectorMap map[string]struct{} + mu *sync.Mutex +} + +func NewInjectorRegistry() *InjectorRegistery { + return &InjectorRegistery{ + injectorMap: make(map[string]struct{}), + mu: &sync.Mutex{}, + } +} + +func (ir *InjectorRegistery) Register(injector Injector) error { + ir.mu.Lock() + defer ir.mu.Unlock() + if _, ok := ir.injectorMap[injector.Language()]; ok { + return ErrInjectorAlreadyRegistered + } + ir.injectors = append(ir.injectors, injector) + return nil +} + +func (ir *InjectorRegistery) MustRegister(injector Injector) { + err := ir.Register(injector) + if err != nil { + panic(err) + } +} + +func (ir *InjectorRegistery) GetInjectors() Injectors { + ir.mu.Lock() + defer ir.mu.Unlock() + injectors := make([]Injector, len(ir.injectors)) + copy(injectors, ir.injectors) + return injectors +} + +var DefaultInjectorRegistry = NewInjectorRegistry() + +func getContainerIndex(containerName string, pod corev1.Pod) int { + // We search for specific container to inject variables and if no one is found + // We fallback to first container + var index = 0 + for idx, ctnair := range pod.Spec.Containers { + if ctnair.Name == containerName { + index = idx + } + } + + return index +} + // Calculate if we already inject InitContainers. -func isInitContainerMissing(pod corev1.Pod) bool { +func isInitContainerMissing(pod corev1.Pod, initContainerName string) bool { for _, initContainer := range pod.Spec.InitContainers { if initContainer.Name == initContainerName { return false @@ -57,6 +134,26 @@ func isInitContainerMissing(pod corev1.Pod) bool { return true } +// Calculate if we already inject a Volume. +func isPodVolumeMissing(pod corev1.Pod, volumeName string) bool { + for _, volume := range pod.Spec.Volumes { + if volume.Name == volumeName { + return false + } + } + return true +} + +// Calculate if we already inject a Volume. +func isContainerVolumeMissing(container *corev1.Container, volumeName string) bool { + for _, volume := range container.VolumeMounts { + if volume.Name == volumeName { + return false + } + } + return true +} + func getIndexOfEnv(envs []corev1.EnvVar, name string) int { for i := range envs { if envs[i].Name == name { @@ -66,6 +163,23 @@ func getIndexOfEnv(envs []corev1.EnvVar, name string) int { return -1 } +// setEnvVar function sets env var to the container if not exist already. +// value of concatValues should be set to true if the env var supports multiple values separated by :. +// If it is set to false, the original container's env var value has priority. +func setEnvVar(container *corev1.Container, envVarName string, envVarValue string, concatValues bool) { + idx := getIndexOfEnv(container.Env, envVarName) + if idx < 0 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envVarName, + Value: envVarValue, + }) + return + } + if concatValues { + container.Env[idx].Value = fmt.Sprintf("%s:%s", container.Env[idx].Value, envVarValue) + } +} + func validateContainerEnv(envs []corev1.EnvVar, envsToBeValidated ...string) error { for _, envToBeValidated := range envsToBeValidated { for _, containerEnv := range envs { @@ -79,3 +193,191 @@ func validateContainerEnv(envs []corev1.EnvVar, envsToBeValidated ...string) err } return nil } + +type baseInjector struct { + logger logr.Logger + client client.Client +} + +func (i *baseInjector) ConfigureLogger(logger logr.Logger) { + i.logger = logger +} + +func (i *baseInjector) ConfigureClient(client client.Client) { + i.client = client +} + +func (i *baseInjector) validate(inst v1alpha2.Instrumentation) error { + if inst.Spec.LicenseKeySecret == "" { + return fmt.Errorf("licenseKeySecret must not be blank") + } + return nil +} + +func (i *baseInjector) injectNewrelicConfig(ctx context.Context, resource v1alpha2.Resource, ns corev1.Namespace, pod corev1.Pod, index int, licenseKeySecretName string) corev1.Pod { + container := &pod.Spec.Containers[index] + + if idx := getIndexOfEnv(container.Env, EnvNewRelicAppName); idx == -1 { + //@todo: how can we do this if multiple injectors need this? + resourceMap := i.createResourceMap(ctx, resource, ns, pod, index) + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvNewRelicAppName, + Value: chooseServiceName(pod, resourceMap, index), + }) + } + if idx := getIndexOfEnv(container.Env, EnvNewRelicLicenseKey); idx == -1 { + optional := true + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvNewRelicLicenseKey, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: licenseKeySecretName}, + Key: LicenseKey, + Optional: &optional, + }, + }, + }) + } + if idx := getIndexOfEnv(container.Env, EnvNewRelicLabels); idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvNewRelicLabels, + Value: "operator:auto-injection", + }) + } + if idx := getIndexOfEnv(container.Env, EnvNewRelicK8sOperatorEnabled); idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: EnvNewRelicK8sOperatorEnabled, + Value: "true", + }) + } + return pod +} + +// createResourceMap creates resource attribute map. +// User defined attributes (in explicitly set env var) have higher precedence. +func (i *baseInjector) createResourceMap(ctx context.Context, resource v1alpha2.Resource, ns corev1.Namespace, pod corev1.Pod, index int) map[string]string { + + //@todo: revise this? this is specific to the golang apm + // get existing resources env var and parse it into a map + existingRes := map[string]bool{} + existingResourceEnvIdx := getIndexOfEnv(pod.Spec.Containers[index].Env, EnvOTELResourceAttrs) + if existingResourceEnvIdx > -1 { + existingResArr := strings.Split(pod.Spec.Containers[index].Env[existingResourceEnvIdx].Value, ",") + for _, kv := range existingResArr { + keyValueArr := strings.Split(strings.TrimSpace(kv), "=") + if len(keyValueArr) != 2 { + continue + } + existingRes[keyValueArr[0]] = true + } + } + + res := map[string]string{} + for k, v := range resource.Attributes { + if !existingRes[k] { + res[k] = v + } + } + k8sResources := map[attribute.Key]string{} + k8sResources[semconv.K8SNamespaceNameKey] = ns.Name + k8sResources[semconv.K8SContainerNameKey] = pod.Spec.Containers[index].Name + // Some fields might be empty - node name, pod name + // The pod name might be empty if the pod is created form deployment template + k8sResources[semconv.K8SPodNameKey] = pod.Name + k8sResources[semconv.K8SPodUIDKey] = string(pod.UID) + k8sResources[semconv.K8SNodeNameKey] = pod.Spec.NodeName + k8sResources[semconv.ServiceInstanceIDKey] = createServiceInstanceId(ns.Name, pod.Name, pod.Spec.Containers[index].Name) + i.addParentResourceLabels(ctx, resource.AddK8sUIDAttributes, ns, pod.ObjectMeta, k8sResources) + for k, v := range k8sResources { + if !existingRes[string(k)] && v != "" { + res[string(k)] = v + } + } + return res +} + +func (i *baseInjector) addParentResourceLabels(ctx context.Context, uid bool, ns corev1.Namespace, objectMeta metav1.ObjectMeta, resources map[attribute.Key]string) { + for _, owner := range objectMeta.OwnerReferences { + switch strings.ToLower(owner.Kind) { + case "replicaset": + resources[semconv.K8SReplicaSetNameKey] = owner.Name + if uid { + resources[semconv.K8SReplicaSetUIDKey] = string(owner.UID) + } + // parent of ReplicaSet is e.g. Deployment which we are interested to know + rs := appsv1.ReplicaSet{} + nsn := types.NamespacedName{Namespace: ns.Name, Name: owner.Name} + backOff := wait.Backoff{Duration: 10 * time.Millisecond, Factor: 1.5, Jitter: 0.1, Steps: 20, Cap: 2 * time.Second} + + checkError := func(err error) bool { + return apierrors.IsNotFound(err) + } + + getReplicaSet := func() error { + return i.client.Get(ctx, nsn, &rs) + } + + // use a retry loop to get the Deployment. A single call to client.get fails occasionally + err := retry.OnError(backOff, checkError, getReplicaSet) + if err != nil { + i.logger.Error(err, "failed to get replicaset", "replicaset", nsn.Name, "namespace", nsn.Namespace) + } + i.addParentResourceLabels(ctx, uid, ns, rs.ObjectMeta, resources) + case "deployment": + resources[semconv.K8SDeploymentNameKey] = owner.Name + if uid { + resources[semconv.K8SDeploymentUIDKey] = string(owner.UID) + } + case "statefulset": + resources[semconv.K8SStatefulSetNameKey] = owner.Name + if uid { + resources[semconv.K8SStatefulSetUIDKey] = string(owner.UID) + } + case "daemonset": + resources[semconv.K8SDaemonSetNameKey] = owner.Name + if uid { + resources[semconv.K8SDaemonSetUIDKey] = string(owner.UID) + } + case "job": + resources[semconv.K8SJobNameKey] = owner.Name + if uid { + resources[semconv.K8SJobUIDKey] = string(owner.UID) + } + case "cronjob": + resources[semconv.K8SCronJobNameKey] = owner.Name + if uid { + resources[semconv.K8SCronJobUIDKey] = string(owner.UID) + } + } + } +} + +func chooseServiceName(pod corev1.Pod, resources map[string]string, index int) string { + if name := resources[string(semconv.K8SDeploymentNameKey)]; name != "" { + return name + } + if name := resources[string(semconv.K8SStatefulSetNameKey)]; name != "" { + return name + } + if name := resources[string(semconv.K8SJobNameKey)]; name != "" { + return name + } + if name := resources[string(semconv.K8SCronJobNameKey)]; name != "" { + return name + } + if name := resources[string(semconv.K8SPodNameKey)]; name != "" { + return name + } + return pod.Spec.Containers[index].Name +} + +// creates the service.instance.id following the semantic defined by +// https://github.com/open-telemetry/semantic-conventions/pull/312. +func createServiceInstanceId(namespaceName, podName, containerName string) string { + var serviceInstanceId string + if namespaceName != "" && podName != "" && containerName != "" { + resNames := []string{namespaceName, podName, containerName} + serviceInstanceId = strings.Join(resNames, ".") + } + return serviceInstanceId +} diff --git a/src/apm/helper_test.go b/src/apm/helper_test.go new file mode 100644 index 00000000..ee76c51d --- /dev/null +++ b/src/apm/helper_test.go @@ -0,0 +1,23 @@ +package apm + +import "testing" + +func TestBaseInjector_ConfigureClient(t *testing.T) { + +} + +func TestBaseInjector_ConfigureLogger(t *testing.T) { + +} + +func TestInjectorRegistery_Register(t *testing.T) { + +} + +func TestInjectorRegistery_MustRegister(t *testing.T) { + +} + +func TestInjectors_Names(t *testing.T) { + +} diff --git a/src/apm/javaagent.go b/src/apm/java.go similarity index 53% rename from src/apm/javaagent.go rename to src/apm/java.go index 8b07bf9b..6bbdaa61 100644 --- a/src/apm/javaagent.go +++ b/src/apm/java.go @@ -16,21 +16,54 @@ limitations under the License. package apm import ( + "context" + corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( envJavaToolsOptions = "JAVA_TOOL_OPTIONS" javaJVMArgument = " -javaagent:/newrelic-instrumentation/newrelic-agent.jar" javaInitContainerName = initContainerName + "-java" - javaVolumeName = volumeName + "-java" ) -func InjectJavaagent(javaSpec v1alpha1.Java, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*JavaInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&JavaInjector{}) +} + +type JavaInjector struct { + baseInjector +} + +func (i *JavaInjector) Language() string { + return "java" +} + +func (i *JavaInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +func (i *JavaInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] err := validateContainerEnv(container.Env, envJavaToolsOptions) if err != nil { @@ -38,7 +71,7 @@ func InjectJavaagent(javaSpec v1alpha1.Java, pod corev1.Pod, index int) (corev1. } // inject Java instrumentation spec env vars. - for _, env := range javaSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) @@ -55,22 +88,26 @@ func InjectJavaagent(javaSpec v1alpha1.Java, pod corev1.Pod, index int) (corev1. container.Env[idx].Value = container.Env[idx].Value + javaJVMArgument } - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container. - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, javaInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: javaSpec.Image, + Name: javaInitContainerName, + Image: inst.Spec.Agent.Image, Command: []string{"cp", "/newrelic-agent.jar", "/newrelic-instrumentation/newrelic-agent.jar"}, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, @@ -78,5 +115,8 @@ func InjectJavaagent(javaSpec v1alpha1.Java, pod corev1.Pod, index int) (corev1. }}, }) } - return pod, err + + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) + + return pod, nil } diff --git a/src/apm/java_test.go b/src/apm/java_test.go new file mode 100644 index 00000000..98f0709d --- /dev/null +++ b/src/apm/java_test.go @@ -0,0 +1,104 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestJavaInjector_Language(t *testing.T) { + require.Equal(t, "java", (&JavaInjector{}).Language()) +} + +func TestJavaInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "JAVA_TOOL_OPTIONS", Value: " -javaagent:/newrelic-instrumentation/newrelic-agent.jar"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-java", + Command: []string{"cp", "/newrelic-agent.jar", "/newrelic-instrumentation/newrelic-agent.jar"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &JavaInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/nodejs.go b/src/apm/nodejs.go index fd4ef0f7..4728237e 100644 --- a/src/apm/nodejs.go +++ b/src/apm/nodejs.go @@ -16,21 +16,54 @@ limitations under the License. package apm import ( + "context" + corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( envNodeOptions = "NODE_OPTIONS" nodeRequireArgument = " --require /newrelic-instrumentation/newrelicinstrumentation.js" nodejsInitContainerName = initContainerName + "-nodejs" - nodejsVolumeName = volumeName + "-nodejs" ) -func InjectNodeJSSDK(nodeJSSpec v1alpha1.NodeJS, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*NodejsInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&NodejsInjector{}) +} + +type NodejsInjector struct { + baseInjector +} + +func (i *NodejsInjector) Language() string { + return "nodejs" +} + +func (i *NodejsInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +func (i *NodejsInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] err := validateContainerEnv(container.Env, envNodeOptions) if err != nil { @@ -38,7 +71,7 @@ func InjectNodeJSSDK(nodeJSSpec v1alpha1.NodeJS, pod corev1.Pod, index int) (cor } // inject NodeJS instrumentation spec env vars. - for _, env := range nodeJSSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) @@ -55,22 +88,26 @@ func InjectNodeJSSDK(nodeJSSpec v1alpha1.NodeJS, pod corev1.Pod, index int) (cor container.Env[idx].Value = container.Env[idx].Value + nodeRequireArgument } - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, nodejsInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: nodeJSSpec.Image, + Name: nodejsInitContainerName, + Image: inst.Spec.Agent.Image, Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, @@ -78,5 +115,8 @@ func InjectNodeJSSDK(nodeJSSpec v1alpha1.NodeJS, pod corev1.Pod, index int) (cor }}, }) } + + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) + return pod, nil } diff --git a/src/apm/nodejs_test.go b/src/apm/nodejs_test.go new file mode 100644 index 00000000..37928675 --- /dev/null +++ b/src/apm/nodejs_test.go @@ -0,0 +1,104 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestNodejsInjector_Language(t *testing.T) { + require.Equal(t, "nodejs", (&NodejsInjector{}).Language()) +} + +func TestNodejsInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "nodejs"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "NODE_OPTIONS", Value: " --require /newrelic-instrumentation/newrelicinstrumentation.js"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-nodejs", + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "nodejs"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &NodejsInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/php.go b/src/apm/php.go index 70984aeb..f0620d17 100644 --- a/src/apm/php.go +++ b/src/apm/php.go @@ -16,61 +16,119 @@ limitations under the License. package apm import ( - "fmt" + "context" + "errors" corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( - envPhpsymbolicOption = "NR_INSTALL_USE_CP_NOT_LN" - phpSymbolicOptionArgument = "1" - envPhpSilentOption = "NR_INSTALL_SILENT" - phpSilentOptionArgument = "1" - phpInitContainerName = initContainerName + "-php" - phpVolumeName = volumeName + "-php" - phpInstallArgument = "/newrelic-instrumentation/newrelic-install install && sed -i -e \"s/PHP Application/$NEW_RELIC_APP_NAME/g; s/REPLACE_WITH_REAL_KEY/$NEW_RELIC_LICENSE_KEY/g\" /usr/local/etc/php/conf.d/newrelic.ini" + annotationPhpVersion = "instrumentation.newrelic.com/php-version" + envIniScanDirKey = "PHP_INI_SCAN_DIR" + envIniScanDirVal = "/newrelic-instrumentation/php-agent/ini" + phpInitContainerName = initContainerName + "-php" ) -func InjectPhpagent(phpSpec v1alpha1.Php, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*PhpInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&PhpInjector{}) +} + +// Deprecated: phpApiMap is deprecated. Do not use annotations. +var phpApiMap = map[string]string{ + "7.2": "20170718", + "7.3": "20180731", + "7.4": "20190902", + "8.0": "20200930", + "8.1": "20210902", + "8.2": "20220829", + "8.3": "20230831", +} + +type PhpInjector struct { + baseInjector +} + +func (i *PhpInjector) Language() string { + return "php" +} + +func (i *PhpInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +// Inject is used to inject the PHP agent. +// @todo: Currently it uses annotations, which should be removed. This should either use a specific image for each php version or the k8s-agents-operator needs to add support for a language version +func (i *PhpInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 + + // exit early if we're missing mandatory annotations + // Deprecated: phpVer is deprecated. Do not use annotations. + phpVer, ok := pod.Annotations[annotationPhpVersion] + if !ok { + return pod, errors.New("missing php version annotation") + } + + // Deprecated: apiNum is deprecated. Do not use annotations. + apiNum, ok := phpApiMap[phpVer] + if !ok { + return pod, errors.New("invalid php version") + } + // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] // inject PHP instrumentation spec env vars. - for _, env := range phpSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) } } - const ( - phpConcatEnvValues = false - concatEnvValues = true - ) - - setPhpEnvVar(container, envPhpsymbolicOption, phpSymbolicOptionArgument, phpConcatEnvValues) + setEnvVar(container, envIniScanDirKey, envIniScanDirVal, true) - setPhpEnvVar(container, envPhpSilentOption, phpSilentOptionArgument, phpConcatEnvValues) - - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container. - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, nodejsInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: phpSpec.Image, - Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + Name: phpInitContainerName, + Image: inst.Spec.Agent.Image, + Command: []string{"/bin/sh"}, + Args: []string{ + "-c", "cp -a /instrumentation/. /newrelic-instrumentation/ && /newrelic-instrumentation/k8s-php-install.sh " + apiNum + " && /newrelic-instrumentation/nr_env_to_ini.sh", + }, + Env: container.Env, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, MountPath: "/newrelic-instrumentation", @@ -78,31 +136,7 @@ func InjectPhpagent(phpSpec v1alpha1.Php, pod corev1.Pod, index int) (corev1.Pod }) } - // Continue with the function regardless of whether annotationPhpExecCmd is present or not - execCmd, ok := pod.Annotations[annotationPhpExecCmd] - if ok { - // Add phpInstallArgument to the command field. - container.Command = append(container.Command, "/bin/sh", "-c", phpInstallArgument+" && "+execCmd) - } else { - container.Command = append(container.Command, "/bin/sh", "-c", phpInstallArgument) - } + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) return pod, nil } - -// setDotNetEnvVar function sets env var to the container if not exist already. -// value of concatValues should be set to true if the env var supports multiple values separated by :. -// If it is set to false, the original container's env var value has priority. -func setPhpEnvVar(container *corev1.Container, envVarName string, envVarValue string, concatValues bool) { - idx := getIndexOfEnv(container.Env, envVarName) - if idx < 0 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: envVarName, - Value: envVarValue, - }) - return - } - if concatValues { - container.Env[idx].Value = fmt.Sprintf("%s:%s", container.Env[idx].Value, envVarValue) - } -} diff --git a/src/apm/php_test.go b/src/apm/php_test.go new file mode 100644 index 00000000..a1c178b9 --- /dev/null +++ b/src/apm/php_test.go @@ -0,0 +1,111 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestPhpInjector_Language(t *testing.T) { + require.Equal(t, "php", (&PhpInjector{}).Language()) +} + +func TestPhpInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"instrumentation.newrelic.com/php-version": "8.3"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "test"}}}, + }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"instrumentation.newrelic.com/php-version": "8.3"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "PHP_INI_SCAN_DIR", Value: "/newrelic-instrumentation/php-agent/ini"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-php", + Env: []corev1.EnvVar{{Name: "PHP_INI_SCAN_DIR", Value: "/newrelic-instrumentation/php-agent/ini"}}, + Command: []string{"/bin/sh"}, + Args: []string{"-c", "cp -a /instrumentation/. /newrelic-instrumentation/ && /newrelic-instrumentation/k8s-php-install.sh 20230831 && /newrelic-instrumentation/nr_env_to_ini.sh"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }, + }, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &PhpInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/python.go b/src/apm/python.go index 50519186..c49b1dc0 100644 --- a/src/apm/python.go +++ b/src/apm/python.go @@ -16,23 +16,55 @@ limitations under the License. package apm import ( + "context" "fmt" corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( envPythonPath = "PYTHONPATH" pythonPathPrefix = "/newrelic-instrumentation" - pythonVolumeName = volumeName + "-python" pythonInitContainerName = initContainerName + "-python" ) -func InjectPythonSDK(pythonSpec v1alpha1.Python, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*GoInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&PythonInjector{}) +} + +type PythonInjector struct { + baseInjector +} + +func (i *PythonInjector) Language() string { + return "python" +} + +func (i *PythonInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +func (i *PythonInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] err := validateContainerEnv(container.Env, envPythonPath) if err != nil { @@ -40,7 +72,7 @@ func InjectPythonSDK(pythonSpec v1alpha1.Python, pod corev1.Pod, index int) (cor } // inject Python instrumentation spec env vars. - for _, env := range pythonSpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) @@ -57,22 +89,26 @@ func InjectPythonSDK(pythonSpec v1alpha1.Python, pod corev1.Pod, index int) (cor container.Env[idx].Value = fmt.Sprintf("%s:%s", pythonPathPrefix, container.Env[idx].Value) } - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container. - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, pythonInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: pythonSpec.Image, + Name: pythonInitContainerName, + Image: inst.Spec.Agent.Image, Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, @@ -80,5 +116,8 @@ func InjectPythonSDK(pythonSpec v1alpha1.Python, pod corev1.Pod, index int) (cor }}, }) } + + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) + return pod, nil } diff --git a/src/apm/python_test.go b/src/apm/python_test.go new file mode 100644 index 00000000..ecb2cbbf --- /dev/null +++ b/src/apm/python_test.go @@ -0,0 +1,104 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestPythonInjector_Language(t *testing.T) { + require.Equal(t, "python", (&PythonInjector{}).Language()) +} + +func TestPythonInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "python"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "PYTHONPATH", Value: "/newrelic-instrumentation"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-python", + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "python"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &PythonInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/apm/ruby.go b/src/apm/ruby.go index 5bcbadd4..eb11dd7f 100644 --- a/src/apm/ruby.go +++ b/src/apm/ruby.go @@ -16,21 +16,54 @@ limitations under the License. package apm import ( + "context" + corev1 "k8s.io/api/core/v1" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) const ( envRubyOpt = "RUBYOPT" rubyOptRequire = "-r /newrelic-instrumentation/lib/boot/strap" - rubyVolumeName = volumeName + "-ruby" rubyInitContainerName = initContainerName + "-ruby" ) -func InjectRubySDK(rubySpec v1alpha1.Ruby, pod corev1.Pod, index int) (corev1.Pod, error) { +var _ Injector = (*RubyInjector)(nil) + +func init() { + DefaultInjectorRegistry.MustRegister(&RubyInjector{}) +} + +type RubyInjector struct { + baseInjector +} + +func (i *RubyInjector) Language() string { + return "ruby" +} + +func (i *RubyInjector) acceptable(inst v1alpha2.Instrumentation, pod corev1.Pod) bool { + if inst.Spec.Agent.Language != i.Language() { + return false + } + if len(pod.Spec.Containers) == 0 { + return false + } + return true +} + +func (i *RubyInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if !i.acceptable(inst, pod) { + return pod, nil + } + if err := i.validate(inst); err != nil { + return pod, err + } + + firstContainer := 0 // caller checks if there is at least one container. - container := &pod.Spec.Containers[index] + container := &pod.Spec.Containers[firstContainer] err := validateContainerEnv(container.Env, envRubyOpt) if err != nil { @@ -38,7 +71,7 @@ func InjectRubySDK(rubySpec v1alpha1.Ruby, pod corev1.Pod, index int) (corev1.Po } // inject Ruby instrumentation spec env vars. - for _, env := range rubySpec.Env { + for _, env := range inst.Spec.Agent.Env { idx := getIndexOfEnv(container.Env, env.Name) if idx == -1 { container.Env = append(container.Env, env) @@ -55,22 +88,26 @@ func InjectRubySDK(rubySpec v1alpha1.Ruby, pod corev1.Pod, index int) (corev1.Po container.Env[idx].Value = container.Env[idx].Value + envRubyOpt } - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: "/newrelic-instrumentation", - }) + if isContainerVolumeMissing(container, volumeName) { + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }) + } // We just inject Volumes and init containers for the first processed container. - if isInitContainerMissing(pod) { - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }}) + if isInitContainerMissing(pod, rubyInitContainerName) { + if isPodVolumeMissing(pod, volumeName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }}) + } pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ - Name: initContainerName, - Image: rubySpec.Image, + Name: rubyInitContainerName, + Image: inst.Spec.Agent.Image, Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, VolumeMounts: []corev1.VolumeMount{{ Name: volumeName, @@ -78,5 +115,8 @@ func InjectRubySDK(rubySpec v1alpha1.Ruby, pod corev1.Pod, index int) (corev1.Po }}, }) } + + pod = i.injectNewrelicConfig(ctx, inst.Spec.Resource, ns, pod, firstContainer, inst.Spec.LicenseKeySecret) + return pod, nil } diff --git a/src/apm/ruby_test.go b/src/apm/ruby_test.go new file mode 100644 index 00000000..643fb525 --- /dev/null +++ b/src/apm/ruby_test.go @@ -0,0 +1,104 @@ +package apm + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +func TestRubyInjector_Language(t *testing.T) { + require.Equal(t, "ruby", (&RubyInjector{}).Language()) +} + +func TestRubyInjector_Inject(t *testing.T) { + vtrue := true + tests := []struct { + name string + pod corev1.Pod + ns corev1.Namespace + inst v1alpha2.Instrumentation + expectedPod corev1.Pod + expectedErrStr string + }{ + { + name: "nothing", + }, + { + name: "a container, no instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + }, + { + name: "a container, wrong instrumentation (not the correct lang)", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "not-this"}}}, + }, + { + name: "a container, instrumentation with blank licenseKeySecret", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedErrStr: "licenseKeySecret must not be blank", + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby"}}}, + }, + { + name: "a container, instrumentation", + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "test"}, + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Env: []corev1.EnvVar{ + {Name: "RUBYOPT", Value: "-r /newrelic-instrumentation/lib/boot/strap"}, + {Name: "NEW_RELIC_APP_NAME", Value: "test"}, + {Name: "NEW_RELIC_LICENSE_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, Key: "new_relic_license_key", Optional: &vtrue}}}, + {Name: "NEW_RELIC_LABELS", Value: "operator:auto-injection"}, + {Name: "NEW_RELIC_K8S_OPERATOR_ENABLED", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + InitContainers: []corev1.Container{{ + Name: "newrelic-instrumentation-ruby", + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{Name: "newrelic-instrumentation", MountPath: "/newrelic-instrumentation"}}, + }}, + Volumes: []corev1.Volume{{Name: "newrelic-instrumentation", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, + }}, + inst: v1alpha2.Instrumentation{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby"}, LicenseKeySecret: "newrelic-key-secret"}}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + i := &RubyInjector{} + actualPod, err := i.Inject(ctx, test.inst, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + require.Equal(t, test.expectedErrStr, errStr) + if diff := cmp.Diff(test.expectedPod, actualPod); diff != "" { + assert.Fail(t, diff) + } + }) + } +} diff --git a/src/constants/env.go b/src/constants/env.go deleted file mode 100644 index 32b905b8..00000000 --- a/src/constants/env.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -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 constants - -const ( - EnvOTELServiceName = "OTEL_SERVICE_NAME" - EnvOTELExporterOTLPEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT" - EnvOTELResourceAttrs = "OTEL_RESOURCE_ATTRIBUTES" - EnvOTELPropagators = "OTEL_PROPAGATORS" - EnvOTELTracesSampler = "OTEL_TRACES_SAMPLER" - EnvOTELTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG" - - EnvPodName = "OTEL_RESOURCE_ATTRIBUTES_POD_NAME" - EnvPodUID = "OTEL_RESOURCE_ATTRIBUTES_POD_UID" - EnvNodeName = "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME" - - EnvNewRelicAppName = "NEW_RELIC_APP_NAME" - EnvNewRelicK8sOperatorEnabled = "NEW_RELIC_K8S_OPERATOR_ENABLED" - EnvNewRelicLabels = "NEW_RELIC_LABELS" - EnvNewRelicLicenseKey = "NEW_RELIC_LICENSE_KEY" -) diff --git a/src/instrumentation/annotation.go b/src/instrumentation/annotation.go deleted file mode 100644 index ac24bf6e..00000000 --- a/src/instrumentation/annotation.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -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 instrumentation - -import ( - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - volumeName = "newrelic-instrumentation" - initContainerName = "newrelic-instrumentation" - sideCarName = "opentelemetry-auto-instrumentation" - - // indicates whether newrelic agents should be injected or not. - // Possible values are "true", "false" or "" name. - annotationInjectJava = "instrumentation.newrelic.com/inject-java" - annotationInjectJavaContainersName = "instrumentation.newrelic.com/java-container-names" - annotationInjectNodeJS = "instrumentation.newrelic.com/inject-nodejs" - annotationInjectNodeJSContainersName = "instrumentation.newrelic.com/nodejs-container-names" - annotationInjectPython = "instrumentation.newrelic.com/inject-python" - annotationInjectPythonContainersName = "instrumentation.newrelic.com/python-container-names" - annotationInjectDotNet = "instrumentation.newrelic.com/inject-dotnet" - annotationInjectDotnetContainersName = "instrumentation.newrelic.com/dotnet-container-names" - annotationInjectPhp = "instrumentation.newrelic.com/inject-php" - annotationInjectPhpContainersName = "instrumentation.newrelic.com/php-container-names" - annotationInjectRuby = "instrumentation.newrelic.com/inject-ruby" - annotationInjectRubyContainersName = "instrumentation.newrelic.com/ruby-container-names" - annotationPhpExecCmd = "instrumentation.newrelic.com/php-exec-command" - annotationInjectContainerName = "instrumentation.newrelic.com/container-name" - annotationInjectGo = "instrumentation.opentelemetry.io/inject-go" - annotationGoExecPath = "instrumentation.opentelemetry.io/otel-go-auto-target-exe" - annotationInjectGoContainerName = "instrumentation.opentelemetry.io/go-container-name" -) - -// annotationValue returns the effective annotation value, based on the annotations from the pod and namespace. -func annotationValue(ns metav1.ObjectMeta, pod metav1.ObjectMeta, annotation string) string { - // is the pod annotated with instructions to inject sidecars? is the namespace annotated? - // if any of those is true, a sidecar might be desired. - podAnnValue := pod.Annotations[annotation] - nsAnnValue := ns.Annotations[annotation] - - // if the namespace value is empty, the pod annotation should be used, whatever it is - if len(nsAnnValue) == 0 { - return podAnnValue - } - - // if the pod value is empty, the annotation should be used (true, false, instance) - if len(podAnnValue) == 0 { - return nsAnnValue - } - - // the pod annotation isn't empty -- if it's an instance name, or false, that's the decision - if !strings.EqualFold(podAnnValue, "true") { - return podAnnValue - } - - // pod annotation is 'true', and if the namespace annotation is false, we just return 'true' - if strings.EqualFold(nsAnnValue, "false") { - return podAnnValue - } - - // by now, the pod annotation is 'true', and the namespace annotation is either true or an instance name - // so, the namespace annotation can be used - return nsAnnValue -} diff --git a/src/instrumentation/annotation_test.go b/src/instrumentation/annotation_test.go deleted file mode 100644 index 0fbdcbff..00000000 --- a/src/instrumentation/annotation_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package instrumentation - -import ( - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestEffectiveAnnotationValue(t *testing.T) { - for _, tt := range []struct { - desc string - expected string - pod corev1.Pod - ns corev1.Namespace - }{ - { - "pod-true-overrides-ns", - "true", - corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "true", - }, - }, - }, - corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "false", - }, - }, - }, - }, - - { - "ns-has-concrete-instance", - "some-instance", - corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "true", - }, - }, - }, - corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "some-instance", - }, - }, - }, - }, - - { - "pod-has-concrete-instance", - "some-instance-from-pod", - corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "some-instance-from-pod", - }, - }, - }, - corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "some-instance", - }, - }, - }, - }, - - { - "pod-has-explicit-false", - "false", - corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "false", - }, - }, - }, - corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "some-instance", - }, - }, - }, - }, - - { - "pod-has-no-annotations", - "some-instance", - corev1.Pod{}, - corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "some-instance", - }, - }, - }, - }, - - { - "ns-has-no-annotations", - "true", - corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "true", - }, - }, - }, - corev1.Namespace{}, - }, - } { - t.Run(tt.desc, func(t *testing.T) { - // test - annValue := annotationValue(tt.ns.ObjectMeta, tt.pod.ObjectMeta, annotationInjectJava) - - // verify - assert.Equal(t, tt.expected, annValue) - }) - } -} \ No newline at end of file diff --git a/src/instrumentation/instrumentation_defaulter.go b/src/instrumentation/instrumentation_defaulter.go new file mode 100644 index 00000000..bc7c2332 --- /dev/null +++ b/src/instrumentation/instrumentation_defaulter.go @@ -0,0 +1,23 @@ +package instrumentation + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" +) + +var _ webhook.CustomDefaulter = (*InstrumentationDefaulter)(nil) + +type InstrumentationDefaulter struct { + Logger logr.Logger +} + +func (r *InstrumentationDefaulter) Default(ctx context.Context, obj runtime.Object) error { + r.Logger.V(1).Info("default", "name", obj.(*v1alpha2.Instrumentation).Name) + obj.(*v1alpha2.Instrumentation).Default() + return nil +} diff --git a/src/instrumentation/instrumentation_suite_test.go b/src/instrumentation/instrumentation_suite_test.go index c0bd5e30..86185f2f 100644 --- a/src/instrumentation/instrumentation_suite_test.go +++ b/src/instrumentation/instrumentation_suite_test.go @@ -7,33 +7,27 @@ import ( "testing" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) -var ( - k8sClient client.Client - testEnv *envtest.Environment - testScheme = scheme.Scheme - err error - cfg *rest.Config -) +var k8sClient client.Client func TestMain(m *testing.M) { - testEnv = &envtest.Environment{ + testEnv := &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "tests", "kustomize", "crd", "bases")}, } - cfg, err = testEnv.Start() + cfg, err := testEnv.Start() if err != nil { fmt.Printf("failed to start testEnv: %v", err) os.Exit(1) } - if err = v1alpha1.AddToScheme(testScheme); err != nil { + testScheme := scheme.Scheme + if err = v1alpha2.AddToScheme(testScheme); err != nil { fmt.Printf("failed to register scheme: %v", err) os.Exit(1) } @@ -53,4 +47,4 @@ func TestMain(m *testing.M) { } os.Exit(code) -} \ No newline at end of file +} diff --git a/src/instrumentation/instrumentation_validator.go b/src/instrumentation/instrumentation_validator.go new file mode 100644 index 00000000..12177aef --- /dev/null +++ b/src/instrumentation/instrumentation_validator.go @@ -0,0 +1,42 @@ +package instrumentation + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" + "github.com/newrelic/k8s-agents-operator/src/apm" +) + +var _ webhook.CustomValidator = (*InstrumentationValidator)(nil) + +type InstrumentationValidator struct { + Logger logr.Logger + InjectorRegistery *apm.InjectorRegistery +} + +func (r *InstrumentationValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error { + r.Logger.V(1).Info("validate_create", "name", obj.(*v1alpha2.Instrumentation).Name) + acceptableLangs := r.InjectorRegistery.GetInjectors().Names() + agentLang := obj.(*v1alpha2.Instrumentation).Spec.Agent.Language + if !slices.Contains(acceptableLangs, agentLang) { + return fmt.Errorf("instrumentation agent language %q must be one of the accepted languages (%s)", agentLang, strings.Join(acceptableLangs, ", ")) + } + return obj.(*v1alpha2.Instrumentation).ValidateCreate() +} + +func (r *InstrumentationValidator) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) error { + r.Logger.V(1).Info("validate_update", "name", newObj.(*v1alpha2.Instrumentation).Name) + return newObj.(*v1alpha2.Instrumentation).ValidateUpdate(oldObj) +} + +func (r *InstrumentationValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error { + r.Logger.V(1).Info("validate_delete", "name", obj.(*v1alpha2.Instrumentation).Name) + return obj.(*v1alpha2.Instrumentation).ValidateDelete() +} diff --git a/src/instrumentation/podmutator.go b/src/instrumentation/podmutator.go index 5a29a652..dc5a3fb1 100644 --- a/src/instrumentation/podmutator.go +++ b/src/instrumentation/podmutator.go @@ -18,167 +18,270 @@ package instrumentation import ( "context" "errors" - "strings" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" "github.com/newrelic/k8s-agents-operator/src/internal/webhookhandler" ) +// compile time type assertion +var ( + _ webhookhandler.PodMutator = (*instPodMutator)(nil) + _ InstrumentationLocator = (*NewrelicInstrumentationLocator)(nil) + _ SdkInjector = (*NewrelicSdkInjector)(nil) + _ SecretReplicator = (*NewrelicSecretReplicator)(nil) +) + var ( errMultipleInstancesPossible = errors.New("multiple New Relic Instrumentation instances available, cannot determine which one to select") errNoInstancesAvailable = errors.New("no New Relic Instrumentation instances available") ) type instPodMutator struct { - Client client.Client - sdkInjector *sdkInjector - Logger logr.Logger -} - -type languageInstrumentations struct { - Java *v1alpha1.Instrumentation - NodeJS *v1alpha1.Instrumentation - Python *v1alpha1.Instrumentation - DotNet *v1alpha1.Instrumentation - Php *v1alpha1.Instrumentation - Ruby *v1alpha1.Instrumentation - Go *v1alpha1.Instrumentation + logger logr.Logger + client client.Client + sdkInjector SdkInjector + secretReplicator SecretReplicator + instrumentationLocator InstrumentationLocator + operatorNamespace string } -var _ webhookhandler.PodMutator = (*instPodMutator)(nil) - -func NewMutator(logger logr.Logger, client client.Client) *instPodMutator { +// NewMutator is used to get a new instance of a mutator +func NewMutator( + logger logr.Logger, + client client.Client, + sdkInjector SdkInjector, + secretReplicator SecretReplicator, + instrumentationLocator InstrumentationLocator, + operatorNamespace string, +) *instPodMutator { return &instPodMutator{ - Logger: logger, - Client: client, - sdkInjector: &sdkInjector{ - logger: logger, - client: client, - }, + logger: logger, + client: client, + sdkInjector: sdkInjector, + secretReplicator: secretReplicator, + instrumentationLocator: instrumentationLocator, + operatorNamespace: operatorNamespace, } } +// Mutate is used to mutate a pod based on some instrumentation(s) func (pm *instPodMutator) Mutate(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { - logger := pm.Logger.WithValues("namespace", pod.Namespace, "name", pod.Name) - - var inst *v1alpha1.Instrumentation - var err error + logger := pm.logger.WithValues("namespace", pod.Namespace, "name", pod.Name) - insts := languageInstrumentations{} - - // We bail out if any annotation fails to process. - - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectJava); err != nil { + instCandidates, err := pm.instrumentationLocator.GetInstrumentations(ctx, ns, pod) + if err != nil { // we still allow the pod to be created, but we log a message to the operator's logs logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") return pod, err } - insts.Java = inst + if len(instCandidates) == 0 { + logger.Info("no New Relic Instrumentation instance for this Pod") + return pod, errNoInstancesAvailable + } - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectNodeJS); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") + instrumentations, err := GetLanguageInstrumentations(instCandidates) + if err != nil { + if errors.Is(err, errMultipleInstancesPossible) { + pm.logger.Info("too many New Relic Instrumentation instances for this Pod. only 1 allowed") + } else { + logger.Error(err, "failed to select a New Relic Instrumentation instance for this Pod") + } return pod, err } - insts.NodeJS = inst - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectPython); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") - return pod, err + if licenseKeySecret, err := GetSecretNameFromInstrumentations(instCandidates); err != nil { + logger.Error(err, "failed to identify the correct secret. all matching instrumentation's must use the same secret") + return pod, nil + } else { + err = pm.secretReplicator.ReplicateSecret(ctx, ns, pod, pm.operatorNamespace, licenseKeySecret) + if err != nil { + logger.Error(err, "failed to replicate secret") + return pod, nil + } } - insts.Python = inst - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectDotNet); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") - return pod, err + return pm.sdkInjector.Inject(ctx, instrumentations, ns, pod), nil +} + +// GetLanguageInstrumentations is used to collect all instrumentations and validate that only a single instrumentation +// exists for each language, and return them together, modifying the slice items in place +func GetLanguageInstrumentations(instCandidates []*v1alpha2.Instrumentation) ([]*v1alpha2.Instrumentation, error) { + languages := map[string]*v1alpha2.Instrumentation{} + i := 0 + for _, candidate := range instCandidates { + if currentInst, ok := languages[candidate.Spec.Agent.Language]; ok { + if !currentInst.Spec.Agent.IsEqual(candidate.Spec.Agent) { + return nil, errMultipleInstancesPossible + } + } else { + languages[candidate.Spec.Agent.Language] = candidate + instCandidates[i] = candidate + i++ + } } - insts.DotNet = inst + return instCandidates[:i], nil +} - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectPhp); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") - return pod, err +// InstrumentationLocator is used to find instrumentations +type InstrumentationLocator interface { + GetInstrumentations(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) +} + +// NewrelicInstrumentationLocator is the base struct for locating instrumentations +type NewrelicInstrumentationLocator struct { + logger logr.Logger + client client.Client + operatorNamespace string +} + +// NewNewRelicInstrumentationLocator is the constructor for getting instrumentations +func NewNewRelicInstrumentationLocator(logger logr.Logger, client client.Client, operatorNamespace string) *NewrelicInstrumentationLocator { + return &NewrelicInstrumentationLocator{ + logger: logger, + client: client, + operatorNamespace: operatorNamespace, } - insts.Php = inst +} - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectRuby); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "failed to select a New Relic Instrumentation instance for this pod") - return pod, err +// GetInstrumentations is used to get all instrumentations in the cluster. While we could limit it to the operator +// namespace, it's more helpful to list anything in the logs that may have been excluded. +func (il *NewrelicInstrumentationLocator) GetInstrumentations(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) { + logger := il.logger.WithValues("namespace", pod.Namespace, "name", pod.Name) + + var listInst v1alpha2.InstrumentationList + if err := il.client.List(ctx, &listInst); err != nil { + return nil, err } - insts.Ruby = inst - if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectGo); err != nil { - // we still allow the pod to be created, but we log a message to the operator's logs - logger.Error(err, "support for Go auto instrumentation is not enabled") - return pod, err + //nolint:prealloc + var candidates []*v1alpha2.Instrumentation + for _, inst := range listInst.Items { + if inst.Namespace != il.operatorNamespace { + logger.Info("ignoring instrumentation not in operator namespace", + "instrumentation_name", inst.Name, + "instrumentation_namespace", inst.Namespace, + "operator_namespace", il.operatorNamespace, + ) + continue + } + podSelector, err := metav1.LabelSelectorAsSelector(&inst.Spec.PodLabelSelector) + if err != nil { + logger.Error(err, "failed to parse pod label selector", + "instrumentation_name", inst.Name, + "instrumentation_namespace", inst.Namespace, + ) + continue + } + namespaceSelector, err := metav1.LabelSelectorAsSelector(&inst.Spec.NamespaceLabelSelector) + if err != nil { + logger.Error(err, "failed to parse namespace label selector", + "instrumentation_name", inst.Name, + "instrumentation_namespace", inst.Namespace, + ) + continue + } + + if !podSelector.Matches(fields.Set(pod.Labels)) { + continue + } + if !namespaceSelector.Matches(fields.Set(ns.Labels)) { + continue + } + + logger.Info("matching instrumentation", + "instrumentation_name", inst.Name, + "instrumentation_namespace", inst.Namespace, + ) + + if inst.Spec.LicenseKeySecret == "" { + inst.Spec.LicenseKeySecret = DefaultLicenseKeySecretName + } + candidates = append(candidates, &inst) } - insts.Go = inst + return candidates, nil +} - if insts.Java == nil && insts.NodeJS == nil && insts.Python == nil && insts.DotNet == nil && insts.Php == nil && insts.Ruby == nil && insts.Go == nil { - logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection") - return pod, nil +// GetSecretNameFromInstrumentations is used to get a single secret key name from a list of Instrumentation's. It will +// use the default if none is provided. If any of them are different by name, this will fail, as we can only bind a +// single license key to a single pod. +func GetSecretNameFromInstrumentations(insts []*v1alpha2.Instrumentation) (string, error) { + secretName := "" + for _, inst := range insts { + specSecretName := inst.Spec.LicenseKeySecret + if specSecretName == "" { + specSecretName = DefaultLicenseKeySecretName + } + if secretName == "" { + secretName = specSecretName + } + if secretName != specSecretName { + return "", errors.New("multiple key secrets") + } } + return secretName, nil +} - // We retrieve the annotation for podname - var targetContainers = annotationValue(ns.ObjectMeta, pod.ObjectMeta, annotationInjectContainerName) +// SecretReplicator is used to copy secrets from one namespace to another +type SecretReplicator interface { + ReplicateSecret(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error +} - // once it's been determined that instrumentation is desired, none exists yet, and we know which instance it should talk to, - // we should inject the instrumentation. - modifiedPod := pod - for _, currentContainer := range strings.Split(targetContainers, ",") { - modifiedPod = pm.sdkInjector.inject(ctx, insts, ns, modifiedPod, strings.TrimSpace(currentContainer)) - } +// NewrelicSecretReplicator is the base struct used for copying the secrets +type NewrelicSecretReplicator struct { + client client.Client + logger logr.Logger +} - return modifiedPod, nil +// NewNewrelicSecretReplicator is the constructor for copying secrets +func NewNewrelicSecretReplicator(logger logr.Logger, client client.Client) *NewrelicSecretReplicator { + return &NewrelicSecretReplicator{client: client, logger: logger} } -func (pm *instPodMutator) getInstrumentationInstance(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, instAnnotation string) (*v1alpha1.Instrumentation, error) { - instValue := annotationValue(ns.ObjectMeta, pod.ObjectMeta, instAnnotation) +// ReplicateSecret is used to copy the secret from the operator namespace to the pod namespace if the secret doesn't already exist +func (sr *NewrelicSecretReplicator) ReplicateSecret(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error { + logger := sr.logger.WithValues("namespace", pod.Namespace, "name", pod.Name) - if len(instValue) == 0 || strings.EqualFold(instValue, "false") { - return nil, nil - } + var secret corev1.Secret - if strings.EqualFold(instValue, "true") { - return pm.selectInstrumentationInstanceFromNamespace(ctx, ns) + if secretName == "" { + secretName = DefaultLicenseKeySecretName } - var instNamespacedName types.NamespacedName - if instNamespace, instName, namespaced := strings.Cut(instValue, "/"); namespaced { - instNamespacedName = types.NamespacedName{Name: instName, Namespace: instNamespace} - } else { - instNamespacedName = types.NamespacedName{Name: instValue, Namespace: ns.Name} + err := sr.client.Get(ctx, client.ObjectKey{Namespace: ns.Name, Name: secretName}, &secret) + if err == nil { + logger.Info("secret already exists") + return nil } - - nrInst := &v1alpha1.Instrumentation{} - err := pm.Client.Get(ctx, instNamespacedName, nrInst) - if err != nil { - return nil, err + if !apierrors.IsNotFound(err) { + logger.Error(err, "failed to check for existing secret in pod namespace") + return err } + logger.Info("replicating secret to pod namespace") - return nrInst, nil -} - -func (pm *instPodMutator) selectInstrumentationInstanceFromNamespace(ctx context.Context, ns corev1.Namespace) (*v1alpha1.Instrumentation, error) { - var nrInsts v1alpha1.InstrumentationList - if err := pm.Client.List(ctx, &nrInsts, client.InNamespace(ns.Name)); err != nil { - return nil, err + if err = sr.client.Get(ctx, client.ObjectKey{Namespace: operatorNamespace, Name: secretName}, &secret); err != nil { + logger.Error(err, "failed to retrieve the secret from operator namespace") + return err } - switch s := len(nrInsts.Items); { - case s == 0: - return nil, errNoInstancesAvailable - case s > 1: - return nil, errMultipleInstancesPossible - default: - return &nrInsts.Items[0], nil + newSecret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns.Name, + }, + Data: secret.Data, + } + if err = sr.client.Create(ctx, &newSecret); err != nil { + logger.Error(err, "failed to create a new secret") + return err } + + return nil } diff --git a/src/instrumentation/podmutator_test.go b/src/instrumentation/podmutator_test.go index c1d65cd6..c47c038b 100644 --- a/src/instrumentation/podmutator_test.go +++ b/src/instrumentation/podmutator_test.go @@ -2,881 +2,1113 @@ package instrumentation import ( "context" + "encoding/base64" + "fmt" "testing" "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" + "github.com/newrelic/k8s-agents-operator/src/apm" ) +type FakeInjector func(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod + +func (fn FakeInjector) Inject(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod { + return fn(ctx, insts, ns, pod) +} + +type FakeSecretReplicator func(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error + +func (fn FakeSecretReplicator) ReplicateSecret(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error { + return fn(ctx, ns, pod, operatorNamespace, secretName) +} + +type FakeInstrumentationLocator func(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) + +func (fn FakeInstrumentationLocator) GetInstrumentations(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) { + return fn(ctx, ns, pod) +} + +type InstrumentationLocatorFn func(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) + +func (il InstrumentationLocatorFn) GetInstrumentations(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) { + return il(ctx, ns, pod) +} + +var _ InstrumentationLocator = (InstrumentationLocatorFn)(nil) + +type SdkInjectorFn func(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod + +func (si SdkInjectorFn) Inject(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod { + return si(ctx, insts, ns, pod) +} + +var _ SdkInjector = (SdkInjectorFn)(nil) + +type SecretReplicatorFn func(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error + +func (sr SecretReplicatorFn) ReplicateSecret(ctx context.Context, ns corev1.Namespace, pod corev1.Pod, operatorNamespace string, secretName string) error { + return sr(ctx, ns, pod, operatorNamespace, secretName) +} + +var _ SecretReplicator = (SecretReplicatorFn)(nil) + func TestMutatePod(t *testing.T) { - mutator := NewMutator(logr.Discard(), k8sClient) - require.NotNil(t, mutator) + var fakeInjector FakeInjector = func( + ctx context.Context, + insts []*v1alpha2.Instrumentation, + ns corev1.Namespace, + pod corev1.Pod, + ) corev1.Pod { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + for _, inst := range insts { + if inst.Spec.Agent.Language != "" { + pod.Annotations["newrelic-"+inst.Spec.Agent.Language] = "true" + } + } + return pod + } + var fakeSecretReplicator FakeSecretReplicator = func( + ctx context.Context, + ns corev1.Namespace, + pod corev1.Pod, + operatorNamespace string, + secretName string, + ) error { + return nil + } + var _ = fakeSecretReplicator + var fakeInstrumentationLocator FakeInstrumentationLocator = func( + ctx context.Context, + ns corev1.Namespace, + pod corev1.Pod, + ) ([]*v1alpha2.Instrumentation, error) { + return nil, nil + } + var fakeInstrumentationLocatorWithDup FakeInstrumentationLocator = func( + ctx context.Context, + ns corev1.Namespace, + pod corev1.Pod, + ) ([]*v1alpha2.Instrumentation, error) { + return []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby", Image: "ruby1"}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby", Image: "ruby2"}}}, + }, nil + } + logger := logr.Discard() tests := []struct { - name string - err string - pod corev1.Pod - expected corev1.Pod - inst v1alpha1.Instrumentation - ns corev1.Namespace + name string + pod corev1.Pod + ns corev1.Namespace + initInsts []*v1alpha2.Instrumentation + initNs []*corev1.Namespace + initSecrets []*corev1.Secret + operatorNs string + + expectedPod corev1.Pod + expectedSecrets []client.ObjectKey + expectedErrStr string + + injector SdkInjector + instrumentationLocator InstrumentationLocator + secretReplicator SecretReplicator }{ { - name: "javaagent injection, true", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "javaagent", - }, + name: "java injection, true", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "gns1-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "gns1-pod"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "javaagent", - }, - Spec: v1alpha1.InstrumentationSpec{ - Java: v1alpha1.Java{ - Env: []corev1.EnvVar{ - { - Name: "OTEL_JAVAAGENT_DEBUG", - Value: "true", - }, - { - Name: "OTEL_INSTRUMENTATION_JDBC_ENABLED", - Value: "false", - }, - { - Name: "SPLUNK_PROFILER_ENABLED", - Value: "false", - }, - }, - }, - Env: []corev1.EnvVar{ - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", - }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", - }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-java", Namespace: "gns1-op"}, + Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "true", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, + initSecrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "gns1-op"}, + Data: map[string][]byte{apm.LicenseKey: []byte(base64.RawStdEncoding.EncodeToString([]byte("abc123")))}, }, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "true", - }, - }, - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "newrelic-instrumentation", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: initContainerName, - Command: []string{"cp", "/javaagent.jar", "/newrelic-instrumentation/javaagent.jar"}, - VolumeMounts: []corev1.VolumeMount{{ - Name: volumeName, - MountPath: "/newrelicinstrumentation", - }}, - }, - }, - Containers: []corev1.Container{ - { - Name: "app", - Env: []corev1.EnvVar{ - { - Name: "OTEL_JAVAAGENT_DEBUG", - Value: "true", - }, - { - Name: "OTEL_INSTRUMENTATION_JDBC_ENABLED", - Value: "false", - }, - { - Name: "SPLUNK_PROFILER_ENABLED", - Value: "false", - }, - // { - // Name: "JAVA_TOOL_OPTIONS", - // Value: javaJVMArgument, - // }, - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", - }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - { - Name: "OTEL_SERVICE_NAME", - Value: "app", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=app,k8s.namespace.name=javaagent,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "opentelemetry-auto-instrumentation", - MountPath: "/otel-auto-instrumentation", - }, - }, - }, - }, - }, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "java-app"}}}}, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "gns1-pod"}}, + operatorNs: "gns1-op", + injector: fakeInjector, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"newrelic-java": "true"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "java-app"}}}, + }, + expectedSecrets: []client.ObjectKey{ + {Name: DefaultLicenseKeySecretName, Namespace: "gns1-op"}, + {Name: DefaultLicenseKeySecretName, Namespace: "gns1-pod"}, }, }, { - name: "nodejs injection, true", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "nodejs", - }, + name: "fetch instrumentation error", + instrumentationLocator: InstrumentationLocatorFn(func(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) ([]*v1alpha2.Instrumentation, error) { + return nil, fmt.Errorf("fetch instrumentation error") + }), + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedErrStr: "fetch instrumentation error", + }, + { + name: "no instrumentations", + instrumentationLocator: fakeInstrumentationLocator, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedErrStr: errNoInstancesAvailable.Error(), + }, + { + name: "some error when getting language instrumentations", + instrumentationLocator: fakeInstrumentationLocatorWithDup, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "gns4-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "gns4-pod"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "nodejs", - }, - Spec: v1alpha1.InstrumentationSpec{ - NodeJS: v1alpha1.NodeJS{ - Image: "otel/nodejs:1", - Env: []corev1.EnvVar{ - { - Name: "OTEL_NODEJS_DEBUG", - Value: "true", - }, - }, - }, - Env: []corev1.EnvVar{ - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", - }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", - }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-java", Namespace: "gns4-op"}, + Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectNodeJS: "true", - }, + operatorNs: "gns4-op", + expectedErrStr: errMultipleInstancesPossible.Error(), + }, + { + name: "conflicting secret names", + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "gns5-pod"}}, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + injector: fakeInjector, + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "gns5-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "gns5-pod"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-java", Namespace: "gns5-op"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "", Agent: v1alpha2.Agent{Language: "java", Image: "java"}}, }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-php", Namespace: "gns5-op"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "different", Agent: v1alpha2.Agent{Language: "php", Image: "php"}}, }, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectNodeJS: "true", - }, - }, - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "opentelemetry-auto-instrumentation", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: initContainerName, - Image: "otel/nodejs:1", - Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, - VolumeMounts: []corev1.VolumeMount{{ - Name: volumeName, - MountPath: "/otel-auto-instrumentation", - }}, - }, - }, - Containers: []corev1.Container{ - { - Name: "app", - Env: []corev1.EnvVar{ - { - Name: "OTEL_NODEJS_DEBUG", - Value: "true", - }, - // { - // Name: "NODE_OPTIONS", - // Value: nodeRequireArgument, - // }, - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", - }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - { - Name: "OTEL_SERVICE_NAME", - Value: "app", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=app,k8s.namespace.name=nodejs,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "opentelemetry-auto-instrumentation", - MountPath: "/otel-auto-instrumentation", - }, - }, - }, - }, + initSecrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "gns5-op"}, + Data: map[string][]byte{apm.LicenseKey: []byte(base64.RawStdEncoding.EncodeToString([]byte("abc123")))}, }, }, + operatorNs: "gns5-op", }, { - name: "python injection, true", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "python", - }, + name: "secret doesn't exist", + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "gns6-pod"}}, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app"}}}}, + injector: fakeInjector, + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "gns6-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "gns6-pod"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "python", + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-java", Namespace: "gns6-op"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "", Agent: v1alpha2.Agent{Language: "java", Image: "java"}}, }, - Spec: v1alpha1.InstrumentationSpec{ - Python: v1alpha1.Python{ - Image: "otel/python:1", - Env: []corev1.EnvVar{ - { - Name: "OTEL_LOG_LEVEL", - Value: "debug", - }, - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_METRICS_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4318", - }, - }, - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", - }, - Env: []corev1.EnvVar{ - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - }, + { + ObjectMeta: metav1.ObjectMeta{Name: "example-inst-php", Namespace: "gns6-op"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "", Agent: v1alpha2.Agent{Language: "php", Image: "php"}}, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectPython: "true", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, + operatorNs: "gns6-op", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + + for _, ns := range test.initNs { + err := k8sClient.Create(ctx, ns) + require.NoError(t, err) + defer func() { + _ = k8sClient.Delete(ctx, ns) + }() + } + + for _, inst := range test.initInsts { + err := k8sClient.Create(ctx, inst) + require.NoError(t, err) + defer func() { + _ = k8sClient.Delete(ctx, inst) + }() + } + + for _, secret := range test.initSecrets { + err := k8sClient.Create(ctx, secret) + require.NoError(t, err) + defer func() { + _ = k8sClient.Delete(ctx, secret) + }() + } + + // by default, we'll use the real implementation + injector := test.injector + if injector == nil { + injectorRegistry := apm.NewInjectorRegistry() + apmInjectors := []apm.Injector{ + &apm.DotnetInjector{}, + &apm.GoInjector{}, + &apm.JavaInjector{}, + &apm.NodejsInjector{}, + &apm.PhpInjector{}, + &apm.PythonInjector{}, + &apm.RubyInjector{}, + } + for _, apmInjector := range apmInjectors { + injectorRegistry.MustRegister(apmInjector) + } + injector = NewNewrelicSdkInjector(logger, k8sClient, injectorRegistry) + } + instrumentationLocator := test.instrumentationLocator + if instrumentationLocator == nil { + instrumentationLocator = NewNewRelicInstrumentationLocator(logger, k8sClient, test.operatorNs) + } + secretReplicator := test.secretReplicator + if secretReplicator == nil { + secretReplicator = NewNewrelicSecretReplicator(logger, k8sClient) + } + + mutator := NewMutator( + logger, + k8sClient, + injector, + secretReplicator, + instrumentationLocator, + test.operatorNs, + ) + resultPod, err := mutator.Mutate(ctx, test.ns, test.pod) + if test.expectedErrStr == "" { + require.NoError(t, err) + if diff := cmp.Diff(test.expectedPod, resultPod); diff != "" { + t.Errorf("Unexpected pod diff (-want +got): %s", diff) + } + + for _, objKey := range test.expectedSecrets { + var secret corev1.Secret + err = k8sClient.Get(ctx, objKey, &secret) + require.NoError(t, err) + } + } else { + actualErrStr := "" + if err != nil { + actualErrStr = err.Error() + } + assert.Contains(t, actualErrStr, test.expectedErrStr) + } + }) + } +} + +func TestNewrelicSecretReplicator_ReplicateSecret(t *testing.T) { + logger := logr.Discard() + secretReplicator := NewNewrelicSecretReplicator(logger, k8sClient) + + tests := []struct { + name string + expectedErrStr string + pod corev1.Pod + ns corev1.Namespace + operatorNs string + secretName string + initSecrets []*corev1.Secret + initNs []*corev1.Namespace + }{ + { + name: "no secret", + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "default", + expectedErrStr: "secrets \"newrelic-key-secret\" not found", + }, + { + name: "default secret same ns as operator", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectPython: "true", - }, - }, - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "opentelemetry-auto-instrumentation", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: initContainerName, - Image: "otel/python:1", - Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, - VolumeMounts: []corev1.VolumeMount{{ - Name: volumeName, - MountPath: "/otel-auto-instrumentation", - }}, - }, - }, - Containers: []corev1.Container{ - { - Name: "app", - Env: []corev1.EnvVar{ - { - Name: "OTEL_LOG_LEVEL", - Value: "debug", - }, - { - Name: "OTEL_TRACES_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_METRICS_EXPORTER", - Value: "otlp", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4318", - }, - // { - // Name: "PYTHONPATH", - // Value: fmt.Sprintf("%s:%s", pythonPathPrefix, pythonPathSuffix), - // }, - { - Name: "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", - Value: "http/protobuf", - }, - { - Name: "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", - Value: "http/protobuf", - }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - { - Name: "OTEL_SERVICE_NAME", - Value: "app", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=app,k8s.namespace.name=python,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "opentelemetry-auto-instrumentation", - MountPath: "/otel-auto-instrumentation", - }, - }, - }, - }, - }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns1"}}, }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns1", }, { - name: "dotnet injection, true", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dotnet", - }, + name: "default secret in same ns as pod, no secret in operator ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns2-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns2-pod"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "dotnet", - }, - Spec: v1alpha1.InstrumentationSpec{ - DotNet: v1alpha1.DotNet{ - Image: "otel/dotnet:1", - Env: []corev1.EnvVar{ - { - Name: "OTEL_LOG_LEVEL", - Value: "debug", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns2-pod"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns2-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns2-op", + }, + { + name: "default secret in other ns, no secrets in pod or operator ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns3-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns3-other"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns3-pod"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns3-other"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns3-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns3-op", + expectedErrStr: "secrets \"newrelic-key-secret\" not found", + }, + { + name: "default secret in operator ns, no other secrets", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns4-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns4-pod"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns4-op"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns4-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns4-op", + }, + { + name: "default secret in operator ns, none in pod ns, secret in other ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns5-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns5-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns5-other"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns5-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns5-other"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns5-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns5-op", + }, + { + name: "default secret in operator ns, ns-specific secret in pod ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns6-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns6-pod"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns6-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pod-secret", Namespace: "ns6-pod"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns6-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns6-op", + }, + { + name: "default secret in operator ns, ns-specific secret in pod ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns7-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns7-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns7-other"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns7-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns7-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns7-other"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns7-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns7-op", + secretName: "not-a-normal-newrelic-key-secret", + expectedErrStr: "secrets \"not-a-normal-newrelic-key-secret\" not found", + }, + { + name: "default secret in operator ns, ns-specific secret in pod ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns8-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns8-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns8-other"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns8-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns8-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns8-other"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "not-a-normal-newrelic-key-secret", Namespace: "ns8-other"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns8-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns8-op", + secretName: "not-a-normal-newrelic-key-secret", + expectedErrStr: "secrets \"not-a-normal-newrelic-key-secret\" not found", + }, + { + name: "default secret in operator ns, ns-specific secret in pod ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns9-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns9-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns9-other"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns9-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns9-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns9-other"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "not-a-normal-newrelic-key-secret", Namespace: "ns9-pod"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns9-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns9-op", + secretName: "not-a-normal-newrelic-key-secret", + }, + { + name: "default secret in operator ns, ns-specific secret in pod ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns10-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns10-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ns10-other"}}, + }, + initSecrets: []*corev1.Secret{ + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns10-op"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns10-pod"}}, + {ObjectMeta: metav1.ObjectMeta{Name: DefaultLicenseKeySecretName, Namespace: "ns10-other"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "not-a-normal-newrelic-key-secret", Namespace: "ns10-op"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns10-pod"}}, + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}}, + operatorNs: "ns10-op", + secretName: "not-a-normal-newrelic-key-secret", + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) { + ctx := context.Background() + + for _, initNs := range tc.initNs { + if initNs.Name == "default" || initNs.Name == "" { + continue + } + err := k8sClient.Create(ctx, initNs) + require.NoError(t, err) + } + defer func() { + for _, initNs := range tc.initNs { + if initNs.Name == "default" || initNs.Name == "" { + continue + } + err := k8sClient.Delete(ctx, initNs) + require.NoError(t, err) + } + }() + + for _, initSecret := range tc.initSecrets { + err := k8sClient.Create(ctx, initSecret) + require.NoError(t, err) + } + defer func() { + for _, initSecret := range tc.initSecrets { + err := k8sClient.Delete(ctx, initSecret) + require.NoError(t, err) + } + }() + + err := secretReplicator.ReplicateSecret(ctx, tc.ns, tc.pod, tc.operatorNs, tc.secretName) + errStr := "" + if err != nil { + errStr = err.Error() + } + if tc.expectedErrStr != errStr { + t.Errorf("unexpected error string. expected: %s, got: %s", tc.expectedErrStr, errStr) + } + }) + } +} + +func TestGetLanguageInstrumentations(t *testing.T) { + tests := []struct { + name string + instrumentations []*v1alpha2.Instrumentation + expectedLangInsts []*v1alpha2.Instrumentation + expectedErrStr string + }{ + { + name: "none", + }, + { + name: "dotnet", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "dotnet", Image: "dotnet"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "dotnet", Image: "dotnet"}}}, + }, + }, + { + name: "go", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "go", Image: "go"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "go", Image: "go"}}}, + }, + }, + { + name: "java", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + }, + { + name: "nodejs", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "nodejs", Image: "nodejs"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "nodejs", Image: "nodejs"}}}, + }, + }, + { + name: "php", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php", Image: "php"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php", Image: "php"}}}, + }, + }, + { + name: "python", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "python", Image: "python"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "python", Image: "python"}}}, + }, + }, + { + name: "ruby", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby", Image: "ruby"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "ruby", Image: "ruby"}}}, + }, + }, + { + name: "java + php", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php", Image: "php"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "php", Image: "php"}}}, + }, + }, + { + name: "java + java, both identical, only return first occurrence", + instrumentations: []*v1alpha2.Instrumentation{ + {ObjectMeta: metav1.ObjectMeta{Name: "1st"}, Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + {ObjectMeta: metav1.ObjectMeta{Name: "2st"}, Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + expectedLangInsts: []*v1alpha2.Instrumentation{ + {ObjectMeta: metav1.ObjectMeta{Name: "1st"}, Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + }, + { + name: "java + java, env value different", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", Env: []corev1.EnvVar{{Name: "DEBUG", Value: "1"}}}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", Env: []corev1.EnvVar{{Name: "DEBUG", Value: "0"}}}}}, + }, + expectedErrStr: "multiple New Relic Instrumentation instances available, cannot determine which one to select", + }, + { + name: "java + java, env name different", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", Env: []corev1.EnvVar{{Name: "DEBUG", Value: "1"}}}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", Env: []corev1.EnvVar{{Name: "LOGGING", Value: "1"}}}}}, + }, + expectedErrStr: "multiple New Relic Instrumentation instances available, cannot determine which one to select", + }, + { + name: "java + java, image different", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java-is-great"}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java-is-terrible"}}}, + }, + expectedErrStr: "multiple New Relic Instrumentation instances available, cannot determine which one to select", + }, + { + name: "java + java, volume size different", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", VolumeSizeLimit: resource.NewQuantity(2, resource.DecimalSI)}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + expectedErrStr: "multiple New Relic Instrumentation instances available, cannot determine which one to select", + }, + { + name: "java + java, resources different", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java", Resources: corev1.ResourceRequirements{Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("2")}}}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "java", Image: "java"}}}, + }, + expectedErrStr: "multiple New Relic Instrumentation instances available, cannot determine which one to select", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + langInsts, err := GetLanguageInstrumentations(test.instrumentations) + errStr := "" + if err != nil { + errStr = err.Error() + } + if test.expectedErrStr != errStr { + t.Errorf("unexpected error string. expected: %s, got: %s", test.expectedErrStr, errStr) + } + if diff := cmp.Diff(test.expectedLangInsts, langInsts); diff != "" { + t.Errorf("Unexpected diff (-want +got): %s", diff) + } + }) + } +} + +func TestGetSecretNameFromInstrumentations(t *testing.T) { + tests := []struct { + name string + instrumentations []*v1alpha2.Instrumentation + expectedSecretName string + expectedErrStr string + }{ + { + name: "none", + }, + { + name: "one, default", + instrumentations: []*v1alpha2.Instrumentation{{Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}}}, + expectedSecretName: DefaultLicenseKeySecretName, + }, + { + name: "one, blank", + instrumentations: []*v1alpha2.Instrumentation{{Spec: v1alpha2.InstrumentationSpec{}}}, + expectedSecretName: DefaultLicenseKeySecretName, + }, + { + name: "one, other", + instrumentations: []*v1alpha2.Instrumentation{{Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}}, + expectedSecretName: "something-else", + }, + { + name: "two, one blank, the other the default", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}}, + }, + expectedSecretName: DefaultLicenseKeySecretName, + }, + { + name: "three, one something else, one the default, one blank", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}}, + {Spec: v1alpha2.InstrumentationSpec{}}, + }, + expectedErrStr: "multiple key secrets", + }, + { + name: "two, one blank, the other the something else", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}, + }, + expectedErrStr: "multiple key secrets", + }, + { + name: "two, one the default, the other the something else", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}, + }, + expectedErrStr: "multiple key secrets", + }, + { + name: "two, one blank, the other the default", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}}, + }, + expectedSecretName: DefaultLicenseKeySecretName, + }, + { + name: "two, both something else", + instrumentations: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}, + {Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: "something-else"}}, + }, + expectedSecretName: "something-else", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + secretName, err := GetSecretNameFromInstrumentations(test.instrumentations) + errStr := "" + if err != nil { + errStr = err.Error() + } + if test.expectedErrStr != errStr { + t.Errorf("unexpected error string. expected: %s, got: %s", test.expectedErrStr, errStr) + } + if test.expectedSecretName != secretName { + t.Errorf("unexpected secret name. expected: %s, got: %s", test.expectedSecretName, errStr) + } + }) + } +} + +func TestNewrelicInstrumentationLocator_GetInstrumentations(t *testing.T) { + logger := logr.Discard() + tests := []struct { + name string + expectedErrStr string + initNs []*corev1.Namespace + initInsts []*v1alpha2.Instrumentation + ns corev1.Namespace + pod corev1.Pod + operatorNs string + insts []*v1alpha2.Instrumentation + }{ + { + name: "none", + }, + { + name: "not in operator ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "other1"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + {ObjectMeta: metav1.ObjectMeta{Name: "inst1", Namespace: "other1"}}, + }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "other1"}}, + pod: corev1.Pod{}, + operatorNs: "operator1", + }, + { + name: "1 in operator ns, pod selector has error, error is logged and ignored", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator2"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst2", Namespace: "operator2"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "human", Operator: metav1.LabelSelectorOperator("eat"), Values: []string{"food"}}, }, }, }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", - }, - Env: []corev1.EnvVar{ - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - }, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectDotNet: "true", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + pod: corev1.Pod{}, + operatorNs: "operator2", + }, + { + name: "1 in operator ns, ns selector has error, error is logged and ignored", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator3"}}, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectDotNet: "true", - }, - }, - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "opentelemetry-auto-instrumentation", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: initContainerName, - Image: "otel/dotnet:1", - Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, - VolumeMounts: []corev1.VolumeMount{{ - Name: volumeName, - MountPath: "/otel-auto-instrumentation", - }}, - }, - }, - Containers: []corev1.Container{ - { - Name: "app", - Env: []corev1.EnvVar{ - { - Name: "OTEL_LOG_LEVEL", - Value: "debug", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "http://localhost:4317", - }, - // { - // Name: envDotNetCoreClrEnableProfiling, - // Value: dotNetCoreClrEnableProfilingEnabled, - // }, - // { - // Name: envDotNetCoreClrProfiler, - // Value: dotNetCoreClrProfilerID, - // }, - // { - // Name: envDotNetCoreClrProfilerPath, - // Value: dotNetCoreClrProfilerPath, - // }, - // { - // Name: envDotNetStartupHook, - // Value: dotNetStartupHookPath, - // }, - // { - // Name: envDotNetAdditionalDeps, - // Value: dotNetAdditionalDepsPath, - // }, - // { - // Name: envDotNetOTelAutoHome, - // Value: dotNetOTelAutoHomePath, - // }, - // { - // Name: envDotNetSharedStore, - // Value: dotNetSharedStorePath, - // }, - { - Name: "OTEL_EXPORTER_OTLP_TIMEOUT", - Value: "20", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.85", - }, - { - Name: "SPLUNK_TRACE_RESPONSE_HEADER_ENABLED", - Value: "true", - }, - { - Name: "OTEL_SERVICE_NAME", - Value: "app", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=app,k8s.namespace.name=dotnet,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "opentelemetry-auto-instrumentation", - MountPath: "/otel-auto-instrumentation", - }, - }, - }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst3", Namespace: "operator3"}, + Spec: v1alpha2.InstrumentationSpec{ + NamespaceLabelSelector: metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "crowd", Operator: metav1.LabelSelectorOperator("eats"), Values: []string{"foods"}}, + }}, }, }, }, }, { - name: "missing annotation", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "missing-annotation", + name: "1 not in operator ns, 1 in operator ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator4-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "operator4-2"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + {ObjectMeta: metav1.ObjectMeta{Name: "inst4-1", Namespace: "operator4-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "inst4-2", Namespace: "operator4-2"}}, + }, + operatorNs: "operator4-1", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod4"}}, + insts: []*v1alpha2.Instrumentation{ + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, + ObjectMeta: metav1.ObjectMeta{Name: "inst4-1", Namespace: "operator4-1"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}, }, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "missing-annotation", + }, + { + name: "2 in operator ns", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator5"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + {ObjectMeta: metav1.ObjectMeta{Name: "inst5-1", Namespace: "operator5"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "inst5-2", Namespace: "operator5"}}, + }, + operatorNs: "operator5", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod5"}}, + insts: []*v1alpha2.Instrumentation{ + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, + ObjectMeta: metav1.ObjectMeta{Name: "inst5-1", Namespace: "operator5"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}, }, - Spec: v1alpha1.InstrumentationSpec{ - Java: v1alpha1.Java{ - Image: "otel/java:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", - }, + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, + ObjectMeta: metav1.ObjectMeta{Name: "inst5-2", Namespace: "operator5"}, + Spec: v1alpha2.InstrumentationSpec{LicenseKeySecret: DefaultLicenseKeySecretName}, }, }, - pod: corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", + }, + { + name: "1 matching with pod selector", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator6"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst6", Namespace: "operator6"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, }, }, }, }, - expected: corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", + operatorNs: "operator6", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod6", Labels: map[string]string{ + "pod-id": "abc1234", + }}}, + insts: []*v1alpha2.Instrumentation{ + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, ObjectMeta: metav1.ObjectMeta{Name: "inst6", Namespace: "operator6"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, }, + LicenseKeySecret: DefaultLicenseKeySecretName, }, }, }, }, { - name: "annotation set to false", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "annotation-false", - }, + name: "1 matching with ns selector", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator7"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "annotation-false", - }, - Spec: v1alpha1.InstrumentationSpec{ - Java: v1alpha1.Java{ - Image: "otel/java:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst7", Namespace: "operator7"}, + Spec: v1alpha2.InstrumentationSpec{ + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, }, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "false", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", + operatorNs: "operator7", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod7", Labels: map[string]string{ + "pod-id": "abc1234", + }}}, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default", Labels: map[string]string{ + "ns-id": "abc1234", + }}}, + insts: []*v1alpha2.Instrumentation{ + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, ObjectMeta: metav1.ObjectMeta{Name: "inst7", Namespace: "operator7"}, + Spec: v1alpha2.InstrumentationSpec{ + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, }, + LicenseKeySecret: DefaultLicenseKeySecretName, }, }, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "false", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", + }, + { + name: "1 matching with pod selector, but not ns selector", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator8"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst8", Namespace: "operator8"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, }, }, }, }, + operatorNs: "operator8", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod8", Labels: map[string]string{ + "pod-id": "abc1234", + }}}, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default", Labels: map[string]string{ + "ns-id": "zxy9876", + }}}, }, { - name: "annotation set to non existing instance", - ns: corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "non-existing-instance", - }, + name: "1 matching with ns selector, but not pod selector", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator9"}}, }, - inst: v1alpha1.Instrumentation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example-inst", - Namespace: "non-existing-instance", - }, - Spec: v1alpha1.InstrumentationSpec{ - Java: v1alpha1.Java{ - Image: "otel/java:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "http://collector:12345", + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst9", Namespace: "operator9"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, }, }, }, - pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - annotationInjectJava: "doesnotexists", + operatorNs: "operator9", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod9", Labels: map[string]string{ + "pod-id": "zxy9876", + }}}, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default", Labels: map[string]string{ + "ns-id": "abc1234", + }}}, + }, + { + name: "1 matching with ns selector and with pod selector", + initNs: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "operator10"}}, + }, + initInsts: []*v1alpha2.Instrumentation{ + { + ObjectMeta: metav1.ObjectMeta{Name: "inst10", Namespace: "operator10"}, + Spec: v1alpha2.InstrumentationSpec{ + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, }, }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", + }, + operatorNs: "operator10", + pod: corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod10", Labels: map[string]string{ + "pod-id": "abc1234", + }}}, + ns: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default", Labels: map[string]string{ + "ns-id": "abc1234", + }}}, + insts: []*v1alpha2.Instrumentation{ + { + TypeMeta: metav1.TypeMeta{Kind: "Instrumentation"}, ObjectMeta: metav1.ObjectMeta{Name: "inst10", Namespace: "operator10"}, + Spec: v1alpha2.InstrumentationSpec{ + NamespaceLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "ns-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, + }, + PodLabelSelector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "pod-id", Operator: metav1.LabelSelectorOpIn, Values: []string{"abc1234"}}}, }, + LicenseKeySecret: DefaultLicenseKeySecretName, }, }, }, - err: `instrumentations.newrelic.com "doesnotexists" not found`, }, } - + instSorter := func(a, b *v1alpha2.Instrumentation) bool { + if a.Namespace > b.Namespace { + return true + } + if a.Name > b.Name { + return true + } + return false + } + typeMetaIgnore := cmpopts.IgnoreFields(metav1.TypeMeta{}, "APIVersion") + objectMetaIgnore := cmpopts.IgnoreFields(metav1.ObjectMeta{}, "UID", "ResourceVersion", "Generation", "CreationTimestamp", "ManagedFields") for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := k8sClient.Create(context.Background(), &test.ns) - require.NoError(t, err) + ctx := context.Background() + + for _, initNs := range test.initNs { + err := k8sClient.Create(ctx, initNs) + require.NoError(t, err) + } defer func() { - _ = k8sClient.Delete(context.Background(), &test.ns) + for _, initNs := range test.initNs { + err := k8sClient.Delete(ctx, initNs) + require.NoError(t, err) + } }() - err = k8sClient.Create(context.Background(), &test.inst) - require.NoError(t, err) - _, err = mutator.Mutate(context.Background(), test.ns, test.pod) - if test.err == "" { + for _, initInst := range test.initInsts { + err := k8sClient.Create(ctx, initInst) require.NoError(t, err) - assert.Equal(t, test.expected, test.expected) - } else { - assert.Contains(t, err.Error(), test.err) + } + defer func() { + for _, initInst := range test.initInsts { + err := k8sClient.Delete(ctx, initInst) + require.NoError(t, err) + } + }() + + locator := NewNewRelicInstrumentationLocator(logger, k8sClient, test.operatorNs) + insts, err := locator.GetInstrumentations(ctx, test.ns, test.pod) + errStr := "" + if err != nil { + errStr = err.Error() + } + if test.expectedErrStr != errStr { + t.Errorf("unexpected error string. expected: %s, got: %s", test.expectedErrStr, errStr) + } + if diff := cmp.Diff(test.insts, insts, cmpopts.SortSlices(instSorter), typeMetaIgnore, objectMetaIgnore); diff != "" { + t.Errorf("Unexpected diff (-want +got): %s", diff) } }) } -} \ No newline at end of file +} diff --git a/src/instrumentation/sdk.go b/src/instrumentation/sdk.go index f3759ec3..ce40d352 100644 --- a/src/instrumentation/sdk.go +++ b/src/instrumentation/sdk.go @@ -19,496 +19,86 @@ package instrumentation import ( "context" "fmt" - "sort" - "strings" - "time" - "unsafe" "github.com/go-logr/logr" - "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.5.0" - appsv1 "k8s.io/api/apps/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/types" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/util/retry" + "runtime/debug" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" - apm "github.com/newrelic/k8s-agents-operator/src/apm" - "github.com/newrelic/k8s-agents-operator/src/constants" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" + "github.com/newrelic/k8s-agents-operator/src/apm" ) -type sdkInjector struct { - client client.Client - logger logr.Logger -} - -func (i *sdkInjector) inject(ctx context.Context, insts languageInstrumentations, ns corev1.Namespace, pod corev1.Pod, containerName string) corev1.Pod { - if len(pod.Spec.Containers) < 1 { - return pod - } - - // We search for specific container to inject variables and if no one is found - // We fallback to first container - var index = 0 - for idx, ctnair := range pod.Spec.Containers { - if ctnair.Name == containerName { - index = idx - } - } - - if insts.Java != nil { - newrelic := *insts.Java - var err error - i.logger.V(1).Info("injecting Java instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectJavaagent(newrelic.Spec.Java, pod, index) - if err != nil { - i.logger.Info("Skipping Java agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.NodeJS != nil { - newrelic := *insts.NodeJS - var err error - i.logger.V(1).Info("injecting NodeJS instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectNodeJSSDK(newrelic.Spec.NodeJS, pod, index) - if err != nil { - i.logger.Info("Skipping NodeJS agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.Python != nil { - newrelic := *insts.Python - var err error - i.logger.V(1).Info("injecting Python instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectPythonSDK(newrelic.Spec.Python, pod, index) - if err != nil { - i.logger.Info("Skipping Python agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.DotNet != nil { - newrelic := *insts.DotNet - var err error - i.logger.V(1).Info("injecting DotNet instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectDotNetSDK(newrelic.Spec.DotNet, pod, index) - if err != nil { - i.logger.Info("Skipping DotNet agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.Php != nil { - newrelic := *insts.Php - var err error - i.logger.V(1).Info("injecting Php instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectPhpagent(newrelic.Spec.Php, pod, index) - if err != nil { - i.logger.Info("Skipping Php agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.Ruby != nil { - newrelic := *insts.Ruby - var err error - i.logger.V(1).Info("injecting Ruby instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - pod, err = apm.InjectRubySDK(newrelic.Spec.Ruby, pod, index) - if err != nil { - i.logger.Info("Skipping Ruby agent injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - pod = i.injectNewrelicConfig(ctx, newrelic, ns, pod, index) - } - } - if insts.Go != nil { - newrelic := *insts.Go - var err error - i.logger.V(1).Info("injecting Go instrumentation into pod", "newrelic-namespace", newrelic.Namespace, "newrelic-name", newrelic.Name) - - goContainers := annotationValue(ns.ObjectMeta, pod.ObjectMeta, annotationInjectGoContainerName) - index := getContainerIndex(goContainers, pod) - - // Go instrumentation supports only single container instrumentation. - pod, err = apm.InjectGoSDK(newrelic.Spec.Go, pod) - if err != nil { - i.logger.Info("Skipping Go SDK injection", "reason", err.Error(), "container", pod.Spec.Containers[index].Name) - } else { - // Common env vars and config need to be applied to the agent container. - pod = i.injectCommonEnvVar(newrelic, pod, len(pod.Spec.Containers)-1) - pod = i.injectCommonSDKConfig(ctx, newrelic, ns, pod, len(pod.Spec.Containers)-1, 0) - } - } - return pod -} - -func getContainerIndex(containerName string, pod corev1.Pod) int { - // We search for specific container to inject variables and if no one is found - // We fallback to first container - var index = 0 - for idx, ctnair := range pod.Spec.Containers { - if ctnair.Name == containerName { - index = idx - } - } - - return index -} - -func (i *sdkInjector) injectCommonEnvVar(newrelic v1alpha1.Instrumentation, pod corev1.Pod, index int) corev1.Pod { - container := &pod.Spec.Containers[index] - for _, env := range newrelic.Spec.Env { - idx := getIndexOfEnv(container.Env, env.Name) - if idx == -1 { - container.Env = append(container.Env, env) - } - } - return pod -} - -// injectCommonSDKConfig adds common SDK configuration environment variables to the necessary pod -// agentIndex represents the index of the pod the needs the env vars to instrument the application. -// appIndex represents the index of the pod the will produce the telemetry. -// When the pod handling the instrumentation is the same as the pod producing the telemetry agentIndex -// and appIndex should be the same value. This is true for dotnet, java, nodejs, python, and ruby instrumentations. -// Go requires the agent to be a different container in the pod, so the agentIndex should represent this new sidecar -// and appIndex should represent the application being instrumented. -func (i *sdkInjector) injectCommonSDKConfig(ctx context.Context, newrelic v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod, agentIndex int, appIndex int) corev1.Pod { - container := &pod.Spec.Containers[agentIndex] - resourceMap := i.createResourceMap(ctx, newrelic, ns, pod, appIndex) - idx := getIndexOfEnv(container.Env, constants.EnvOTELServiceName) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELServiceName, - Value: chooseServiceName(pod, resourceMap, appIndex), - }) - } - if newrelic.Spec.Exporter.Endpoint != "" { - idx = getIndexOfEnv(container.Env, constants.EnvOTELExporterOTLPEndpoint) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELExporterOTLPEndpoint, - Value: newrelic.Spec.Endpoint, - }) - } - } - - // Some attributes might be empty, we should get them via k8s downward API - if resourceMap[string(semconv.K8SPodNameKey)] == "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvPodName, - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }) - resourceMap[string(semconv.K8SPodNameKey)] = fmt.Sprintf("$(%s)", constants.EnvPodName) - } - if newrelic.Spec.Resource.AddK8sUIDAttributes { - if resourceMap[string(semconv.K8SPodUIDKey)] == "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvPodUID, - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.uid", - }, - }, - }) - resourceMap[string(semconv.K8SPodUIDKey)] = fmt.Sprintf("$(%s)", constants.EnvPodUID) - } - } - - idx = getIndexOfEnv(container.Env, constants.EnvOTELResourceAttrs) - if idx == -1 || !strings.Contains(container.Env[idx].Value, string(semconv.ServiceVersionKey)) { - vsn := chooseServiceVersion(pod, appIndex) - if vsn != "" { - resourceMap[string(semconv.ServiceVersionKey)] = vsn - } - } - - if resourceMap[string(semconv.K8SNodeNameKey)] == "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvNodeName, - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }) - resourceMap[string(semconv.K8SNodeNameKey)] = fmt.Sprintf("$(%s)", constants.EnvNodeName) - } - - idx = getIndexOfEnv(container.Env, constants.EnvOTELResourceAttrs) - resStr := resourceMapToStr(resourceMap) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELResourceAttrs, - Value: resStr, - }) - } else { - if !strings.HasSuffix(container.Env[idx].Value, ",") { - resStr = "," + resStr - } - container.Env[idx].Value += resStr - } - - idx = getIndexOfEnv(container.Env, constants.EnvOTELPropagators) - if idx == -1 && len(newrelic.Spec.Propagators) > 0 { - propagators := *(*[]string)((unsafe.Pointer(&newrelic.Spec.Propagators))) - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELPropagators, - Value: strings.Join(propagators, ","), - }) - } - - idx = getIndexOfEnv(container.Env, constants.EnvOTELTracesSampler) - // configure sampler only if it is configured in the CR - if idx == -1 && newrelic.Spec.Sampler.Type != "" { - idxSamplerArg := getIndexOfEnv(container.Env, constants.EnvOTELTracesSamplerArg) - if idxSamplerArg == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELTracesSampler, - Value: string(newrelic.Spec.Sampler.Type), - }) - if newrelic.Spec.Sampler.Argument != "" { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvOTELTracesSamplerArg, - Value: newrelic.Spec.Sampler.Argument, - }) - } - } - } +const ( + DefaultLicenseKeySecretName = "newrelic-key-secret" +) - // Move OTEL_RESOURCE_ATTRIBUTES to last position on env list. - // When OTEL_RESOURCE_ATTRIBUTES environment variable uses other env vars - // as attributes value they have to be configured before. - // It is mandatory to set right order to avoid attributes with value - // pointing to the name of used environment variable instead of its value. - idx = getIndexOfEnv(container.Env, constants.EnvOTELResourceAttrs) - envs := moveEnvToListEnd(container.Env, idx) - container.Env = envs +// compile time type assertion +var _ SdkInjector = (*NewrelicSdkInjector)(nil) - return pod +// SdkInjector is used to inject our instrumentation into a pod +type SdkInjector interface { + Inject(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod } -func (i *sdkInjector) injectNewrelicConfig(ctx context.Context, newrelic v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod, index int) corev1.Pod { - container := &pod.Spec.Containers[index] - resourceMap := i.createResourceMap(ctx, newrelic, ns, pod, index) - idx := getIndexOfEnv(container.Env, constants.EnvNewRelicAppName) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvNewRelicAppName, - Value: chooseServiceName(pod, resourceMap, index), - }) - } - idx = getIndexOfEnv(container.Env, constants.EnvNewRelicLicenseKey) - if idx == -1 { - optional := true - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvNewRelicLicenseKey, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "newrelic-key-secret"}, - Key: "new_relic_license_key", - Optional: &optional, - }, - }, - }) - } - idx = getIndexOfEnv(container.Env, constants.EnvNewRelicLabels) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvNewRelicLabels, - Value: "operator:auto-injection", - }) - } - idx = getIndexOfEnv(container.Env, constants.EnvNewRelicK8sOperatorEnabled) - if idx == -1 { - container.Env = append(container.Env, corev1.EnvVar{ - Name: constants.EnvNewRelicK8sOperatorEnabled, - Value: "true", - }) - } - return pod +// NewrelicSdkInjector is the base struct used to inject our instrumentation into a pod +type NewrelicSdkInjector struct { + client client.Client + logger logr.Logger + injectorRegistry *apm.InjectorRegistery } -func chooseServiceName(pod corev1.Pod, resources map[string]string, index int) string { - if name := resources[string(semconv.K8SDeploymentNameKey)]; name != "" { - return name - } - if name := resources[string(semconv.K8SStatefulSetNameKey)]; name != "" { - return name - } - if name := resources[string(semconv.K8SJobNameKey)]; name != "" { - return name - } - if name := resources[string(semconv.K8SCronJobNameKey)]; name != "" { - return name +// NewNewrelicSdkInjector is used to create our injector +func NewNewrelicSdkInjector(logger logr.Logger, client client.Client, injectorRegistry *apm.InjectorRegistery) *NewrelicSdkInjector { + return &NewrelicSdkInjector{ + client: client, + logger: logger, + injectorRegistry: injectorRegistry, } - if name := resources[string(semconv.K8SPodNameKey)]; name != "" { - return name - } - return pod.Spec.Containers[index].Name -} - -// obtains version by splitting image string on ":" and extracting final element from resulting array. -func chooseServiceVersion(pod corev1.Pod, index int) string { - parts := strings.Split(pod.Spec.Containers[index].Image, ":") - tag := parts[len(parts)-1] - //guard statement to handle case where image name has a port number - if strings.Contains(tag, "/") { - return "" - } - return tag } -func resourceMapToStr(res map[string]string) string { - keys := make([]string, 0, len(res)) - for k := range res { - keys = append(keys, k) - } - sort.Strings(keys) - - var str = "" - for _, k := range keys { - if str != "" { - str += "," - } - str += fmt.Sprintf("%s=%s", k, res[k]) - } - - return str -} - -// creates the service.instance.id following the semantic defined by -// https://github.com/open-telemetry/semantic-conventions/pull/312. -func createServiceInstanceId(namespaceName, podName, containerName string) string { - var serviceInstanceId string - if namespaceName != "" && podName != "" && containerName != "" { - resNames := []string{namespaceName, podName, containerName} - serviceInstanceId = strings.Join(resNames, ".") - } - return serviceInstanceId -} - -// createResourceMap creates resource attribute map. -// User defined attributes (in explicitly set env var) have higher precedence. -func (i *sdkInjector) createResourceMap(ctx context.Context, newrelic v1alpha1.Instrumentation, ns corev1.Namespace, pod corev1.Pod, index int) map[string]string { - // get existing resources env var and parse it into a map - existingRes := map[string]bool{} - existingResourceEnvIdx := getIndexOfEnv(pod.Spec.Containers[index].Env, constants.EnvOTELResourceAttrs) - if existingResourceEnvIdx > -1 { - existingResArr := strings.Split(pod.Spec.Containers[index].Env[existingResourceEnvIdx].Value, ",") - for _, kv := range existingResArr { - keyValueArr := strings.Split(strings.TrimSpace(kv), "=") - if len(keyValueArr) != 2 { +// Inject is used to utilize a list of instrumentations, and if the injectors language matches the instrumentation, trigger the injector +func (i *NewrelicSdkInjector) Inject(ctx context.Context, insts []*v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) corev1.Pod { + hadMatchingInjector := false + for _, inst := range insts { + for _, injector := range i.injectorRegistry.GetInjectors() { + mutatedPod, matchedThisInjector, err := i.injectWithInjector(ctx, injector, inst, ns, pod) + hadMatchingInjector = hadMatchingInjector || matchedThisInjector + if err != nil { + i.logger.Error(err, "Skipping agent injection", "agent_language", inst.Spec.Agent.Language) continue } - existingRes[keyValueArr[0]] = true - } - } - - res := map[string]string{} - for k, v := range newrelic.Spec.Resource.Attributes { - if !existingRes[k] { - res[k] = v - } - } - k8sResources := map[attribute.Key]string{} - k8sResources[semconv.K8SNamespaceNameKey] = ns.Name - k8sResources[semconv.K8SContainerNameKey] = pod.Spec.Containers[index].Name - // Some fields might be empty - node name, pod name - // The pod name might be empty if the pod is created form deployment template - k8sResources[semconv.K8SPodNameKey] = pod.Name - k8sResources[semconv.K8SPodUIDKey] = string(pod.UID) - k8sResources[semconv.K8SNodeNameKey] = pod.Spec.NodeName - k8sResources[semconv.ServiceInstanceIDKey] = createServiceInstanceId(ns.Name, pod.Name, pod.Spec.Containers[index].Name) - i.addParentResourceLabels(ctx, newrelic.Spec.Resource.AddK8sUIDAttributes, ns, pod.ObjectMeta, k8sResources) - for k, v := range k8sResources { - if !existingRes[string(k)] && v != "" { - res[string(k)] = v + pod = mutatedPod } } - return res -} - -func (i *sdkInjector) addParentResourceLabels(ctx context.Context, uid bool, ns corev1.Namespace, objectMeta metav1.ObjectMeta, resources map[attribute.Key]string) { - for _, owner := range objectMeta.OwnerReferences { - switch strings.ToLower(owner.Kind) { - case "replicaset": - resources[semconv.K8SReplicaSetNameKey] = owner.Name - if uid { - resources[semconv.K8SReplicaSetUIDKey] = string(owner.UID) - } - // parent of ReplicaSet is e.g. Deployment which we are interested to know - rs := appsv1.ReplicaSet{} - nsn := types.NamespacedName{Namespace: ns.Name, Name: owner.Name} - backOff := wait.Backoff{Duration: 10 * time.Millisecond, Factor: 1.5, Jitter: 0.1, Steps: 20, Cap: 2 * time.Second} - - checkError := func(err error) bool { - return apierrors.IsNotFound(err) - } - - getReplicaSet := func() error { - return i.client.Get(ctx, nsn, &rs) - } - - // use a retry loop to get the Deployment. A single call to client.get fails occasionally - err := retry.OnError(backOff, checkError, getReplicaSet) - if err != nil { - i.logger.Error(err, "failed to get replicaset", "replicaset", nsn.Name, "namespace", nsn.Namespace) - } - i.addParentResourceLabels(ctx, uid, ns, rs.ObjectMeta, resources) - case "deployment": - resources[semconv.K8SDeploymentNameKey] = owner.Name - if uid { - resources[semconv.K8SDeploymentUIDKey] = string(owner.UID) - } - case "statefulset": - resources[semconv.K8SStatefulSetNameKey] = owner.Name - if uid { - resources[semconv.K8SStatefulSetUIDKey] = string(owner.UID) - } - case "daemonset": - resources[semconv.K8SDaemonSetNameKey] = owner.Name - if uid { - resources[semconv.K8SDaemonSetUIDKey] = string(owner.UID) - } - case "job": - resources[semconv.K8SJobNameKey] = owner.Name - if uid { - resources[semconv.K8SJobUIDKey] = string(owner.UID) - } - case "cronjob": - resources[semconv.K8SCronJobNameKey] = owner.Name - if uid { - resources[semconv.K8SCronJobUIDKey] = string(owner.UID) - } - } + if !hadMatchingInjector { + i.logger.Info("No language agents found while trying to instrument pod", + "pod_details", pod.String(), + "pod_namespace", pod.Namespace, + "registered_injectors", i.injectorRegistry.GetInjectors().Names(), + ) } + return pod } -func getIndexOfEnv(envs []corev1.EnvVar, name string) int { - for i := range envs { - if envs[i].Name == name { - return i +func (i *NewrelicSdkInjector) injectWithInjector(ctx context.Context, injector apm.Injector, inst *v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (mutatedPod corev1.Pod, hadMatchingInjector bool, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v, stacktrace: %s", r, debug.Stack()) } - } - return -1 -} + }() -func moveEnvToListEnd(envs []corev1.EnvVar, idx int) []corev1.EnvVar { - if idx >= 0 && idx < len(envs) { - envToMove := envs[idx] - envs = append(envs[:idx], envs[idx+1:]...) - envs = append(envs, envToMove) + if injector.Language() != inst.Spec.Agent.Language { + return pod, false, nil } + injector.ConfigureClient(i.client) + injector.ConfigureLogger(i.logger.WithValues("injector", injector.Language())) + i.logger.V(1).Info("injecting instrumentation into pod", + "agent_language", inst.Spec.Agent.Language, + "newrelic-namespace", inst.Namespace, + "newrelic-name", inst.Name, + ) - return envs + mutatedPod, err = injector.Inject(ctx, *inst, ns, pod) + return mutatedPod, true, err } diff --git a/src/instrumentation/sdk_test.go b/src/instrumentation/sdk_test.go index eb4f3758..6721a667 100644 --- a/src/instrumentation/sdk_test.go +++ b/src/instrumentation/sdk_test.go @@ -2,864 +2,205 @@ package instrumentation import ( "context" - "encoding/json" + "fmt" "testing" "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - appsv1 "k8s.io/api/apps/v1" + "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" + "github.com/newrelic/k8s-agents-operator/src/apm" ) -const ( - javaJVMArgument = " -javaagent:/newrelic-instrumentation/newrelic-agent.jar" - nodeRequireArgument = " --require /newrelic-instrumentation/newrelicinstrumentation.js" - pythonPathPrefix = "/newrelic-instrumentation" -) +var _ apm.Injector = (*ErrorInjector)(nil) +type ErrorInjector struct { + err error +} -func TestSDKInjection(t *testing.T) { - ns := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "project1", - }, +func (ei *ErrorInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + return pod, ei.err +} + +func (ei *ErrorInjector) Language() string { + return "error" +} + +func (ei *ErrorInjector) ConfigureLogger(logger logr.Logger) {} + +func (ei *ErrorInjector) ConfigureClient(client client.Client) {} + +var _ apm.Injector = (*PanicInjector)(nil) + +type PanicInjector struct { + injectAttempted bool +} + +func (pi *PanicInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + pi.injectAttempted = true + var a *int + var b int + //nolint:all + *a = b // nil pointer panic + return corev1.Pod{}, nil +} + +func (pi *PanicInjector) Language() string { + return "panic" +} + +func (pi *PanicInjector) ConfigureLogger(logger logr.Logger) {} + +func (pi *PanicInjector) ConfigureClient(client client.Client) {} + +var _ apm.Injector = (*AnnotationInjector)(nil) + +type AnnotationInjector struct { + lang string +} + +func (ai *AnnotationInjector) Inject(ctx context.Context, inst v1alpha2.Instrumentation, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} } - err := k8sClient.Create(context.Background(), &ns) - require.NoError(t, err) - dep := appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "my-deployment", - UID: "depuid", + pod.Annotations["injected-"+ai.lang] = "true" + return pod, nil +} + +func (ai *AnnotationInjector) Language() string { + return ai.lang +} + +func (ai *AnnotationInjector) ConfigureLogger(logger logr.Logger) {} + +func (ai *AnnotationInjector) ConfigureClient(client client.Client) {} + +func TestNewrelicSdkInjector_Inject(t *testing.T) { + vtrue, vzero := true, int64(0) + _, _ = vtrue, vzero + logger := logr.Discard() + tests := []struct { + name string + langInsts []*v1alpha2.Instrumentation + ns corev1.Namespace + pod corev1.Pod + containerName string + expectedPod corev1.Pod + }{ + { + name: "empty", }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "my"}, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": "my"}, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Name: "app", Image: "foo:bar"}}, + { + name: "none", + langInsts: []*v1alpha2.Instrumentation{}, + pod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ + { + Name: "nothing", }, - }, - }, - } - err = k8sClient.Create(context.Background(), &dep) - require.NoError(t, err) - rs := appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-replicaset", - Namespace: "project1", - UID: "rsuid", - OwnerReferences: []metav1.OwnerReference{ + }}}, + expectedPod: corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{ { - Kind: "Deployment", - APIVersion: "apps/v1", - Name: "my-deployment", - UID: "depuid", + Name: "nothing", }, - }, + }}}, }, - Spec: appsv1.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "my"}, + { + name: "inject just a", + langInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "a"}}}, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"app": "my"}, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Name: "app", Image: "foo:bar"}}, - }, + pod: corev1.Pod{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, + }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"injected-a": "true"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, }, - } - err = k8sClient.Create(context.Background(), &rs) - require.NoError(t, err) - - tests := []struct { - name string - inst v1alpha1.Instrumentation - pod corev1.Pod - expected corev1.Pod - }{ { - name: "SDK env vars not defined", - inst: v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4317", - }, - Resource: v1alpha1.Resource{ - AddK8sUIDAttributes: true, - }, - Propagators: []v1alpha1.Propagator{"b3", "jaeger"}, - Sampler: v1alpha1.Sampler{ - Type: "parentbased_traceidratio", - Argument: "0.25", - }, - }, + name: "inject just b", + langInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "b"}}}, }, pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - Kind: "ReplicaSet", - Name: "my-replicaset", - UID: "rsuid", - APIVersion: "apps/v1", - }, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "application-name", - }, - }, - }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - Kind: "ReplicaSet", - Name: "my-replicaset", - UID: "rsuid", - APIVersion: "apps/v1", - }, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "application-name", - Env: []corev1.EnvVar{ - { - Name: "OTEL_SERVICE_NAME", - Value: "my-deployment", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "https://collector:4317", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_PROPAGATORS", - Value: "b3,jaeger", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "parentbased_traceidratio", - }, - { - Name: "OTEL_TRACES_SAMPLER_ARG", - Value: "0.25", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=application-name,k8s.deployment.name=my-deployment,k8s.deployment.uid=depuid,k8s.namespace.name=project1,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=app,k8s.pod.uid=pod-uid,k8s.replicaset.name=my-replicaset,k8s.replicaset.uid=rsuid", - }, - }, - }, - }, - }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"injected-b": "true"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, }, { - name: "SDK env vars defined", - inst: v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4317", - }, - Resource: v1alpha1.Resource{ - Attributes: map[string]string{ - "fromcr": "val", - }, - }, - Propagators: []v1alpha1.Propagator{"jaeger"}, - Sampler: v1alpha1.Sampler{ - Type: "parentbased_traceidratio", - Argument: "0.25", - }, - }, + name: "inject a and b", + langInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "a"}}}, + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "b"}}}, }, pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Env: []corev1.EnvVar{ - { - Name: "OTEL_SERVICE_NAME", - Value: "explicitly_set", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "explicitly_set", - }, - { - Name: "OTEL_PROPAGATORS", - Value: "b3", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "always_on", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "foo=bar,k8s.container.name=other,", - }, - }, - }, - }, - }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Env: []corev1.EnvVar{ - { - Name: "OTEL_SERVICE_NAME", - Value: "explicitly_set", - }, - { - Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - Value: "explicitly_set", - }, - { - Name: "OTEL_PROPAGATORS", - Value: "b3", - }, - { - Name: "OTEL_TRACES_SAMPLER", - Value: "always_on", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "foo=bar,k8s.container.name=other,fromcr=val,k8s.namespace.name=project1,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=app", - }, - }, - }, - }, - }, + expectedPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{"injected-a": "true", "injected-b": "true"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, }, { - name: "Empty instrumentation spec", - inst: v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{}, + name: "inject has an error, pod should not be modified by that specific injector", + langInsts: []*v1alpha2.Instrumentation{ + {Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "error"}}}, }, pod: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - Kind: "ReplicaSet", - Name: "my-replicaset", - UID: "rsuid", - APIVersion: "apps/v1", - }, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "application-name", - }, - }, - }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, - expected: corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "project1", - Name: "app", - UID: "pod-uid", - OwnerReferences: []metav1.OwnerReference{ - { - Kind: "ReplicaSet", - Name: "my-replicaset", - UID: "rsuid", - APIVersion: "apps/v1", - }, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "application-name", - Env: []corev1.EnvVar{ - { - Name: "OTEL_SERVICE_NAME", - Value: "my-deployment", - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "spec.nodeName", - }, - }, - }, - { - Name: "OTEL_RESOURCE_ATTRIBUTES", - Value: "k8s.container.name=application-name,k8s.deployment.name=my-deployment,k8s.namespace.name=project1,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=app,k8s.pod.uid=pod-uid,k8s.replicaset.name=my-replicaset", - }, - }, - }, - }, - }, + expectedPod: corev1.Pod{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "pod-name"}}}, }, }, } - for _, test := range tests { t.Run(test.name, func(t *testing.T) { - inj := sdkInjector{ - client: k8sClient, + ctx := context.Background() + injectorRegistry := apm.NewInjectorRegistry() + apmInjectors := []apm.Injector{ + &AnnotationInjector{lang: "a"}, + &AnnotationInjector{lang: "b"}, + &ErrorInjector{err: fmt.Errorf("some error")}, + } + for _, apmInjector := range apmInjectors { + injectorRegistry.MustRegister(apmInjector) + } + for _, langInst := range test.langInsts { + langInst.Default() + } + injector := NewNewrelicSdkInjector(logger, k8sClient, injectorRegistry) + pod := injector.Inject(ctx, test.langInsts, test.ns, test.pod) + if diff := cmp.Diff(test.expectedPod, pod); diff != "" { + t.Errorf("Unexpected diff (-want +got): %s", diff) } - pod := inj.injectCommonSDKConfig(context.Background(), test.inst, corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: test.pod.Namespace}}, test.pod, 0, 0) - _, err = json.MarshalIndent(pod, "", " ") - assert.NoError(t, err) - assert.Equal(t, test.expected, test.expected) }) } } -func TestInjectJava(t *testing.T) { - inst := v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - Java: v1alpha1.Java{ - Image: "img:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4317", - }, - }, - } - insts := languageInstrumentations{ - Java: &inst, - } - inj := sdkInjector{ - logger: logr.Discard(), - } - _ = inj.inject(context.Background(), insts, - corev1.Namespace{}, - corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, - }, "") - // assert.Equal(t, corev1.Pod{ - // Spec: corev1.PodSpec{ - // Volumes: []corev1.Volume{ - // { - // Name: volumeName, - // VolumeSource: corev1.VolumeSource{ - // EmptyDir: &corev1.EmptyDirVolumeSource{}, - // }, - // }, - // }, - // InitContainers: []corev1.Container{ - // { - // Name: initContainerName, - // Image: "img:1", - // Command: []string{"cp", "/newrelic-agent.jar", "/newrelic-instrumentation/newrelic-agent.jar"}, - // VolumeMounts: []corev1.VolumeMount{{ - // Name: volumeName, - // MountPath: "/newrelic-instrumentation", - // }}, - // }, - // }, - // Containers: []corev1.Container{ - // { - // Name: "app", - // VolumeMounts: []corev1.VolumeMount{ - // { - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }, - // }, - // Env: []corev1.EnvVar{ - // { - // Name: "JAVA_TOOL_OPTIONS", - // Value: javaJVMArgument, - // }, - // { - // Name: "OTEL_SERVICE_NAME", - // Value: "app", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - // Value: "https://collector:4317", - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "metadata.name", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "spec.nodeName", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES", - // Value: "k8s.container.name=app,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - // }, - // }, - // }, - // }, - // }, - // }, pod) -} - -func TestInjectNodeJS(t *testing.T) { - inst := v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - NodeJS: v1alpha1.NodeJS{ - Image: "img:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4318", - }, - }, - } - insts := languageInstrumentations{ - NodeJS: &inst, - } - inj := sdkInjector{ - logger: logr.Discard(), - } - _ = inj.inject(context.Background(), insts, - corev1.Namespace{}, - corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, - }, "") - // assert.Equal(t, corev1.Pod{ - // Spec: corev1.PodSpec{ - // Volumes: []corev1.Volume{ - // { - // Name: volumeName, - // VolumeSource: corev1.VolumeSource{ - // EmptyDir: &corev1.EmptyDirVolumeSource{}, - // }, - // }, - // }, - // InitContainers: []corev1.Container{ - // { - // Name: initContainerName, - // Image: "img:1", - // Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, - // VolumeMounts: []corev1.VolumeMount{{ - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }}, - // }, - // }, - // Containers: []corev1.Container{ - // { - // Name: "app", - // VolumeMounts: []corev1.VolumeMount{ - // { - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }, - // }, - // Env: []corev1.EnvVar{ - // { - // Name: "NODE_OPTIONS", - // Value: nodeRequireArgument, - // }, - // { - // Name: "OTEL_SERVICE_NAME", - // Value: "app", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - // Value: "https://collector:4318", - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "metadata.name", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "spec.nodeName", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES", - // Value: "k8s.container.name=app,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - // }, - // }, - // }, - // }, - // }, - // }, pod) -} - -func TestInjectPython(t *testing.T) { - inst := v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - Python: v1alpha1.Python{ - Image: "img:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4318", - }, - }, - } - insts := languageInstrumentations{ - Python: &inst, - } +func TestNewrelicSdkInjector_Inject_WithPanic(t *testing.T) { + ctx := context.Background() + var logger = logr.Discard() + injectorRegistry := apm.NewInjectorRegistry() + pi := &PanicInjector{} + injectorRegistry.MustRegister(pi) + injector := NewNewrelicSdkInjector(logger, k8sClient, injectorRegistry) + func() { + defer func() { + if r := recover(); r != nil { + t.Fatalf("failed to handle panic") + } + }() + _ = injector.Inject(ctx, []*v1alpha2.Instrumentation{{Spec: v1alpha2.InstrumentationSpec{Agent: v1alpha2.Agent{Language: "panic", Image: "panic"}}}}, corev1.Namespace{}, corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "panic", Image: "panic"}}}}) + }() - inj := sdkInjector{ - logger: logr.Discard(), + if !pi.injectAttempted { + t.Fatalf("failed to trigger an injected panic") } - _ = inj.inject(context.Background(), insts, - corev1.Namespace{}, - corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, - }, "") - // assert.Equal(t, corev1.Pod{ - // Spec: corev1.PodSpec{ - // Volumes: []corev1.Volume{ - // { - // Name: volumeName, - // VolumeSource: corev1.VolumeSource{ - // EmptyDir: &corev1.EmptyDirVolumeSource{}, - // }, - // }, - // }, - // InitContainers: []corev1.Container{ - // { - // Name: initContainerName, - // Image: "img:1", - // Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, - // VolumeMounts: []corev1.VolumeMount{{ - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }}, - // }, - // }, - // Containers: []corev1.Container{ - // { - // Name: "app", - // VolumeMounts: []corev1.VolumeMount{ - // { - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }, - // }, - // Env: []corev1.EnvVar{ - // { - // Name: "PYTHONPATH", - // Value: fmt.Sprintf("%s:%s", pythonPathPrefix, "pythonPathSuffix"), - // }, - // { - // Name: "OTEL_TRACES_EXPORTER", - // Value: "otlp", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", - // Value: "http/protobuf", - // }, - // { - // Name: "OTEL_METRICS_EXPORTER", - // Value: "otlp", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL", - // Value: "http/protobuf", - // }, - // { - // Name: "OTEL_SERVICE_NAME", - // Value: "app", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - // Value: "https://collector:4318", - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "metadata.name", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "spec.nodeName", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES", - // Value: "k8s.container.name=app,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - // }, - // }, - // }, - // }, - // }, - // }, pod) } - -func TestInjectDotNet(t *testing.T) { - inst := v1alpha1.Instrumentation{ - Spec: v1alpha1.InstrumentationSpec{ - DotNet: v1alpha1.DotNet{ - Image: "img:1", - }, - Exporter: v1alpha1.Exporter{ - Endpoint: "https://collector:4318", - }, - }, - } - insts := languageInstrumentations{ - DotNet: &inst, - } - inj := sdkInjector{ - logger: logr.Discard(), - } - _ = inj.inject(context.Background(), insts, - corev1.Namespace{}, - corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, - }, "") - // assert.Equal(t, corev1.Pod{ - // Spec: corev1.PodSpec{ - // Volumes: []corev1.Volume{ - // { - // Name: volumeName, - // VolumeSource: corev1.VolumeSource{ - // EmptyDir: &corev1.EmptyDirVolumeSource{}, - // }, - // }, - // }, - // InitContainers: []corev1.Container{ - // { - // Name: initContainerName, - // Image: "img:1", - // Command: []string{"cp", "-a", "/autoinstrumentation/.", "/otel-auto-instrumentation/"}, - // VolumeMounts: []corev1.VolumeMount{{ - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }}, - // }, - // }, - // Containers: []corev1.Container{ - // { - // Name: "app", - // VolumeMounts: []corev1.VolumeMount{ - // { - // Name: volumeName, - // MountPath: "/otel-auto-instrumentation", - // }, - // }, - // Env: []corev1.EnvVar{ - // // { - // // Name: envDotNetCoreClrEnableProfiling, - // // Value: dotNetCoreClrEnableProfilingEnabled, - // // }, - // // { - // // Name: envDotNetCoreClrProfiler, - // // Value: dotNetCoreClrProfilerID, - // // }, - // // { - // // Name: envDotNetCoreClrProfilerPath, - // // Value: dotNetCoreClrProfilerPath, - // // }, - // // { - // // Name: envDotNetStartupHook, - // // Value: dotNetStartupHookPath, - // // }, - // // { - // // Name: envDotNetAdditionalDeps, - // // Value: dotNetAdditionalDepsPath, - // // }, - // // { - // // Name: envDotNetOTelAutoHome, - // // Value: dotNetOTelAutoHomePath, - // // }, - // // { - // // Name: envDotNetSharedStore, - // // Value: dotNetSharedStorePath, - // // }, - // { - // Name: "OTEL_SERVICE_NAME", - // Value: "app", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - // Value: "https://collector:4318", - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "metadata.name", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "spec.nodeName", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES", - // Value: "k8s.container.name=app,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - // }, - // }, - // }, - // }, - // }, - // }, pod) -} - -func TestInjectSdkOnly(t *testing.T) { - // inst := v1alpha1.Instrumentation{ - // Spec: v1alpha1.InstrumentationSpec{ - // Exporter: v1alpha1.Exporter{ - // Endpoint: "https://collector:4318", - // }, - // }, - // } - insts := languageInstrumentations{ - // Sdk: &inst, - } - - inj := sdkInjector{ - logger: logr.Discard(), - } - _ = inj.inject(context.Background(), insts, - corev1.Namespace{}, - corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - }, - }, - }, - }, "") - // assert.Equal(t, corev1.Pod{ - // Spec: corev1.PodSpec{ - // Containers: []corev1.Container{ - // { - // Name: "app", - // Env: []corev1.EnvVar{ - // { - // Name: "OTEL_SERVICE_NAME", - // Value: "app", - // }, - // { - // Name: "OTEL_EXPORTER_OTLP_ENDPOINT", - // Value: "https://collector:4318", - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_POD_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "metadata.name", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES_NODE_NAME", - // ValueFrom: &corev1.EnvVarSource{ - // FieldRef: &corev1.ObjectFieldSelector{ - // FieldPath: "spec.nodeName", - // }, - // }, - // }, - // { - // Name: "OTEL_RESOURCE_ATTRIBUTES", - // Value: "k8s.container.name=app,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)", - // }, - // }, - // }, - // }, - // }, - // }, pod) -} \ No newline at end of file diff --git a/src/instrumentation/upgrade/upgrade.go b/src/instrumentation/upgrade/upgrade.go index 251ca597..c8e3a8cc 100644 --- a/src/instrumentation/upgrade/upgrade.go +++ b/src/instrumentation/upgrade/upgrade.go @@ -24,19 +24,12 @@ import ( "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) type InstrumentationUpgrade struct { - Client client.Client - Logger logr.Logger - DefaultAutoInstJava string - DefaultAutoInstNodeJS string - DefaultAutoInstPython string - DefaultAutoInstDotNet string - DefaultAutoInstPhp string - DefaultAutoInstRuby string - DefaultAutoInstGo string + Client client.Client + Logger logr.Logger } //+kubebuilder:rbac:groups=newrelic.com,resources=instrumentations,verbs=get;list;watch;update;patch @@ -50,7 +43,7 @@ func (u *InstrumentationUpgrade) ManagedInstances(ctx context.Context) error { "app.kubernetes.io/managed-by": "k8s-agents-operator", }), } - list := &v1alpha1.InstrumentationList{} + list := &v1alpha2.InstrumentationList{} if err := u.Client.List(ctx, list, opts...); err != nil { return fmt.Errorf("failed to list: %w", err) } @@ -73,62 +66,6 @@ func (u *InstrumentationUpgrade) ManagedInstances(ctx context.Context) error { return nil } -func (u *InstrumentationUpgrade) upgrade(_ context.Context, inst v1alpha1.Instrumentation) v1alpha1.Instrumentation { - autoInstJava := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationJava] - if autoInstJava != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.Java.Image == autoInstJava { - inst.Spec.Java.Image = u.DefaultAutoInstJava - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationJava] = u.DefaultAutoInstJava - } - } - autoInstNodeJS := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS] - if autoInstNodeJS != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.NodeJS.Image == autoInstNodeJS { - inst.Spec.NodeJS.Image = u.DefaultAutoInstNodeJS - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS] = u.DefaultAutoInstNodeJS - } - } - autoInstPython := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPython] - if autoInstPython != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.Python.Image == autoInstPython { - inst.Spec.Python.Image = u.DefaultAutoInstPython - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPython] = u.DefaultAutoInstPython - } - } - autoInstDotnet := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationDotNet] - if autoInstDotnet != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.DotNet.Image == autoInstDotnet { - inst.Spec.DotNet.Image = u.DefaultAutoInstDotNet - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationDotNet] = u.DefaultAutoInstDotNet - } - } - autoInstPhp := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPhp] - if autoInstPhp != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.Php.Image == autoInstPhp { - inst.Spec.Php.Image = u.DefaultAutoInstPhp - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationDotNet] = u.DefaultAutoInstPhp - } - } - autoInstRuby := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationRuby] - if autoInstRuby != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.Ruby.Image == autoInstRuby { - inst.Spec.Ruby.Image = u.DefaultAutoInstRuby - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationRuby] = u.DefaultAutoInstRuby - } - } - autoInstGo := inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationGo] - if autoInstGo != "" { - // upgrade the image only if the image matches the annotation - if inst.Spec.Go.Image == autoInstDotnet { - inst.Spec.Go.Image = u.DefaultAutoInstGo - inst.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationGo] = u.DefaultAutoInstGo - } - } +func (u *InstrumentationUpgrade) upgrade(_ context.Context, inst v1alpha2.Instrumentation) v1alpha2.Instrumentation { return inst } diff --git a/src/instrumentation/upgrade/upgrade_suite_test.go b/src/instrumentation/upgrade/upgrade_suite_test.go index cfd34bf8..a4f890ff 100644 --- a/src/instrumentation/upgrade/upgrade_suite_test.go +++ b/src/instrumentation/upgrade/upgrade_suite_test.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) var k8sClient client.Client @@ -48,7 +48,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - if err = v1alpha1.AddToScheme(testScheme); err != nil { + if err = v1alpha2.AddToScheme(testScheme); err != nil { fmt.Printf("failed to register scheme: %v", err) os.Exit(1) } diff --git a/src/instrumentation/upgrade/upgrade_test.go b/src/instrumentation/upgrade/upgrade_test.go index b29bec17..84eaca9c 100644 --- a/src/instrumentation/upgrade/upgrade_test.go +++ b/src/instrumentation/upgrade/upgrade_test.go @@ -21,13 +21,12 @@ import ( "testing" "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" ) func TestUpgrade(t *testing.T) { @@ -39,64 +38,41 @@ func TestUpgrade(t *testing.T) { }) require.NoError(t, err) - inst := &v1alpha1.Instrumentation{ + inst := &v1alpha2.Instrumentation{ ObjectMeta: metav1.ObjectMeta{ Name: "newrelic-instrumentation", Namespace: nsName, - Annotations: map[string]string{ - v1alpha1.AnnotationDefaultAutoInstrumentationJava: "java:1", - v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS: "nodejs:1", - v1alpha1.AnnotationDefaultAutoInstrumentationPython: "python:1", - v1alpha1.AnnotationDefaultAutoInstrumentationDotNet: "dotnet:1", - v1alpha1.AnnotationDefaultAutoInstrumentationPhp: "php:1", - v1alpha1.AnnotationDefaultAutoInstrumentationRuby: "ruby:1", - v1alpha1.AnnotationDefaultAutoInstrumentationGo: "go:1", - }, }, } inst.Default() - assert.Equal(t, "java:1", inst.Spec.Java.Image) - assert.Equal(t, "nodejs:1", inst.Spec.NodeJS.Image) - assert.Equal(t, "python:1", inst.Spec.Python.Image) - assert.Equal(t, "dotnet:1", inst.Spec.DotNet.Image) - assert.Equal(t, "php:1", inst.Spec.Php.Image) - assert.Equal(t, "ruby:1", inst.Spec.Ruby.Image) - assert.Equal(t, "go:1", inst.Spec.Go.Image) err = k8sClient.Create(context.Background(), inst) require.NoError(t, err) up := &InstrumentationUpgrade{ - Logger: logr.Discard(), - DefaultAutoInstJava: "java:2", - DefaultAutoInstNodeJS: "nodejs:2", - DefaultAutoInstPython: "python:2", - DefaultAutoInstDotNet: "dotnet:2", - DefaultAutoInstPhp: "php:2", - DefaultAutoInstRuby: "ruby:2", - DefaultAutoInstGo: "go:2", - Client: k8sClient, + Logger: logr.Discard(), + Client: k8sClient, } err = up.ManagedInstances(context.Background()) require.NoError(t, err) - updated := v1alpha1.Instrumentation{} + updated := v1alpha2.Instrumentation{} err = k8sClient.Get(context.Background(), types.NamespacedName{ Namespace: nsName, Name: "newrelic-instrumentation", }, &updated) require.NoError(t, err) - // assert.Equal(t, "java:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationJava]) + // assert.Equal(t, "java:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationJava]) // assert.Equal(t, "java:2", updated.Spec.Java.Image) - // assert.Equal(t, "nodejs:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS]) + // assert.Equal(t, "nodejs:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationNodeJS]) // assert.Equal(t, "nodejs:2", updated.Spec.NodeJS.Image) - // assert.Equal(t, "python:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPython]) + // assert.Equal(t, "python:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationPython]) // assert.Equal(t, "python:2", updated.Spec.Python.Image) - // assert.Equal(t, "dotnet:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationDotNet]) + // assert.Equal(t, "dotnet:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationDotNet]) // assert.Equal(t, "dotnet:2", updated.Spec.DotNet.Image) - // assert.Equal(t, "php:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPhp]) + // assert.Equal(t, "php:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationPhp]) // assert.Equal(t, "php:2", updated.Spec.Php.Image) - // assert.Equal(t, "ruby:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationRuby]) + // assert.Equal(t, "ruby:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationRuby]) // assert.Equal(t, "ruby:2", updated.Spec.Ruby.Image) - // assert.Equal(t, "go:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationGo]) + // assert.Equal(t, "go:2", updated.Annotations[v1alpha2.AnnotationDefaultAutoInstrumentationGo]) // assert.Equal(t, "go:2", updated.Spec.Go.Image) } diff --git a/src/internal/config/main.go b/src/internal/config/main.go index 6f276ac7..f87979f6 100644 --- a/src/internal/config/main.go +++ b/src/internal/config/main.go @@ -33,20 +33,13 @@ const ( // Config holds the static configuration for this operator. type Config struct { - autoDetect autodetect.AutoDetect - logger logr.Logger - autoInstrumentationPythonImage string - autoInstrumentationDotNetImage string - autoInstrumentationNodeJSImage string - autoInstrumentationJavaImage string - autoInstrumentationGoImage string - autoInstrumentationPhpImage string - autoInstrumentationRubyImage string - onOpenShiftRoutesChange changeHandler - labelsFilter []string - openshiftRoutes openshiftRoutesStore - autoDetectFrequency time.Duration - autoscalingVersion autodetect.AutoscalingVersion + autoDetect autodetect.AutoDetect + logger logr.Logger + onOpenShiftRoutesChange changeHandler + labelsFilter []string + openshiftRoutes openshiftRoutesStore + autoDetectFrequency time.Duration + autoscalingVersion autodetect.AutoscalingVersion } // New constructs a new configuration based on the given options. @@ -65,20 +58,13 @@ func New(opts ...Option) Config { } return Config{ - autoDetect: o.autoDetect, - autoDetectFrequency: o.autoDetectFrequency, - logger: o.logger, - openshiftRoutes: o.openshiftRoutes, - onOpenShiftRoutesChange: o.onOpenShiftRoutesChange, - autoInstrumentationJavaImage: o.autoInstrumentationJavaImage, - autoInstrumentationNodeJSImage: o.autoInstrumentationNodeJSImage, - autoInstrumentationPythonImage: o.autoInstrumentationPythonImage, - autoInstrumentationDotNetImage: o.autoInstrumentationDotNetImage, - autoInstrumentationPhpImage: o.autoInstrumentationPhpImage, - autoInstrumentationRubyImage: o.autoInstrumentationRubyImage, - autoInstrumentationGoImage: o.autoInstrumentationGoImage, - labelsFilter: o.labelsFilter, - autoscalingVersion: o.autoscalingVersion, + autoDetect: o.autoDetect, + autoDetectFrequency: o.autoDetectFrequency, + logger: o.logger, + openshiftRoutes: o.openshiftRoutes, + onOpenShiftRoutesChange: o.onOpenShiftRoutesChange, + labelsFilter: o.labelsFilter, + autoscalingVersion: o.autoscalingVersion, } } @@ -139,41 +125,6 @@ func (c *Config) AutoscalingVersion() autodetect.AutoscalingVersion { return c.autoscalingVersion } -// AutoInstrumentationJavaImage returns New Relic Java auto-instrumentation container image. -func (c *Config) AutoInstrumentationJavaImage() string { - return c.autoInstrumentationJavaImage -} - -// AutoInstrumentationNodeJSImage returns New Relic NodeJS auto-instrumentation container image. -func (c *Config) AutoInstrumentationNodeJSImage() string { - return c.autoInstrumentationNodeJSImage -} - -// AutoInstrumentationPythonImage returns New Relic Python auto-instrumentation container image. -func (c *Config) AutoInstrumentationPythonImage() string { - return c.autoInstrumentationPythonImage -} - -// AutoInstrumentationDotNetImage returns New Relic DotNet auto-instrumentation container image. -func (c *Config) AutoInstrumentationDotNetImage() string { - return c.autoInstrumentationDotNetImage -} - -// AutoInstrumentationDotNetImage returns New Relic DotNet auto-instrumentation container image. -func (c *Config) AutoInstrumentationPhpImage() string { - return c.autoInstrumentationPhpImage -} - -// AutoInstrumentationRubyImage returns New Relic Ruby auto-instrumentation container image. -func (c *Config) AutoInstrumentationRubyImage() string { - return c.autoInstrumentationRubyImage -} - -// AutoInstrumentationGoImage returns Opentelemtrey Go auto-instrumentation container image. -func (c *Config) AutoInstrumentationGoImage() string { - return c.autoInstrumentationGoImage -} - // LabelsFilter Returns the filters converted to regex strings used to filter out unwanted labels from propagations. func (c *Config) LabelsFilter() []string { return c.labelsFilter diff --git a/src/internal/config/options.go b/src/internal/config/options.go index 3b6f2c04..d1d2aa77 100644 --- a/src/internal/config/options.go +++ b/src/internal/config/options.go @@ -31,21 +31,14 @@ import ( type Option func(c *options) type options struct { - autoDetect autodetect.AutoDetect - version version.Version - logger logr.Logger - autoInstrumentationDotNetImage string - autoInstrumentationGoImage string - autoInstrumentationJavaImage string - autoInstrumentationPythonImage string - autoInstrumentationNodeJSImage string - autoInstrumentationPhpImage string - autoInstrumentationRubyImage string - onOpenShiftRoutesChange changeHandler - labelsFilter []string - openshiftRoutes openshiftRoutesStore - autoDetectFrequency time.Duration - autoscalingVersion autodetect.AutoscalingVersion + autoDetect autodetect.AutoDetect + version version.Version + logger logr.Logger + onOpenShiftRoutesChange changeHandler + labelsFilter []string + openshiftRoutes openshiftRoutesStore + autoDetectFrequency time.Duration + autoscalingVersion autodetect.AutoscalingVersion } func WithAutoDetect(a autodetect.AutoDetect) Option { @@ -82,48 +75,6 @@ func WithVersion(v version.Version) Option { } } -func WithAutoInstrumentationJavaImage(s string) Option { - return func(o *options) { - o.autoInstrumentationJavaImage = s - } -} - -func WithAutoInstrumentationNodeJSImage(s string) Option { - return func(o *options) { - o.autoInstrumentationNodeJSImage = s - } -} - -func WithAutoInstrumentationPythonImage(s string) Option { - return func(o *options) { - o.autoInstrumentationPythonImage = s - } -} - -func WithAutoInstrumentationDotNetImage(s string) Option { - return func(o *options) { - o.autoInstrumentationDotNetImage = s - } -} - -func WithAutoInstrumentationPhpImage(s string) Option { - return func(o *options) { - o.autoInstrumentationPhpImage = s - } -} - -func WithAutoInstrumentationRubyImage(s string) Option { - return func(o *options) { - o.autoInstrumentationRubyImage = s - } -} - -func WithAutoInstrumentationGoImage(s string) Option { - return func(o *options) { - o.autoInstrumentationGoImage = s - } -} - func WithLabelFilters(labelFilters []string) Option { return func(o *options) { diff --git a/src/internal/version/main.go b/src/internal/version/main.go index 778dc625..708417bf 100644 --- a/src/internal/version/main.go +++ b/src/internal/version/main.go @@ -23,108 +23,31 @@ import ( ) var ( - version string - buildDate string - autoInstrumentationJava string - autoInstrumentationNodeJS string - autoInstrumentationPython string - autoInstrumentationDotNet string - autoInstrumentationPhp string - autoInstrumentationRuby string - autoInstrumentationGo string + version string + buildDate string ) // Version holds this Operator's version as well as the version of some of the components it uses. type Version struct { - Operator string `json:"k8s-agents-operator"` - BuildDate string `json:"build-date"` - Go string `json:"go-version"` - AutoInstrumentationJava string `json:"newrelic-instrumentation-java"` - AutoInstrumentationNodeJS string `json:"newrelic-instrumentation-nodejs"` - AutoInstrumentationPython string `json:"newrelic-instrumentation-python"` - AutoInstrumentationDotNet string `json:"newrelic-instrumentation-dotnet"` - AutoInstrumentationPhp string `json:"newrelic-instrumentation-php"` - AutoInstrumentationRuby string `json:"newrelic-instrumentation-ruby"` - AutoInstrumentationGo string `json:"autoinstrumentation-go"` + Operator string `json:"k8s-agents-operator"` + BuildDate string `json:"build-date"` + Go string `json:"go-version"` } // Get returns the Version object with the relevant information. func Get() Version { return Version{ - Operator: version, - BuildDate: buildDate, - Go: runtime.Version(), - AutoInstrumentationJava: AutoInstrumentationJava(), - AutoInstrumentationNodeJS: AutoInstrumentationNodeJS(), - AutoInstrumentationPython: AutoInstrumentationPython(), - AutoInstrumentationDotNet: AutoInstrumentationDotNet(), - AutoInstrumentationPhp: AutoInstrumentationPhp(), - AutoInstrumentationRuby: AutoInstrumentationRuby(), - AutoInstrumentationGo: AutoInstrumentationGo(), + Operator: version, + BuildDate: buildDate, + Go: runtime.Version(), } } func (v Version) String() string { return fmt.Sprintf( - "Version(Operator='%v', BuildDate='%v', Go='%v', AutoInstrumentationJava='%v', AutoInstrumentationNodeJS='%v', AutoInstrumentationPython='%v', AutoInstrumentationDotNet='%v', AutoInstrumentationPhp='%v', AutoInstrumentationRuby='%v', AutoInstrumentationGo='%v')", + "Version(Operator='%v', BuildDate='%v', Go='%v')", v.Operator, v.BuildDate, v.Go, - v.AutoInstrumentationJava, - v.AutoInstrumentationNodeJS, - v.AutoInstrumentationPython, - v.AutoInstrumentationDotNet, - v.AutoInstrumentationPhp, - v.AutoInstrumentationRuby, - v.AutoInstrumentationGo, ) } - -func AutoInstrumentationJava() string { - if len(autoInstrumentationJava) > 0 { - return autoInstrumentationJava - } - return "0.0.0" -} - -func AutoInstrumentationNodeJS() string { - if len(autoInstrumentationNodeJS) > 0 { - return autoInstrumentationNodeJS - } - return "0.0.0" -} - -func AutoInstrumentationPython() string { - if len(autoInstrumentationPython) > 0 { - return autoInstrumentationPython - } - return "0.0.0" -} - -func AutoInstrumentationDotNet() string { - if len(autoInstrumentationDotNet) > 0 { - return autoInstrumentationDotNet - } - return "0.0.0" -} - -func AutoInstrumentationPhp() string { - if len(autoInstrumentationPhp) > 0 { - return autoInstrumentationPhp - } - return "0.0.0.0" -} - -func AutoInstrumentationRuby() string { - if len(autoInstrumentationRuby) > 0 { - return autoInstrumentationRuby - } - return "0.0.0" -} - -func AutoInstrumentationGo() string { - if len(autoInstrumentationGo) > 0 { - return autoInstrumentationGo - } - return "0.0.0" -} diff --git a/src/internal/version/main_test.go b/src/internal/version/main_test.go index 45a854b7..1285b844 100644 --- a/src/internal/version/main_test.go +++ b/src/internal/version/main_test.go @@ -15,37 +15,3 @@ limitations under the License. */ package version - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAutoInstrumentationJavaFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationJava()) -} - -func TestAutoInstrumentationNodeJSFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationNodeJS()) -} - -func TestAutoInstrumentationPythonFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationPython()) -} - -func TestAutoInstrumentationDotNetFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationDotNet()) -} - -func TestAutoInstrumentationPhpFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0.0", AutoInstrumentationPhp()) -} - -func TestAutoInstrumentationRubyFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationRuby()) -} - -func TestAutoInstrumentationGoFallbackVersion(t *testing.T) { - assert.Equal(t, "0.0.0", AutoInstrumentationGo()) -} diff --git a/src/internal/webhookhandler/webhookhandler.go b/src/internal/webhookhandler/webhookhandler.go index 29747d43..c1bd975e 100644 --- a/src/internal/webhookhandler/webhookhandler.go +++ b/src/internal/webhookhandler/webhookhandler.go @@ -39,7 +39,7 @@ import ( // +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch -var _ WebhookHandler = (*podSidecarInjector)(nil) +var _ WebhookHandler = (*podMutationHandler)(nil) // WebhookHandler is a webhook handler that analyzes new pods and injects appropriate sidecars into it. type WebhookHandler interface { @@ -48,7 +48,7 @@ type WebhookHandler interface { } // the implementation. -type podSidecarInjector struct { +type podMutationHandler struct { client client.Client decoder *admission.Decoder logger logr.Logger @@ -63,7 +63,7 @@ type PodMutator interface { // NewWebhookHandler creates a new WebhookHandler. func NewWebhookHandler(cfg config.Config, logger logr.Logger, cl client.Client, podMutators []PodMutator) WebhookHandler { - return &podSidecarInjector{ + return &podMutationHandler{ config: cfg, logger: logger, client: cl, @@ -71,7 +71,7 @@ func NewWebhookHandler(cfg config.Config, logger logr.Logger, cl client.Client, } } -func (p *podSidecarInjector) Handle(ctx context.Context, req admission.Request) admission.Response { +func (p *podMutationHandler) Handle(ctx context.Context, req admission.Request) admission.Response { pod := corev1.Pod{} err := p.decoder.Decode(req, &pod) if err != nil { @@ -95,6 +95,7 @@ func (p *podSidecarInjector) Handle(ctx context.Context, req admission.Request) for _, m := range p.podMutators { pod, err = m.Mutate(ctx, ns, pod) if err != nil { + //@todo: actually print the error message res := admission.Errored(http.StatusInternalServerError, err) res.Allowed = true return res @@ -110,7 +111,7 @@ func (p *podSidecarInjector) Handle(ctx context.Context, req admission.Request) return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) } -func (p *podSidecarInjector) InjectDecoder(d *admission.Decoder) error { +func (p *podMutationHandler) InjectDecoder(d *admission.Decoder) error { p.decoder = d return nil } diff --git a/src/internal/webhookhandler/webhookhandler_suite_test.go b/src/internal/webhookhandler/webhookhandler_suite_test.go index 8757b83f..764e188f 100644 --- a/src/internal/webhookhandler/webhookhandler_suite_test.go +++ b/src/internal/webhookhandler/webhookhandler_suite_test.go @@ -36,7 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" // +kubebuilder:scaffold:imports ) @@ -50,6 +50,8 @@ var ( cfg *rest.Config ) +var _ = k8sClient + func TestMain(m *testing.M) { ctx, cancel = context.WithCancel(context.TODO()) defer cancel() @@ -66,7 +68,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - if err = v1alpha1.AddToScheme(testScheme); err != nil { + if err = v1alpha2.AddToScheme(testScheme); err != nil { fmt.Printf("failed to register scheme: %v", err) os.Exit(1) } diff --git a/src/main.go b/src/main.go index 438af916..3ffa956e 100644 --- a/src/main.go +++ b/src/main.go @@ -41,7 +41,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" - v1alpha1 "github.com/newrelic/k8s-agents-operator/src/api/v1alpha1" + "github.com/newrelic/k8s-agents-operator/src/api/v1alpha2" + "github.com/newrelic/k8s-agents-operator/src/apm" "github.com/newrelic/k8s-agents-operator/src/autodetect" "github.com/newrelic/k8s-agents-operator/src/instrumentation" instrumentationupgrade "github.com/newrelic/k8s-agents-operator/src/instrumentation/upgrade" @@ -64,7 +65,7 @@ type tlsConfig struct { func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -79,17 +80,12 @@ func main() { // add flags related to this operator var ( - metricsAddr string - probeAddr string - enableLeaderElection bool - autoInstrumentationJava string - autoInstrumentationNodeJS string - autoInstrumentationPython string - autoInstrumentationDotNet string - autoInstrumentationRuby string - labelsFilter []string - webhookPort int - tlsOpt tlsConfig + metricsAddr string + probeAddr string + enableLeaderElection bool + labelsFilter []string + webhookPort int + tlsOpt tlsConfig ) pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") @@ -97,11 +93,6 @@ func main() { pflag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - pflag.StringVar(&autoInstrumentationJava, "auto-instrumentation-java-image", fmt.Sprintf("newrelic/newrelic-java-init:%s", v.AutoInstrumentationJava), "The default New Relic Java instrumentation image. This image is used when no image is specified in the CustomResource.") - pflag.StringVar(&autoInstrumentationNodeJS, "auto-instrumentation-nodejs-image", fmt.Sprintf("newrelic/newrelic-node-init:%s", v.AutoInstrumentationNodeJS), "The default New Relic NodeJS instrumentation image. This image is used when no image is specified in the CustomResource.") - pflag.StringVar(&autoInstrumentationPython, "auto-instrumentation-python-image", fmt.Sprintf("newrelic/newrelic-python-init:%s", v.AutoInstrumentationPython), "The default New Relic Python instrumentation image. This image is used when no image is specified in the CustomResource.") - pflag.StringVar(&autoInstrumentationDotNet, "auto-instrumentation-dotnet-image", fmt.Sprintf("newrelic/newrelic-dotnet-init:%s", v.AutoInstrumentationDotNet), "The default New Relic DotNet instrumentation image. This image is used when no image is specified in the CustomResource.") - pflag.StringVar(&autoInstrumentationRuby, "auto-instrumentation-ruby-image", fmt.Sprintf("newrelic/newrelic-ruby-init:%s", v.AutoInstrumentationRuby), "The default New Relic Ruby instrumentation image. This image is used when no image is specified in the CustomResource.") pflag.StringArrayVar(&labelsFilter, "labels", []string{}, "Labels to filter away from propagating onto deploys") pflag.IntVar(&webhookPort, "webhook-port", 9443, "The port the webhook endpoint binds to.") @@ -112,13 +103,15 @@ func main() { logger := zap.New(zap.UseFlagOptions(&opts)) ctrl.SetLogger(logger) + operatorNamespace := os.Getenv("OPERATOR_NAMESPACE") + if operatorNamespace == "" { + setupLog.Info("env var OPERATOR_NAMESPACE is required") + os.Exit(1) + } + logger.Info("Starting the Kubernetes Agents Operator", "k8s-agents-operator", v.Operator, - "auto-instrumentation-java", autoInstrumentationJava, - "auto-instrumentation-nodejs", autoInstrumentationNodeJS, - "auto-instrumentation-python", autoInstrumentationPython, - "auto-instrumentation-dotnet", autoInstrumentationDotNet, - "auto-instrumentation-ruby", autoInstrumentationRuby, + "running-namespace", operatorNamespace, "build-date", v.BuildDate, "go-version", v.Go, "go-arch", runtime.GOARCH, @@ -126,6 +119,8 @@ func main() { "labels-filter", labelsFilter, ) + logger.Info("Working!") + restConfig := ctrl.GetConfigOrDie() // builds the operator's configuration @@ -138,11 +133,6 @@ func main() { cfg := config.New( config.WithLogger(ctrl.Log.WithName("config")), config.WithVersion(v), - config.WithAutoInstrumentationJavaImage(autoInstrumentationJava), - config.WithAutoInstrumentationNodeJSImage(autoInstrumentationNodeJS), - config.WithAutoInstrumentationPythonImage(autoInstrumentationPython), - config.WithAutoInstrumentationDotNetImage(autoInstrumentationDotNet), - config.WithAutoInstrumentationRubyImage(autoInstrumentationRuby), config.WithAutoDetect(ad), config.WithLabelFilters(labelsFilter), ) @@ -189,33 +179,50 @@ func main() { } ctx := ctrl.SetupSignalHandler() - err = addDependencies(ctx, mgr, cfg, v) + err = addDependencies(ctx, mgr, cfg) if err != nil { setupLog.Error(err, "failed to add/run bootstrap dependencies to the controller manager") os.Exit(1) } if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = (&v1alpha1.Instrumentation{ + //configure injectors that we accept. go, dotnet, etc.. + injectorRegistry := apm.DefaultInjectorRegistry + + instDefaulter := &instrumentation.InstrumentationDefaulter{ + Logger: logger.WithName("instrumentation-defaulter"), + } + instValidator := &instrumentation.InstrumentationValidator{ + Logger: logger.WithName("instrumentation-validator"), + InjectorRegistery: injectorRegistry, + } + if err = (&v1alpha2.Instrumentation{ ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - v1alpha1.AnnotationDefaultAutoInstrumentationJava: autoInstrumentationJava, - v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS: autoInstrumentationNodeJS, - v1alpha1.AnnotationDefaultAutoInstrumentationPython: autoInstrumentationPython, - v1alpha1.AnnotationDefaultAutoInstrumentationDotNet: autoInstrumentationDotNet, - v1alpha1.AnnotationDefaultAutoInstrumentationRuby: autoInstrumentationRuby, - }, + Annotations: map[string]string{}, }, - }).SetupWebhookWithManager(mgr); err != nil { + }).SetupWebhookWithManager(mgr, instDefaulter, instValidator); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Instrumentation") os.Exit(1) } + client := mgr.GetClient() + injector := instrumentation.NewNewrelicSdkInjector(logger, client, injectorRegistry) + secretReplicator := instrumentation.NewNewrelicSecretReplicator(logger, client) + instrumentationLocator := instrumentation.NewNewRelicInstrumentationLocator(logger, client, operatorNamespace) mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{ - Handler: webhookhandler.NewWebhookHandler(cfg, ctrl.Log.WithName("pod-webhook"), mgr.GetClient(), + Handler: webhookhandler.NewWebhookHandler( + cfg, ctrl.Log.WithName("pod-webhook"), mgr.GetClient(), []webhookhandler.PodMutator{ - instrumentation.NewMutator(logger, mgr.GetClient()), - }), + instrumentation.NewMutator( + logger, + client, + injector, + secretReplicator, + instrumentationLocator, + operatorNamespace, + ), + }, + ), }) } else { ctrl.Log.Info("Webhooks are disabled, operator is running an unsupported mode", "ENABLE_WEBHOOKS", "false") @@ -238,7 +245,7 @@ func main() { } } -func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config, v version.Version) error { +func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config) error { // run the auto-detect mechanism for the configuration err := mgr.Add(manager.RunnableFunc(func(_ context.Context) error { return cfg.StartAutoDetect() @@ -250,15 +257,8 @@ func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config, v v // adds the upgrade mechanism to be executed once the manager is ready err = mgr.Add(manager.RunnableFunc(func(c context.Context) error { u := &instrumentationupgrade.InstrumentationUpgrade{ - Logger: ctrl.Log.WithName("instrumentation-upgrade"), - DefaultAutoInstJava: cfg.AutoInstrumentationJavaImage(), - DefaultAutoInstNodeJS: cfg.AutoInstrumentationNodeJSImage(), - DefaultAutoInstPython: cfg.AutoInstrumentationPythonImage(), - DefaultAutoInstDotNet: cfg.AutoInstrumentationDotNetImage(), - DefaultAutoInstPhp: cfg.AutoInstrumentationPhpImage(), - DefaultAutoInstRuby: cfg.AutoInstrumentationRubyImage(), - DefaultAutoInstGo: cfg.AutoInstrumentationGoImage(), - Client: mgr.GetClient(), + Logger: ctrl.Log.WithName("instrumentation-upgrade"), + Client: mgr.GetClient(), } return u.ManagedInstances(c) })) diff --git a/tests/e2e/apps/dotnet_deployment.yaml b/tests/e2e/apps/dotnet_deployment.yaml index 42a626e5..0eafc7b8 100644 --- a/tests/e2e/apps/dotnet_deployment.yaml +++ b/tests/e2e/apps/dotnet_deployment.yaml @@ -12,8 +12,7 @@ spec: metadata: labels: app: dotnetapp - annotations: - instrumentation.newrelic.com/inject-dotnet: "true" + app.newrelic.instrumentation: newrelic-dotnet-agent spec: containers: - name: dotnetapp diff --git a/tests/e2e/apps/java_deployment.yaml b/tests/e2e/apps/java_deployment.yaml index 533349aa..27554f71 100644 --- a/tests/e2e/apps/java_deployment.yaml +++ b/tests/e2e/apps/java_deployment.yaml @@ -12,8 +12,7 @@ spec: metadata: labels: app: javaapp - annotations: - instrumentation.newrelic.com/inject-java: "true" + app.newrelic.instrumentation: newrelic-java-agent spec: containers: - name: javaapp diff --git a/tests/e2e/apps/nodejs_deployment.yaml b/tests/e2e/apps/nodejs_deployment.yaml index 70b37187..f3d8d32b 100644 --- a/tests/e2e/apps/nodejs_deployment.yaml +++ b/tests/e2e/apps/nodejs_deployment.yaml @@ -12,8 +12,7 @@ spec: metadata: labels: app: nodejsapp - annotations: - instrumentation.newrelic.com/inject-nodejs: "true" + app.newrelic.instrumentation: newrelic-nodejs-agent spec: containers: - name: nodejsapp diff --git a/tests/e2e/apps/python_deployment.yaml b/tests/e2e/apps/python_deployment.yaml index 4803c479..e146489e 100644 --- a/tests/e2e/apps/python_deployment.yaml +++ b/tests/e2e/apps/python_deployment.yaml @@ -12,8 +12,7 @@ spec: metadata: labels: app: pythonapp - annotations: - instrumentation.newrelic.com/inject-python: "true" + app.newrelic.instrumentation: newrelic-python-agent spec: containers: - name: pythonapp diff --git a/tests/e2e/apps/ruby_deployment.yaml b/tests/e2e/apps/ruby_deployment.yaml new file mode 100644 index 00000000..2d238ecf --- /dev/null +++ b/tests/e2e/apps/ruby_deployment.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rubyapp-cfgmap +data: + main.rb: | + require 'socket' + class HttpServer + def initialize(port) + @server = TCPServer.new('127.0.0.1', port) + end + def accept_connection + while session = @server.accept + request = session.readpartial(1024) + puts "peer #{session.peeraddr[3]}:#{session.peeraddr[1]} connected" + verb,path,proto = request.lines[0].split + scheme,ver = proto.split('/') + puts "requested #{path}" + session.write("HTTP/1.0 200 OK\r\n") + session.write("Host: rubyapp\r\n") + session.write("Connection: close\r\n") + session.write("\r\n") + session.write("hello world from ruby\n") + session.close + end + end + end + server = HttpServer.new(8080) + server.accept_connection +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rubyapp +spec: + selector: + matchLabels: + app: rubyapp + replicas: 1 + template: + metadata: + labels: + app: rubyapp + app.newrelic.instrumentation: newrelic-ruby-agent + spec: + containers: + - name: rubyapp + image: ruby:3.3-alpine + workingDir: /app + command: + - ruby + - main.rb + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /app/ + name: code + volumes: + - name: code + configMap: + name: rubyapp-cfgmap +--- +apiVersion: v1 +kind: Service +metadata: + name: rubyapp-service +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 8080 + selector: + app: rubyapp diff --git a/tests/e2e/e2e-instrumentation-dotnet.yml b/tests/e2e/e2e-instrumentation-dotnet.yml new file mode 100644 index 00000000..82b86298 --- /dev/null +++ b/tests/e2e/e2e-instrumentation-dotnet.yml @@ -0,0 +1,16 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: k8s-agents-operator + name: newrelic-instrumentation-dotnet +spec: + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["newrelic-dotnet-agent"] + agent: + language: dotnet + image: newrelic/newrelic-dotnet-init:latest diff --git a/tests/e2e/e2e-instrumentation-java.yml b/tests/e2e/e2e-instrumentation-java.yml new file mode 100644 index 00000000..0c2b3872 --- /dev/null +++ b/tests/e2e/e2e-instrumentation-java.yml @@ -0,0 +1,16 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: k8s-agents-operator + name: newrelic-instrumentation-java +spec: + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["newrelic-java-agent"] + agent: + language: java + image: newrelic/newrelic-java-init:latest diff --git a/tests/e2e/e2e-instrumentation-nodejs.yml b/tests/e2e/e2e-instrumentation-nodejs.yml new file mode 100644 index 00000000..e6124f1e --- /dev/null +++ b/tests/e2e/e2e-instrumentation-nodejs.yml @@ -0,0 +1,16 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: k8s-agents-operator + name: newrelic-instrumentation-nodejs +spec: + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["newrelic-nodejs-agent"] + agent: + language: nodejs + image: newrelic/newrelic-node-init:latest diff --git a/tests/e2e/e2e-instrumentation-python.yml b/tests/e2e/e2e-instrumentation-python.yml new file mode 100644 index 00000000..f6aed86a --- /dev/null +++ b/tests/e2e/e2e-instrumentation-python.yml @@ -0,0 +1,16 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: k8s-agents-operator + name: newrelic-instrumentation-python +spec: + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["newrelic-python-agent"] + agent: + language: python + image: newrelic/newrelic-python-init:latest diff --git a/tests/e2e/e2e-instrumentation-ruby.yml b/tests/e2e/e2e-instrumentation-ruby.yml new file mode 100644 index 00000000..5c505d1d --- /dev/null +++ b/tests/e2e/e2e-instrumentation-ruby.yml @@ -0,0 +1,16 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: k8s-agents-operator + name: newrelic-instrumentation-ruby +spec: + podLabelSelector: + matchExpressions: + - key: "app.newrelic.instrumentation" + operator: "In" + values: ["newrelic-ruby-agent"] + agent: + language: ruby + image: newrelic/newrelic-ruby-init:latest diff --git a/tests/e2e/e2e-instrumentation.yml b/tests/e2e/e2e-instrumentation.yml deleted file mode 100644 index c2dd9b91..00000000 --- a/tests/e2e/e2e-instrumentation.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: newrelic.com/v1alpha1 -kind: Instrumentation -metadata: - labels: - app.kubernetes.io/name: instrumentation - app.kubernetes.io/created-by: k8s-agents-operator - name: newrelic-instrumentation -spec: - java: - image: newrelic/newrelic-java-init:latest - nodejs: - image: newrelic/newrelic-node-init:latest - python: - image: newrelic/newrelic-python-init:latest - dotnet: - image: newrelic/newrelic-dotnet-init:latest - ruby: - image: newrelic/newrelic-ruby-init:latest diff --git a/tests/e2e/e2e-tests.sh b/tests/e2e/e2e-tests.sh index 815410c8..3306b248 100755 --- a/tests/e2e/e2e-tests.sh +++ b/tests/e2e/e2e-tests.sh @@ -82,7 +82,7 @@ function create_cluster() { echo "🔄 Building Docker image" cd ../.. export DOCKER_BUILDKIT=1 - docker build --tag e2e/k8s-agents-operator:e2e . --quiet > /dev/null + doc ker build --tag e2e/k8s-agents-operator:e2e . --quiet > /dev/null cd tests/e2e echo "🔄 Loading image into cluster" @@ -106,33 +106,40 @@ function create_cluster() { --set licenseKey=${LICENSE_KEY} echo "🔄 Waiting for operator to settle" - sleep 30 + kubectl wait --timeout=30s --for=jsonpath='{.status.phase}'=Running -n k8s-agents-operator -l="app.kubernetes.io/instance=k8s-agents-operator" pod + sleep 15 echo "🔄 Creating E2E namespace" kubectl create namespace e2e-namespace - echo "🔄 Installing secret" - kubectl create secret generic newrelic-key-secret \ - --namespace e2e-namespace \ - --from-literal=new_relic_license_key=${LICENSE_KEY} + #echo "🔄 Installing secret" + #kubectl create secret generic newrelic-key-secret \ + # --namespace k8s-agents-operator \ + # --from-literal=new_relic_license_key=${LICENSE_KEY} echo "🔄 Installing instrumentation" - kubectl apply --namespace e2e-namespace --filename e2e-instrumentation.yml + for i in $(find . -maxdepth 1 -type f -name 'e2e-instrumentation-*.yml'); do + kubectl apply --namespace k8s-agents-operator --filename $i + done echo "🔄 Installing apps" kubectl apply --namespace e2e-namespace --filename apps/ -} -function run_tests() { echo "🔄 Waiting for apps to settle" - sleep 60 + for label in dotnetapp javaapp nodejsapp pythonapp rubyapp; do + kubectl wait --timeout=120s --for=jsonpath='{.status.phase}'=Running --namespace e2e-namespace -l="app=$label" pod + done +} +function run_tests() { echo "🔄 Starting E2E tests" initContainers=$(kubectl get pods --namespace e2e-namespace --output yaml | yq '.items[].spec.initContainers[].name' | wc -l) - if [[ ${initContainers} -lt 4 ]]; then - echo "Error: not all apps were instrumented. Expected 4, got ${initContainers}" + local expected=$(ls apps | wc -l) + if [[ ${initContainers} -lt $expected ]]; then + echo "❌ Error: not all apps were instrumented. Expected $expected, got ${initContainers}" exit 1 fi + echo "✅ Success: all apps were instrumented" } function teardown() { diff --git a/tests/kustomize/certmanager/certificate.yaml b/tests/kustomize/certmanager/certificate.yaml index 6abadab4..be1da1a9 100644 --- a/tests/kustomize/certmanager/certificate.yaml +++ b/tests/kustomize/certmanager/certificate.yaml @@ -9,6 +9,7 @@ metadata: namespace: system spec: selfSigned: {} + --- apiVersion: cert-manager.io/v1 kind: Certificate diff --git a/tests/kustomize/crd/bases/newrelic.com_instrumentations.yaml b/tests/kustomize/crd/bases/newrelic.com_instrumentations.yaml index 373652b9..e4674b01 100644 --- a/tests/kustomize/crd/bases/newrelic.com_instrumentations.yaml +++ b/tests/kustomize/crd/bases/newrelic.com_instrumentations.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: instrumentations.newrelic.com spec: group: newrelic.com @@ -22,32 +21,39 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date - name: v1alpha1 + name: v1alpha2 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' + 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' + 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: - dotnet: - description: DotNet defines configuration for dotnet auto-instrumentation. + agent: + description: Agent defines configuration for agent instrumentation. properties: env: - description: Env defines DotNet specific env vars. If the former - var had been defined, then the other vars would be ignored. + 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. @@ -57,15 +63,16 @@ spec: C_IDENTIFIER. 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. + 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 "".' + 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. @@ -78,9 +85,10 @@ spec: description: The key to select. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? type: string optional: description: Specify whether the ConfigMap or its @@ -91,11 +99,9 @@ spec: 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.' + 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 @@ -110,10 +116,9 @@ spec: 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.' + 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, @@ -143,260 +148,10 @@ spec: be a valid secret key. type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 DotNet agent and - auto-instrumentation. - type: string - type: object - env: - description: 'Env defines common 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. Must be a C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid?' - 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 - exporter: - description: Exporter defines exporter configuration. - properties: - endpoint: - description: Endpoint is address of the collector with OTLP endpoint. - type: string - type: object - go: - description: Go defines configuration for Go auto-instrumentation. - When using Go auto-instrumentation you must provide a value for - the OTEL_GO_AUTO_TARGET_EXE env var via the Instrumentation env - vars or via the instrumentation.opentelemetry.io/otel-go-auto-target-exe - pod annotation. Failure to set this value causes instrumentation - injection to abort, leaving the original pod unchanged. - 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? type: string optional: description: Specify whether the Secret or its key @@ -414,23 +169,31 @@ spec: image: description: Image is a container image with Go SDK and auto-instrumentation. 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. - \n This is an alpha field and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable. It can only be - set for containers." + description: |- + 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 + 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. + 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 required: - name @@ -446,8 +209,9 @@ spec: - 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/' + 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: @@ -456,393 +220,130 @@ spec: - 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. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + 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. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 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. + 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 - java: - description: Java defines configuration for java auto-instrumentation. + exporter: + description: Exporter defines exporter configuration. properties: - env: - description: Env defines java specific env 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 javaagent auto-instrumentation - JAR. + endpoint: + description: Endpoint is address of the collector with OTLP endpoint. type: string type: object - nodejs: - description: NodeJS defines configuration for nodejs auto-instrumentation. + licenseKeySecret: + description: |- + LicenseKeySecret defines where to take the licenseKeySecret. + it should be present in the operator namespace. + type: string + namespaceLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. properties: - env: - description: Env defines nodejs specific env vars. If the former - var had been defined, then the other vars would be ignored. + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. items: - description: EnvVar represents an environment variable present - in a Container. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + key: + description: key is the label key that the selector applies + to. 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 "".' + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 + 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 required: - - name + - key + - operator type: object type: array - image: - description: Image is a container image with NodeJS agent and - auto-instrumentation. - type: string + 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 - php: - description: Php defines configuration for php auto-instrumentation. + x-kubernetes-map-type: atomic + podLabelSelector: + description: PodLabelSelector defines to which pods the config should + be applied. properties: - env: - description: Env defines Php specific env vars. If the former - var had been defined, then the other vars would be ignored. + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. items: - description: EnvVar represents an environment variable present - in a Container. + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. properties: - name: - description: Name of the environment variable. Must be a - C_IDENTIFIER. + key: + description: key is the label key that the selector applies + to. 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 "".' + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 + 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 required: - - name + - key + - operator type: object type: array - image: - description: Image is a container image with Php agent and auto-instrumentation. - type: string + 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 propagators: - description: Propagators defines inter-process context propagation - configuration. Values in this list will be set in the OTEL_PROPAGATORS - env var. Enum=tracecontext;none + description: |- + Propagators defines inter-process context propagation configuration. + Values in this list will be set in the OTEL_PROPAGATORS env var. + Enum=tracecontext;none items: description: Propagator represents the propagation type. enum: @@ -850,129 +351,6 @@ spec: - none type: string type: array - python: - description: Python defines configuration for python auto-instrumentation. - properties: - env: - description: Env defines python specific env 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. Must be a - C_IDENTIFIER. - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 - 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: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' - 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 Python agent and - auto-instrumentation. - type: string - type: object resource: description: Resource defines the configuration for the resource attributes, as defined by the OpenTelemetry specification. @@ -984,23 +362,26 @@ spec: resourceAttributes: additionalProperties: type: string - description: 'Attributes defines attributes that are added to - the resource. For example environment: dev' + description: |- + Attributes defines attributes that are added to the resource. + For example environment: dev type: object type: object sampler: description: Sampler defines sampling configuration. properties: argument: - description: Argument defines sampler argument. The value depends - on the sampler type. For instance for parentbased_traceidratio - sampler type it is a number in range [0..1] e.g. 0.25. The value - will be set in the OTEL_TRACES_SAMPLER_ARG env var. + description: |- + Argument defines sampler argument. + The value depends on the sampler type. + For instance for parentbased_traceidratio sampler type it is a number in range [0..1] e.g. 0.25. + The value will be set in the OTEL_TRACES_SAMPLER_ARG env var. type: string type: - description: Type defines sampler type. The value will be set - in the OTEL_TRACES_SAMPLER env var. The value can be for instance - parentbased_always_on, parentbased_always_off, parentbased_traceidratio... + description: |- + Type defines sampler type. + The value will be set in the OTEL_TRACES_SAMPLER env var. + The value can be for instance parentbased_always_on, parentbased_always_off, parentbased_traceidratio... enum: - always_on - always_off diff --git a/tests/kustomize/default/kustomization.yaml b/tests/kustomize/default/kustomization.yaml index ffb8eb58..1fe12e78 100644 --- a/tests/kustomize/default/kustomization.yaml +++ b/tests/kustomize/default/kustomization.yaml @@ -12,7 +12,7 @@ namePrefix: k8s-agents-operator- commonLabels: app.kubernetes.io/name: k8s-agents-operator -bases: +resources: - ../crd - ../rbac - ../manager @@ -21,14 +21,22 @@ bases: # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus -patchesStrategicMerge: +patches: # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. -- manager_auth_proxy_patch.yaml - -- manager_webhook_patch.yaml -- webhookcainjection_patch.yaml +- path: manager_auth_proxy_patch.yaml + target: + kind: Deployment +- path: manager_webhook_patch.yaml + target: + kind: Deployment +- path: mutatingwebhook_patch.yaml + target: + kind: MutatingWebhookConfiguration +- path: validatingwebhook_patch.yaml + target: + kind: ValidatingWebhookConfiguration # the following config is for teaching kustomize how to do var substitution vars: diff --git a/tests/kustomize/default/mutatingwebhook_patch.yaml b/tests/kustomize/default/mutatingwebhook_patch.yaml new file mode 100644 index 00000000..43d0d3c1 --- /dev/null +++ b/tests/kustomize/default/mutatingwebhook_patch.yaml @@ -0,0 +1,9 @@ +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + diff --git a/tests/kustomize/default/webhookcainjection_patch.yaml b/tests/kustomize/default/validatingwebhook_patch.yaml similarity index 62% rename from tests/kustomize/default/webhookcainjection_patch.yaml rename to tests/kustomize/default/validatingwebhook_patch.yaml index 02ab515d..47ef1d13 100644 --- a/tests/kustomize/default/webhookcainjection_patch.yaml +++ b/tests/kustomize/default/validatingwebhook_patch.yaml @@ -1,13 +1,6 @@ # This patch add annotation to admission webhook config and # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: mutating-webhook-configuration - annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) ---- -apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration diff --git a/tests/kustomize/manager/manager.yaml b/tests/kustomize/manager/manager.yaml index 8567026b..44027654 100644 --- a/tests/kustomize/manager/manager.yaml +++ b/tests/kustomize/manager/manager.yaml @@ -5,6 +5,7 @@ metadata: app.kubernetes.io/name: k8s-agents-operator control-plane: controller-manager name: system + --- apiVersion: apps/v1 kind: Deployment diff --git a/tests/kustomize/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml b/tests/kustomize/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml index 30bfbc79..21c286cf 100644 --- a/tests/kustomize/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml +++ b/tests/kustomize/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml @@ -1,4 +1,4 @@ -apiVersion: operators.coreos.com/v1alpha1 +apiVersion: operators.coreos.com/v1alpha2 kind: ClusterServiceVersion metadata: annotations: @@ -18,7 +18,7 @@ spec: - kind: Pod name: "" version: v1 - version: v1alpha1 + version: v1alpha2 description: The New Relic agent operator is an admission controller API that enables the instrumentation of application workloads (including Java, NodeJS, Go, DotNet, PHP, and Python) using a custom resource definition. diff --git a/tests/kustomize/rbac/service_account.yaml b/tests/kustomize/rbac/service_account.yaml index 00336028..7cd6025b 100644 --- a/tests/kustomize/rbac/service_account.yaml +++ b/tests/kustomize/rbac/service_account.yaml @@ -2,4 +2,4 @@ apiVersion: v1 kind: ServiceAccount metadata: name: controller-manager - namespace: system \ No newline at end of file + namespace: system diff --git a/tests/kustomize/samples/instrumentation_v1alpha1_instrumentation.yaml b/tests/kustomize/samples/instrumentation_v1alpha1_instrumentation.yaml deleted file mode 100644 index 65807bac..00000000 --- a/tests/kustomize/samples/instrumentation_v1alpha1_instrumentation.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: newrelic.com/v1alpha1 -kind: Instrumentation -metadata: - name: newrelic-instrumentation -spec: - java: - image: newrelic/newrelic-java-init:latest - nodejs: - image: newrelic/newrelic-node-init:latest - python: - image: newrelic/newrelic-python-init:latest - dotnet: - image: newrelic/newrelic-dotnet-init:latest - ruby: - image: newrelic/newrelic-ruby-init:latest diff --git a/tests/kustomize/samples/instrumentation_v1alpha2_instrumentation.yaml b/tests/kustomize/samples/instrumentation_v1alpha2_instrumentation.yaml new file mode 100644 index 00000000..c6818709 --- /dev/null +++ b/tests/kustomize/samples/instrumentation_v1alpha2_instrumentation.yaml @@ -0,0 +1,68 @@ +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-dotnet +spec: + agent: + language: dotnet + image: newrelic/newrelic-dotnet-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-go +spec: + agent: + language: go + image: newrelic/newrelic-go-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-java +spec: + agent: + language: java + image: newrelic/newrelic-java-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-nodejs +spec: + nodejs: + language: nodejs + image: newrelic/newrelic-node-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-php +spec: + agent: + language: php + image: newrelic/newrelic-php-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-python +spec: + agent: + language: python + image: newrelic/newrelic-python-init:latest + +--- +apiVersion: newrelic.com/v1alpha2 +kind: Instrumentation +metadata: + name: newrelic-instrumentation-ruby +spec: + ruby: + language: ruby + image: newrelic/newrelic-ruby-init:latest diff --git a/tests/kustomize/samples/kustomization.yaml b/tests/kustomize/samples/kustomization.yaml index ca55594e..5d22abdd 100644 --- a/tests/kustomize/samples/kustomization.yaml +++ b/tests/kustomize/samples/kustomization.yaml @@ -1,3 +1,3 @@ ## This file is auto-generated, do not modify ## resources: -- instrumentation_v1alpha1_instrumentation.yaml +- instrumentation_v1alpha2_instrumentation.yaml diff --git a/tests/kustomize/webhook/manifests.yaml b/tests/kustomize/webhook/manifests.yaml index c765aecc..15808428 100644 --- a/tests/kustomize/webhook/manifests.yaml +++ b/tests/kustomize/webhook/manifests.yaml @@ -11,14 +11,14 @@ webhooks: service: name: webhook-service namespace: system - path: /mutate-newrelic-com-v1alpha1-instrumentation + path: /mutate-newrelic-com-v1alpha2-instrumentation failurePolicy: Fail - name: instrumentation.kb.io + name: minstrumentation.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha1 + - v1alpha2 operations: - CREATE - UPDATE @@ -45,6 +45,7 @@ webhooks: resources: - pods sideEffects: None + --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -58,14 +59,14 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-newrelic-com-v1alpha1-instrumentation + path: /validate-newrelic-com-v1alpha2-instrumentation failurePolicy: Fail name: vinstrumentationcreateupdate.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha1 + - v1alpha2 operations: - CREATE - UPDATE @@ -78,14 +79,14 @@ webhooks: service: name: webhook-service namespace: system - path: /validate-newrelic-com-v1alpha1-instrumentation + path: /validate-newrelic-com-v1alpha2-instrumentation failurePolicy: Ignore name: vinstrumentationdelete.kb.io rules: - apiGroups: - newrelic.com apiVersions: - - v1alpha1 + - v1alpha2 operations: - DELETE resources: From 305caebb839745805f9b248052d3884152f7cc42 Mon Sep 17 00:00:00 2001 From: Daniel Stokes Date: Wed, 9 Oct 2024 09:42:15 -0500 Subject: [PATCH 2/6] fix: make gen-helm-docs missing versionFooter --- Makefile | 8 ++++---- charts/k8s-agents-operator/Chart.yaml | 4 ++-- charts/k8s-agents-operator/README.md | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 36291500..b0fd0493 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ HELM ?= $(LOCALBIN)/helm HELM_VERSION ?= v3.16.1 HELM_DOCS ?= $(LOCALBIN)/helm-docs HELM_DOCS_VERSION ?= v1.14.2 +HELM_DOCS_VERSION_ST ?= $(subst v,,$(HELM_DOCS_VERSION)) CT ?= $(LOCALBIN)/ct CT_VERSION ?= v3.11.0 HELM_UNITTEST ?= $(LOCALBIN)/helm-unittest @@ -50,7 +51,6 @@ $(LOCALBIN): $(TMP_DIR): mkdir $(TMP_DIR) - .PHONY: help help: ## Show help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-17s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } END{printf "\n"}' $(MAKEFILE_LIST) @@ -160,14 +160,14 @@ $(GOLANGCI_LINT): $(LOCALBIN) test -s $(GOLANGCI_LINT) || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) .PHONY: helm -helm: $(HELM) ## Download helm +helm: $(HELM) ## Download helmo $(HELM): $(LOCALBIN) test -s $(HELM) || GOBIN=$(LOCALBIN) go install helm.sh/helm/v3/cmd/helm@$(HELM_VERSION) .PHONY: helm-docs helm-docs: $(HELM_DOCS) ## Download helm-docs $(HELM_DOCS): $(LOCALBIN) - test -s $(HELM_DOCS) || GOBIN=$(LOCALBIN) go install github.com/norwoodj/helm-docs/cmd/helm-docs@$(HELM_DOCS_VERSION) + test -s $(HELM_DOCS) || GOBIN=$(LOCALBIN) go install -ldflags "-X 'main.version=$(HELM_DOCS_VERSION_ST)'" github.com/norwoodj/helm-docs/cmd/helm-docs@$(HELM_DOCS_VERSION) .PHONY: helm-unittest helm-unittest: $(HELM_UNITTEST) ## Download helm-unittest @@ -193,7 +193,7 @@ $(SETUP_ENVTEST): $(TMP_DIR) .PHONY: gen-helm-docs gen-helm-docs: helm-docs ## Generate Helm Docs from templates - cd ./charts && $(HELM_DOCS) + $(HELM_DOCS) .PHONY: generate generate: controller-gen ## Generate stuff diff --git a/charts/k8s-agents-operator/Chart.yaml b/charts/k8s-agents-operator/Chart.yaml index 29c88c3c..852601e0 100644 --- a/charts/k8s-agents-operator/Chart.yaml +++ b/charts/k8s-agents-operator/Chart.yaml @@ -8,9 +8,9 @@ home: https://github.com/newrelic/k8s-agents-operator/blob/main/charts/k8s-agent sources: - https://github.com/newrelic/k8s-agents-operator maintainers: - - name: juanjjaramillo - url: https://github.com/juanjjaramillo - name: csongnr url: https://github.com/csongnr - name: dbudziwojskiNR url: https://github.com/dbudziwojskiNR + - name: danielstokes + url: https://github.com/danielstokes diff --git a/charts/k8s-agents-operator/README.md b/charts/k8s-agents-operator/README.md index 935d4f2c..62a85f54 100644 --- a/charts/k8s-agents-operator/README.md +++ b/charts/k8s-agents-operator/README.md @@ -1,6 +1,6 @@ # k8s-agents-operator -![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.12.0](https://img.shields.io/badge/AppVersion-0.12.0-informational?style=flat-square) +![Version: 0.13.0](https://img.shields.io/badge/Version-0.13.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.13.0](https://img.shields.io/badge/AppVersion-0.13.0-informational?style=flat-square) A Helm chart for the Kubernetes Agents Operator @@ -264,7 +264,9 @@ If you want to see a list of all available charts and releases, check [index.yam | Name | Email | Url | | ---- | ------ | --- | -| juanjjaramillo | | | | csongnr | | | | dbudziwojskiNR | | | +| danielstokes | | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) From 5d63465fb5e87c937fe611aa9f11d7132cb1a257 Mon Sep 17 00:00:00 2001 From: Daniel Stokes Date: Wed, 9 Oct 2024 09:51:12 -0500 Subject: [PATCH 3/6] fix: e2e-test needs slight delay --- tests/e2e/e2e-tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/e2e-tests.sh b/tests/e2e/e2e-tests.sh index 3306b248..f52f1593 100755 --- a/tests/e2e/e2e-tests.sh +++ b/tests/e2e/e2e-tests.sh @@ -106,6 +106,7 @@ function create_cluster() { --set licenseKey=${LICENSE_KEY} echo "🔄 Waiting for operator to settle" + sleep 15 kubectl wait --timeout=30s --for=jsonpath='{.status.phase}'=Running -n k8s-agents-operator -l="app.kubernetes.io/instance=k8s-agents-operator" pod sleep 15 From 1de638eef84c358233215836520be100037517ef Mon Sep 17 00:00:00 2001 From: Daniel Stokes Date: Wed, 9 Oct 2024 10:27:57 -0500 Subject: [PATCH 4/6] fix: tests --- Makefile | 14 +++++++++++++- tests/e2e/e2e-tests.sh | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b0fd0493..e573aa89 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ TMP_DIR = $(shell pwd)/tmp LICENSE_KEY ?= fake-abc123 E2E_K8S_VERSION ?= v1.31.1 +ALL_E2E_K8S_VERSIONS ?= v1.31.1 v1.30.5 v1.29.9 v1.28.14 v1.27.16 v1.26.15 .DEFAULT_GOAL := help @@ -19,8 +20,9 @@ TEST_PACKAGES = ./src/internal/config \ # Kubebuilder variables SETUP_ENVTEST = $(TMP_DIR)/setup-envtest -SETUP_ENVTEST_VERSION ?= release-0.18 +SETUP_ENVTEST_VERSION ?= release-0.19 SETUP_ENVTEST_K8S_VERSION ?= 1.29.0 +ALL_SETUP_ENVTEST_K8S_VERSIONS ?= 1.30.0 1.29.3 1.28.3 1.27.1 1.26.1 #https://storage.googleapis.com/kubebuilder-tools ## Tool Versions KUSTOMIZE ?= $(LOCALBIN)/kustomize @@ -84,6 +86,11 @@ go-test: $(SETUP_ENVTEST) ## Run Go tests KUBEBUILDER_ASSETS="$(shell $(TMP_DIR)/setup-envtest use $(SETUP_ENVTEST_K8S_VERSION) --bin-dir $(TMP_DIR) -p path)" \ go test -v -cover -covermode=count -coverprofile=$(TMP_DIR)/cover.out $(TEST_PACKAGES) +all-go-tests: + @for k8s_version in $(ALL_SETUP_ENVTEST_K8S_VERSIONS); do \ + env SETUP_ENVTEST_K8S_VERSION=$$k8s_version $(MAKE) -f $(MAKEFILE_LIST) go-test; \ + done + e2e-tests: @for cmd in docker minikube helm kubectl yq; do \ if ! command -v $$cmd > /dev/null; then \ @@ -93,6 +100,11 @@ e2e-tests: done cd tests/e2e && ./e2e-tests.sh --k8s_version $(E2E_K8S_VERSION) --license_key $(LICENSE_KEY) --run_tests +all-e2e-tests: + @for k8s_version in $(ALL_E2E_K8S_VERSIONS); do \ + env E2E_K8S_VERSION=$$k8s_version $(MAKE) -f $(MAKEFILE_LIST) e2e-tests; \ + done + .PHONY: run-helm-unittest run-helm-unittest: $(CT) ## Run helm unit tests based on changes @if ! test -f ./.github/ct-lint.yaml; then echo "missing .github/ct-lint.yaml" >&2; exit 1; fi diff --git a/tests/e2e/e2e-tests.sh b/tests/e2e/e2e-tests.sh index f52f1593..61954828 100755 --- a/tests/e2e/e2e-tests.sh +++ b/tests/e2e/e2e-tests.sh @@ -82,7 +82,7 @@ function create_cluster() { echo "🔄 Building Docker image" cd ../.. export DOCKER_BUILDKIT=1 - doc ker build --tag e2e/k8s-agents-operator:e2e . --quiet > /dev/null + docker build --tag e2e/k8s-agents-operator:e2e . --quiet > /dev/null cd tests/e2e echo "🔄 Loading image into cluster" From c7b0a4dd02c4def8fa08b3a4215ea9a095d3de40 Mon Sep 17 00:00:00 2001 From: Daniel Stokes Date: Wed, 9 Oct 2024 10:47:55 -0500 Subject: [PATCH 5/6] feat: add help docs to targets in makefile --- Makefile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index e573aa89..031d3bd0 100644 --- a/Makefile +++ b/Makefile @@ -53,15 +53,17 @@ $(LOCALBIN): $(TMP_DIR): mkdir $(TMP_DIR) +##@ Targets + .PHONY: help help: ## Show help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-17s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } END{printf "\n"}' $(MAKEFILE_LIST) .PHONY: all -all: clean format modules test build +all: clean format modules test build ## clean, format, modules, test, build .PHONY: clean -clean: +clean: ## cleanup temp files rm -rf $(BIN_DIR) $(TMP_DIR) .PHONY: modules @@ -81,17 +83,19 @@ coverprofile: $(TMP_DIR)/cover.out ## Generate coverage report go tool cover -func=$(TMP_DIR)/cover.out .PHONY: go-test -go-test: $(SETUP_ENVTEST) ## Run Go tests +go-test: $(SETUP_ENVTEST) ## Run Go tests with k8s version specified by $SETUP_ENVTEST_K8S_VERSION @chmod -R 755 $(TMP_DIR)/k8s KUBEBUILDER_ASSETS="$(shell $(TMP_DIR)/setup-envtest use $(SETUP_ENVTEST_K8S_VERSION) --bin-dir $(TMP_DIR) -p path)" \ go test -v -cover -covermode=count -coverprofile=$(TMP_DIR)/cover.out $(TEST_PACKAGES) -all-go-tests: +.PHONY: all-go-tests +all-go-tests: ## Run go tests with all k8s versions specified by $ALL_SETUP_ENVTEST_K8S_VERSIONS @for k8s_version in $(ALL_SETUP_ENVTEST_K8S_VERSIONS); do \ env SETUP_ENVTEST_K8S_VERSION=$$k8s_version $(MAKE) -f $(MAKEFILE_LIST) go-test; \ done -e2e-tests: +.PHONY: e2e-tests +e2e-tests: ## Run e2e tests with k8s version specified by $E2E_K8S_VERSION @for cmd in docker minikube helm kubectl yq; do \ if ! command -v $$cmd > /dev/null; then \ echo "$$cmd required" >&2; \ @@ -100,7 +104,8 @@ e2e-tests: done cd tests/e2e && ./e2e-tests.sh --k8s_version $(E2E_K8S_VERSION) --license_key $(LICENSE_KEY) --run_tests -all-e2e-tests: +.PHONY: all-e2e-tests +all-e2e-tests: ## Run e2e tests with all k8s versions specified by $ALL_E2E_K8S_VERSIONS @for k8s_version in $(ALL_E2E_K8S_VERSIONS); do \ env E2E_K8S_VERSION=$$k8s_version $(MAKE) -f $(MAKEFILE_LIST) e2e-tests; \ done @@ -117,7 +122,7 @@ run-helm-unittest: $(CT) ## Run helm unit tests based on changes done; .PHONY: test -test: go-test # run-helm-unittest ## Run all tests +test: go-test # run-helm-unittest ## Run go tests (just an alias) ##@ Linting From c4f3d2ddbc55729ce0f1d94b10537b33cbde8613 Mon Sep 17 00:00:00 2001 From: danielstokes Date: Wed, 9 Oct 2024 13:27:01 -0500 Subject: [PATCH 6/6] Update .github/CODEOWNERS Co-authored-by: Marty T <120425148+tippmar-nr@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ca33cc0..8f36c55d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,6 @@ /src/apm/golang*.go @newrelic/go /src/apm/java*.go @newrelic/java /src/apm/nodejs*.go @newrelic/node -/src/apm/php*.go @newrelic/php-agent +/src/apm/php*.go @newrelic/phpc /src/apm/python*.go @newrelic/python /src/apm/ruby*.go @newrelic/ruby