diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6b9c6f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI Pipeline + +on: + push: + branches: + - main + paths: + - 'new-relic-deployment-marker/**' + pull_request: + branches: + - '*' + paths: + - 'new-relic-deployment-marker/**' + +jobs: + test: + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'pull_request' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + cd new-relic-deployment-marker + python -m pip install --upgrade pip + pip install --no-cache-dir -r test/requirements.txt + + - name: Run tests + run: | + cd new-relic-deployment-marker + python -m pytest -p no:cacheprovider test/test_unit.py --verbose --capture=no + + build-and-push-docker: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: new-relic-deployment-marker + file: new-relic-deployment-marker/Dockerfile + push: true + tags: sykescottages/bitbucket-pipes:new-relic-deployment-marker diff --git a/new-relic-deployment-marker/.gitignore b/new-relic-deployment-marker/.gitignore index 1ace890..7539b3e 100644 --- a/new-relic-deployment-marker/.gitignore +++ b/new-relic-deployment-marker/.gitignore @@ -3,6 +3,169 @@ config .env .DS_Store aws +.env .terraform _provider.tf +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/new-relic-deployment-marker/CHANGELOG.md b/new-relic-deployment-marker/CHANGELOG.md new file mode 100644 index 0000000..4716008 --- /dev/null +++ b/new-relic-deployment-marker/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [0.0.1] - 2024-07-15 +### Added +- Adding capability of finding application IDs in new relic and marking those apms with deployment marker. It uses Bitbucket commit hash as default for revisions. +- Backwards compatibility with previews bach pipe. + diff --git a/new-relic-deployment-marker/Dockerfile b/new-relic-deployment-marker/Dockerfile index 82874ff..8ecc043 100644 --- a/new-relic-deployment-marker/Dockerfile +++ b/new-relic-deployment-marker/Dockerfile @@ -1,5 +1,10 @@ -FROM newrelic/cli:latest +FROM python:3.11-slim -COPY pipe.sh / +COPY requirements.txt / +WORKDIR / -ENTRYPOINT [ "/pipe.sh" ] +RUN pip install --no-cache-dir -r requirements.txt +COPY pipe / +COPY pipe.yml . + +CMD ["python3", "/pipe.py"] \ No newline at end of file diff --git a/new-relic-deployment-marker/README.md b/new-relic-deployment-marker/README.md index 38775ab..0693796 100644 --- a/new-relic-deployment-marker/README.md +++ b/new-relic-deployment-marker/README.md @@ -1,26 +1,30 @@ # New Relic Deployment Marker Mark new deployments in New Relic with this Bitbucket Pipe -## YAML Definition - -Add the following snippet to the script section of your `bitbucket-pipelines.yml` file: - -```yaml -- pipe: docker://sykescottages/bitbucket-pipes:new-relic-deployment-marker - NEW_RELIC_API_KEY: '' - NEW_RELIC_APPLICATION_ID: '' - NEW_RELIC_REGION: '' - DEPLOYMENT_USER: '' - DEPLOYMENT_REVISION: '' -``` +## Important +If you do not follow the new relic naming convention the deployment marker might not work. Make sure you're following the naming convention when generating an APM. -## Variables +## YAML Definition +### Variables | Variable | Usage | | --------------------- | ----------------------------------------------------------- | -| NEW_RELIC_API_KEY (*) | New Relic API Key | -| NEW_RELIC_APPLICATION_ID (*) | The ID of the application you want to tag. | -| DEPLOYMENT_REVISION (*) | The revision or the deployment ID to mark in NR| -| NEW_RELIC_REGION | Region the data is in. Defaults to US | +| NEW_RELIC_API_KEY (*) | New Relic API Key | +| APPLICATION_NAME (*) | The name of the application in New Relic to find the ID | +| ENVIRONMENT(*) | Environment name where the deployment is taking place (e.g., production, staging). | +| COMPONENT_TYPE (*) | The component type of the application in New Relic. Valid options are Web, Cmd, and Cron.| +| DEPLOYMENT_REVISION (*)| The revision or the deployment ID to mark in NR| | DEPLOYMENT_USER | User responsible for this deployment, defaults to bitbucket.pipeline | (*) = required variable. This variable needs to be specified always when using the pipe. + +Add the following snippet to the script section of your `bitbucket-pipelines.yml` file: + +```yaml +- pipe: docker://sykescottages/bitbucket-pipes:new-relic-deployment-marker + NEW_RELIC_API_KEY: '' # Required. New Relic API Key. + APPLICATION_NAME: '' # Required. The name of the application to find the ID for. + COMPONENT_TYPE: '' # Required. The component type of the application to find the ID for. Options are Web, Cmd, and Cron. + ENVIRONMENT: '' # Optional. Environment name. + REGION: '' # Optional. Region name. + DEPLOYMENT_REVISION: '' # Optional. Bitbucket commit hash for the deployment. +``` \ No newline at end of file diff --git a/new-relic-deployment-marker/docker-compose.yml b/new-relic-deployment-marker/docker-compose.yml new file mode 100644 index 0000000..19183bc --- /dev/null +++ b/new-relic-deployment-marker/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.9' + +services: + pipe: + build: . + environment: + - PYTHONUNBUFFERED=1 + env_file: + - .env + volumes: + - ./pipe/pipe.py:/pipe.py + test: + build: + context: . + dockerfile: test/Dockerfile + volumes: + - .:/app + working_dir: /app + command: python -m pytest -p no:cacheprovider test/test_unit.py --verbose --capture=no \ No newline at end of file diff --git a/new-relic-deployment-marker/env.example b/new-relic-deployment-marker/env.example new file mode 100644 index 0000000..ad5c9b6 --- /dev/null +++ b/new-relic-deployment-marker/env.example @@ -0,0 +1,14 @@ +V2 variables +APPLICATION_NAME="AppName" +COMPONENT_TYPE="Web" +ENVIRONMENT="Staging" +SHORT_REGION="Ew1" +DEPLOYMENT_REVISION="build-1234" +DEPLOYMENT_USER="pipe.user" -> Optional +NEW_RELIC_API_KEY="NRAK-***" + +V1 variables +DEPLOYMENT_REVISION="build-123" +DEPLOYMENT_USER="pipe.user" -> Optional +NEW_RELIC_API_KEY="NRAK-***" +NEW_RELIC_APPLICATION_ID="1234" \ No newline at end of file diff --git a/new-relic-deployment-marker/pipe.sh b/new-relic-deployment-marker/pipe.sh deleted file mode 100755 index 529e493..0000000 --- a/new-relic-deployment-marker/pipe.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -set -e -set -o pipefail - -## Required Parameters -NEW_RELIC_API_KEY=${NEW_RELIC_API_KEY} -NEW_RELIC_APPLICATION_ID=${NEW_RELIC_APPLICATION_ID} -DEPLOYMENT_REVISION=${DEPLOYMENT_REVISION} - -## Optional Parameters -DEPLOYMENT_USER=${DEPLOYMENT_USER:='bitbucket.pipeline'} -NEW_RELIC_REGION=${NEW_RELIC_REGION:='US'} - -if [ -z "$NEW_RELIC_APPLICATION_ID" ]; then - echo "Missing Application ID" - exit 1; -fi - -OLD_IFS=$IFS -IFS=, -for APP in $NEW_RELIC_APPLICATION_ID; do - newrelic apm deployment create \ - --applicationId "${APP}" \ - --user "${DEPLOYMENT_USER}" \ - --revision "${DEPLOYMENT_REVISION}" -done -IFS=$OLD_IFS - diff --git a/new-relic-deployment-marker/pipe.yml b/new-relic-deployment-marker/pipe.yml new file mode 100644 index 0000000..0494940 --- /dev/null +++ b/new-relic-deployment-marker/pipe.yml @@ -0,0 +1,19 @@ +name: "New Relic Deployment Marker" +description: "This Bitbucket Pipe helps you add deployment marker to New Relic APM. It finds the application ID in New Relic based on the application name and component type." +category: Utilities +variables: + - name: NEW_RELIC_API_KEY + default: "$NEW_RELIC_API_KEY" + description: "New Relic API Key" + - name: DEPLOYMENT_USER + default: "bitbucket.pipeline" + - name: DEPLOYMENT_REVISION + default: "$BITBUCKET_COMMIT" +repository: https://github.com/SykesCottages/bitbucket-pipes +vendor: + name: NewRelic +maintainer: + name: Sykes Cottages +tags: + - deployment + - NewRelic diff --git a/new-relic-deployment-marker/pipe/__init__.py b/new-relic-deployment-marker/pipe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-relic-deployment-marker/pipe/pipe.py b/new-relic-deployment-marker/pipe/pipe.py new file mode 100644 index 0000000..930b45a --- /dev/null +++ b/new-relic-deployment-marker/pipe/pipe.py @@ -0,0 +1,153 @@ +import os +import requests +from typing import Dict, List, Any, Type +from abc import ABC, abstractmethod +from cerberus import Validator +from bitbucket_pipes_toolkit import Pipe, get_logger +import yaml + +logger = get_logger() + +v2_schema = { + 'NEW_RELIC_API_KEY': {'type': 'string', 'required': True}, + 'APPLICATION_NAME': {'type': 'string', 'required': True}, + 'COMPONENT_TYPE': {'type': 'string', 'required': True}, + 'ENVIRONMENT': {'type': 'string', 'required': True}, + 'SHORT_REGION': {'type': 'string', 'required': True}, + 'DEPLOYMENT_USER': {'type': 'string', 'required': False, 'default': 'bitbucket.pipeline'}, + 'DEPLOYMENT_REVISION': {'type': 'string', 'required': True}, +} + +v1_schema = { + 'NEW_RELIC_API_KEY': {'type': 'string', 'required': True}, + 'NEW_RELIC_APPLICATION_ID': {'type': 'string', 'required': True}, + 'DEPLOYMENT_REVISION': {'type': 'string', 'required': True}, + 'DEPLOYMENT_USER': {'type': 'string', 'required': False, 'default': 'bitbucket.pipeline'}, +} + +class Config: + def __init__(self, schema: Dict[str, Any]) -> None: + self.validator = Validator(schema) + self.config = self.load_config() + + def load_config(self) -> Dict[str, str]: + config = {key: os.getenv(key, default=value.get('default')) for key, value in self.validator.schema.items()} + if not self.validator.validate(config): + raise ValueError(f"Configuration validation error: {self.validator.errors}") + return self.validator.document + + def get(self, key: str) -> str: + return self.config[key] + + def get_app_name_pattern(self) -> str: + return f'%{self.get("APPLICATION_NAME")}%{self.get("ENVIRONMENT")}%{self.get("SHORT_REGION")}%{self.get("COMPONENT_TYPE")}' + +class NewRelicClient: + def __init__(self, api_key: str) -> None: + self.api_key = api_key + self.base_url = 'https://api.newrelic.com/v2/' + + def search_applications(self, app_name_pattern: str) -> List[Dict[str, Any]]: + headers = { + 'X-Api-Key': self.api_key, + 'Content-Type': 'application/json' + } + url = f'{self.base_url}applications.json' + params = { + 'filter[name]': app_name_pattern + } + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json()['applications'] + + def create_deployment_marker(self, app_id: str, user: str, revision: str, description: str) -> None: + url = f'{self.base_url}applications/{app_id}/deployments.json' + headers = { + 'X-Api-Key': self.api_key, + 'Content-Type': 'application/json' + } + payload = { + 'deployment': { + 'revision': revision, + 'changelog': description, + 'user': user + } + } + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + +class DeploymentRunner(ABC): + def __init__(self, client: NewRelicClient, config: Config) -> None: + self.client = client + self.config = config + + @abstractmethod + def run(self) -> None: + pass + +class V1Deployment(DeploymentRunner): + def run(self) -> None: + logger.info("Starting New Relic Deployment with v1 schema") + + app_ids = self.config.get('NEW_RELIC_APPLICATION_ID').split(',') + user = self.config.get('DEPLOYMENT_USER') + revision = self.config.get('DEPLOYMENT_REVISION') + description = "Deployed new version with v1 schema" + + for app_id in app_ids: + try: + self.client.create_deployment_marker(app_id, user, revision, description) + logger.info(f"Deployment marker created for Application ID {app_id} with v1 schema") + except requests.RequestException as e: + logger.error(f"Error creating deployment marker for Application ID {app_id}: {str(e)}") + raise + +class V2Deployment(DeploymentRunner): + def run(self) -> None: + logger.info("Starting New Relic Deployment with v2 schema") + app_name_pattern = self.config.get_app_name_pattern() + logger.info(f"Searching applications with pattern: {app_name_pattern}") + + try: + applications = self.client.search_applications(app_name_pattern) + except requests.RequestException as e: + logger.error(f"Error searching applications: {str(e)}") + raise + + for app in applications: + app_id = app['id'] + app_name = app['name'] + logger.info(f"Application ID: {app_id}, Name: {app_name}") + + try: + deployment_description = "Deployed new version" + self.client.create_deployment_marker( + app_id, self.config.get('DEPLOYMENT_USER'), self.config.get('DEPLOYMENT_REVISION'), deployment_description) + logger.info(f"Deployment marker created for Application ID {app_id}") + except requests.RequestException as e: + logger.error(f"Error creating deployment marker for Application ID {app_id}: {str(e)}") + raise + +class NewRelicDeploymentPipe(Pipe): + def __init__(self, schema: Dict[str, Any], pipe_metadata: Dict[str, Any], deployment_cls: Type[DeploymentRunner]) -> None: + super().__init__(schema=schema, pipe_metadata=pipe_metadata) + self.config = Config(schema) + self.client = NewRelicClient(self.config.get('NEW_RELIC_API_KEY')) + self.deployment = deployment_cls(self.client, self.config) + + def run(self) -> None: + self.deployment.run() + +if __name__ == '__main__': + with open('/pipe.yml', 'r') as metadata_file: + metadata = yaml.safe_load(metadata_file.read()) + + if 'NEW_RELIC_APPLICATION_ID' in os.environ: + schema = v1_schema + deployment_cls = V1Deployment + else: + schema = v2_schema + deployment_cls = V2Deployment + + pipe = NewRelicDeploymentPipe(schema=schema, pipe_metadata=metadata, deployment_cls=deployment_cls) + pipe.run() \ No newline at end of file diff --git a/new-relic-deployment-marker/requirements.txt b/new-relic-deployment-marker/requirements.txt new file mode 100644 index 0000000..677c522 --- /dev/null +++ b/new-relic-deployment-marker/requirements.txt @@ -0,0 +1,4 @@ +cerberus +requests +bitbucket-pipes-toolkit==4.* +PyYAML \ No newline at end of file diff --git a/new-relic-deployment-marker/start.sh b/new-relic-deployment-marker/start.sh new file mode 100755 index 0000000..f7b3eb0 --- /dev/null +++ b/new-relic-deployment-marker/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker compose run --rm pipe \ No newline at end of file diff --git a/new-relic-deployment-marker/test.sh b/new-relic-deployment-marker/test.sh new file mode 100755 index 0000000..7320d8b --- /dev/null +++ b/new-relic-deployment-marker/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker compose run --rm test \ No newline at end of file diff --git a/new-relic-deployment-marker/test/Dockerfile b/new-relic-deployment-marker/test/Dockerfile new file mode 100644 index 0000000..16bab06 --- /dev/null +++ b/new-relic-deployment-marker/test/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . . + +RUN pip install --no-cache-dir -r test/requirements.txt diff --git a/new-relic-deployment-marker/test/__init__.py b/new-relic-deployment-marker/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/new-relic-deployment-marker/test/requirements.txt b/new-relic-deployment-marker/test/requirements.txt new file mode 100644 index 0000000..8dda129 --- /dev/null +++ b/new-relic-deployment-marker/test/requirements.txt @@ -0,0 +1,7 @@ +pytest==7.* +pytest-mock +cerberus +requests +bitbucket-pipes-toolkit==4.* +PyYAML +requests_mock \ No newline at end of file diff --git a/new-relic-deployment-marker/test/test_unit.py b/new-relic-deployment-marker/test/test_unit.py new file mode 100644 index 0000000..152243f --- /dev/null +++ b/new-relic-deployment-marker/test/test_unit.py @@ -0,0 +1,165 @@ +import unittest +from unittest.mock import patch, Mock +import requests_mock +import os +from pipe.pipe import Config, NewRelicClient, V1Deployment, V2Deployment, NewRelicDeploymentPipe, v1_schema, v2_schema + +class TestConfig(unittest.TestCase): + @patch.dict(os.environ, { + 'NEW_RELIC_API_KEY': '12345', + 'APPLICATION_NAME': 'MyApp', + 'COMPONENT_TYPE': 'backend', + 'ENVIRONMENT': 'production', + 'SHORT_REGION': 'us-west-2', + 'DEPLOYMENT_REVISION': 'rev123' + }) + def test_load_config(self): + """Test that environment variables are loaded correctly.""" + schema = { + 'NEW_RELIC_API_KEY': {'type': 'string', 'required': True}, + 'APPLICATION_NAME': {'type': 'string', 'required': True}, + 'COMPONENT_TYPE': {'type': 'string', 'required': True}, + 'ENVIRONMENT': {'type': 'string', 'required': True}, + 'SHORT_REGION': {'type': 'string', 'required': True}, + 'DEPLOYMENT_REVISION': {'type': 'string', 'required': True} + } + config = Config(schema) + self.assertEqual(config.get('NEW_RELIC_API_KEY'), '12345') + +class TestNewRelicClient(unittest.TestCase): + def setUp(self): + self.api_key = '12345' + self.client = NewRelicClient(self.api_key) + + @patch('requests.get') + def test_search_applications(self, mock_get): + """Test that search_applications sends a request to the correct URL with correct headers.""" + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {'applications': []} + + self.client.search_applications('MyApp%production%us-west-2%backend') + mock_get.assert_called_once_with( + 'https://api.newrelic.com/v2/applications.json', + headers={'X-Api-Key': '12345', 'Content-Type': 'application/json'}, + params={'filter[name]': 'MyApp%production%us-west-2%backend'} + ) + +class TestV1Deployment(unittest.TestCase): + @patch('pipe.pipe.NewRelicClient') + def test_run_v1_deployment(self, mock_client_class): + client = mock_client_class.return_value + client.create_deployment_marker.return_value = None # Assume this function returns None + + config = Mock(spec=Config) + config.get.return_value = '12345' + + runner = V1Deployment(client, config) + runner.run() + + client.create_deployment_marker.assert_called_once() + +class TestV2Deployment(unittest.TestCase): + @patch('pipe.pipe.NewRelicClient') + def test_run_v2_deployment(self, mock_client_class): + client = mock_client_class.return_value + client.search_applications.return_value = [{'id': 'app_id_1', 'name': 'MyApp'}] + client.create_deployment_marker.return_value = None + + config = Mock(spec=Config) + config.get_app_name_pattern.return_value = 'MyApp%production%us-west-2%backend' + config.get.side_effect = lambda key: {'DEPLOYMENT_USER': 'bitbucket.pipeline', 'DEPLOYMENT_REVISION': 'rev123'}[key] + + runner = V2Deployment(client, config) + runner.run() + + client.search_applications.assert_called_once_with('MyApp%production%us-west-2%backend') + client.create_deployment_marker.assert_called_once_with('app_id_1', 'bitbucket.pipeline', 'rev123', 'Deployed new version') + +class TestNewRelicDeploymentPipe(unittest.TestCase): + @patch('pipe.pipe.NewRelicDeploymentPipe.run') + @patch.dict(os.environ, { + 'NEW_RELIC_API_KEY': '12345', + 'NEW_RELIC_APPLICATION_ID': 'app123', + 'DEPLOYMENT_REVISION': 'rev123' + }) + def test_pipe_initialization_v1(self, mock_run): + """Test that the pipe initializes the correct deployment class based on environment variables.""" + mock_run.return_value = None + with patch('pipe.pipe.V1Deployment', spec=V1Deployment) as mock_v1_deployment_class: + pipe = NewRelicDeploymentPipe(v1_schema, {}, mock_v1_deployment_class) + self.assertIsInstance(pipe.deployment, V1Deployment) + + @patch('pipe.pipe.NewRelicDeploymentPipe.run') + @patch.dict(os.environ, { + 'NEW_RELIC_API_KEY': '12345', + 'APPLICATION_NAME': 'MyApp', + 'COMPONENT_TYPE': 'backend', + 'ENVIRONMENT': 'production', + 'SHORT_REGION': 'us-west-2', + 'DEPLOYMENT_REVISION': 'rev123' + }) + def test_pipe_initialization_v2(self, mock_run): + """Test that the pipe initializes the correct deployment class based on environment variables.""" + mock_run.return_value = None + with patch('pipe.pipe.V2Deployment', spec=V2Deployment) as mock_v2_deployment_class: + pipe = NewRelicDeploymentPipe(v2_schema, {}, mock_v2_deployment_class) + self.assertIsInstance(pipe.deployment, V2Deployment) + +class TestNewRelicDeploymentIntegration(unittest.TestCase): + @patch.dict(os.environ, { + 'NEW_RELIC_API_KEY': '12345', + 'NEW_RELIC_APPLICATION_ID': '670454875,670457807,670457807,670436027,670452035,670456380,670456055,670251708', + 'DEPLOYMENT_REVISION': 'rev123', + 'DEPLOYMENT_USER': 'test_user' + }) + @requests_mock.Mocker() + def test_v1_deployment(self, mock_requests): + """Integration test for V1 deployment with multiple application IDs.""" + # The application endpoint needs mocking so a offline test can be made. + app_ids = ['670454875', '670457807', '670457807', '670436027', '670452035', '670456380', '670456055', '670251708'] + for app_id in app_ids: + mock_requests.post(f'https://api.newrelic.com/v2/applications/{app_id}/deployments.json', status_code=201) + + # Because we don't use versioning for the images metadate can be blank. + pipe = NewRelicDeploymentPipe(v1_schema, {}, V1Deployment) + pipe.run() + + for app_id in app_ids: + url = f'https://api.newrelic.com/v2/applications/{app_id}/deployments.json' + self.assertTrue(mock_requests.called) + self.assertEqual(mock_requests.call_count, len(app_ids)) + self.assertTrue(mock_requests.request_history) + self.assertTrue(any(req.url == url for req in mock_requests.request_history)) + + @patch.dict(os.environ, { + 'NEW_RELIC_API_KEY': '12345', + 'APPLICATION_NAME': 'MyApp', + 'COMPONENT_TYPE': 'backend', + 'ENVIRONMENT': 'production', + 'SHORT_REGION': 'us-west-2', + 'DEPLOYMENT_REVISION': 'rev123', + 'DEPLOYMENT_USER': 'test_user' + }) + @requests_mock.Mocker() + def test_v2_deployment(self, mock_requests): + """Integration test for V2 deployment.""" + # The application endpoint needs mocking so a offline test can be made. + mock_requests.get('https://api.newrelic.com/v2/applications.json', + json={'applications': [{'id': 'app_id_1', 'name': 'MyApp'}]}) + + mock_requests.post('https://api.newrelic.com/v2/applications/app_id_1/deployments.json', status_code=201) + + # Because we don't use versioning for the images metadate can be blank. + pipe = NewRelicDeploymentPipe(v2_schema, {}, V2Deployment) + pipe.run() + + self.assertTrue(mock_requests.called) + self.assertEqual(mock_requests.call_count, 2) + self.assertTrue(mock_requests.request_history) + self.assertEqual(mock_requests.request_history[0].url, + 'https://api.newrelic.com/v2/applications.json?filter%5Bname%5D=%25MyApp%25production%25us-west-2%25backend') + self.assertEqual(mock_requests.request_history[1].url, + 'https://api.newrelic.com/v2/applications/app_id_1/deployments.json') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file