Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync Development -> Main #7

Merged
merged 8 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ jobs:

- uses: Gr1N/setup-poetry@v8

- name: Check dependencies
run: make doctor

- uses: actions/cache@v2
with:
path: .venv
Expand Down
6 changes: 0 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
* Linux: [https://graphviz.org/download](https://graphviz.org/download/)
* Windows: [https://graphviz.org/download](https://graphviz.org/download/)

To confirm these system dependencies are configured correctly:

```text
$ make bootstrap
$ make doctor
```

### Installation

Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ $(DEPENDENCIES): poetry.lock
@ rm -rf ~/Library/Preferences/pypoetry
@ poetry config virtualenvs.in-project true
poetry install
@ touch $@

ifndef CI
poetry.lock: pyproject.toml
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Wppt
# Wppt
#### Webhook Payload Proxy Transformer
[![Unix Build Status](https://img.shields.io/github/actions/workflow/status/grafuls/wppt/main.yml?branch=main&label=linux)](https://github.com/grafuls/wppt/actions)
[![Coverage Status](https://img.shields.io/codecov/c/gh/grafuls/wppt)](https://codecov.io/gh/grafuls/wppt)

Expand All @@ -10,7 +11,7 @@ Wppt(Pronounced: [ˈwɪpɪt]) provides an easy way to intercept, manage, manipul

Some services/platforms don't provide an easy-to-use integration vehicle for transforming and syncing requests, payloads and automation e.g. Gitlab -> Jira. Instead of paying for an expensive third-party service to provide integrations you can do this easily yourself on-premise.

## How wppt Does Work?
## How Does `wppt` Work?

Wppt leverages Flask dynamic routing. The endpoint is variable and defined via one or multiple yaml files.
Based on the endpoint url, `wppt` parses all the yaml files stored on the `transformers` directory, and retrieves the outgoing webhook url and the translations. It then parses all the translations and converts the existing data from the incoming webhook into a new payload structure as defined on the yaml.
Expand All @@ -24,7 +25,7 @@ gitlab2jira:
translations:
data:
name: '[{data[project][name]}][{data[object_kind]}] {data[object_attributes][title]}'
description: 'Description: {data[object_attributes][description]}\nURL:{data[object_attributes][url]}'
description: 'Description: {data[object_attributes][description]}\nURL:{data[object_attributes][url]}'
```

Given the following incoming webhook payload to `http://{FQDN}:5005/gitlab2jira/`:
Expand Down Expand Up @@ -60,14 +61,30 @@ Given the following incoming webhook payload to `http://{FQDN}:5005/gitlab2jira/

Install it directly into a poetry virtual environment:

```text
```bash
$ git clone https://github.com/redhat-performance/wppt
$ cd wppt
$ make install
```

## Usage

After installation, the server can be started with:

```text
```bash
$ make run
```

### Via Podman

#### Building the image
```bash
$ cd docker
$ podman build -t wppt .
```

#### Running
```bash
$ podman run -it --rm -v /path/to/local/transformers/:/opt/wppt/transformers -p 5005:5005 wppt
```

23 changes: 23 additions & 0 deletions bin/checksum
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import hashlib
import sys


def run(paths):
sha = hashlib.sha1()

for path in paths:
try:
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha.update(chunk)
except IOError:
sha.update(path.encode())

print(sha.hexdigest())


if __name__ == '__main__':
run(sys.argv[1:])
22 changes: 22 additions & 0 deletions bin/open
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys


COMMANDS = {
'linux': "open",
'win32': "cmd /c start",
'cygwin': "cygstart",
'darwin': "open",
}


def run(path):
command = COMMANDS.get(sys.platform, "open")
os.system(command + ' ' + path)


if __name__ == '__main__':
run(sys.argv[-1])
20 changes: 20 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use the official Python base image
FROM python:3.11
LABEL authors="grafuls"

RUN apt-get install git patch make

# Set the working directory inside the container
WORKDIR /opt
RUN git clone https://github.com/redhat-performance/wppt
WORKDIR /opt/wppt
RUN git checkout development

# Install Poetry
RUN pip install poetry

# Install project dependencies
RUN make install

# Set the entrypoint command to run the project
CMD ["make", "run"]
18 changes: 7 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ license = "GPL3"
authors = ["Gonzalo Rafuls <[email protected]>"]

readme = "README.md"
homepage = "https://pypi.org/project/wppt"
documentation = "https://wppt.readthedocs.io"
homepage = "https://github.com/redhat-performance/wppt"
documentation = "https://github.com/redhat-performance/wppt/blob/main/README.md"
repository = "https://github.com/redhat-performance/wppt"

keywords = []
classifiers = [
# TODO: update this list to match your application: https://pypi.org/pypi?%3Aaction=list_classifiers
"Development Status :: 1 - Planning",
"Development Status :: 3 - Alpha",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
Expand All @@ -29,17 +29,17 @@ classifiers = [

python = "^3.11"

# TODO: Remove these and add your library's requirements
flask = "*"
requests = "*"
flask = "3.0.2"
requests = "2.31.0"
pyyaml = "6.0.1"

[tool.poetry.dev-dependencies]

# Formatters
black = "^22.1"
tomli = "*" # missing 'black' dependency
isort = "^5.10"
ipdb = "*" # missing 'black' dependency
ipdb = "0.13.13" # missing 'black' dependency

# Linters
mypy = "^1.0"
Expand Down Expand Up @@ -68,10 +68,6 @@ sniffer = "*"
MacFSEvents = { version = "*", platform = "darwin" }
pync = { version = "*", platform = "darwin" }

[tool.poetry.scripts]

wppt = "wppt.cli:main"

[tool.black]

quiet = true
Expand Down
13 changes: 11 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""Integration tests configuration file."""
import pytest

# pylint: disable=unused-import
from wppt.app import app as flask_app

@pytest.fixture(scope="module")
def test_client():
"""
| Creates a test client for the app.
"""
with flask_app.test_client() as testing_client:
with flask_app.app_context():
yield testing_client
7 changes: 7 additions & 0 deletions tests/fixtures/transformers/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
test:
enabled: true
target_webhook: https://example.com/rest/api/
translations:
data:
name: '[{data[project][name]}][{data[object_kind]}] {data[object_attributes][title]}'
description: 'Description: {data[object_attributes][description]}\nURL:{data[object_attributes][url]}'
76 changes: 76 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from unittest.mock import patch
from pytest import raises
import requests

class TestApp:

@patch('wppt.app.parse_definitions')
@patch('wppt.app.traverse_format_dict')
@patch('wppt.app.requests.post')
def test_dinamic_transformer_enabled_transformer(self, mock_post, mock_traverse_format_dict, mock_parse_definitions, test_client):
mock_parse_definitions.return_value = {
'transformer1': {
'enabled': True,
'translations': {'key1': 'value1'},
'target_webhook': 'https://example.com/webhook'
}
}
mock_traverse_format_dict.return_value = None
mock_post.return_value.status_code = 200

response = test_client.post('/transformer1/', json={'key1': 'value1'})

assert response.status_code == 200
assert response.json == {'status_code': 200, 'message': 'Transformers executed.'}
mock_traverse_format_dict.assert_called_once_with({'key1': 'value1'}, {'key1': 'value1'})

@patch('wppt.app.parse_definitions')
def test_dinamic_transformer_disabled_transformer(self, mock_parse_definitions, test_client):
mock_parse_definitions.return_value = {
'transformer1': {
'enabled': False,
'translations': {'key1': 'value1'},
'target_webhook': 'https://example.com/webhook'
}
}
response = test_client.post('/transformer1/', json={'key1': 'value1'})

assert response.json[0] == 'Transformer transformer1 is disabled'
assert response.json[1] == 400

@patch('wppt.app.parse_definitions')
@patch('wppt.app.traverse_format_dict', side_effect=KeyError('Invalid key'))
def test_dinamic_transformer_invalid_key(self, mock_traverse_format_dict, mock_parse_definitions, test_client):
mock_parse_definitions.return_value = {
'transformer1': {
'enabled': True,
'translations': {'key1': 'value1'},
'target_webhook': 'https://example.com/webhook'
}
}

response = test_client.post('/transformer1/', json={'key1': 'value1'})

assert response.json[0] == "'Invalid key'"
assert response.json[1] == 400
mock_traverse_format_dict.assert_called_once_with({'key1': 'value1'}, {'key1': 'value1'})

@patch('wppt.app.parse_definitions')
@patch('wppt.app.traverse_format_dict')
@patch('wppt.app.requests.post', side_effect=requests.exceptions.RequestException('Failed to send request'))
def test_dinamic_transformer_failed_request(self, mock_post, mock_traverse_format_dict, mock_parse_definitions, test_client):
mock_parse_definitions.return_value = {
'transformer1': {
'enabled': True,
'translations': {'key1': 'value1'},
'target_webhook': 'https://example.com/webhook'
}
}
mock_traverse_format_dict.return_value = None
mock_post.side_effect = requests.exceptions.RequestException('Failed to send request')

response = test_client.post('/transformer1/', json={'key1': 'value1'})

assert response.json['message'] == 'Failed to send the transformed webhook for transformer1.'
assert response.json['status_code'] == 400
mock_traverse_format_dict.assert_called_once_with({'key1': 'value1'}, {'key1': 'value1'})
45 changes: 45 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest.mock import patch
from wppt.utils import parse_definitions, walk_dir, read_yaml, traverse_format_dict

class TestUtils:

@patch('wppt.utils.walk_dir')
@patch('wppt.utils.read_yaml')
def test_parse_definitions(self, mock_read_yaml, mock_walk_dir):
mock_walk_dir.return_value = ['/path/to/file1.yaml', '/path/to/file2.yaml']
mock_read_yaml.side_effect = [{'key1': 'value1'}, {'key2': 'value2'}]

directory = '/path/to/directory'
expected_definitions = {'key1': 'value1', 'key2': 'value2'}
assert parse_definitions(directory) == expected_definitions

def test_walk_dir(self):
directory = 'tests/fixtures/transformers'
file_extension = '.yaml'
expected_file_list = ['tests/fixtures/transformers/test.yaml']

assert walk_dir(directory, file_extension) == expected_file_list

def test_read_yaml(self):
file_path = '../wppt/transformers/gitlab2jira.yaml'
expected_yaml_definitions = {'key1': 'value1', 'key2': 'value2'}

# Mocking the yaml.load function
with patch('yaml.load') as mock_yaml_load:
mock_yaml_load.return_value = {'key1': 'value1', 'key2': 'value2'}

file_path = '../wppt/transformers/gitlab2jira.yaml'
expected_yaml_definitions = {'key1': 'value1', 'key2': 'value2'}

with patch('builtins.open', create=True) as mock_open:
mock_open.return_value.__enter__.return_value.read.return_value = 'key1: value1\nkey2: value2\n'
yaml_definitions = read_yaml(file_path)
assert yaml_definitions == expected_yaml_definitions

def test_traverse_format_dict(self):
dictionary = {'key1': 'Hello {data}', 'key2': 'Welcome {data}'}
data = 'World'
expected_dictionary = {'key1': 'Hello World', 'key2': 'Welcome World'}

traverse_format_dict(dictionary, data)
assert dictionary == expected_dictionary
Loading