diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..0f046820 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9fef250b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Build the manager binary +FROM golang:1.20 as builder +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/main.go cmd/main.go +COPY api/ api/ +COPY internal/ internal/ +COPY pkg/ pkg/ +COPY versions.txt versions.txt + +ARG VERSION_PKG +ARG VERSION +ARG VERSION_DATE +ARG NEWRELIC_INSTRUMENTATION_JAVA_VERSION +ARG NEWRELIC_INSTRUMENTATION_NODEJS_VERSION +ARG NEWRELIC_INSTRUMENTATION_PYTHON_VERSION +ARG NEWRELIC_INSTRUMENTATION_DOTNET_VERSION +ARG NEWRELIC_INSTRUMENTATION_PHP_VERSION +ARG AUTO_INSTRUMENTATION_GO_VERSION +# Build +# the GOARCH has not a default value to allow the binary be built according to the host where the command +# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO +# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, +# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags="-X ${VERSION_PKG}.version=${VERSION} -X ${VERSION_PKG}.buildDate=${VERSION_DATE} -X ${VERSION_PKG}.autoInstrumentationJava=${NEWRELIC_INSTRUMENTATION_JAVA_VERSION} -X ${VERSION_PKG}.autoInstrumentationNodeJS=${NEWRELIC_INSTRUMENTATION_NODEJS_VERSION} -X ${VERSION_PKG}.autoInstrumentationPython=${NEWRELIC_INSTRUMENTATION_PYTHON_VERSION} -X ${VERSION_PKG}.autoInstrumentationDotNet=${NEWRELIC_INSTRUMENTATION_DOTNET_VERSION} -X ${VERSION_PKG}.autoInstrumentationPhp=${NEWRELIC_INSTRUMENTATION_PHP_VERSION}" -a -o manager cmd/main.go + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/manager . +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..8c5e06d7 --- /dev/null +++ b/Makefile @@ -0,0 +1,315 @@ +# VERSION defines the project version for the bundle. +# Update this value when you upgrade the version of your project. +# To re-generate a bundle for another specific version without changing the standard setup, you can: +# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) +# - use environment variables to overwrite this value (e.g export VERSION=0.0.2) +VERSION ?= "$(shell git describe --tags | sed 's/^v//')" +VERSION_DATE ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') +VERSION_PKG ?= "github.com/newrelic-experimental/newrelic-aganet-operator/internal/version" +OPERATOR_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-agent-operator | awk -F= '{print $$2}')" +NEWRELIC_INSTRUMENTATION_JAVA_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-instrumentation-java | awk -F= '{print $$2}')" +NEWRELIC_INSTRUMENTATION_NODEJS_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-instrumentation-nodejs | awk -F= '{print $$2}')" +NEWRELIC_INSTRUMENTATION_PYTHON_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-instrumentation-python | awk -F= '{print $$2}')" +NEWRELIC_INSTRUMENTATION_DOTNET_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-instrumentation-dotnet | awk -F= '{print $$2}')" +NEWRELIC_INSTRUMENTATION_PHP_VERSION ?= "$(shell grep -v '\#' versions.txt | grep newrelic-instrumentation-php | awk -F= '{print $$2}')" +AUTO_INSTRUMENTATION_GO_VERSION ?= "$(shell grep -v '\#' versions.txt | grep autoinstrumentation-go | awk -F= '{print $$2}')" +LD_FLAGS ?= "-X ${VERSION_PKG}.version=${VERSION} -X ${VERSION_PKG}.buildDate=${VERSION_DATE} -X ${VERSION_PKG}.autoInstrumentationJava=${NEWRELIC_INSTRUMENTATION_JAVA_VERSION} -X ${VERSION_PKG}.autoInstrumentationNodeJS=${NEWRELIC_INSTRUMENTATION_NODEJS_VERSION} -X ${VERSION_PKG}.autoInstrumentationPython=${NEWRELIC_INSTRUMENTATION_PYTHON_VERSION} -X ${VERSION_PKG}.autoInstrumentationGo=${AUTO_INSTRUMENTATION_GO_VERSION} -X ${VERSION_PKG}.autoInstrumentationDotNet=${NEWRELIC_INSTRUMENTATION_DOTNET_VERSION} -X ${VERSION_PKG}.autoInstrumentationPhp=${NEWRELIC_INSTRUMENTATION_PHP_VERSION}" +ARCH ?= $(shell go env GOARCH) + +# Image URL to use all building/pushing image targets +IMG_PREFIX ?= ghcr.io/${USER}/newrelic-agent-operator +IMG_REPO ?= newrelic-agent-operator +IMG ?= ${IMG_PREFIX}/${IMG_REPO}:${VERSION} +BUNDLE_IMG ?= ${IMG_PREFIX}/${IMG_REPO}-bundle:${VERSION} + +# Options for 'bundle-build' +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +CRD_OPTIONS ?= "crd:generateEmbeddedObjectMeta=true" + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# by default, do not run the manager with webhooks enabled. This only affects local runs, not the build or in-cluster deployments. +ENABLE_WEBHOOKS ?= false + +# If we are running in CI, run go test in verbose mode +ifeq (,$(CI)) +GOTEST_OPTS=-race +else +GOTEST_OPTS=-race -v +endif + +START_KIND_CLUSTER ?= true + +KUBE_VERSION ?= 1.24 +KIND_CONFIG ?= kind-$(KUBE_VERSION).yaml + +OPERATOR_SDK_VERSION ?= 1.27.0 + +CERTMANAGER_VERSION ?= 1.10.0 + +ifndef ignore-not-found + ignore-not-found = false +endif + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +.PHONY: ensure-generate-is-noop +ensure-generate-is-noop: VERSION=$(OPERATOR_VERSION) +ensure-generate-is-noop: USER=${USER} +ensure-generate-is-noop: set-image-controller generate bundle + @# on make bundle config/manager/kustomization.yaml includes changes, which should be ignored for the below check + @git restore config/manager/kustomization.yaml + @git diff -s --exit-code apis/v1alpha1/zz_generated.*.go || (echo "Build failed: a model has been changed but the generated resources aren't up to date. Run 'make generate' and update your PR." && exit 1) + @git diff -s --exit-code bundle config || (echo "Build failed: the bundle, config files has been changed but the generated bundle, config files aren't up to date. Run 'make bundle' and update your PR." && git diff && exit 1) + @git diff -s --exit-code bundle.Dockerfile || (echo "Build failed: the bundle.Dockerfile file has been changed. The file should be the same as generated one. Run 'make bundle' and update your PR." && git diff && exit 1) + +.PHONY: all +all: manager + +# Build manager binary +.PHONY: manager +manager: generate fmt vet + go build -o bin/manager main.go + +# Run against the configured Kubernetes cluster in ~/.kube/config +.PHONY: run +run: generate fmt vet manifests + ENABLE_WEBHOOKS=$(ENABLE_WEBHOOKS) go run -ldflags ${LD_FLAGS} ./main.go --zap-devel + +# Install CRDs into a cluster +.PHONY: install +install: manifests kustomize + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +# Uninstall CRDs from a cluster +.PHONY: uninstall +uninstall: manifests kustomize + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +# Set the controller image parameters +.PHONY: set-image-controller +set-image-controller: manifests kustomize + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + +# Deploy controller in the current Kubernetes context, configured in ~/.kube/config +.PHONY: deploy +deploy: set-image-controller + $(KUSTOMIZE) build config/default | kubectl apply -f - + go run hack/check-operator-ready.go 300 + +# Undeploy controller in the current Kubernetes context, configured in ~/.kube/config +.PHONY: undeploy +undeploy: set-image-controller + $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +# Generates the released manifests +.PHONY: release-artifacts +release-artifacts: set-image-controller + mkdir -p dist + $(KUSTOMIZE) build config/default -o dist/newrelic-agent-operator.yaml + +# Generate manifests e.g. CRD, RBAC etc. +.PHONY: manifests +manifests: controller-gen + $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + +# Run go fmt against code +.PHONY: fmt +fmt: + go fmt ./... + +# Run go vet against code +.PHONY: vet +vet: + go vet ./... + +# Run go lint against code +.PHONY: lint +lint: + golangci-lint run + +# Generate code +.PHONY: generate +generate: controller-gen + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.PHONY: scorecard-tests +scorecard-tests: operator-sdk + $(OPERATOR_SDK) scorecard -w=5m bundle || (echo "scorecard test failed" && exit 1) + +# Build the container image, used only for local dev purposes +# buildx is used to ensure same results for arm based systems (m1/2 chips) +.PHONY: container +container: + docker buildx build --load --platform linux/${ARCH} -t ${IMG} --build-arg VERSION_PKG=${VERSION_PKG} --build-arg VERSION=${VERSION} --build-arg VERSION_DATE=${VERSION_DATE} --build-arg NEWRELIC_INSTRUMENTATION_JAVA_VERSION=${NEWRELIC_INSTRUMENTATION_JAVA_VERSION} --build-arg NEWRELIC_INSTRUMENTATION_NODEJS_VERSION=${NEWRELIC_INSTRUMENTATION_NODEJS_VERSION} --build-arg NEWRELIC_INSTRUMENTATION_PYTHON_VERSION=${NEWRELIC_INSTRUMENTATION_PYTHON_VERSION} --build-arg NEWRELIC_INSTRUMENTATION_DOTNET_VERSION=${NEWRELIC_INSTRUMENTATION_DOTNET_VERSION} --build-arg NEWRELIC_INSTRUMENTATION_PHP_VERSION=${NEWRELIC_INSTRUMENTATION_PHP_VERSION} . + +# Push the container image, used only for local dev purposes +.PHONY: container-push +container-push: + docker push ${IMG} + +.PHONY: start-kind +start-kind: +ifeq (true,$(START_KIND_CLUSTER)) + kind create cluster --config $(KIND_CONFIG) +endif + +.PHONY: install-openshift-routes +install-openshift-routes: + ./hack/install-openshift-routes.sh + +.PHONY: load-image-all +load-image-all: load-image-operator load-image-target-allocator load-image-operator-opamp-bridge + +.PHONY: load-image-operator +load-image-operator: container +ifeq (true,$(START_KIND_CLUSTER)) + kind load docker-image $(IMG) +else + $(MAKE) container-push +endif + +.PHONY: cert-manager +cert-manager: cmctl + # Consider using cmctl to install the cert-manager once install command is not experimental + kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v${CERTMANAGER_VERSION}/cert-manager.yaml + $(CMCTL) check api --wait=5m + +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +CMCTL = $(shell pwd)/bin/cmctl +.PHONY: cmctl +cmctl: + @{ \ + set -e ;\ + if (`pwd`/bin/cmctl version | grep ${CERTMANAGER_VERSION}) > /dev/null 2>&1 ; then \ + exit 0; \ + fi ;\ + TMP_DIR=$$(mktemp -d) ;\ + curl -L -o $$TMP_DIR/cmctl.tar.gz https://github.com/jetstack/cert-manager/releases/download/v$(CERTMANAGER_VERSION)/cmctl-`go env GOOS`-`go env GOARCH`.tar.gz ;\ + tar xzf $$TMP_DIR/cmctl.tar.gz -C $$TMP_DIR ;\ + [ -d bin ] || mkdir bin ;\ + mv $$TMP_DIR/cmctl $(CMCTL) ;\ + rm -rf $$TMP_DIR ;\ + } + +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen + +## Tool Versions +KUSTOMIZE_VERSION ?= v5.0.0 +CONTROLLER_TOOLS_VERSION ?= v0.11.3 + +.PHONY: kustomize +kustomize: ## Download kustomize locally if necessary. + $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +# go-get-tool will 'go get' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +define go-get-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +go get -d $(2)@$(3) ;\ +GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef + +.PHONY: kuttl +kuttl: +ifeq (, $(shell which kubectl-kuttl)) + echo ${PATH} + ls -l /usr/local/bin + which kubectl-kuttl + + @{ \ + set -e ;\ + echo "" ;\ + echo "ERROR: kuttl not found." ;\ + echo "Please check https://kuttl.dev/docs/cli.html for installation instructions and try again." ;\ + echo "" ;\ + exit 1 ;\ + } +else +KUTTL=$(shell which kubectl-kuttl) +endif + +.PHONY: kind +kind: +ifeq (, $(shell which kind)) + @{ \ + set -e ;\ + echo "" ;\ + echo "ERROR: kind not found." ;\ + echo "Please check https://kind.sigs.k8s.io/docs/user/quick-start/#installation for installation instructions and try again." ;\ + echo "" ;\ + exit 1 ;\ + } +else +KIND=$(shell which kind) +endif + +OPERATOR_SDK = $(shell pwd)/bin/operator-sdk +.PHONY: operator-sdk +operator-sdk: + @{ \ + set -e ;\ + if (`pwd`/bin/operator-sdk version | grep ${OPERATOR_SDK_VERSION}) > /dev/null 2>&1 ; then \ + exit 0; \ + fi ;\ + [ -d bin ] || mkdir bin ;\ + curl -L -o $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/v${OPERATOR_SDK_VERSION}/operator-sdk_`go env GOOS`_`go env GOARCH`;\ + chmod +x $(OPERATOR_SDK) ;\ + } + +# Generate bundle manifests and metadata, then validate generated files. +.PHONY: bundle +bundle: kustomize operator-sdk manifests set-image-controller + $(OPERATOR_SDK) generate kustomize manifests -q + $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + $(OPERATOR_SDK) bundle validate ./bundle + ./hack/ignore-createdAt-bundle.sh + + +# Build the bundle image, used only for local dev purposes +.PHONY: bundle-build +bundle-build: + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: + docker push $(BUNDLE_IMG) + +HELMIFY ?= $(LOCALBIN)/helmify +HELMIFY_VERSION ?= v0.3.34 + +.PHONY: helmify +helmify: $(HELMIFY) ## Download helmify locally if necessary. +$(HELMIFY): $(LOCALBIN) + test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@$(HELMIFY_VERSION) + +helm: manifests kustomize helmify + $(KUSTOMIZE) build config/default | $(HELMIFY) \ No newline at end of file diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..b9525d61 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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 contains API Schema definitions for the v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=newrelic.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "newrelic.com", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/instrumentation_types.go b/api/v1alpha1/instrumentation_types.go new file mode 100644 index 00000000..e391cf83 --- /dev/null +++ b/api/v1alpha1/instrumentation_types.go @@ -0,0 +1,225 @@ +/* +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 ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InstrumentationSpec defines the desired state of Instrumentation +type InstrumentationSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Exporter defines exporter configuration. + // +optional + Exporter `json:"exporter,omitempty"` + + // Resource defines the configuration for the resource attributes, as defined by the OpenTelemetry specification. + // +optional + Resource Resource `json:"resource,omitempty"` + + // Propagators defines inter-process context propagation configuration. + // Values in this list will be set in the OTEL_PROPAGATORS env var. + // Enum=tracecontext;none + // +optional + Propagators []Propagator `json:"propagators,omitempty"` + + // Sampler defines sampling configuration. + // +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. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Java defines configuration for java auto-instrumentation. + // +optional + Java Java `json:"java,omitempty"` + + // NodeJS defines configuration for nodejs auto-instrumentation. + // +optional + NodeJS NodeJS `json:"nodejs,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"` + + // 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"` +} + +type Resource struct { + // Attributes defines attributes that are added to the resource. + // For example environment: dev + // +optional + Attributes map[string]string `json:"resourceAttributes,omitempty"` + + // AddK8sUIDAttributes defines whether K8s UID attributes should be collected (e.g. k8s.deployment.uid). + // +optional + AddK8sUIDAttributes bool `json:"addK8sUIDAttributes,omitempty"` +} + +// Exporter defines OTLP exporter configuration. +type Exporter struct { + // Endpoint is address of the collector with OTLP endpoint. + // +optional + Endpoint string `json:"endpoint,omitempty"` +} + +// Sampler defines sampling configuration. +type Sampler struct { + // 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... + // +optional + Type SamplerType `json:"type,omitempty"` + + // 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. + // +optional + 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 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. + VolumeSizeLimit *resource.Quantity `json:"volumeLimitSize,omitempty"` + + // Env defines Go specific env vars. There are four layers for env vars' definitions and + // the precedence order is: `original container env vars` > `language specific env vars` > `common env vars` > `instrument spec configs' vars`. + // If the former var had been defined, then the other vars would be ignored. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Resources describes the compute resource requirements. + // +optional + Resources corev1.ResourceRequirements `json:"resourceRequirements,omitempty"` +} + +// InstrumentationStatus defines the observed state of Instrumentation +type InstrumentationStatus struct { +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:shortName=nragent;nragents +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +operator-sdk:csv:customresourcedefinitions:displayName="New Relic Instrumentation" +// +operator-sdk:csv:customresourcedefinitions:resources={{Pod,v1}} + +// Instrumentation is the Schema for the instrumentations API +type Instrumentation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InstrumentationSpec `json:"spec,omitempty"` + Status InstrumentationStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// InstrumentationList contains a list of Instrumentation +type InstrumentationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Instrumentation `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Instrumentation{}, &InstrumentationList{}) +} diff --git a/api/v1alpha1/instrumentation_webhook.go b/api/v1alpha1/instrumentation_webhook.go new file mode 100644 index 00000000..ef276521 --- /dev/null +++ b/api/v1alpha1/instrumentation_webhook.go @@ -0,0 +1,154 @@ +/* +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" + 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"] = "newrelic-agent-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.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.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/api/v1alpha1/propagators.go b/api/v1alpha1/propagators.go new file mode 100644 index 00000000..99852c92 --- /dev/null +++ b/api/v1alpha1/propagators.go @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +type ( + // Propagator represents the propagation type. + // +kubebuilder:validation:Enum=tracecontext;none + Propagator string +) + +const ( + // TraceContext represents W3C Trace Context. + TraceContext Propagator = "tracecontext" + // None represents automatically configured propagator. + None Propagator = "none" +) diff --git a/api/v1alpha1/samplers.go b/api/v1alpha1/samplers.go new file mode 100644 index 00000000..fde3bba8 --- /dev/null +++ b/api/v1alpha1/samplers.go @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +type ( + // SamplerType represents sampler type. + // +kubebuilder:validation:Enum=always_on;always_off;traceidratio;parentbased_always_on;parentbased_always_off;parentbased_traceidratio + SamplerType string +) + +const ( + // AlwaysOn represents AlwaysOnSampler. + AlwaysOn SamplerType = "always_on" + // AlwaysOff represents AlwaysOffSampler. + AlwaysOff SamplerType = "always_off" + // TraceIDRatio represents TraceIdRatioBased. + TraceIDRatio SamplerType = "traceidratio" + // ParentBasedAlwaysOn represents ParentBased(root=AlwaysOnSampler). + ParentBasedAlwaysOn SamplerType = "parentbased_always_on" + // ParentBasedAlwaysOff represents ParentBased(root=AlwaysOffSampler). + ParentBasedAlwaysOff SamplerType = "parentbased_always_off" + // ParentBasedTraceIDRatio represents ParentBased(root=TraceIdRatioBased). + ParentBasedTraceIDRatio SamplerType = "parentbased_traceidratio" +) diff --git a/api/v1alpha1/upgrade_strategy.go b/api/v1alpha1/upgrade_strategy.go new file mode 100644 index 00000000..89a42af7 --- /dev/null +++ b/api/v1alpha1/upgrade_strategy.go @@ -0,0 +1,31 @@ +/* +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 + +type ( + // UpgradeStrategy represents how the operator will handle upgrades to the CR when a newer version of the operator is deployed + // +kubebuilder:validation:Enum=automatic;none + UpgradeStrategy string +) + +const ( + // UpgradeStrategyAutomatic specifies that the operator will automatically apply upgrades to the CR. + UpgradeStrategyAutomatic UpgradeStrategy = "automatic" + + // UpgradeStrategyNone specifies that the operator will not apply any upgrades to the CR. + UpgradeStrategyNone UpgradeStrategy = "none" +) diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go new file mode 100644 index 00000000..ff2303a0 --- /dev/null +++ b/api/v1alpha1/webhook_suite_test.go @@ -0,0 +1,132 @@ +/* +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 ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1 "k8s.io/api/admission/v1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&Instrumentation{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..609b6386 --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,327 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DotNet) DeepCopyInto(out *DotNet) { + *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 DotNet. +func (in *DotNet) DeepCopy() *DotNet { + if in == nil { + return nil + } + out := new(DotNet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Exporter) DeepCopyInto(out *Exporter) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Exporter. +func (in *Exporter) DeepCopy() *Exporter { + if in == nil { + return nil + } + out := new(Exporter) + in.DeepCopyInto(out) + 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 + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instrumentation. +func (in *Instrumentation) DeepCopy() *Instrumentation { + if in == nil { + return nil + } + out := new(Instrumentation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Instrumentation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationList) DeepCopyInto(out *InstrumentationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Instrumentation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationList. +func (in *InstrumentationList) DeepCopy() *InstrumentationList { + if in == nil { + return nil + } + out := new(InstrumentationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InstrumentationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationSpec) DeepCopyInto(out *InstrumentationSpec) { + *out = *in + out.Exporter = in.Exporter + in.Resource.DeepCopyInto(&out.Resource) + if in.Propagators != nil { + in, out := &in.Propagators, &out.Propagators + *out = make([]Propagator, len(*in)) + 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.Go.DeepCopyInto(&out.Go) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationSpec. +func (in *InstrumentationSpec) DeepCopy() *InstrumentationSpec { + if in == nil { + return nil + } + out := new(InstrumentationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstrumentationStatus) DeepCopyInto(out *InstrumentationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstrumentationStatus. +func (in *InstrumentationStatus) DeepCopy() *InstrumentationStatus { + if in == nil { + return nil + } + out := new(InstrumentationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *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 *Resource) DeepCopyInto(out *Resource) { + *out = *in + if in.Attributes != nil { + in, out := &in.Attributes, &out.Attributes + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. +func (in *Resource) DeepCopy() *Resource { + if in == nil { + return nil + } + out := new(Resource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Sampler) DeepCopyInto(out *Sampler) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sampler. +func (in *Sampler) DeepCopy() *Sampler { + if in == nil { + return nil + } + out := new(Sampler) + in.DeepCopyInto(out) + return out +} diff --git a/bundle.Dockerfile b/bundle.Dockerfile new file mode 100644 index 00000000..48f7eb6b --- /dev/null +++ b/bundle.Dockerfile @@ -0,0 +1,20 @@ +FROM scratch + +# Core bundle labels. +LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 +LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ +LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ +LABEL operators.operatorframework.io.bundle.package.v1=newrelic-agent-operator +LABEL operators.operatorframework.io.bundle.channels.v1=alpha +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.27.0 +LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 +LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4-alpha + +# Labels for testing. +LABEL operators.operatorframework.io.test.mediatype.v1=scorecard+v1 +LABEL operators.operatorframework.io.test.config.v1=tests/scorecard/ + +# Copy files to locations specified by labels. +COPY bundle/manifests /manifests/ +COPY bundle/metadata /metadata/ +COPY bundle/tests/scorecard /tests/scorecard/ diff --git a/bundle/manifests/newrelic-agent-operator-controller-manager-metrics-service_v1_service.yaml b/bundle/manifests/newrelic-agent-operator-controller-manager-metrics-service_v1_service.yaml new file mode 100644 index 00000000..741f7337 --- /dev/null +++ b/bundle/manifests/newrelic-agent-operator-controller-manager-metrics-service_v1_service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + name: newrelic-agent-operator-controller-manager-metrics-service +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/bundle/manifests/newrelic-agent-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/newrelic-agent-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 00000000..2aa9f823 --- /dev/null +++ b/bundle/manifests/newrelic-agent-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/name: newrelic-agent-operator + name: newrelic-agent-operator-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get diff --git a/bundle/manifests/newrelic-agent-operator-webhook-service_v1_service.yaml b/bundle/manifests/newrelic-agent-operator-webhook-service_v1_service.yaml new file mode 100644 index 00000000..e9da8ced --- /dev/null +++ b/bundle/manifests/newrelic-agent-operator-webhook-service_v1_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/name: newrelic-agent-operator + name: newrelic-agent-operator-webhook-service +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/bundle/manifests/newrelic-agent-operator.clusterserviceversion.yaml b/bundle/manifests/newrelic-agent-operator.clusterserviceversion.yaml new file mode 100644 index 00000000..1a15b410 --- /dev/null +++ b/bundle/manifests/newrelic-agent-operator.clusterserviceversion.yaml @@ -0,0 +1,341 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "newrelic.com/v1alpha1", + "kind": "Instrumentation", + "metadata": { + "name": "newrelic-instrumentation" + }, + "spec": { + "dotnet": { + "image": "ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-dotnet:latest" + }, + "go": { + "image": "ghcr.io/open-telemetry/opentelemetry-go-instrumentation/autoinstrumentation-go:latest" + }, + "java": { + "image": "ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-java:latest" + }, + "nodejs": { + "image": "ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-nodejs:latest" + }, + "php": { + "image": "ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-php:latest" + }, + "python": { + "image": "ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-python:latest" + } + } + } + ] + capabilities: Basic Install + createdAt: "2024-04-03T03:14:46Z" + operators.operatorframework.io/builder: operator-sdk-v1.27.0 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4-alpha + name: newrelic-agent-operator.v0.1.5 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Instrumentation is the Schema for the instrumentations API + displayName: New Relic Instrumentation + kind: Instrumentation + name: instrumentations.newrelic.com + resources: + - kind: Pod + name: "" + version: v1 + version: v1alpha1 + 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. + displayName: New Relic Agent Operator + icon: + - base64data: "PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MTkgMTU4Ljk3Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6IzAwYWM2OTt9LmNscy0ye2ZpbGw6IzFjZTc4Mzt9LmNscy0ze2ZpbGw6IzFkMjUyYzt9PC9zdHlsZT48L2RlZnM+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjExMS4xOSA1NS4wMyAxMTEuMTkgMTAzLjk0IDY4Ljg0IDEyOC40IDY4Ljg0IDE1OC45NyAxMzcuNjggMTE5LjIzIDEzNy42OCAzOS43NCAxMTEuMTkgNTUuMDMiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iNjguODQgMzAuNTggMTExLjE5IDU1LjAzIDEzNy42OCAzOS43NCAxMzcuNjggMzkuNzQgNjguODQgMCAwIDM5Ljc0IDAgMzkuNzQgMjYuNDggNTUuMDMgNjguODQgMzAuNTgiLz48cG9seWdvbiBjbGFzcz0iY2xzLTMiIHBvaW50cz0iNDIuMzYgOTQuNzggNDIuMzYgMTQzLjY5IDY4Ljg0IDE1OC45NyA2OC44NCA3OS40OSAwIDM5Ljc0IDAgNzAuMzIgNDIuMzYgOTQuNzgiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0yNDIuMTcsNTAuMTRjLTE0LjgyLDAtMjEuODQsOS4zNi0yMS44NCw5LjM2aC0uNzhMMjE4LDUxLjdIMjAwLjA1djc5LjU2aDE5LjV2LTQ2YzAtMTAuMTQsNy0xNy4xNiwxNy4xNi0xNy4xNnMxNy4xNiw3LDE3LjE2LDE3LjE2djQ2aDE5LjVWODMuNjhDMjczLjM3LDYzLjQsMjYwLjExLDUwLjE0LDI0Mi4xNyw1MC4xNFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBvbHlnb24gY2xhc3M9ImNscy0zIiBwb2ludHM9IjQ0Mi40NyA5My40NSA0NDEuMzUgOTMuNDUgNDI4LjA5IDQxLjE5IDQwOC4yNSA0MS4xOSAzOTQuOTkgOTMuNDUgMzkzLjg4IDkzLjQ1IDM4MC42MSA0MS4xOSAzNjAuMzMgNDEuMTkgMzgwLjYxIDEyMC43NSA0MDQuMzYgMTIwLjc1IDQxNy42MSA2OS4yNyA0MTguNzMgNjkuMjcgNDMxLjk5IDEyMC43NSA0NTUuNzMgMTIwLjc1IDQ3Ni4wMSA0MS4xOSA0NTUuNzMgNDEuMTkgNDQyLjQ3IDkzLjQ1Ii8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNTQ1LjcyLDU4LjcyaC0uNzhsLTEuNTYtN0g1Mjd2NzkuNTVoMTkuNXYtNDZjMC0xMC4xNCw0LjY4LTE0LjgyLDE0LjgyLTE0LjgyaDEwVjUxLjcxaC0xMS42QTE3LjU2LDE3LjU2LDAsMCwwLDU0NS43Miw1OC43MloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNjE0LjQ3LDUwLjE0Yy0yMy4zOSwwLTQwLjU1LDE3LjE2LTQwLjU1LDQxLjM0czE2LjE5LDQxLjM0LDQwLjU1LDQxLjM0YzE5LjczLDAsMzEuNjEtMTEuNjEsMzYuNTYtMjAuMTVsLTE3LjktNi4zOGMtMS43NywzLjI0LTguOTEsOS40Ny0xOC42Niw5LjQ3LTExLjM3LDAtMTkuNDktNy4xMi0yMS4wNS0xOGg1OS4yN2EzNS4zOCwzNS4zOCwwLDAsMCwuNzgtNy44QzY1My40Nyw2Ny4zLDYzNi4zMSw1MC4xNCw2MTQuNDcsNTAuMTRaTTU5My40Miw4NC40NmMyLjM0LTEwLjE0LDkuMzYtMTcuOTQsMjEuMDUtMTcuOTQsMTAuOTMsMCwxNy45NCw3LjgsMTkuNSwxNy45NFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMzI2LjQsNTAuMTRjLTIzLjQsMC00MC41NiwxNy4xNi00MC41Niw0MS4zNFMzMDIsMTMyLjgyLDMyNi40LDEzMi44MmMxOS43MywwLDMxLjYtMTEuNjEsMzYuNTUtMjAuMTVsLTE3LjktNi4zOGMtMS43NywzLjI0LTguOSw5LjQ3LTE4LjY1LDkuNDctMTEuMzcsMC0xOS41LTcuMTItMjEuMDYtMThoNTkuMjhhMzUuMzgsMzUuMzgsMCwwLDAsLjc4LTcuOEMzNjUuNCw2Ny4zLDM0OC4yNCw1MC4xNCwzMjYuNCw1MC4xNFpNMzA1LjM0LDg0LjQ2YzIuMzQtMTAuMTQsOS4zNi0xNy45NCwyMS4wNi0xNy45NCwxMC45MiwwLDE3Ljk0LDcuOCwxOS41LDE3Ljk0WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwIC0xMC41MSkiLz48cmVjdCBjbGFzcz0iY2xzLTMiIHg9IjY5Mi4xNCIgeT0iOS43OCIgd2lkdGg9IjE5LjUiIGhlaWdodD0iMTkuNSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTc3NS40NSwxMTQuODhjLTExLjcsMC0yMS4wNi05LjM2LTIxLjA2LTIzLjRzOS4zNi0yMy40LDIxLjA2LTIzLjQsMTYuMzgsNy44LDE3Ljk0LDEyLjQ4bDE3LjY2LTYuMjhjLTQuMjgtMTEuMTEtMTQuNzgtMjQuMTQtMzUuNi0yNC4xNC0yMy40LDAtNDAuNTYsMTcuMTYtNDAuNTYsNDEuMzRzMTcuMTYsNDEuMzQsNDAuNTYsNDEuMzRjMjEsMCwzMS41LTEzLjI0LDM1LjctMjQuODhsLTE3Ljc2LTYuMzJDNzkxLjgzLDEwNy4wOCw3ODcuMTUsMTE0Ljg4LDc3NS40NSwxMTQuODhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTAgLTEwLjUxKSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMyIgcG9pbnRzPSI2NDUuNjMgMjcuMTEgNjU2LjcgMjcuMTEgNjU2LjcgMTIwLjc1IDY3Ni4yIDEyMC43NSA2NzYuMiA5Ljc4IDY0NS42MyA5Ljc4IDY0NS42MyAyNy4xMSIvPjxyZWN0IGNsYXNzPSJjbHMtMyIgeD0iNjkyLjE0IiB5PSI0MS4xOSIgd2lkdGg9IjE5LjUiIGhlaWdodD0iNzkuNTYiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik04MjEuNTksMTE2YTcuNTIsNy41MiwwLDEsMCw3LjQxLDcuNTJBNy4yOCw3LjI4LDAsMCwwLDgyMS41OSwxMTZabTAsMTMuODlhNi4zNyw2LjM3LDAsMSwxLDYuMjYtNi4zN0E2LjEyLDYuMTIsMCwwLDEsODIxLjU5LDEyOS44NVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNODI0LjgyLDEyMi4xM2EyLjY0LDIuNjQsMCwwLDAtMi44Mi0yLjYyaC0zLjM0djcuODRoMS4xNXYtMi43MmgxLjA1bDIuNzEsMi43Mkg4MjVsLTIuNzEtMi43MkEyLjUzLDIuNTMsMCwwLDAsODI0LjgyLDEyMi4xM1ptLTUsMS4zNXYtMi44Mkg4MjJhMS41LDEuNSwwLDAsMSwxLjY4LDEuNDdjMCwuODMtLjUzLDEuMzUtMS42OCwxLjM1WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwIC0xMC41MSkiLz48L3N2Zz4=" + mediatype: "image/svg+xml" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - list + - watch + - apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - update + - apiGroups: + - newrelic.com + resources: + - instrumentations + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: newrelic-agent-operator-controller-manager + deployments: + - label: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + name: newrelic-agent-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + spec: + containers: + - args: + - --metrics-addr=127.0.0.1:8080 + - --enable-leader-election + - --zap-log-level=info + - --zap-time-encoding=rfc3339nano + image: ghcr.io/andrew-lozoya/newrelic-agent-operator/newrelic-agent-operator:0.1.5 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 64Mi + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + serviceAccountName: newrelic-agent-operator-controller-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: newrelic-agent-operator-controller-manager-service-cert + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: newrelic-agent-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - newrelic + - instrumentation + - apm + links: + - name: Newrelic Agent Operator + url: https://newrelic-agent-operator.domain + maintainers: + - email: alozoya@newrelic.com + name: Andrew Lozoya + maturity: alpha + provider: + name: New Relic + url: newrelic.com + version: 0.1.5 + webhookdefinitions: + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: newrelic-agent-operator-controller-manager + failurePolicy: Fail + generateName: instrumentation.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-newrelic-com-v1alpha1-instrumentation + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: newrelic-agent-operator-controller-manager + failurePolicy: Ignore + generateName: mpod.kb.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - pods + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-v1-pod + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: newrelic-agent-operator-controller-manager + failurePolicy: Fail + generateName: vinstrumentationcreateupdate.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - instrumentations + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-newrelic-com-v1alpha1-instrumentation + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: newrelic-agent-operator-controller-manager + failurePolicy: Ignore + generateName: vinstrumentationdelete.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha1 + operations: + - DELETE + resources: + - instrumentations + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-newrelic-com-v1alpha1-instrumentation diff --git a/bundle/manifests/newrelic.com_instrumentations.yaml b/bundle/manifests/newrelic.com_instrumentations.yaml new file mode 100644 index 00000000..696e0feb --- /dev/null +++ b/bundle/manifests/newrelic.com_instrumentations.yaml @@ -0,0 +1,1028 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + labels: + app.kubernetes.io/name: newrelic-agent-operator + name: instrumentations.newrelic.com +spec: + group: newrelic.com + names: + kind: Instrumentation + listKind: InstrumentationList + plural: instrumentations + shortNames: + - nragent + - nragents + singular: instrumentation + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Instrumentation is the Schema for the instrumentations API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstrumentationSpec defines the desired state of Instrumentation + properties: + dotnet: + description: DotNet defines configuration for dotnet auto-instrumentation. + properties: + env: + description: Env defines DotNet 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 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?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + 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." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec.resourceClaims of the Pod where this field + is used. It makes that resource available inside a + container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. 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. + 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. + 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. + type: string + type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + env: + description: Env defines nodejs 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 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 + required: + - name + type: object + type: array + image: + description: Image is a container image with Php agent and auto-instrumentation. + type: string + type: object + 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 + items: + description: Propagator represents the propagation type. + enum: + - tracecontext + - 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. + properties: + addK8sUIDAttributes: + description: AddK8sUIDAttributes defines whether K8s UID attributes + should be collected (e.g. k8s.deployment.uid). + type: boolean + resourceAttributes: + additionalProperties: + type: string + 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. + 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... + enum: + - always_on + - always_off + - traceidratio + - parentbased_always_on + - parentbased_always_off + - parentbased_traceidratio + type: string + type: object + type: object + status: + description: InstrumentationStatus defines the observed state of Instrumentation + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml new file mode 100644 index 00000000..241d12cc --- /dev/null +++ b/bundle/metadata/annotations.yaml @@ -0,0 +1,14 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: newrelic-agent-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.27.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4-alpha + + # Annotations for testing. + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/bundle/tests/scorecard/config.yaml b/bundle/tests/scorecard/config.yaml new file mode 100644 index 00000000..efe0cbad --- /dev/null +++ b/bundle/tests/scorecard/config.yaml @@ -0,0 +1,50 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: + - entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: basic + test: basic-check-spec-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-bundle-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-crds-have-validation-test + storage: + spec: + mountPath: {} + - entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-crds-have-resources-test + storage: + spec: + mountPath: {} +storage: + spec: + mountPath: {} diff --git a/charts/newrelic-agent-operator/.helmignore b/charts/newrelic-agent-operator/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/newrelic-agent-operator/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/newrelic-agent-operator/Chart.yaml b/charts/newrelic-agent-operator/Chart.yaml new file mode 100644 index 00000000..00237c41 --- /dev/null +++ b/charts/newrelic-agent-operator/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: newrelic-agent-operator +description: A Helm chart for the New Relic Agent Operator +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.5 +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.2" diff --git a/charts/newrelic-agent-operator/templates/NOTES.txt b/charts/newrelic-agent-operator/templates/NOTES.txt new file mode 100644 index 00000000..800e1c34 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/NOTES.txt @@ -0,0 +1,41 @@ +This project is currently in experimental phases and is provided AS-IS WITHOUT WARRANTY OR DEDICATED SUPPORT. +Issues and contributions should be reported to the project's GitHub. +{{- if (include "newrelic-agent-operator.areValuesValid" .) }} +===================================== + + ******** + **************** + ********** **********, + &&&**** ****/((( + &&&&&&& (((((( + &&&&&&&&&& (((((( + &&&&&&&& (((((( + &&&&& (((((( + &&&&& (((((((( + &&&&& .(((((((((( + &&&&&(((((((( + &&&(((, + +Your deployment of the New Relic Agent Operator is complete. +You can check on the progress of this by running the following command: + +kubectl get deployments -o wide -w --namespace {{ .Release.Namespace }} {{ template "newrelic-agent-operator.fullname" . }} + +WARNING: This deployment will be incomplete until configure your Instrumentation custom resource definition. +===================================== + +Please visit https://github.com/newrelic-experimental/newrelic-agent-operator for instructions on how to create & configure the +Instrumentation custom resource definition required by the Operator. +{{- else -}} + +############################################################################## +#### ERROR: You did not set a license key. #### +############################################################################## + +This deployment will be incomplete until you get your ingest license key from New Relic. + +Then run: + + helm upgrade {{ .Release.Name }} path/to/chart/ \ + --set licenseKey=(your-license-key) +{{- end -}} \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/_helpers.tpl b/charts/newrelic-agent-operator/templates/_helpers.tpl new file mode 100644 index 00000000..1bfe71d8 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "newrelic-agent-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "newrelic-agent-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "newrelic-agent-operator.chart" -}} +{{- printf "%s" .Chart.Name | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "newrelic-agent-operator.labels" -}} +helm.sh/chart: {{ include "newrelic-agent-operator.chart" . }} +{{ include "newrelic-agent-operator.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "newrelic-agent-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "newrelic-agent-operator.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "newrelic-agent-operator.serviceAccountName" -}} +{{- if .Values.controllerManager.manager.serviceAccount.create }} +{{- default (include "newrelic-agent-operator.name" .) .Values.controllerManager.manager.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.controllerManager.manager.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Return the licenseKey +*/}} +{{- define "newrelic-agent-operator.licenseKey" -}} +{{- if .Values.global}} + {{- if .Values.global.licenseKey }} + {{- .Values.global.licenseKey -}} + {{- else -}} + {{- .Values.licenseKey | default "" -}} + {{- end -}} +{{- else -}} + {{- .Values.licenseKey | default "" -}} +{{- end -}} +{{- end -}} + +{{/* +Returns if the template should render, it checks if the required values are set. +*/}} +{{- define "newrelic-agent-operator.areValuesValid" -}} +{{- $licenseKey := include "newrelic-agent-operator.licenseKey" . -}} +{{- and (or $licenseKey)}} +{{- end -}} diff --git a/charts/newrelic-agent-operator/templates/certmanager.yaml b/charts/newrelic-agent-operator/templates/certmanager.yaml new file mode 100644 index 00000000..3b25c764 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/certmanager.yaml @@ -0,0 +1,18 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-serving-cert + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + dnsNames: + - '{{ template "newrelic-agent-operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc' + - '{{ template "newrelic-agent-operator.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc.{{ + .Values.kubernetesClusterDomain }}' + issuerRef: + kind: Issuer + name: '{{ template "newrelic-agent-operator.fullname" . }}-selfsigned-issuer' + secretName: newrelic-agent-operator-controller-manager-service-cert + subject: + organizationalUnits: + - newrelic-agent-operator \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/deployment.yaml b/charts/newrelic-agent-operator/templates/deployment.yaml new file mode 100644 index 00000000..f48801d8 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/deployment.yaml @@ -0,0 +1,95 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }} + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }} + labels: + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.controllerManager.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 6 }} + template: + metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 8 }} + spec: + containers: + - args: + - --metrics-addr=127.0.0.1:8080 + {{- if .Values.controllerManager.manager.leaderElection.enabled }} + - --enable-leader-election + {{- end }} + - --zap-log-level=info + - --zap-time-encoding=rfc3339nano + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + - name: ENABLE_WEBHOOKS + value: "true" + image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag + | default .Chart.AppVersion }} + imagePullPolicy: Always + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.controllerManager.manager.resources | nindent 10 + }} + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=0 + env: + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag + | default .Chart.AppVersion }} + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent + 10 }} + serviceAccountName: {{ template "newrelic-agent-operator.serviceAccountName" . }} + terminationGracePeriodSeconds: 10 + {{- if or .Values.admissionWebhooks.create .Values.admissionWebhooks.secretName }} + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: {{ default (printf "%s-controller-manager-service-cert" (include "newrelic-agent-operator.fullname" .)) .Values.admissionWebhooks.secretName }} + {{- end }} + securityContext: +{{ toYaml .Values.securityContext | indent 8 }} diff --git a/charts/newrelic-agent-operator/templates/instrumentation-crd.yaml b/charts/newrelic-agent-operator/templates/instrumentation-crd.yaml new file mode 100644 index 00000000..1257bbae --- /dev/null +++ b/charts/newrelic-agent-operator/templates/instrumentation-crd.yaml @@ -0,0 +1,1027 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: instrumentations.newrelic.com + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + group: newrelic.com + names: + kind: Instrumentation + listKind: InstrumentationList + plural: instrumentations + shortNames: + - nragent + - nragents + singular: instrumentation + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Instrumentation is the Schema for the instrumentations API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstrumentationSpec defines the desired state of Instrumentation + properties: + dotnet: + description: DotNet defines configuration for dotnet auto-instrumentation. + properties: + env: + description: Env defines DotNet 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 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?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + 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." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec.resourceClaims of the Pod where this field + is used. It makes that resource available inside a + container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. 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. + 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. + 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. + type: string + type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + env: + description: Env defines nodejs 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 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 + required: + - name + type: object + type: array + image: + description: Image is a container image with Php agent and auto-instrumentation. + type: string + type: object + 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 + items: + description: Propagator represents the propagation type. + enum: + - tracecontext + - 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. + properties: + addK8sUIDAttributes: + description: AddK8sUIDAttributes defines whether K8s UID attributes + should be collected (e.g. k8s.deployment.uid). + type: boolean + resourceAttributes: + additionalProperties: + type: string + 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. + 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... + enum: + - always_on + - always_off + - traceidratio + - parentbased_always_on + - parentbased_always_off + - parentbased_traceidratio + type: string + type: object + type: object + status: + description: InstrumentationStatus defines the observed state of Instrumentation + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/leader-election-rbac.yaml b/charts/newrelic-agent-operator/templates/leader-election-rbac.yaml new file mode 100644 index 00000000..686ecdd9 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/leader-election-rbac.yaml @@ -0,0 +1,49 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-leader-election-role + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-leader-election-rolebinding + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ template "newrelic-agent-operator.fullname" . }}-leader-election-role' +subjects: +- kind: ServiceAccount + name: '{{ template "newrelic-agent-operator.fullname" . }}' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/manager-rbac.yaml b/charts/newrelic-agent-operator/templates/manager-rbac.yaml new file mode 100644 index 00000000..37128b04 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/manager-rbac.yaml @@ -0,0 +1,76 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-manager-role + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - list + - watch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - update +- apiGroups: + - newrelic.com + resources: + - instrumentations + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-manager-rolebinding + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ template "newrelic-agent-operator.fullname" . }}-manager-role' +subjects: +- kind: ServiceAccount + name: '{{ template "newrelic-agent-operator.fullname" . }}' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/mutating-webhook-configuration.yaml b/charts/newrelic-agent-operator/templates/mutating-webhook-configuration.yaml new file mode 100644 index 00000000..8bd6d33d --- /dev/null +++ b/charts/newrelic-agent-operator/templates/mutating-webhook-configuration.yaml @@ -0,0 +1,49 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "newrelic-agent-operator.fullname" . }}-serving-cert + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ template "newrelic-agent-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 "newrelic-agent-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/newrelic-agent-operator/templates/newrelic_license_secret.yaml b/charts/newrelic-agent-operator/templates/newrelic_license_secret.yaml new file mode 100644 index 00000000..feff7ad9 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/newrelic_license_secret.yaml @@ -0,0 +1,14 @@ +{{- $licenseKey := include "newrelic-agent-operator.licenseKey" . -}} +{{- if $licenseKey }} +apiVersion: v1 +kind: Secret +metadata: + name: "newrelic-key-secret" + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "newrelic-agent-operator.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +type: Opaque +data: + new_relic_license_key: {{ $licenseKey | b64enc }} +{{- end }} \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/proxy-rbac.yaml b/charts/newrelic-agent-operator/templates/proxy-rbac.yaml new file mode 100644 index 00000000..ce81bd91 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/proxy-rbac.yaml @@ -0,0 +1,34 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-proxy-role + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-proxy-rolebinding + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: '{{ template "newrelic-agent-operator.fullname" . }}-proxy-role' +subjects: +- kind: ServiceAccount + name: '{{ template "newrelic-agent-operator.fullname" . }}' + namespace: '{{ .Release.Namespace }}' \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/reader-rbac.yaml b/charts/newrelic-agent-operator/templates/reader-rbac.yaml new file mode 100644 index 00000000..a7a0eb4c --- /dev/null +++ b/charts/newrelic-agent-operator/templates/reader-rbac.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-metrics-reader + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/selfsigned-issuer.yaml b/charts/newrelic-agent-operator/templates/selfsigned-issuer.yaml new file mode 100644 index 00000000..e8fe15c3 --- /dev/null +++ b/charts/newrelic-agent-operator/templates/selfsigned-issuer.yaml @@ -0,0 +1,8 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-selfsigned-issuer + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + selfSigned: {} \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/service.yaml b/charts/newrelic-agent-operator/templates/service.yaml new file mode 100644 index 00000000..de07693e --- /dev/null +++ b/charts/newrelic-agent-operator/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }} + labels: + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.metricsService.type }} + selector: + app.kubernetes.io/name: {{ include "newrelic-agent-operator.chart" . }} + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} + ports: + {{- .Values.metricsService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/charts/newrelic-agent-operator/templates/validating-webhook-configuration.yaml b/charts/newrelic-agent-operator/templates/validating-webhook-configuration.yaml new file mode 100644 index 00000000..9d3d42de --- /dev/null +++ b/charts/newrelic-agent-operator/templates/validating-webhook-configuration.yaml @@ -0,0 +1,48 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "newrelic-agent-operator.fullname" . }}-serving-cert + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ template "newrelic-agent-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 "newrelic-agent-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/newrelic-agent-operator/templates/webhook-service.yaml b/charts/newrelic-agent-operator/templates/webhook-service.yaml new file mode 100644 index 00000000..d065ceae --- /dev/null +++ b/charts/newrelic-agent-operator/templates/webhook-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "newrelic-agent-operator.fullname" . }}-webhook-service + labels: + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.webhookService.type }} + selector: + app.kubernetes.io/name: {{ include "newrelic-agent-operator.chart" . }} + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + {{- include "newrelic-agent-operator.labels" . | nindent 4 }} + ports: + {{- .Values.webhookService.ports | toYaml | nindent 2 -}} \ No newline at end of file diff --git a/charts/newrelic-agent-operator/values.yaml b/charts/newrelic-agent-operator/values.yaml new file mode 100644 index 00000000..f34d0105 --- /dev/null +++ b/charts/newrelic-agent-operator/values.yaml @@ -0,0 +1,63 @@ +# IMPORTANT: Specify your New Relic Ingest key here. +# licenseKey: + +controllerManager: + replicas: 1 + kubeRbacProxy: + image: + repository: gcr.io/kubebuilder/kube-rbac-proxy + tag: v0.14.0 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + + ## Provide New Relic Agent Operator manager container image and resources. + manager: + image: + repository: ghcr.io/newrelic-experimental/newrelic-agent-operator/newrelic-agent-operator + tag: main + resources: + requests: + cpu: 100m + memory: 64Mi + # -- Create the manager ServiceAccount + serviceAccount: + create: true + # name: nameOverride + + ## Enable leader election mechanism for protecting against split brain if multiple operator pods/replicas are started. + ## See more at https://docs.openshift.com/container-platform/4.10/operators/operator_sdk/osdk-leader-election.html + leaderElection: + enabled: true + +kubernetesClusterDomain: cluster.local +metricsService: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + type: ClusterIP +webhookService: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + type: ClusterIP + +## SecurityContext holds pod-level security attributes and common container settings. +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +securityContext: + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + fsGroup: 65532 + +## Admission webhooks make sure only requests with correctly formatted rules will get into the Operator. +admissionWebhooks: + create: true + secretName: "" diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..662a4226 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,292 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "os" + "runtime" + "strings" + "time" + + routev1 "github.com/openshift/api/route/v1" + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + k8sapiflag "k8s.io/component-base/cli/flag" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + v1alpha1 "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" + "github.com/newrelic-experimental/newrelic-agent-operator/internal/config" + "github.com/newrelic-experimental/newrelic-agent-operator/internal/version" + "github.com/newrelic-experimental/newrelic-agent-operator/internal/webhookhandler" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/autodetect" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/instrumentation" + instrumentationupgrade "github.com/newrelic-experimental/newrelic-agent-operator/pkg/instrumentation/upgrade" + // +kubebuilder:scaffold:imports +) + +var ( + scheme = k8sruntime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +type tlsConfig struct { + minVersion string + cipherSuites []string +} + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(routev1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme +} + +func main() { + // registers any flags that underlying libraries might use + opts := zap.Options{} + opts.BindFlags(flag.CommandLine) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + + v := version.Get() + + // add flags related to this operator + var ( + metricsAddr string + probeAddr string + enableLeaderElection bool + autoInstrumentationJava string + autoInstrumentationNodeJS string + autoInstrumentationPython string + autoInstrumentationDotNet string + autoInstrumentationPhp string + autoInstrumentationGo string + labelsFilter []string + webhookPort int + tlsOpt tlsConfig + ) + + pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") + pflag.StringVar(&probeAddr, "health-probe-addr", ":8081", "The address the probe endpoint binds to.") + 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("ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-java:%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("ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-nodejs:%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("ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-python:%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("ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-dotnet:%s", v.AutoInstrumentationDotNet), "The default New Relic DotNet instrumentation image. This image is used when no image is specified in the CustomResource.") + pflag.StringVar(&autoInstrumentationPhp, "auto-instrumentation-php-image", fmt.Sprintf("ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-php:%s", v.AutoInstrumentationDotNet), "The default New Relic Php instrumentation image. This image is used when no image is specified in the CustomResource.") + pflag.StringVar(&autoInstrumentationGo, "auto-instrumentation-go-image", fmt.Sprintf("ghcr.io/open-telemetry/opentelemetry-go-instrumentation/autoinstrumentation-go:%s", v.AutoInstrumentationGo), "The default Opentelemtry Go 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.") + pflag.StringVar(&tlsOpt.minVersion, "tls-min-version", "VersionTLS12", "Minimum TLS version supported. Value must match version names from https://golang.org/pkg/crypto/tls/#pkg-constants.") + pflag.StringSliceVar(&tlsOpt.cipherSuites, "tls-cipher-suites", nil, "Comma-separated list of cipher suites for the server. Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). If omitted, the default Go cipher suites will be used") + pflag.Parse() + + logger := zap.New(zap.UseFlagOptions(&opts)) + ctrl.SetLogger(logger) + + logger.Info("Starting the New Relic Agent Operator", + "newrelic-agent-operator", v.Operator, + "auto-instrumentation-java", autoInstrumentationJava, + "auto-instrumentation-nodejs", autoInstrumentationNodeJS, + "auto-instrumentation-python", autoInstrumentationPython, + "auto-instrumentation-dotnet", autoInstrumentationDotNet, + "auto-instrumentation-php", autoInstrumentationPhp, + "auto-instrumentation-go", autoInstrumentationGo, + "build-date", v.BuildDate, + "go-version", v.Go, + "go-arch", runtime.GOARCH, + "go-os", runtime.GOOS, + "labels-filter", labelsFilter, + ) + + restConfig := ctrl.GetConfigOrDie() + + // builds the operator's configuration + ad, err := autodetect.New(restConfig) + if err != nil { + setupLog.Error(err, "failed to setup auto-detect routine") + os.Exit(1) + } + + cfg := config.New( + config.WithLogger(ctrl.Log.WithName("config")), + config.WithVersion(v), + config.WithAutoInstrumentationJavaImage(autoInstrumentationJava), + config.WithAutoInstrumentationNodeJSImage(autoInstrumentationNodeJS), + config.WithAutoInstrumentationPythonImage(autoInstrumentationPython), + config.WithAutoInstrumentationDotNetImage(autoInstrumentationDotNet), + config.WithAutoInstrumentationPhpImage(autoInstrumentationPhp), + config.WithAutoInstrumentationGoImage(autoInstrumentationGo), + config.WithAutoDetect(ad), + config.WithLabelFilters(labelsFilter), + ) + + watchNamespace, found := os.LookupEnv("WATCH_NAMESPACE") + if found { + setupLog.Info("watching namespace(s)", "namespaces", watchNamespace) + } else { + setupLog.Info("the env var WATCH_NAMESPACE isn't set, watching all namespaces") + } + + // see https://github.com/openshift/library-go/blob/4362aa519714a4b62b00ab8318197ba2bba51cb7/pkg/config/leaderelection/leaderelection.go#L104 + leaseDuration := time.Second * 137 + renewDeadline := time.Second * 107 + retryPeriod := time.Second * 26 + + optionsTlSOptsFuncs := []func(*tls.Config){ + func(config *tls.Config) { tlsConfigSetting(config, tlsOpt) }, + } + + mgrOptions := ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: webhookPort, + TLSOpts: optionsTlSOptsFuncs, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "9f7554c3.newrelic.com", + Namespace: watchNamespace, + LeaseDuration: &leaseDuration, + RenewDeadline: &renewDeadline, + RetryPeriod: &retryPeriod, + } + + if strings.Contains(watchNamespace, ",") { + mgrOptions.Namespace = "" + mgrOptions.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ",")) + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + ctx := ctrl.SetupSignalHandler() + err = addDependencies(ctx, mgr, cfg, v) + 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{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1alpha1.AnnotationDefaultAutoInstrumentationJava: autoInstrumentationJava, + v1alpha1.AnnotationDefaultAutoInstrumentationNodeJS: autoInstrumentationNodeJS, + v1alpha1.AnnotationDefaultAutoInstrumentationPython: autoInstrumentationPython, + v1alpha1.AnnotationDefaultAutoInstrumentationDotNet: autoInstrumentationDotNet, + v1alpha1.AnnotationDefaultAutoInstrumentationPhp: autoInstrumentationPhp, + v1alpha1.AnnotationDefaultAutoInstrumentationGo: autoInstrumentationGo, + }, + }, + }).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Instrumentation") + os.Exit(1) + } + + mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{ + Handler: webhookhandler.NewWebhookHandler(cfg, ctrl.Log.WithName("pod-webhook"), mgr.GetClient(), + []webhookhandler.PodMutator{ + instrumentation.NewMutator(logger, mgr.GetClient()), + }), + }) + } else { + ctrl.Log.Info("Webhooks are disabled, operator is running an unsupported mode", "ENABLE_WEBHOOKS", "false") + } + // +kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func addDependencies(_ context.Context, mgr ctrl.Manager, cfg config.Config, v version.Version) error { + // run the auto-detect mechanism for the configuration + err := mgr.Add(manager.RunnableFunc(func(_ context.Context) error { + return cfg.StartAutoDetect() + })) + if err != nil { + return fmt.Errorf("failed to start the auto-detect mechanism: %w", err) + } + + // 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(), + DefaultAutoInstGo: cfg.AutoInstrumentationGoImage(), + Client: mgr.GetClient(), + } + return u.ManagedInstances(c) + })) + if err != nil { + return fmt.Errorf("failed to upgrade Instrumentation instances: %w", err) + } + return nil +} + +// This function get the option from command argument (tlsConfig), check the validity through k8sapiflag +// and set the config for webhook server. +// refer to https://pkg.go.dev/k8s.io/component-base/cli/flag +func tlsConfigSetting(cfg *tls.Config, tlsOpt tlsConfig) { + // TLSVersion helper function returns the TLS Version ID for the version name passed. + version, err := k8sapiflag.TLSVersion(tlsOpt.minVersion) + if err != nil { + setupLog.Error(err, "TLS version invalid") + } + cfg.MinVersion = version + + // TLSCipherSuites helper function returns a list of cipher suite IDs from the cipher suite names passed. + cipherSuiteIDs, err := k8sapiflag.TLSCipherSuites(tlsOpt.cipherSuites) + if err != nil { + setupLog.Error(err, "Failed to convert TLS cipher suite name to ID") + } + cfg.CipherSuites = cipherSuiteIDs +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..03218f23 --- /dev/null +++ b/config/README.md @@ -0,0 +1,19 @@ +## Structure + +The `config` folder contains the Kustomize resources that are used to assemble the operator's deployment units + +``` +. +├── certmanager # Kustomize options dealing with cert-manager +├── crd # Kustomize options for our CRDs +│   ├── bases # auto generated based on the code annotations (`make manifests`) +│   └── patches # patches to apply to the generated CRD +├── default # Kustomize's "entry point", generating the distribution YAML file +├── manager # the operator's Deployment +├── manifests # the resulting CSV +│   └── bases +├── prometheus # ServiceMonitor that exposes our operator's metrics +├── rbac # RBAC rules +├── samples # Set of examples of how to accomplish specific scenarios. Those are bundled in the final CSV +└── webhook # Webhook configuration and service +``` diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml new file mode 100644 index 00000000..7586e1ab --- /dev/null +++ b/config/certmanager/certificate.yaml @@ -0,0 +1,29 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for +# breaking changes +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: newrelic-agent-operator-controller-manager-service-cert # this secret will not be prefixed, since it's not managed by kustomize + subject: + organizationalUnits: + - "newrelic-agent-operator" diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..bebea5a5 --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..90d7c313 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/config/crd/bases/newrelic.com_instrumentations.yaml b/config/crd/bases/newrelic.com_instrumentations.yaml new file mode 100644 index 00000000..373652b9 --- /dev/null +++ b/config/crd/bases/newrelic.com_instrumentations.yaml @@ -0,0 +1,1021 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null + name: instrumentations.newrelic.com +spec: + group: newrelic.com + names: + kind: Instrumentation + listKind: InstrumentationList + plural: instrumentations + shortNames: + - nragent + - nragents + singular: instrumentation + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Instrumentation is the Schema for the instrumentations API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstrumentationSpec defines the desired state of Instrumentation + properties: + dotnet: + description: DotNet defines configuration for dotnet auto-instrumentation. + properties: + env: + description: Env defines DotNet 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 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?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: Image is a container image with Go SDK and auto-instrumentation. + type: string + 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." + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry in + pod.spec.resourceClaims of the Pod where this field + is used. It makes that resource available inside a + container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, otherwise + to an implementation-defined value. 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. + 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. + 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. + type: string + type: object + nodejs: + description: NodeJS defines configuration for nodejs auto-instrumentation. + properties: + env: + description: Env defines nodejs 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 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 + required: + - name + type: object + type: array + image: + description: Image is a container image with Php agent and auto-instrumentation. + type: string + type: object + 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 + items: + description: Propagator represents the propagation type. + enum: + - tracecontext + - 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. + properties: + addK8sUIDAttributes: + description: AddK8sUIDAttributes defines whether K8s UID attributes + should be collected (e.g. k8s.deployment.uid). + type: boolean + resourceAttributes: + additionalProperties: + type: string + 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. + 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... + enum: + - always_on + - always_off + - traceidratio + - parentbased_always_on + - parentbased_always_off + - parentbased_traceidratio + type: string + type: object + type: object + status: + description: InstrumentationStatus defines the observed state of Instrumentation + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml new file mode 100644 index 00000000..e37d2dd0 --- /dev/null +++ b/config/crd/kustomization.yaml @@ -0,0 +1,17 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/newrelic.com_instrumentations.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# patches here are for enabling the conversion webhook for each CRD +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# patches here are for enabling the CA injection for each CRD +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000..6f83d9a9 --- /dev/null +++ b/config/crd/kustomizeconfig.yaml @@ -0,0 +1,17 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 00000000..7040b232 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,60 @@ +# Adds namespace to all resources. +namespace: newrelic-agent-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: newrelic-agent-operator- + +# Labels to add to all resources and selectors. +commonLabels: + app.kubernetes.io/name: newrelic-agent-operator + +bases: +- ../crd +- ../rbac +- ../manager +- ../webhook +- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: + # 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 + +# the following config is for teaching kustomize how to do var substitution +vars: +- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml + fieldref: + fieldpath: metadata.namespace +- name: CERTIFICATE_NAME + objref: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # this name should match the one in certificate.yaml +- name: SERVICE_NAMESPACE # namespace of the service + objref: + kind: Service + version: v1 + name: webhook-service + fieldref: + fieldpath: metadata.namespace +- name: SERVICE_NAME + objref: + kind: Service + version: v1 + name: webhook-service diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000..f44f9c71 --- /dev/null +++ b/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,35 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + name: https + protocol: TCP + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + - "--zap-log-level=info" + - "--zap-time-encoding=rfc3339nano" diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..cac2e0f4 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: newrelic-agent-operator-controller-manager-service-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 00000000..02ab515d --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# 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 + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml new file mode 100644 index 00000000..69c5fed4 --- /dev/null +++ b/config/manager/kustomization.yaml @@ -0,0 +1,8 @@ +resources: +- manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: ghcr.io/andrew-lozoya/newrelic-agent-operator/newrelic-agent-operator + newTag: 0.1.5 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 00000000..1e3e0db8 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager +spec: + selector: + matchLabels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + spec: + containers: + - args: + - --enable-leader-election + image: controller + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/config/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml b/config/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml new file mode 100644 index 00000000..8b3094ab --- /dev/null +++ b/config/manifests/bases/newrelic-agent-operator.clusterserviceversion.yaml @@ -0,0 +1,56 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: '[]' + capabilities: Basic Install + name: newrelic-agent-operator.v0.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Instrumentation is the Schema for the instrumentations API + displayName: New Relic Instrumentation + kind: Instrumentation + name: instrumentations.newrelic.com + resources: + - kind: Pod + name: "" + version: v1 + version: v1alpha1 + 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. + displayName: New Relic Agent Operator + icon: + - base64data: "PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4MTkgMTU4Ljk3Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6IzAwYWM2OTt9LmNscy0ye2ZpbGw6IzFjZTc4Mzt9LmNscy0ze2ZpbGw6IzFkMjUyYzt9PC9zdHlsZT48L2RlZnM+PHBvbHlnb24gY2xhc3M9ImNscy0xIiBwb2ludHM9IjExMS4xOSA1NS4wMyAxMTEuMTkgMTAzLjk0IDY4Ljg0IDEyOC40IDY4Ljg0IDE1OC45NyAxMzcuNjggMTE5LjIzIDEzNy42OCAzOS43NCAxMTEuMTkgNTUuMDMiLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iNjguODQgMzAuNTggMTExLjE5IDU1LjAzIDEzNy42OCAzOS43NCAxMzcuNjggMzkuNzQgNjguODQgMCAwIDM5Ljc0IDAgMzkuNzQgMjYuNDggNTUuMDMgNjguODQgMzAuNTgiLz48cG9seWdvbiBjbGFzcz0iY2xzLTMiIHBvaW50cz0iNDIuMzYgOTQuNzggNDIuMzYgMTQzLjY5IDY4Ljg0IDE1OC45NyA2OC44NCA3OS40OSAwIDM5Ljc0IDAgNzAuMzIgNDIuMzYgOTQuNzgiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik0yNDIuMTcsNTAuMTRjLTE0LjgyLDAtMjEuODQsOS4zNi0yMS44NCw5LjM2aC0uNzhMMjE4LDUxLjdIMjAwLjA1djc5LjU2aDE5LjV2LTQ2YzAtMTAuMTQsNy0xNy4xNiwxNy4xNi0xNy4xNnMxNy4xNiw3LDE3LjE2LDE3LjE2djQ2aDE5LjVWODMuNjhDMjczLjM3LDYzLjQsMjYwLjExLDUwLjE0LDI0Mi4xNyw1MC4xNFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBvbHlnb24gY2xhc3M9ImNscy0zIiBwb2ludHM9IjQ0Mi40NyA5My40NSA0NDEuMzUgOTMuNDUgNDI4LjA5IDQxLjE5IDQwOC4yNSA0MS4xOSAzOTQuOTkgOTMuNDUgMzkzLjg4IDkzLjQ1IDM4MC42MSA0MS4xOSAzNjAuMzMgNDEuMTkgMzgwLjYxIDEyMC43NSA0MDQuMzYgMTIwLjc1IDQxNy42MSA2OS4yNyA0MTguNzMgNjkuMjcgNDMxLjk5IDEyMC43NSA0NTUuNzMgMTIwLjc1IDQ3Ni4wMSA0MS4xOSA0NTUuNzMgNDEuMTkgNDQyLjQ3IDkzLjQ1Ii8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNTQ1LjcyLDU4LjcyaC0uNzhsLTEuNTYtN0g1Mjd2NzkuNTVoMTkuNXYtNDZjMC0xMC4xNCw0LjY4LTE0LjgyLDE0LjgyLTE0LjgyaDEwVjUxLjcxaC0xMS42QTE3LjU2LDE3LjU2LDAsMCwwLDU0NS43Miw1OC43MloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNNjE0LjQ3LDUwLjE0Yy0yMy4zOSwwLTQwLjU1LDE3LjE2LTQwLjU1LDQxLjM0czE2LjE5LDQxLjM0LDQwLjU1LDQxLjM0YzE5LjczLDAsMzEuNjEtMTEuNjEsMzYuNTYtMjAuMTVsLTE3LjktNi4zOGMtMS43NywzLjI0LTguOTEsOS40Ny0xOC42Niw5LjQ3LTExLjM3LDAtMTkuNDktNy4xMi0yMS4wNS0xOGg1OS4yN2EzNS4zOCwzNS4zOCwwLDAsMCwuNzgtNy44QzY1My40Nyw2Ny4zLDYzNi4zMSw1MC4xNCw2MTQuNDcsNTAuMTRaTTU5My40Miw4NC40NmMyLjM0LTEwLjE0LDkuMzYtMTcuOTQsMjEuMDUtMTcuOTQsMTAuOTMsMCwxNy45NCw3LjgsMTkuNSwxNy45NFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNMzI2LjQsNTAuMTRjLTIzLjQsMC00MC41NiwxNy4xNi00MC41Niw0MS4zNFMzMDIsMTMyLjgyLDMyNi40LDEzMi44MmMxOS43MywwLDMxLjYtMTEuNjEsMzYuNTUtMjAuMTVsLTE3LjktNi4zOGMtMS43NywzLjI0LTguOSw5LjQ3LTE4LjY1LDkuNDctMTEuMzcsMC0xOS41LTcuMTItMjEuMDYtMThoNTkuMjhhMzUuMzgsMzUuMzgsMCwwLDAsLjc4LTcuOEMzNjUuNCw2Ny4zLDM0OC4yNCw1MC4xNCwzMjYuNCw1MC4xNFpNMzA1LjM0LDg0LjQ2YzIuMzQtMTAuMTQsOS4zNi0xNy45NCwyMS4wNi0xNy45NCwxMC45MiwwLDE3Ljk0LDcuOCwxOS41LDE3Ljk0WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwIC0xMC41MSkiLz48cmVjdCBjbGFzcz0iY2xzLTMiIHg9IjY5Mi4xNCIgeT0iOS43OCIgd2lkdGg9IjE5LjUiIGhlaWdodD0iMTkuNSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTc3NS40NSwxMTQuODhjLTExLjcsMC0yMS4wNi05LjM2LTIxLjA2LTIzLjRzOS4zNi0yMy40LDIxLjA2LTIzLjQsMTYuMzgsNy44LDE3Ljk0LDEyLjQ4bDE3LjY2LTYuMjhjLTQuMjgtMTEuMTEtMTQuNzgtMjQuMTQtMzUuNi0yNC4xNC0yMy40LDAtNDAuNTYsMTcuMTYtNDAuNTYsNDEuMzRzMTcuMTYsNDEuMzQsNDAuNTYsNDEuMzRjMjEsMCwzMS41LTEzLjI0LDM1LjctMjQuODhsLTE3Ljc2LTYuMzJDNzkxLjgzLDEwNy4wOCw3ODcuMTUsMTE0Ljg4LDc3NS40NSwxMTQuODhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTAgLTEwLjUxKSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMyIgcG9pbnRzPSI2NDUuNjMgMjcuMTEgNjU2LjcgMjcuMTEgNjU2LjcgMTIwLjc1IDY3Ni4yIDEyMC43NSA2NzYuMiA5Ljc4IDY0NS42MyA5Ljc4IDY0NS42MyAyNy4xMSIvPjxyZWN0IGNsYXNzPSJjbHMtMyIgeD0iNjkyLjE0IiB5PSI0MS4xOSIgd2lkdGg9IjE5LjUiIGhlaWdodD0iNzkuNTYiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik04MjEuNTksMTE2YTcuNTIsNy41MiwwLDEsMCw3LjQxLDcuNTJBNy4yOCw3LjI4LDAsMCwwLDgyMS41OSwxMTZabTAsMTMuODlhNi4zNyw2LjM3LDAsMSwxLDYuMjYtNi4zN0E2LjEyLDYuMTIsMCwwLDEsODIxLjU5LDEyOS44NVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMCAtMTAuNTEpIi8+PHBhdGggY2xhc3M9ImNscy0zIiBkPSJNODI0LjgyLDEyMi4xM2EyLjY0LDIuNjQsMCwwLDAtMi44Mi0yLjYyaC0zLjM0djcuODRoMS4xNXYtMi43MmgxLjA1bDIuNzEsMi43Mkg4MjVsLTIuNzEtMi43MkEyLjUzLDIuNTMsMCwwLDAsODI0LjgyLDEyMi4xM1ptLTUsMS4zNXYtMi44Mkg4MjJhMS41LDEuNSwwLDAsMSwxLjY4LDEuNDdjMCwuODMtLjUzLDEuMzUtMS42OCwxLjM1WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEwIC0xMC41MSkiLz48L3N2Zz4=" + mediatype: "image/svg+xml" + install: + spec: + deployments: null + strategy: "" + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - newrelic + - instrumentation + - apm + links: + - name: Newrelic Agent Operator + url: https://newrelic-agent-operator.domain + maintainers: + - email: alozoya@newrelic.com + name: Andrew Lozoya + maturity: alpha + provider: + name: New Relic + url: newrelic.com + version: 0.0.0 diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml new file mode 100644 index 00000000..63ca74d7 --- /dev/null +++ b/config/manifests/kustomization.yaml @@ -0,0 +1,4 @@ +resources: +- ../default +- ../samples +- ../scorecard diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml new file mode 100644 index 00000000..ed137168 --- /dev/null +++ b/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml new file mode 100644 index 00000000..3c0ef839 --- /dev/null +++ b/config/prometheus/monitor.yaml @@ -0,0 +1,26 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: false + ca: + secret: + key: ca.crt + name: newrelic-agent-operator-controller-manager-service-cert + selector: + matchLabels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 00000000..bd4af137 --- /dev/null +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: ["/metrics"] + verbs: ["get"] diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000..618f5e41 --- /dev/null +++ b/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000..ec7acc0a --- /dev/null +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000..85cab765 --- /dev/null +++ b/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + protocol: TCP + selector: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml new file mode 100644 index 00000000..27cb1d17 --- /dev/null +++ b/config/rbac/kustomization.yaml @@ -0,0 +1,13 @@ +resources: +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml new file mode 100644 index 00000000..7dc16c42 --- /dev/null +++ b/config/rbac/leader_election_role.yaml @@ -0,0 +1,33 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000..1d1321ed --- /dev/null +++ b/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 00000000..242cff2c --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - namespaces + verbs: + - list + - watch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - update +- apiGroups: + - newrelic.com + resources: + - instrumentations + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 00000000..2070ede4 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 00000000..00336028 --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system \ No newline at end of file diff --git a/config/samples/instrumentation_v1alpha1_instrumentation.yaml b/config/samples/instrumentation_v1alpha1_instrumentation.yaml new file mode 100644 index 00000000..e665a561 --- /dev/null +++ b/config/samples/instrumentation_v1alpha1_instrumentation.yaml @@ -0,0 +1,17 @@ +apiVersion: newrelic.com/v1alpha1 +kind: Instrumentation +metadata: + name: newrelic-instrumentation +spec: + java: + image: ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-java:latest + nodejs: + image: ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-nodejs:latest + python: + image: ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-python:latest + dotnet: + image: ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-dotnet:latest + php: + image: ghcr.io/newrelic-experimental/newrelic-agent-operator/instrumentation-php:latest + go: + image: ghcr.io/open-telemetry/opentelemetry-go-instrumentation/autoinstrumentation-go:latest \ No newline at end of file diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 00000000..ca55594e --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,3 @@ +## This file is auto-generated, do not modify ## +resources: +- instrumentation_v1alpha1_instrumentation.yaml diff --git a/config/scorecard/bases/config.yaml b/config/scorecard/bases/config.yaml new file mode 100644 index 00000000..c7704784 --- /dev/null +++ b/config/scorecard/bases/config.yaml @@ -0,0 +1,7 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: [] diff --git a/config/scorecard/kustomization.yaml b/config/scorecard/kustomization.yaml new file mode 100644 index 00000000..50cd2d08 --- /dev/null +++ b/config/scorecard/kustomization.yaml @@ -0,0 +1,16 @@ +resources: +- bases/config.yaml +patchesJson6902: +- path: patches/basic.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +- path: patches/olm.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +#+kubebuilder:scaffold:patchesJson6902 diff --git a/config/scorecard/patches/basic.config.yaml b/config/scorecard/patches/basic.config.yaml new file mode 100644 index 00000000..fb742649 --- /dev/null +++ b/config/scorecard/patches/basic.config.yaml @@ -0,0 +1,10 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: basic + test: basic-check-spec-test diff --git a/config/scorecard/patches/olm.config.yaml b/config/scorecard/patches/olm.config.yaml new file mode 100644 index 00000000..adaaf65d --- /dev/null +++ b/config/scorecard/patches/olm.config.yaml @@ -0,0 +1,30 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-bundle-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-crds-have-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.27.0 + labels: + suite: olm + test: olm-crds-have-resources-test diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..9cf26134 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..25e21e3c --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..c765aecc --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,93 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + 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: webhook-service + namespace: system + 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: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + 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: webhook-service + namespace: system + path: /validate-newrelic-com-v1alpha1-instrumentation + failurePolicy: Ignore + name: vinstrumentationdelete.kb.io + rules: + - apiGroups: + - newrelic.com + apiVersions: + - v1alpha1 + operations: + - DELETE + resources: + - instrumentations + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..3114c1b3 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,14 @@ + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + targetPort: 9443 + protocol: TCP + selector: + app.kubernetes.io/name: newrelic-agent-operator + control-plane: controller-manager diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..508dfa7c --- /dev/null +++ b/go.mod @@ -0,0 +1,79 @@ +module github.com/newrelic-experimental/newrelic-agent-operator + +go 1.20 + +retract v1.51.0 + +require ( + github.com/go-logr/logr v1.2.3 + 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 + go.opentelemetry.io/otel v1.11.2 + k8s.io/api v0.26.3 + k8s.io/apimachinery v0.26.3 + k8s.io/client-go v0.26.3 + k8s.io/component-base v0.26.3 + sigs.k8s.io/controller-runtime v0.14.5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + 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-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + 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 + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + 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 + 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 + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + 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/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 + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..fe22bc77 --- /dev/null +++ b/go.sum @@ -0,0 +1,633 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +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= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +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-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= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= +github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= +github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/openshift/api v3.9.0+incompatible h1:fJ/KsefYuZAjmrr3+5U9yZIZbTOpVkDDLDLFresAeYs= +github.com/openshift/api v3.9.0+incompatible/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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/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= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +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/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= +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= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +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= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= +k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= +k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE= +k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ= +k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= +k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +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/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= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s= +sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/config/change_handler.go b/internal/config/change_handler.go new file mode 100644 index 00000000..4816a5ee --- /dev/null +++ b/internal/config/change_handler.go @@ -0,0 +1,65 @@ +/* +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 config contains the operator's runtime configuration. +package config + +import ( + "sync" + + "github.com/go-logr/logr" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// changeHandler is implemented by any structure that is able to register callbacks +// and call them using one single method. +type changeHandler interface { + // Do will call every registered callback. + Do() error + // Register this function as a callback that will be executed when Do() is called. + Register(f func() error) +} + +// newOnChange returns a thread-safe ChangeHandler. +func newOnChange() changeHandler { + return &onChange{ + logger: logf.Log.WithName("change-handler"), + } +} + +type onChange struct { + logger logr.Logger + + callbacks []func() error + muCallbacks sync.Mutex +} + +func (o *onChange) Do() error { + o.muCallbacks.Lock() + defer o.muCallbacks.Unlock() + for _, fn := range o.callbacks { + if err := fn(); err != nil { + o.logger.Error(err, "change callback failed") + } + } + return nil +} + +func (o *onChange) Register(f func() error) { + o.muCallbacks.Lock() + defer o.muCallbacks.Unlock() + o.callbacks = append(o.callbacks, f) +} diff --git a/internal/config/change_handler_test.go b/internal/config/change_handler_test.go new file mode 100644 index 00000000..13ff6a89 --- /dev/null +++ b/internal/config/change_handler_test.go @@ -0,0 +1,43 @@ +/* +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 config contains the operator's runtime configuration. +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChangeHandler(t *testing.T) { + // prepare + internal := 0 + callback := func() error { + internal += 1 + return nil + } + h := newOnChange() + + h.Register(callback) + + for i := 0; i < 5; i++ { + assert.Equal(t, i, internal) + require.NoError(t, h.Do()) + assert.Equal(t, i+1, internal) + } +} diff --git a/internal/config/main.go b/internal/config/main.go new file mode 100644 index 00000000..ac3c21a0 --- /dev/null +++ b/internal/config/main.go @@ -0,0 +1,208 @@ +/* +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 config + +import ( + "sync" + "time" + + "github.com/go-logr/logr" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/newrelic-experimental/newrelic-agent-operator/internal/version" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/autodetect" +) + +const ( + defaultAutoDetectFrequency = 5 * time.Second +) + +// 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 + onOpenShiftRoutesChange changeHandler + labelsFilter []string + openshiftRoutes openshiftRoutesStore + autoDetectFrequency time.Duration + autoscalingVersion autodetect.AutoscalingVersion +} + +// New constructs a new configuration based on the given options. +func New(opts ...Option) Config { + // initialize with the default values + o := options{ + autoDetectFrequency: defaultAutoDetectFrequency, + logger: logf.Log.WithName("config"), + openshiftRoutes: newOpenShiftRoutesWrapper(), + version: version.Get(), + autoscalingVersion: autodetect.DefaultAutoscalingVersion, + onOpenShiftRoutesChange: newOnChange(), + } + for _, opt := range opts { + opt(&o) + } + + 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, + autoInstrumentationGoImage: o.autoInstrumentationGoImage, + labelsFilter: o.labelsFilter, + autoscalingVersion: o.autoscalingVersion, + } +} + +// StartAutoDetect attempts to automatically detect relevant information for this operator. This will block until the first +// run is executed and will schedule periodic updates. +func (c *Config) StartAutoDetect() error { + err := c.AutoDetect() + go c.periodicAutoDetect() + + return err +} + +func (c *Config) periodicAutoDetect() { + ticker := time.NewTicker(c.autoDetectFrequency) + + for range ticker.C { + if err := c.AutoDetect(); err != nil { + c.logger.Info("auto-detection failed", "error", err) + } + } +} + +// AutoDetect attempts to automatically detect relevant information for this operator. +func (c *Config) AutoDetect() error { + c.logger.V(2).Info("auto-detecting the configuration based on the environment") + + ora, err := c.autoDetect.OpenShiftRoutesAvailability() + if err != nil { + return err + } + + if c.openshiftRoutes.Get() != ora { + c.logger.V(1).Info("openshift routes detected", "available", ora) + c.openshiftRoutes.Set(ora) + if err = c.onOpenShiftRoutesChange.Do(); err != nil { + // Don't fail if the callback failed, as auto-detection itself worked. + c.logger.Error(err, "configuration change notification failed for callback") + } + } + + hpaVersion, err := c.autoDetect.HPAVersion() + if err != nil { + return err + } + c.autoscalingVersion = hpaVersion + c.logger.V(2).Info("autoscaling version detected", "autoscaling-version", c.autoscalingVersion.String()) + + return nil +} + +// OpenShiftRoutes represents the availability of the OpenShift Routes API. +func (c *Config) OpenShiftRoutes() autodetect.OpenShiftRoutesAvailability { + return c.openshiftRoutes.Get() +} + +// AutoscalingVersion represents the preferred version of autoscaling. +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 +} + +// 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 +} + +// RegisterOpenShiftRoutesChangeCallback registers the given function as a callback that +// is called when the OpenShift Routes detection detects a change. +func (c *Config) RegisterOpenShiftRoutesChangeCallback(f func() error) { + c.onOpenShiftRoutesChange.Register(f) +} + +type openshiftRoutesStore interface { + Set(ora autodetect.OpenShiftRoutesAvailability) + Get() autodetect.OpenShiftRoutesAvailability +} + +func newOpenShiftRoutesWrapper() openshiftRoutesStore { + return &openshiftRoutesWrapper{ + current: autodetect.OpenShiftRoutesNotAvailable, + } +} + +type openshiftRoutesWrapper struct { + mu sync.Mutex + current autodetect.OpenShiftRoutesAvailability +} + +func (p *openshiftRoutesWrapper) Set(ora autodetect.OpenShiftRoutesAvailability) { + p.mu.Lock() + p.current = ora + p.mu.Unlock() +} + +func (p *openshiftRoutesWrapper) Get() autodetect.OpenShiftRoutesAvailability { + p.mu.Lock() + ora := p.current + p.mu.Unlock() + return ora +} diff --git a/internal/config/main_test.go b/internal/config/main_test.go new file mode 100644 index 00000000..0200163d --- /dev/null +++ b/internal/config/main_test.go @@ -0,0 +1,110 @@ +/* +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 config_test + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/newrelic-experimental/newrelic-agent-operator/internal/config" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/autodetect" +) + +func TestNewConfig(t *testing.T) { + // prepare + cfg := config.New( + config.WithPlatform(autodetect.OpenShiftRoutesNotAvailable), + ) + + // test + assert.Equal(t, autodetect.OpenShiftRoutesNotAvailable, cfg.OpenShiftRoutes()) +} + +func TestOnPlatformChangeCallback(t *testing.T) { + // prepare + calledBack := false + mock := &mockAutoDetect{ + OpenShiftRoutesAvailabilityFunc: func() (autodetect.OpenShiftRoutesAvailability, error) { + return autodetect.OpenShiftRoutesAvailable, nil + }, + } + cfg := config.New( + config.WithAutoDetect(mock), + config.WithOnOpenShiftRoutesChangeCallback(func() error { + calledBack = true + return nil + }), + ) + + // sanity check + require.Equal(t, autodetect.OpenShiftRoutesNotAvailable, cfg.OpenShiftRoutes()) + + // test + err := cfg.AutoDetect() + require.NoError(t, err) + + // verify + assert.Equal(t, autodetect.OpenShiftRoutesAvailable, cfg.OpenShiftRoutes()) + assert.True(t, calledBack) +} + +func TestAutoDetectInBackground(t *testing.T) { + // prepare + wg := &sync.WaitGroup{} + wg.Add(2) + mock := &mockAutoDetect{ + OpenShiftRoutesAvailabilityFunc: func() (autodetect.OpenShiftRoutesAvailability, error) { + wg.Done() + return autodetect.OpenShiftRoutesNotAvailable, nil + }, + } + cfg := config.New( + config.WithAutoDetect(mock), + config.WithAutoDetectFrequency(100*time.Millisecond), + ) + + // sanity check + require.Equal(t, autodetect.OpenShiftRoutesNotAvailable, cfg.OpenShiftRoutes()) + + // test + err := cfg.StartAutoDetect() + require.NoError(t, err) + + // verify + wg.Wait() +} + +var _ autodetect.AutoDetect = (*mockAutoDetect)(nil) + +type mockAutoDetect struct { + OpenShiftRoutesAvailabilityFunc func() (autodetect.OpenShiftRoutesAvailability, error) +} + +func (m *mockAutoDetect) HPAVersion() (autodetect.AutoscalingVersion, error) { + return autodetect.DefaultAutoscalingVersion, nil +} + +func (m *mockAutoDetect) OpenShiftRoutesAvailability() (autodetect.OpenShiftRoutesAvailability, error) { + if m.OpenShiftRoutesAvailabilityFunc != nil { + return m.OpenShiftRoutesAvailabilityFunc() + } + return autodetect.OpenShiftRoutesNotAvailable, nil +} diff --git a/internal/config/options.go b/internal/config/options.go new file mode 100644 index 00000000..0d96751f --- /dev/null +++ b/internal/config/options.go @@ -0,0 +1,143 @@ +/* +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 config + +import ( + "regexp" + "strings" + "time" + + "github.com/go-logr/logr" + + "github.com/newrelic-experimental/newrelic-agent-operator/internal/version" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/autodetect" +) + +// Option represents one specific configuration option. +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 + onOpenShiftRoutesChange changeHandler + labelsFilter []string + openshiftRoutes openshiftRoutesStore + autoDetectFrequency time.Duration + autoscalingVersion autodetect.AutoscalingVersion +} + +func WithAutoDetect(a autodetect.AutoDetect) Option { + return func(o *options) { + o.autoDetect = a + } +} +func WithAutoDetectFrequency(t time.Duration) Option { + return func(o *options) { + o.autoDetectFrequency = t + } +} +func WithLogger(logger logr.Logger) Option { + return func(o *options) { + o.logger = logger + } +} +func WithOnOpenShiftRoutesChangeCallback(f func() error) Option { + return func(o *options) { + if o.onOpenShiftRoutesChange == nil { + o.onOpenShiftRoutesChange = newOnChange() + } + o.onOpenShiftRoutesChange.Register(f) + } +} +func WithPlatform(ora autodetect.OpenShiftRoutesAvailability) Option { + return func(o *options) { + o.openshiftRoutes.Set(ora) + } +} +func WithVersion(v version.Version) Option { + return func(o *options) { + o.version = v + } +} + +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 WithAutoInstrumentationGoImage(s string) Option { + return func(o *options) { + o.autoInstrumentationGoImage = s + } +} + +func WithLabelFilters(labelFilters []string) Option { + return func(o *options) { + + filters := []string{} + for _, pattern := range labelFilters { + var result strings.Builder + + for i, literal := range strings.Split(pattern, "*") { + + // Replace * with .* + if i > 0 { + result.WriteString(".*") + } + + // Quote any regular expression meta characters in the + // literal text. + result.WriteString(regexp.QuoteMeta(literal)) + } + filters = append(filters, result.String()) + } + + o.labelsFilter = filters + } +} diff --git a/internal/version/main.go b/internal/version/main.go new file mode 100644 index 00000000..49b00add --- /dev/null +++ b/internal/version/main.go @@ -0,0 +1,119 @@ +/* +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 version contains the operator's version, as well as versions of underlying components. +package version + +import ( + "fmt" + "runtime" +) + +var ( + version string + buildDate string + autoInstrumentationJava string + autoInstrumentationNodeJS string + autoInstrumentationPython string + autoInstrumentationDotNet string + autoInstrumentationPhp string + autoInstrumentationGo 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:"newrelic-agent-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"` + AutoInstrumentationGo string `json:"autoinstrumentation-go"` +} + +// 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(), + AutoInstrumentationGo: AutoInstrumentationGo(), + } +} + +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', AutoInstrumentationGo='%v')", + v.Operator, + v.BuildDate, + v.Go, + v.AutoInstrumentationJava, + v.AutoInstrumentationNodeJS, + v.AutoInstrumentationPython, + v.AutoInstrumentationDotNet, + v.AutoInstrumentationPhp, + 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 AutoInstrumentationGo() string { + if len(autoInstrumentationGo) > 0 { + return autoInstrumentationGo + } + return "0.0.0" +} diff --git a/internal/version/main_test.go b/internal/version/main_test.go new file mode 100644 index 00000000..2a6ee914 --- /dev/null +++ b/internal/version/main_test.go @@ -0,0 +1,47 @@ +/* +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 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 TestAutoInstrumentationGoFallbackVersion(t *testing.T) { + assert.Equal(t, "0.0.0", AutoInstrumentationGo()) +} diff --git a/internal/webhookhandler/webhookhandler.go b/internal/webhookhandler/webhookhandler.go new file mode 100644 index 00000000..b15fa0f0 --- /dev/null +++ b/internal/webhookhandler/webhookhandler.go @@ -0,0 +1,116 @@ +/* +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 webhookhandler contains the webhook that injects sidecars into pods. +package webhookhandler + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/newrelic-experimental/newrelic-agent-operator/internal/config" +) + +// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=ignore,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,sideEffects=none,admissionReviewVersions=v1 +// +kubebuilder:rbac:groups="",resources=namespaces,verbs=list;watch +// +kubebuilder:rbac:groups=newrelic.com,resources=instrumentations,verbs=get;list;watch +// +kubebuilder:rbac:groups="apps",resources=replicasets,verbs=get;list;watch +// +kubebuilder:rbac:groups=route.openshift.io,resources=routes;routes/custom-host,verbs=get;list;watch;create;update;patch;delete +// +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) + +// WebhookHandler is a webhook handler that analyzes new pods and injects appropriate sidecars into it. +type WebhookHandler interface { + admission.Handler + admission.DecoderInjector +} + +// the implementation. +type podSidecarInjector struct { + client client.Client + decoder *admission.Decoder + logger logr.Logger + podMutators []PodMutator + config config.Config +} + +// PodMutator mutates a pod. +type PodMutator interface { + Mutate(ctx context.Context, ns corev1.Namespace, pod corev1.Pod) (corev1.Pod, error) +} + +// NewWebhookHandler creates a new WebhookHandler. +func NewWebhookHandler(cfg config.Config, logger logr.Logger, cl client.Client, podMutators []PodMutator) WebhookHandler { + return &podSidecarInjector{ + config: cfg, + logger: logger, + client: cl, + podMutators: podMutators, + } +} + +func (p *podSidecarInjector) Handle(ctx context.Context, req admission.Request) admission.Response { + pod := corev1.Pod{} + err := p.decoder.Decode(req, &pod) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + // we use the req.Namespace here because the pod might have not been created yet + ns := corev1.Namespace{} + err = p.client.Get(ctx, types.NamespacedName{Name: req.Namespace, Namespace: ""}, &ns) + if err != nil { + res := admission.Errored(http.StatusInternalServerError, err) + // By default, admission.Errored sets Allowed to false which blocks pod creation even though the failurePolicy=ignore. + // Allowed set to true makes sure failure does not block pod creation in case of an error. + // Using the http.StatusInternalServerError creates a k8s event associated with the replica set. + // The admission.Allowed("").WithWarnings(err.Error()) or http.StatusBadRequest does not + // create any event. Additionally, an event/log cannot be created explicitly because the pod name is not known. + res.Allowed = true + return res + } + + for _, m := range p.podMutators { + pod, err = m.Mutate(ctx, ns, pod) + if err != nil { + res := admission.Errored(http.StatusInternalServerError, err) + res.Allowed = true + return res + } + } + + marshaledPod, err := json.Marshal(pod) + if err != nil { + res := admission.Errored(http.StatusInternalServerError, err) + res.Allowed = true + return res + } + return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) +} + +func (p *podSidecarInjector) InjectDecoder(d *admission.Decoder) error { + p.decoder = d + return nil +} diff --git a/internal/webhookhandler/webhookhandler_suite_test.go b/internal/webhookhandler/webhookhandler_suite_test.go new file mode 100644 index 00000000..83d0ee39 --- /dev/null +++ b/internal/webhookhandler/webhookhandler_suite_test.go @@ -0,0 +1,144 @@ +/* +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 webhookhandler_test + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +var ( + k8sClient client.Client + testEnv *envtest.Environment + testScheme *runtime.Scheme = scheme.Scheme + ctx context.Context + cancel context.CancelFunc + err error + cfg *rest.Config +) + +func TestMain(m *testing.M) { + ctx, cancel = context.WithCancel(context.TODO()) + defer cancel() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + cfg, err = testEnv.Start() + if err != nil { + fmt.Printf("failed to start testEnv: %v", err) + os.Exit(1) + } + + if err = v1alpha1.AddToScheme(testScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + if err != nil { + fmt.Printf("failed to setup a Kubernetes client: %v", err) + os.Exit(1) + } + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, mgrErr := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: testScheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + if mgrErr != nil { + fmt.Printf("failed to start webhook server: %v", mgrErr) + os.Exit(1) + } + + ctx, cancel = context.WithCancel(context.TODO()) + defer cancel() + go func() { + if err = mgr.Start(ctx); err != nil { + fmt.Printf("failed to start manager: %v", err) + os.Exit(1) + } + }() + + // wait for the webhook server to get ready + wg := &sync.WaitGroup{} + wg.Add(1) + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + go func(wg *sync.WaitGroup) { + defer wg.Done() + if err = retry.OnError(wait.Backoff{ + Steps: 20, + Duration: 10 * time.Millisecond, + Factor: 1.5, + Jitter: 0.1, + Cap: time.Second * 30, + }, func(error) bool { + return true + }, func() error { + // #nosec G402 + conn, tlsErr := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if tlsErr != nil { + return tlsErr + } + _ = conn.Close() + return nil + }); err != nil { + fmt.Printf("failed to wait for webhook server to be ready: %v", err) + os.Exit(1) + } + }(wg) + wg.Wait() + + code := m.Run() + + err = testEnv.Stop() + if err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + + os.Exit(code) +} diff --git a/pkg/autodetect/main.go b/pkg/autodetect/main.go new file mode 100644 index 00000000..e9738312 --- /dev/null +++ b/pkg/autodetect/main.go @@ -0,0 +1,128 @@ +/* +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 autodetect is for auto-detecting traits from the environment (platform, APIs, ...). +package autodetect + +import ( + "errors" + "sort" + + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" +) + +var _ AutoDetect = (*autoDetect)(nil) + +// AutoDetect provides an assortment of routines that auto-detect traits based on the runtime. +type AutoDetect interface { + OpenShiftRoutesAvailability() (OpenShiftRoutesAvailability, error) + HPAVersion() (AutoscalingVersion, error) +} + +type autoDetect struct { + dcl discovery.DiscoveryInterface +} + +type AutoscalingVersion int + +const ( + AutoscalingVersionV2 AutoscalingVersion = iota + AutoscalingVersionV2Beta2 + AutoscalingVersionUnknown +) + +const DefaultAutoscalingVersion = AutoscalingVersionV2 + +// New creates a new auto-detection worker, using the given client when talking to the current cluster. +func New(restConfig *rest.Config) (AutoDetect, error) { + dcl, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + // it's pretty much impossible to get into this problem, as most of the + // code branches from the previous call just won't fail at all, + // but let's handle this error anyway... + return nil, err + } + + return &autoDetect{ + dcl: dcl, + }, nil +} + +// OpenShiftRoutesAvailability checks if OpenShift Route are available. +func (a *autoDetect) OpenShiftRoutesAvailability() (OpenShiftRoutesAvailability, error) { + apiList, err := a.dcl.ServerGroups() + if err != nil { + return OpenShiftRoutesNotAvailable, err + } + + apiGroups := apiList.Groups + for i := 0; i < len(apiGroups); i++ { + if apiGroups[i].Name == "route.openshift.io" { + return OpenShiftRoutesAvailable, nil + } + } + + return OpenShiftRoutesNotAvailable, nil +} + +func (a *autoDetect) HPAVersion() (AutoscalingVersion, error) { + apiList, err := a.dcl.ServerGroups() + if err != nil { + return AutoscalingVersionUnknown, err + } + + for _, apiGroup := range apiList.Groups { + if apiGroup.Name == "autoscaling" { + // Sort this so we can make sure to get v2 before v2beta2 + versions := apiGroup.Versions + sort.Slice(versions, func(i, j int) bool { + return versions[i].Version < versions[j].Version + }) + + for _, version := range versions { + if version.Version == "v2" || version.Version == "v2beta2" { + return ToAutoScalingVersion(version.Version), nil + } + } + return AutoscalingVersionUnknown, errors.New("Failed to find appropriate version of apiGroup autoscaling, only v2 and v2beta2 are supported") + } + } + + return AutoscalingVersionUnknown, errors.New("Failed to find apiGroup autoscaling") +} + +func (v AutoscalingVersion) String() string { + switch v { + case AutoscalingVersionV2: + return "v2" + case AutoscalingVersionV2Beta2: + return "v2beta2" + case AutoscalingVersionUnknown: + return "unknown" + } + return "unknown" +} + +func ToAutoScalingVersion(version string) AutoscalingVersion { + switch version { + case "v2": + return AutoscalingVersionV2 + case "v2beta2": + return AutoscalingVersionV2Beta2 + } + return AutoscalingVersionUnknown +} diff --git a/pkg/autodetect/main_test.go b/pkg/autodetect/main_test.go new file mode 100644 index 00000000..5eb0be3e --- /dev/null +++ b/pkg/autodetect/main_test.go @@ -0,0 +1,86 @@ +/* +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 autodetect_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/autodetect" +) + +func TestDetectPlatformBasedOnAvailableAPIGroups(t *testing.T) { + for _, tt := range []struct { + apiGroupList *metav1.APIGroupList + expected autodetect.OpenShiftRoutesAvailability + }{ + { + &metav1.APIGroupList{}, + autodetect.OpenShiftRoutesNotAvailable, + }, + { + &metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "route.openshift.io", + }, + }, + }, + autodetect.OpenShiftRoutesAvailable, + }, + } { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + output, err := json.Marshal(tt.apiGroupList) + require.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(output) + require.NoError(t, err) + })) + defer server.Close() + + autoDetect, err := autodetect.New(&rest.Config{Host: server.URL}) + require.NoError(t, err) + + // test + ora, err := autoDetect.OpenShiftRoutesAvailability() + + // verify + assert.NoError(t, err) + assert.Equal(t, tt.expected, ora) + } +} + +func TestAutoscalingVersionToString(t *testing.T) { + assert.Equal(t, "v2", autodetect.AutoscalingVersionV2.String()) + assert.Equal(t, "v2beta2", autodetect.AutoscalingVersionV2Beta2.String()) + assert.Equal(t, "unknown", autodetect.AutoscalingVersionUnknown.String()) +} + +func TestToAutoScalingVersion(t *testing.T) { + assert.Equal(t, autodetect.AutoscalingVersionV2, autodetect.ToAutoScalingVersion("v2")) + assert.Equal(t, autodetect.AutoscalingVersionV2Beta2, autodetect.ToAutoScalingVersion("v2beta2")) + assert.Equal(t, autodetect.AutoscalingVersionUnknown, autodetect.ToAutoScalingVersion("fred")) +} diff --git a/pkg/autodetect/openshiftroutes.go b/pkg/autodetect/openshiftroutes.go new file mode 100644 index 00000000..d5acb5ce --- /dev/null +++ b/pkg/autodetect/openshiftroutes.go @@ -0,0 +1,33 @@ +/* +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 autodetect is for auto-detecting traits from the environment (APIs, ...). +package autodetect + +// OpenShiftRoutesAvailability holds the auto-detected OpenShift Routes availability API. +type OpenShiftRoutesAvailability int + +const ( + // OpenShiftRoutesAvailable represents the route.openshift.io API is available. + OpenShiftRoutesAvailable OpenShiftRoutesAvailability = iota + + // OpenShiftRoutesNotAvailable represents the route.openshift.io API is not available. + OpenShiftRoutesNotAvailable +) + +func (p OpenShiftRoutesAvailability) String() string { + return [...]string{"Available", "NotAvailable"}[p] +} diff --git a/pkg/constants/env.go b/pkg/constants/env.go new file mode 100644 index 00000000..d2d9b0e8 --- /dev/null +++ b/pkg/constants/env.go @@ -0,0 +1,34 @@ +/* +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" + EnvNewRelicLicenseKey = "NEW_RELIC_LICENSE_KEY" + EnvNewRelicLabels = "NEW_RELIC_LABELS" +) diff --git a/pkg/instrumentation/annotation.go b/pkg/instrumentation/annotation.go new file mode 100644 index 00000000..1774a107 --- /dev/null +++ b/pkg/instrumentation/annotation.go @@ -0,0 +1,75 @@ +/* +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 ( + // 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" + 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/pkg/instrumentation/dotnet.go b/pkg/instrumentation/dotnet.go new file mode 100644 index 00000000..56c3bc0e --- /dev/null +++ b/pkg/instrumentation/dotnet.go @@ -0,0 +1,119 @@ +/* +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 ( + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +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" + dotnetInitContainerName = initContainerName + "-dotnet" +) + +func injectDotNetSDK(dotNetSpec v1alpha1.DotNet, pod corev1.Pod, index int) (corev1.Pod, error) { + + // caller checks if there is at least one container. + container := &pod.Spec.Containers[index] + + // 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 { + 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 + // if it is already set, then we assume that .NET newrelic-instrumentation is already configured for this container + if getIndexOfEnv(dotNetSpec.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 { + 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) + + 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", + }) + + // 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{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: dotNetSpec.Image, + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }}, + }) + } + 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) + } +} diff --git a/pkg/instrumentation/golang.go b/pkg/instrumentation/golang.go new file mode 100644 index 00000000..8cc9d9ae --- /dev/null +++ b/pkg/instrumentation/golang.go @@ -0,0 +1,96 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package instrumentation + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +const ( + envOtelTargetExe = "OTEL_GO_AUTO_TARGET_EXE" + + kernelDebugVolumeName = "kernel-debug" + kernelDebugVolumePath = "/sys/kernel/debug" +) + +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") + } + + // skip instrumentation when more than one containers provided + containerNames := "" + ok := false + containerNames, ok = pod.Annotations[annotationInjectContainerName] + + if ok && len(strings.Split(containerNames, ",")) > 1 { + return pod, fmt.Errorf("go instrumentation cannot be injected into a pod, multiple containers configured") + } + + true := true + zero := int64(0) + pod.Spec.ShareProcessNamespace = &true + + goAgent := corev1.Container{ + Name: sideCarName, + Image: goSpec.Image, + Resources: goSpec.Resources, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: &zero, + Privileged: &true, + }, + VolumeMounts: []corev1.VolumeMount{ + { + MountPath: "/sys/kernel/debug", + Name: kernelDebugVolumeName, + }, + }, + } + + // Annotation takes precedence for OTEL_GO_AUTO_TARGET_EXE + execPath, ok := pod.Annotations[annotationGoExecPath] + if ok { + goAgent.Env = append(goAgent.Env, corev1.EnvVar{ + Name: envOtelTargetExe, + Value: execPath, + }) + } + + // Inject Go instrumentation spec env vars. + // For Go, env vars must be added to the agent contain + for _, env := range goSpec.Env { + idx := getIndexOfEnv(goAgent.Env, env.Name) + if idx == -1 { + goAgent.Env = append(goAgent.Env, env) + } + } + + pod.Spec.Containers = append(pod.Spec.Containers, goAgent) + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: kernelDebugVolumeName, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: kernelDebugVolumePath, + }, + }, + }) + return pod, nil +} diff --git a/pkg/instrumentation/helper.go b/pkg/instrumentation/helper.go new file mode 100644 index 00000000..cda5fbd9 --- /dev/null +++ b/pkg/instrumentation/helper.go @@ -0,0 +1,30 @@ +/* +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 ( + corev1 "k8s.io/api/core/v1" +) + +// Calculate if we already inject InitContainers. +func isInitContainerMissing(pod corev1.Pod) bool { + for _, initContainer := range pod.Spec.InitContainers { + if initContainer.Name == initContainerName { + return false + } + } + return true +} diff --git a/pkg/instrumentation/javaagent.go b/pkg/instrumentation/javaagent.go new file mode 100644 index 00000000..be9cef5d --- /dev/null +++ b/pkg/instrumentation/javaagent.go @@ -0,0 +1,82 @@ +/* +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 ( + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +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) { + // caller checks if there is at least one container. + container := &pod.Spec.Containers[index] + + err := validateContainerEnv(container.Env, envJavaToolsOptions) + if err != nil { + return pod, err + } + + // inject Java instrumentation spec env vars. + for _, env := range javaSpec.Env { + idx := getIndexOfEnv(container.Env, env.Name) + if idx == -1 { + container.Env = append(container.Env, env) + } + } + + idx := getIndexOfEnv(container.Env, envJavaToolsOptions) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envJavaToolsOptions, + Value: javaJVMArgument, + }) + } else { + container.Env[idx].Value = container.Env[idx].Value + javaJVMArgument + } + + 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{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: javaSpec.Image, + Command: []string{"cp", "/newrelic-agent.jar", "/newrelic-instrumentation/newrelic-agent.jar"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }}, + }) + } + return pod, err +} diff --git a/pkg/instrumentation/nodejs.go b/pkg/instrumentation/nodejs.go new file mode 100644 index 00000000..b0b811bf --- /dev/null +++ b/pkg/instrumentation/nodejs.go @@ -0,0 +1,82 @@ +/* +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 ( + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +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) { + // caller checks if there is at least one container. + container := &pod.Spec.Containers[index] + + err := validateContainerEnv(container.Env, envNodeOptions) + if err != nil { + return pod, err + } + + // inject NodeJS instrumentation spec env vars. + for _, env := range nodeJSSpec.Env { + idx := getIndexOfEnv(container.Env, env.Name) + if idx == -1 { + container.Env = append(container.Env, env) + } + } + + idx := getIndexOfEnv(container.Env, envNodeOptions) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envNodeOptions, + Value: nodeRequireArgument, + }) + } else if idx > -1 { + container.Env[idx].Value = container.Env[idx].Value + nodeRequireArgument + } + + 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{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: nodeJSSpec.Image, + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }}, + }) + } + return pod, nil +} diff --git a/pkg/instrumentation/php.go b/pkg/instrumentation/php.go new file mode 100644 index 00000000..2d8e4792 --- /dev/null +++ b/pkg/instrumentation/php.go @@ -0,0 +1,108 @@ +/* +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 ( + "fmt" + + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +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" +) + +func injectPhpagent(phpSpec v1alpha1.Php, pod corev1.Pod, index int) (corev1.Pod, error) { + // caller checks if there is at least one container. + container := &pod.Spec.Containers[index] + + // inject PHP instrumentation spec env vars. + for _, env := range phpSpec.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) + + setPhpEnvVar(container, envPhpSilentOption, phpSilentOptionArgument, phpConcatEnvValues) + + 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{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: phpSpec.Image, + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }}, + }) + } + + // 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) + } + + 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/pkg/instrumentation/podmutator.go b/pkg/instrumentation/podmutator.go new file mode 100644 index 00000000..06dfcea0 --- /dev/null +++ b/pkg/instrumentation/podmutator.go @@ -0,0 +1,176 @@ +/* +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 ( + "context" + "errors" + "strings" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" + "github.com/newrelic-experimental/newrelic-agent-operator/internal/webhookhandler" +) + +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 + Go *v1alpha1.Instrumentation +} + +var _ webhookhandler.PodMutator = (*instPodMutator)(nil) + +func NewMutator(logger logr.Logger, client client.Client) *instPodMutator { + return &instPodMutator{ + Logger: logger, + Client: client, + sdkInjector: &sdkInjector{ + logger: logger, + client: client, + }, + } +} + +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 + + insts := languageInstrumentations{} + + // We bail out if any annotation fails to process. + + if inst, err = pm.getInstrumentationInstance(ctx, ns, pod, annotationInjectJava); 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 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") + 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 + } + 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 + } + insts.DotNet = inst + + 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 + } + insts.Php = 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 + } + insts.Go = inst + + if insts.Java == nil && insts.NodeJS == nil && insts.Python == nil && insts.DotNet == nil && insts.Php == nil && insts.Go == nil { + logger.V(1).Info("annotation not present in deployment, skipping instrumentation injection") + return pod, nil + } + + // We retrieve the annotation for podname + var targetContainers = annotationValue(ns.ObjectMeta, pod.ObjectMeta, annotationInjectContainerName) + + // 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)) + } + + return modifiedPod, nil +} + +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) + + if len(instValue) == 0 || strings.EqualFold(instValue, "false") { + return nil, nil + } + + if strings.EqualFold(instValue, "true") { + return pm.selectInstrumentationInstanceFromNamespace(ctx, ns) + } + + 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} + } + + nrInst := &v1alpha1.Instrumentation{} + err := pm.Client.Get(ctx, instNamespacedName, nrInst) + if err != nil { + return nil, err + } + + 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 + } + + switch s := len(nrInsts.Items); { + case s == 0: + return nil, errNoInstancesAvailable + case s > 1: + return nil, errMultipleInstancesPossible + default: + return &nrInsts.Items[0], nil + } +} diff --git a/pkg/instrumentation/python.go b/pkg/instrumentation/python.go new file mode 100644 index 00000000..6cfedabb --- /dev/null +++ b/pkg/instrumentation/python.go @@ -0,0 +1,85 @@ +/* +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 ( + "fmt" + + corev1 "k8s.io/api/core/v1" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +const ( + envPythonPath = "PYTHONPATH" + pythonPathPrefix = "/newrelic-instrumentation/newrelic/bootstrap" + pythonPathSuffix = "/newrelic-instrumentation" + pythonVolumeName = volumeName + "-python" + pythonInitContainerName = initContainerName + "-python" +) + +func injectPythonSDK(pythonSpec v1alpha1.Python, pod corev1.Pod, index int) (corev1.Pod, error) { + // caller checks if there is at least one container. + container := &pod.Spec.Containers[index] + + err := validateContainerEnv(container.Env, envPythonPath) + if err != nil { + return pod, err + } + + // inject Python instrumentation spec env vars. + for _, env := range pythonSpec.Env { + idx := getIndexOfEnv(container.Env, env.Name) + if idx == -1 { + container.Env = append(container.Env, env) + } + } + + idx := getIndexOfEnv(container.Env, envPythonPath) + if idx == -1 { + container.Env = append(container.Env, corev1.EnvVar{ + Name: envPythonPath, + Value: fmt.Sprintf("%s:%s", pythonPathPrefix, pythonPathSuffix), + }) + } else if idx > -1 { + container.Env[idx].Value = fmt.Sprintf("%s:%s:%s", pythonPathPrefix, container.Env[idx].Value, pythonPathSuffix) + } + + 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{}, + }}) + + pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{ + Name: initContainerName, + Image: pythonSpec.Image, + Command: []string{"cp", "-a", "/instrumentation/.", "/newrelic-instrumentation/"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: volumeName, + MountPath: "/newrelic-instrumentation", + }}, + }) + } + return pod, nil +} diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go new file mode 100644 index 00000000..6f140321 --- /dev/null +++ b/pkg/instrumentation/sdk.go @@ -0,0 +1,523 @@ +/* +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 ( + "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" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" + "github.com/newrelic-experimental/newrelic-agent-operator/pkg/constants" +) + +const ( + volumeName = "newrelic-instrumentation" + initContainerName = "newrelic-instrumentation" + sideCarName = "opentelemetry-auto-instrumentation" +) + +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 = 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 = 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 = 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 = 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 = 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.Go != nil { + origPod := pod + 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 = 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) + + // Ensure that after all the env var coalescing we have a value for OTEL_GO_AUTO_TARGET_EXE + idx := getIndexOfEnv(pod.Spec.Containers[len(pod.Spec.Containers)-1].Env, envOtelTargetExe) + if idx == -1 { + i.logger.Info("Skipping Go SDK injection", "reason", "OTEL_GO_AUTO_TARGET_EXE not set", "container", pod.Spec.Containers[index].Name) + pod = origPod + } + } + } + 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, and python 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, + }) + } + } + } + + // 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 + + return 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: "NEW_RELIC_LABELS", + Value: "operator:auto-injection", + }) + } + return pod +} + +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 +} + +// 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 { + 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 + } + } + 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) + } + } + } +} + +func getIndexOfEnv(envs []corev1.EnvVar, name string) int { + for i := range envs { + if envs[i].Name == name { + return i + } + } + 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) + } + + return envs +} + +func validateContainerEnv(envs []corev1.EnvVar, envsToBeValidated ...string) error { + for _, envToBeValidated := range envsToBeValidated { + for _, containerEnv := range envs { + if containerEnv.Name == envToBeValidated { + if containerEnv.ValueFrom != nil { + return fmt.Errorf("the container defines env var value via ValueFrom, envVar: %s", containerEnv.Name) + } + break + } + } + } + return nil +} diff --git a/pkg/instrumentation/upgrade/upgrade.go b/pkg/instrumentation/upgrade/upgrade.go new file mode 100644 index 00000000..e7ddc5ec --- /dev/null +++ b/pkg/instrumentation/upgrade/upgrade.go @@ -0,0 +1,125 @@ +/* +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 upgrade + +import ( + "context" + "fmt" + "reflect" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/newrelic-experimental/newrelic-agent-operator/api/v1alpha1" +) + +type InstrumentationUpgrade struct { + Client client.Client + Logger logr.Logger + DefaultAutoInstJava string + DefaultAutoInstNodeJS string + DefaultAutoInstPython string + DefaultAutoInstDotNet string + DefaultAutoInstPhp string + DefaultAutoInstGo string +} + +//+kubebuilder:rbac:groups=newrelic.com,resources=instrumentations,verbs=get;list;watch;update;patch + +// ManagedInstances upgrades managed instances by the newrelic-agent-operator. +func (u *InstrumentationUpgrade) ManagedInstances(ctx context.Context) error { + u.Logger.Info("looking for managed Instrumentation instances to upgrade") + + opts := []client.ListOption{ + client.MatchingLabels(map[string]string{ + "app.kubernetes.io/managed-by": "newrelic-agent-operator", + }), + } + list := &v1alpha1.InstrumentationList{} + if err := u.Client.List(ctx, list, opts...); err != nil { + return fmt.Errorf("failed to list: %w", err) + } + + for i := range list.Items { + toUpgrade := list.Items[i] + upgraded := u.upgrade(ctx, toUpgrade) + if !reflect.DeepEqual(upgraded, toUpgrade) { + // use update instead of patch because the patch does not upgrade annotations + if err := u.Client.Update(ctx, &upgraded); err != nil { + u.Logger.Error(err, "failed to apply changes to instance", "name", upgraded.Name, "namespace", upgraded.Namespace) + continue + } + } + } + + if len(list.Items) == 0 { + u.Logger.Info("no instances to upgrade") + } + 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 + } + } + 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 + } + } + return inst +} diff --git a/pkg/instrumentation/upgrade/upgrade_suite_test.go b/pkg/instrumentation/upgrade/upgrade_suite_test.go new file mode 100644 index 00000000..e8d5da51 --- /dev/null +++ b/pkg/instrumentation/upgrade/upgrade_suite_test.go @@ -0,0 +1,71 @@ +/* +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 upgrade + +import ( + "fmt" + "os" + "path/filepath" + "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-experimental/newrelic-agent-operator/api/v1alpha1" +) + +var k8sClient client.Client +var testEnv *envtest.Environment +var testScheme = scheme.Scheme +var err error +var cfg *rest.Config + +func TestMain(m *testing.M) { + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "config", "crd", "bases"), + }, + } + + 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 { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + if err != nil { + fmt.Printf("failed to setup a Kubernetes client: %v", err) + os.Exit(1) + } + + code := m.Run() + + err = testEnv.Stop() + if err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + + os.Exit(code) +} diff --git a/pkg/instrumentation/upgrade/upgrade_test.go b/pkg/instrumentation/upgrade/upgrade_test.go new file mode 100644 index 00000000..efbc0ef7 --- /dev/null +++ b/pkg/instrumentation/upgrade/upgrade_test.go @@ -0,0 +1,97 @@ +/* +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 upgrade + +import ( + "context" + "strings" + "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-experimental/newrelic-agent-operator/api/v1alpha1" +) + +func TestUpgrade(t *testing.T) { + nsName := strings.ToLower(t.Name()) + err := k8sClient.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + }, + }) + require.NoError(t, err) + + inst := &v1alpha1.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.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, "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", + DefaultAutoInstGo: "go:2", + Client: k8sClient, + } + err = up.ManagedInstances(context.Background()) + require.NoError(t, err) + + updated := v1alpha1.Instrumentation{} + err = k8sClient.Get(context.Background(), types.NamespacedName{ + Namespace: nsName, + Name: "my-inst", + }, &updated) + require.NoError(t, err) + assert.Equal(t, "java:2", updated.Annotations[v1alpha1.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.Spec.NodeJS.Image) + assert.Equal(t, "python:2", updated.Annotations[v1alpha1.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.Spec.DotNet.Image) + assert.Equal(t, "php:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationPhp]) + assert.Equal(t, "php:2", updated.Spec.Php.Image) + assert.Equal(t, "go:2", updated.Annotations[v1alpha1.AnnotationDefaultAutoInstrumentationGo]) + assert.Equal(t, "go:2", updated.Spec.Go.Image) +}