diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..98e55a7 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,137 @@ +# Copyright 2010 New Relic, Inc. +# +# 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. +--- +name: Python Agent CI + +on: + pull_request: + push: + # Run on push to main or PR. + branches: + - main + # Do not run when a tag is created. + tags-ignore: + - "**" + release: + types: + - published + +env: + INITCONTAINER_LANGUAGE: python + K8S_OPERATOR_IMAGE_TAG: edge + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # 3.3.0 + + - name: Start minikube + uses: medyagh/setup-minikube@317d92317e473a10540357f1f4b2878b80ee7b95 # 0.0.16 + + - name: Deploy cert-manager to minikube + run: | + helm repo add jetstack https://charts.jetstack.io --force-update + helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.14.5 --set installCRDs=true + sleep 5 + kubectl wait --for=condition=Ready -n cert-manager --all pods + + - name: Deploy New Relic k8s-agents-operator to minikube + run: | + helm repo add k8s-agents-operator https://newrelic.github.io/k8s-agents-operator + helm upgrade --install k8s-agents-operator k8s-agents-operator/k8s-agents-operator \ + --namespace=default \ + --set=licenseKey=${{ secrets.NEW_RELIC_LICENSE_KEY }} \ + --set=controllerManager.manager.image.tag=${{ env.K8S_OPERATOR_IMAGE_TAG }} + sleep 5 + kubectl wait --for=condition=Ready -n default --all pods + + - name: Build init container + run: | + minikube image build -t e2e/newrelic-${{ env.INITCONTAINER_LANGUAGE }}-init:e2e ${{ env.INITCONTAINER_LANGUAGE }}/ + + - name: Build test app container + run: | + minikube image build -t e2e/test-app-${{ env.INITCONTAINER_LANGUAGE }}:e2e tests/${{ env.INITCONTAINER_LANGUAGE }}/ + + - name: Run e2e-test + uses: newrelic/newrelic-integration-e2e-action@a97ced80a4841c8c6261d1f9dca6706b1d89acb1 # 1.11.0 + with: + retry_seconds: 60 + retry_attempts: 5 + agent_enabled: false + spec_path: tests/${{ env.INITCONTAINER_LANGUAGE }}/test-specs.yml + account_id: ${{ secrets.NEW_RELIC_ACCOUNT_ID }} + api_key: ${{ secrets.NEW_RELIC_API_KEY }} + license_key: ${{ secrets.NEW_RELIC_LICENSE_KEY }} + + publish: + if: github.event_name == 'release' && endsWith(github.ref, '_python') + runs-on: ubuntu-latest + needs: + - test + + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Extract Agent Version + id: version + run: | + agent_version=${{ github.ref_name }} # Use tag name + agent_version=${agent_version##v} # Remove v prefix + agent_version=${agent_version%%_${{ env.INITCONTAINER_LANGUAGE }}} # Remove language suffix + echo "agent_version=${agent_version}" | tee -a "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # 3.3.0 + + - name: Generate Docker metadata (tags and labels) + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # 5.5.1 + with: + images: newrelic/newrelic-${{ env.INITCONTAINER_LANGUAGE }}-init + tags: | + type=raw,value=${{ steps.version.outputs.agent_version }} + type=raw,value=latest + + - name: Login to Docker Hub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # 3.1.0 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and publish init container image + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # 5.3.0 + with: + push: true + context: ${{ env.INITCONTAINER_LANGUAGE }}/ + platforms: linux/amd64,linux/arm64,linux/arm + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + AGENT_VERSION=${{ steps.version.outputs.agent_version }} diff --git a/python/Dockerfile b/python/Dockerfile index 7f3d6e1..605397b 100644 --- a/python/Dockerfile +++ b/python/Dockerfile @@ -9,17 +9,39 @@ # - Grant the necessary access to `/instrumentation` directory. `chmod -R go+r /instrumentation` FROM python:3.10-alpine AS build + WORKDIR /operator-build -ARG version -ENV NEW_RELIC_EXTENSIONS = False -# WARNING: Disabling optional C extension components of the Python agent -# will result in some non core features of the Python agent, such as -# capacity analysis instance busy metrics, that will not be available. -# Pure Python versions of code supporting some features, rather than the -# optimised C versions, will also be used resulting in additional overheads. -ADD requirements.txt . -RUN mkdir workspace && pip install --target workspace newrelic==$version +# Install dependencies +COPY requirements-builder.txt . +RUN pip install -r requirements-builder.txt + +# Download and prepare wheels +ARG AGENT_VERSION +ENV AGENT_VERSION=${AGENT_VERSION} +COPY download_wheels.py . +RUN python ./download_wheels.py + +# Install sdist without extensions to set up the directory +ENV NEW_RELIC_EXTENSIONS False +ENV WRAPT_DISABLE_EXTENSIONS True +RUN pip install ./workspace/newrelic.tar.gz --target=./workspace/newrelic && \ + rm ./workspace/newrelic.tar.gz + +# Install pip as a vendored package +COPY requirements-vendor.txt . +RUN mkdir -p ./workspace/vendor && \ + pip install --target=./workspace/vendor -r requirements-vendor.txt + +# Install sitecustomize and newrelic_k8s_operator modules +RUN cp ./workspace/newrelic/newrelic/bootstrap/sitecustomize.py ./workspace/sitecustomize.py +COPY newrelic_k8s_operator.py ./workspace/ + +# *** TODO: Remove this after sitecustomize changes have been released with the latest copy of the agent *** +COPY sitecustomize.py ./workspace/sitecustomize.py + +# initcontainer FROM busybox + COPY --from=build /operator-build/workspace /instrumentation RUN chmod -R go+r /instrumentation diff --git a/python/download_wheels.py b/python/download_wheels.py new file mode 100644 index 0000000..183c06c --- /dev/null +++ b/python/download_wheels.py @@ -0,0 +1,107 @@ +# Copyright 2010 New Relic, Inc. +# +# 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. + +import requests +import os +import shutil +import subprocess +import tempfile + + +AGENT_VERSION = os.getenv("AGENT_VERSION", "").lstrip("v") +FILE_DIR = os.path.dirname(__file__) +WORKSPACE_DIR = os.path.join(FILE_DIR, "workspace") + + +def main(): + """ + Download and unpack wheels and sdist for a given agent version as part of the build process for the Python agent init container. + """ + with requests.session() as session: + # Fetch JSON list of all agent package artifacts + resp = session.get("https://pypi.org/pypi/newrelic/json") + resp.raise_for_status() + resp_dict = resp.json() + + if AGENT_VERSION: + # Grab the supplied release version + release = resp_dict["releases"][AGENT_VERSION] + else: + # Grab latest release version + release = list(resp_dict["releases"].values())[-1] + + # Filter artifacts for wheels and tarballs + wheel_urls = [ + artifact["url"] + for artifact in release + if artifact["url"].endswith(".whl") + ] + + tarball_url = [ + artifact["url"] + for artifact in release + if artifact["url"].endswith(".tar.gz") + ][0] + + # Make workspace directory + if not os.path.exists(WORKSPACE_DIR): + os.mkdir(WORKSPACE_DIR) + + # Download tarball + resp = session.get(tarball_url) + resp.raise_for_status() + + tarball_filepath = os.path.join(WORKSPACE_DIR, "newrelic.tar.gz") + with open(tarball_filepath, "wb") as file: + file.write(resp.content) + + # Download and extract all wheels + for wheel_url in wheel_urls: + # Download wheel + resp = session.get(wheel_url) + resp.raise_for_status() + + # Write wheel to file under a folder of the same name + with tempfile.TemporaryDirectory() as wheel_temp_dir: + # Compute paths + wheel_filename = wheel_url.split("/")[-1] + wheel_filepath = os.path.join(wheel_temp_dir, wheel_filename) + + # Write wheel file to tempdir + with open(wheel_filepath, "wb") as file: + file.write(resp.content) + + # Unpack wheel using wheel command + subprocess.run( + ["wheel", "unpack", str(wheel_filename)], + cwd=wheel_temp_dir, + ) + + # Delete wheel file after unpacking + os.remove(wheel_filepath) + + # Move unpacked module from tempdir to final destination + unpacked_module_folder = os.path.join( + wheel_temp_dir, list(os.listdir(wheel_temp_dir))[0] + ) + + # Put wheel contents into folders named after the original wheel types + final_wheel_dest = os.path.join( + WORKSPACE_DIR, wheel_filename.rstrip(".whl") + ) + shutil.move(str(unpacked_module_folder), str(final_wheel_dest)) + + +if __name__ == "__main__": + main() diff --git a/python/newrelic_k8s_operator.py b/python/newrelic_k8s_operator.py new file mode 100644 index 0000000..f7df6b9 --- /dev/null +++ b/python/newrelic_k8s_operator.py @@ -0,0 +1,77 @@ +# Copyright 2010 New Relic, Inc. +# +# 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. + +import os +import sys + +INSTRUMENTATION_PATH = os.path.dirname(__file__) +INSTRUMENTATION_VENDOR_PATH = os.path.join(INSTRUMENTATION_PATH, "vendor") +SDIST_PATH = str(os.path.join(INSTRUMENTATION_PATH, "newrelic")) + + +def get_supported_tags(): + """ + Load pip and run compatibility_tags.get_supported(). If pip is not present + or does not contain this function, load our vendored version of pip instead. + """ + try: + # Attempt to use any existing installs of pip to find supported tags + from pip._internal.utils.compatibility_tags import get_supported + + return get_supported() + except Exception: + pass + + # Unable to find pip, or pip version does not have compatibility tags + if "pip" in sys.modules: + # Save original pip module and load our own copy + original_pip = sys.modules["pip"] + del sys.modules["pip"] + else: + original_pip = None + + try: + # Insert our vendored version of pip into the path, and try to return + # the supported tags again. + sys.path.insert(0, INSTRUMENTATION_VENDOR_PATH) + from pip._internal.utils.compatibility_tags import get_supported + + return get_supported() + finally: + # Remove our vendored version of pip from the path + if INSTRUMENTATION_VENDOR_PATH in sys.path: + del sys.path[sys.path.index(INSTRUMENTATION_VENDOR_PATH)] + + # Replace original pip module for compatibility + if original_pip: + sys.modules["pip"] = original_pip + elif "pip" in sys.modules: + del sys.modules["pip"] + + +def find_supported_newrelic_distribution(): + """ + Return the path to a supported wheel or sdist included under INSTRUMENTATION_PATH by the Python agent init container. + """ + try: + wheels = list(os.listdir(INSTRUMENTATION_PATH)) + for tag in get_supported_tags(): + tag = str(tag) + for wheel in wheels: + if tag in wheel: + return str(os.path.join(INSTRUMENTATION_PATH, wheel)) + except Exception: + pass + + return SDIST_PATH diff --git a/python/requirements-builder.txt b/python/requirements-builder.txt new file mode 100644 index 0000000..789119d --- /dev/null +++ b/python/requirements-builder.txt @@ -0,0 +1,2 @@ +requests +setuptools>=40.8.0 \ No newline at end of file diff --git a/python/requirements-vendor.txt b/python/requirements-vendor.txt new file mode 100644 index 0000000..4d6aa0d --- /dev/null +++ b/python/requirements-vendor.txt @@ -0,0 +1 @@ +pip \ No newline at end of file diff --git a/python/sitecustomize.py b/python/sitecustomize.py new file mode 100644 index 0000000..cc3a3ae --- /dev/null +++ b/python/sitecustomize.py @@ -0,0 +1,218 @@ +# Copyright 2010 New Relic, Inc. +# +# 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. + +import os +import sys +import time + + +# Avoiding additional imports by defining PY2 manually +PY2 = sys.version_info[0] == 2 + +# Define some debug logging routines to help sort out things when this +# all doesn't work as expected. + +startup_debug = os.environ.get("NEW_RELIC_STARTUP_DEBUG", "off").lower() in ("on", "true", "1") + +def log_message(text, *args, **kwargs): + critical = kwargs.get("critical", False) + if startup_debug or critical: + text = text % args + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + sys.stdout.write("NEWRELIC: %s (%d) - %s\n" % (timestamp, os.getpid(), text)) + sys.stdout.flush() + + +def del_sys_path_entry(path): + if path and path in sys.path: + try: + del sys.path[sys.path.index(path)] + except Exception: + pass + + +log_message("New Relic Bootstrap (%s)", __file__) + +log_message("working_directory = %r", os.getcwd()) + +log_message("sys.prefix = %r", os.path.normpath(sys.prefix)) + +try: + log_message("sys.real_prefix = %r", sys.real_prefix) +except AttributeError: + pass + +log_message("sys.version_info = %r", sys.version_info) +log_message("sys.executable = %r", sys.executable) + +if hasattr(sys, "flags"): + log_message("sys.flags = %r", sys.flags) + +log_message("sys.path = %r", sys.path) + +for name in sorted(os.environ.keys()): + if name.startswith("NEW_RELIC_") or name.startswith("PYTHON"): + if name == "NEW_RELIC_LICENSE_KEY": + continue + log_message("%s = %r", name, os.environ.get(name)) + +# We need to import the original sitecustomize.py file if it exists. We +# can't just try and import the existing one as we will pick up +# ourselves again. Even if we remove ourselves from sys.modules and +# remove the bootstrap directory from sys.path, still not sure that the +# import system will not have cached something and return a reference to +# ourselves rather than searching again. What we therefore do is use the +# imp module to find the module, excluding the bootstrap directory from +# the search, and then load what was found. + +boot_directory = os.path.dirname(__file__) +log_message("boot_directory = %r", boot_directory) + +del_sys_path_entry(boot_directory) + +try: + if PY2: + import imp + + module_spec = imp.find_module("sitecustomize", sys.path) + else: + from importlib.machinery import PathFinder + + module_spec = PathFinder.find_spec("sitecustomize", path=sys.path) + +except ImportError: + pass +else: + if module_spec is not None: # Import error not raised in importlib + log_message("sitecustomize = %r", module_spec) + + if PY2: + imp.load_module("sitecustomize", *module_spec) + else: + module_spec.loader.load_module("sitecustomize") + +# Because the PYTHONPATH environment variable has been amended and the +# bootstrap directory added, if a Python application creates a sub +# process which runs a different Python interpreter, then it will still +# load this sitecustomize.py. If that is for a different Python version +# it will cause problems if we then try and import and initialize the +# agent. We therefore need to try our best to verify that we are running +# in the same Python installation as the original newrelic-admin script +# which was run and only continue if we are. + +expected_python_prefix = os.environ.get("NEW_RELIC_PYTHON_PREFIX") +actual_python_prefix = os.path.realpath(os.path.normpath(sys.prefix)) + +expected_python_version = os.environ.get("NEW_RELIC_PYTHON_VERSION") +actual_python_version = ".".join(map(str, sys.version_info[:2])) + +python_prefix_matches = expected_python_prefix == actual_python_prefix +python_version_matches = expected_python_version == actual_python_version +k8s_operator_enabled = os.environ.get("NEW_RELIC_K8S_OPERATOR_ENABLED", "off").lower() in ("on", "true", "1") + +log_message("python_prefix_matches = %r", python_prefix_matches) +log_message("python_version_matches = %r", python_version_matches) +log_message("k8s_operator_enabled = %r", k8s_operator_enabled) + +if k8s_operator_enabled or (python_prefix_matches and python_version_matches): + # We also need to skip agent initialisation if neither the license + # key or config file environment variables are set. We do this as + # some people like to use a common startup script which always uses + # the wrapper script, and which controls whether the agent is + # actually run based on the presence of the environment variables. + + license_key = os.environ.get("NEW_RELIC_LICENSE_KEY", None) + developer_mode = os.environ.get("NEW_RELIC_DEVELOPER_MODE", "off").lower() in ("on", "true", "1") + config_file = os.environ.get("NEW_RELIC_CONFIG_FILE", None) + environment = os.environ.get("NEW_RELIC_ENVIRONMENT", None) + initialize_agent = bool(license_key or config_file or developer_mode) + + log_message("initialize_agent = %r", initialize_agent) + + if initialize_agent: + + if not k8s_operator_enabled: + # When installed as an egg with buildout, the root directory for + # packages is not listed in sys.path and scripts instead set it + # after Python has started up. This will cause importing of + # 'newrelic' module to fail. What we do is see if the root + # directory where the package is held is in sys.path and if not + # insert it. For good measure we remove it after having imported + # 'newrelic' module to reduce chance that will cause any issues. + # If it is a buildout created script, it will replace the whole + # sys.path again later anyway. + root_directory = os.path.dirname(os.path.dirname(boot_directory)) + log_message("root_directory = %r", root_directory) + + new_relic_path = root_directory + do_insert_path = root_directory not in sys.path + else: + # When installed with the kubernetes operator, we need to attempt + # to find a distribution from our initcontainer that matches the + # current environment. For wheels, this is platform dependent and we + # rely on pip to identify the correct wheel to use. If no suitable + # wheel can be found, we will fall back to the sdist and disable + # extensions. Once the appropriate distribution is found, we import + # it and leave the entry in sys.path. This allows users to import + # the 'newrelic' module later and use our APIs in their code. + try: + sys.path.insert(0, boot_directory) + from newrelic_k8s_operator import find_supported_newrelic_distribution + finally: + del_sys_path_entry(boot_directory) + + new_relic_path = find_supported_newrelic_distribution() + do_insert_path = True + + # Now that the appropriate location of the module has been identified, + # either by the kubernetes operator or this script, we are ready to import + # the 'newrelic' module to make it available in sys.modules. If the location + # containing it was not found on sys.path, do_insert_path will be set and + # the location will be inserted into sys.path. The module is then imported, + # and the sys.path entry is removed afterwards to reduce chance that will + # cause any issues. + + log_message("new_relic_path = %r" % new_relic_path) + log_message("do_insert_path = %r" % do_insert_path) + + try: + if do_insert_path: + sys.path.insert(0, new_relic_path) + + import newrelic + + log_message("agent_version = %r", newrelic.version) + finally: + if do_insert_path: + del_sys_path_entry(new_relic_path) + + # Finally initialize the agent. + import newrelic.config + newrelic.config.initialize(config_file, environment) + else: + log_message("New Relic could not start due to missing configuration. Either NEW_RELIC_LICENSE_KEY or NEW_RELIC_CONFIG_FILE are required.") +else: + log_message( + """New Relic could not start because the newrelic-admin script was called from a Python installation that is different from the Python installation that is currently running. To fix this problem, call the newrelic-admin script from the Python installation that is currently running (details below). + +newrelic-admin Python directory: %r +current Python directory: %r +newrelic-admin Python version: %r +current Python version: %r""", + expected_python_prefix, + actual_python_prefix, + expected_python_version, + actual_python_version, + critical=True, + ) diff --git a/tests/python/Dockerfile b/tests/python/Dockerfile new file mode 100644 index 0000000..ad1c99a --- /dev/null +++ b/tests/python/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12.3-bullseye + +RUN pip install flask==3.0.3 gunicorn==22.0.0 + +COPY main.py . + +CMD ["gunicorn", "main:app", "-b", "0.0.0.0:8000", "--workers=1"] \ No newline at end of file diff --git a/tests/python/chart/.helmignore b/tests/python/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/tests/python/chart/.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/tests/python/chart/Chart.yaml b/tests/python/chart/Chart.yaml new file mode 100644 index 0000000..fd79f52 --- /dev/null +++ b/tests/python/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: test-app-python +description: A Helm chart for Kubernetes +type: application +version: 1.0.0 +appVersion: "1.0.0" diff --git a/tests/python/chart/templates/deployment.yaml b/tests/python/chart/templates/deployment.yaml new file mode 100644 index 0000000..250234a --- /dev/null +++ b/tests/python/chart/templates/deployment.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app-python +spec: + selector: + matchLabels: + app: test-app-python + replicas: 1 + template: + metadata: + labels: + app: test-app-python + annotations: + instrumentation.newrelic.com/inject-python: "true" + spec: + containers: + - name: test-app-python + image: e2e/test-app-python:e2e + imagePullPolicy: Never + ports: + - containerPort: 8000 + env: + - name: NEW_RELIC_APP_NAME + value: k8s-e2e-test-app-python + - name: NEW_RELIC_LABELS + value: "testKey:{{ .Values.scenarioTag | default "NOTSET" }}" +--- +apiVersion: v1 +kind: Service +metadata: + name: test-app-python-service +spec: + type: NodePort + ports: + - port: 8000 + targetPort: 8000 + selector: + app: test-app-python diff --git a/tests/python/chart/templates/instrumentation.yaml b/tests/python/chart/templates/instrumentation.yaml new file mode 100644 index 0000000..86d160e --- /dev/null +++ b/tests/python/chart/templates/instrumentation.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: newrelic.com/v1alpha1 +kind: Instrumentation +metadata: + labels: + app.kubernetes.io/name: instrumentation + app.kubernetes.io/created-by: newrelic-agent-operator + name: newrelic-instrumentation +spec: + python: + image: e2e/newrelic-python-init:e2e + # Configure agent for testing using environment variables + env: + - name: NEW_RELIC_STARTUP_TIMEOUT + value: "30" + - name: NEW_RELIC_STARTUP_DEBUG + value: "true" diff --git a/tests/python/chart/values.yaml b/tests/python/chart/values.yaml new file mode 100644 index 0000000..44eb261 --- /dev/null +++ b/tests/python/chart/values.yaml @@ -0,0 +1 @@ +scenarioTag: "" \ No newline at end of file diff --git a/tests/python/main.py b/tests/python/main.py new file mode 100644 index 0000000..b1def4a --- /dev/null +++ b/tests/python/main.py @@ -0,0 +1,17 @@ +import sys +import traceback + +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello_world(): + try: + from newrelic.agent import current_transaction + assert current_transaction(), "No active transaction." + except Exception: + return "".join(traceback.format_exception(*sys.exc_info())), 417 + + return "

Hello, World!

" diff --git a/tests/python/test-specs.yml b/tests/python/test-specs.yml new file mode 100644 index 0000000..9269ccd --- /dev/null +++ b/tests/python/test-specs.yml @@ -0,0 +1,15 @@ +description: End-to-end tests for python initcontainer +custom_test_key: tags.testKey +scenarios: + - description: This scenario will verify that a transaction is reported by the test app after a curl request + before: + - helm install test-python ./chart/ --set=scenarioTag="${SCENARIO_TAG}" -n default + - sleep 5 + - kubectl wait --for=condition=Ready -n default --all pods + - curl --fail-with-body $(minikube service test-app-python-service --url -n default) + tests: + nrqls: + - query: SELECT latest(duration) AS duration FROM Transaction WHERE appName = 'k8s-e2e-test-app-python' + expected_results: + - key: "duration" + lowerBoundedValue: 0.0