diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8ec1e68 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: run tests +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install pipenv and any other packages + run: | + python -m pip install pipenv + pipenv install --dev + - name: Run pytest + run: pipenv run pytest service + - name: Lint with flake8 + run: pipenv run flake8 service diff --git a/.gitignore b/.gitignore index 511a264..9941260 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ # Byte-compiled / optimized / DLL files +*.pyc __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - # Distribution / packaging .Python env/ @@ -15,15 +13,16 @@ dist/ downloads/ eggs/ .eggs/ -./lib/ +lib/ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg -.venv +Pipfile.lock *~ # PyInstaller @@ -44,8 +43,9 @@ htmlcov/ .cache nosetests.xml coverage.xml -*,cover +*.cover .hypothesis/ +.pytest_cache/ # Translations *.mo @@ -53,6 +53,14 @@ coverage.xml # Django stuff: *.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ @@ -60,5 +68,36 @@ docs/_build/ # PyBuilder target/ -# pyenv python configuration file +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv .python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +#Pipfile.lock + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml index c17b74d..74585db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,20 @@ # This file will be regenerated if you run travis_pypi_setup.py language: python -python: - - 3.6 - - 3.8 +matrix: + include: + - python: 3.8 + - python: 3.9 # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: pip install -U tox-travis +install: + - pip install pipenv + - pipenv install --dev # command to run tests, e.g. python setup.py test -script: tox +script: + - pipenv run flake8 service + - pipenv run pytest service # After you create the Github repo and add it to Travis, run the # travis_pypi_setup.py script to finish PyPI deployment setup diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d3e9bac..c070bb3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -66,9 +66,15 @@ Ready to contribute? Here's how to set up `rtls` for local development. 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - $ mkvirtualenv rtls - $ cd rtls/ - $ python setup.py develop +First, go into the pipenv virtual environment:: + + $ pipenv update --dev + $ pipenv shell + +Running the local development server:: + + (pkg_syncer)[]$ cd ./service + (pkg_syncer)[]$ python main.py 4. Create a branch for local development:: @@ -76,13 +82,10 @@ Ready to contribute? Here's how to set up `rtls` for local development. Now you can make your changes locally. -5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: +5. When you're done making changes, check that your changes pass flake8 and the tests:: - $ flake8 rtls tests - $ python setup.py test or py.test - $ tox - - To get flake8 and tox, just pip install them into your virtualenv. + (rtls)[]$ flake8 service + (rtls)[]$ pytest service 6. Commit your changes and push your branch to GitHub:: @@ -101,14 +104,13 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 3.6 and 3.7. Check +3. The pull request should work for Python 3.8. Check https://travis-ci.org/release-depot/rtls/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- -To run a subset of tests:: - +To run a single test:: - $ python -m unittest tests.test_rtls + (rtls)[]$ service/tests/test_rtls.py::test_rtls diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..d80f2ca --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +ansible-bender = "*" +autopep8 = "*" +flake8 = "*" +pylint = "*" +pytest = "*" +selinux = "*" + +[packages] +rtls = {editable = true, path = "./service"} diff --git a/README.rst b/README.rst index 3797cd3..1fc66ab 100644 --- a/README.rst +++ b/README.rst @@ -24,11 +24,39 @@ RTLS, or Real Time Locator System, is an abstraction layer for tracking changes * Free software: MIT license * Documentation: https://rtls.readthedocs.io. +API +--- -Features --------- +Test call - /rtls/[package]/[changeId] (GET) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Test call:: + + $ curl localhost:8080/rtls/mypackage/I123456 + {"msg": "Test with mypackage"} + +Development +----------- + +See **CONTRIBUTING.rst**. + +Container image creation +~~~~~~~~~~~~~~~~~~~~~~~~ + +Building the container image:: + + $ pipenv shell + $ ansible-bender build ./build.yml + +Running the container image:: + + $ podman run -d --name rtls -p 8080:80 localhost/rtls:latest + +Reading the container logs:: + + $ podman logs rtls + $ podman exec -it rtls cat /var/log/nginx/error.log | less -* TODO Credits ------- diff --git a/build.yml b/build.yml new file mode 100644 index 0000000..e4dc5f0 --- /dev/null +++ b/build.yml @@ -0,0 +1,23 @@ +--- +- name: rtls build + hosts: all + vars: + www_root: '/var/www/rtls/service' + ansible_bender: + base_image: 'registry.fedoraproject.org/fedora:latest' + cache_tasks: False + layering: False + target_image: + name: rtls + environment: + PIPENV_VENV_IN_PROJECT: 'True' + entrypoint: "{{ www_root }}/service_run.sh" + working_dir: "{{ www_root }}" + tasks: + - name: Setup the base + include_role: + name: base + + - name: Setup the app + include_role: + name: app diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 8606480..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,12 +0,0 @@ -pip==19.2.3 -bump2version==0.5.11 -wheel==0.33.6 -watchdog==0.9.0 -flake8==3.7.8 -tox==3.14.0 -coverage==4.5.4 -Sphinx==1.8.5 -twine==1.14.0 - -pytest==4.6.5 -pytest-runner==5.1 \ No newline at end of file diff --git a/roles/app/tasks/main.yml b/roles/app/tasks/main.yml new file mode 100644 index 0000000..b6741d4 --- /dev/null +++ b/roles/app/tasks/main.yml @@ -0,0 +1,38 @@ +--- + +- name: Setting up the app directory + file: + path: "{{ www_root }}" + state: directory + +- name: Copy app code + copy: + src: "{{ playbook_dir }}/service" + dest: "{{ app_root }}" + +- name: Copy python packaging files + copy: + src: "{{ item }}" + dest: "{{ app_root }}" + loop: + - "{{ playbook_dir }}/service/setup.py" + - "{{ playbook_dir }}/service/requirements.txt" + +- name: Copy Pipenv files + copy: + src: "{{ item }}" + dest: "{{ www_root }}" + mode: preserve + loop: + - "{{ playbook_dir }}/Pipfile" + +- name: Setup venv + shell: + cmd: "pipenv update" + chdir: "{{ app_root }}" + +- name: Copy files + template: + src: templates/service_run.j2 + dest: "{{ app_root }}/service_run.sh" + mode: 'o+x' diff --git a/roles/app/templates/service_run.j2 b/roles/app/templates/service_run.j2 new file mode 100644 index 0000000..536689c --- /dev/null +++ b/roles/app/templates/service_run.j2 @@ -0,0 +1,9 @@ +#! /bin/bash + +source {{ www_root }}/.venv/bin/activate +cd {{ www_root }} +nginx +# Timeout value is in seconds +gunicorn wsgi:app \ + --bind 127.0.0.1:8000 \ + --timeout 300 diff --git a/roles/app/vars/main.yml b/roles/app/vars/main.yml new file mode 100644 index 0000000..d6b3f47 --- /dev/null +++ b/roles/app/vars/main.yml @@ -0,0 +1,3 @@ +--- + +app_root: '{{ www_root }}' diff --git a/roles/base/files/proxy_params b/roles/base/files/proxy_params new file mode 100644 index 0000000..df75bc5 --- /dev/null +++ b/roles/base/files/proxy_params @@ -0,0 +1,4 @@ +proxy_set_header Host $http_host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; diff --git a/roles/base/files/rtls.conf b/roles/base/files/rtls.conf new file mode 100644 index 0000000..175dd84 --- /dev/null +++ b/roles/base/files/rtls.conf @@ -0,0 +1,9 @@ +server { + listen 80; + server_name server_domain_or_IP; + + location / { + include proxy_params; + proxy_pass http://127.0.0.1:8000; + } +} diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml new file mode 100644 index 0000000..96d8c7c --- /dev/null +++ b/roles/base/tasks/main.yml @@ -0,0 +1,35 @@ +--- + +- name: Install packages + dnf: + name: + - pipenv + - nginx + - python38 + state: present + install_weak_deps: no + +- name: Clean dnf metadata + shell: 'dnf clean all' + args: + warn: no + +- name: Remove dnf cache + file: + path: /var/lib/dnf + state: absent + +- name: Set up base directory for apps + file: + path: "{{ www_root }}" + state: directory + +- name: Copy nginx config + copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: nginx + group: nginx + loop: + - {'src': 'rtls.conf', 'dest': '/etc/nginx/conf.d'} + - {'src': 'proxy_params', 'dest': '/etc/nginx'} diff --git a/rtls/__init__.py b/rtls/__init__.py deleted file mode 100644 index e27470e..0000000 --- a/rtls/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Top-level package for Real Time Locator System.""" - -__author__ = """Jason Guiditta""" -__email__ = 'jguiditt@redhat.com' -__version__ = '0.0.1' diff --git a/rtls/rtls.py b/rtls/rtls.py deleted file mode 100644 index 7fbbae4..0000000 --- a/rtls/rtls.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Main module.""" diff --git a/service/main.py b/service/main.py new file mode 100644 index 0000000..4f0474a --- /dev/null +++ b/service/main.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +""" Main runner for the service """ + +from flask import Flask +from flask_restful import Api + +from resources.rtls import Rtls + + +def create_app(): + app = Flask(__name__) + api = Api(app) + + api.add_resource(Rtls, '/rtls/', + '/rtls//', + resource_class_kwargs={'logger': app.logger}) + + return app + + +def run(): + from logging import INFO + app = create_app() + app.logger.setLevel(INFO) + app.run(port=8080) + + +def run_debug(): + from logging import DEBUG + app = create_app() + app.logger.setLevel(DEBUG) + app.run(port=8080) + + +if __name__ == '__main__': + run_debug() diff --git a/service/requirements.txt b/service/requirements.txt new file mode 100644 index 0000000..c660644 --- /dev/null +++ b/service/requirements.txt @@ -0,0 +1,2 @@ +-i https://pypi.org/simple +- e . diff --git a/service/resources/rtls.py b/service/resources/rtls.py new file mode 100644 index 0000000..95fb927 --- /dev/null +++ b/service/resources/rtls.py @@ -0,0 +1,18 @@ +from flask_restful import Resource + + +class Rtls(Resource): + def __init__(self, **kwargs): + self.logger = kwargs.get('logger') + + def get(self, package, change_id): + return {'msg': f"Test with {package}"}, 200 + + def post(self, package, change_id): + pass + + def put(self, package, change_id): + pass + + def delete(self, package, change_id): + pass diff --git a/service/setup.py b/service/setup.py new file mode 100644 index 0000000..523eb1e --- /dev/null +++ b/service/setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +from setuptools import setup, find_packages + + +setup( + author="Jason Guiditta", + author_email='jguiditt@redhat.com', + classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', + ], + description=("RTLS, or Real Time Locator System, is an abstraction layer " + "for tracking changes to code through their lifecycle"), + entry_points={}, + include_package_data=True, + install_requires=[ + 'flask', + 'flask_restful', + 'gunicorn', + ], + license="MIT license", + name='rtls', + packages=find_packages(include=['rtls']), + tests_require=[ + 'pytest' + ], + url='https://github.com/release-depot/rtls', + version='0.0.1', + zip_safe=False, +) diff --git a/service/tests/__init__.py b/service/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/tests/conftest.py b/service/tests/conftest.py new file mode 100644 index 0000000..d86a420 --- /dev/null +++ b/service/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from main import create_app + + +@pytest.fixture +def app(): + app = create_app() + app.testing = True + return app + + +@pytest.fixture +def client(app): + return app.test_client() diff --git a/service/tests/test_rtls.py b/service/tests/test_rtls.py new file mode 100644 index 0000000..d0ddbe8 --- /dev/null +++ b/service/tests/test_rtls.py @@ -0,0 +1,14 @@ +import json + + +def test_rtls(client): + """ + GIVEN a valid package name and change_id + WHEN the rtls API is called + THEN a response containing the package name is returned + """ + response = client.get("/rtls/test_pkg/12345") + + assert response.status_code == 200 + data = json.loads(response.data) + assert "test_pkg" in data['msg'] diff --git a/service/wsgi.py b/service/wsgi.py new file mode 100644 index 0000000..da73ba7 --- /dev/null +++ b/service/wsgi.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +""" WSGI wrapper script """ + +from main import create_app + +app = create_app() + +if __name__ == '__main__': + app.run() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ac0e6fe..0000000 --- a/setup.cfg +++ /dev/null @@ -1,26 +0,0 @@ -[bumpversion] -current_version = 0.0.1 -commit = True -tag = True - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:rtls/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - -[bdist_wheel] -universal = 1 - -[flake8] -exclude = docs - -[aliases] -# Define setup.py command aliases here -test = pytest - -[tool:pytest] -collect_ignore = ['setup.py'] - diff --git a/setup.py b/setup.py deleted file mode 100644 index c72bc30..0000000 --- a/setup.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The setup script.""" - -from setuptools import setup, find_packages - -with open('README.rst') as readme_file: - readme = readme_file.read() - -with open('HISTORY.rst') as history_file: - history = history_file.read() - -requirements = [ - # TODO: put package requirements here -] - -setup_requirements = [ - # TODO(jguiditta): put setup requirements (distutils extensions, etc.) here - 'pytest-runner', -] - -test_requirements = [ - # TODO: put package test requirements here - 'pytest>=3', -] - -setup( - name='rtls', - version='0.0.1', - description=" RTLS, or Real Time Locator System, is an abstraction layer for tracking changes to code through their lifecycle ", - long_description=readme + '\n\n' + history, - author="Jason Guiditta", - author_email='jguiditt@redhat.com', - url='https://github.com/release-depot/rtls', - packages=find_packages(include=['rtls']), - entry_points={}, - include_package_data=True, - install_requires=requirements, - license="MIT license", - zip_safe=False, - keywords='rtls', - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - ], - test_suite='tests', - tests_require=test_requirements, - setup_requires=setup_requirements, -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 0327ed3..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit test package for rtls.""" diff --git a/tests/test_rtls.py b/tests/test_rtls.py deleted file mode 100644 index e8d3e39..0000000 --- a/tests/test_rtls.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python - -"""Tests for `rtls` package.""" - -import pytest - - -from rtls import rtls - - -@pytest.fixture -def response(): - """Sample pytest fixture. - - See more at: http://doc.pytest.org/en/latest/fixture.html - """ - # import requests - # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') - - -def test_content(response): - """Sample pytest test function with the pytest fixture as an argument.""" - # from bs4 import BeautifulSoup - # assert 'GitHub' in BeautifulSoup(response.content).title.string diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 20aabf9..0000000 --- a/tox.ini +++ /dev/null @@ -1,27 +0,0 @@ -[tox] -envlist = py35, py36, py37, py38, flake8 - -[travis] -python = - 3.8: py38 - 3.7: py37 - 3.6: py36 - 3.5: py35 - -[testenv:flake8] -basepython = python -deps = flake8 -commands = flake8 rtls tests - -[testenv] -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/requirements_dev.txt -; If you want to make tox run the tests with the same versions, create a -; requirements.txt with the pinned versions and uncomment the following line: -; -r{toxinidir}/requirements.txt -commands = - pip install -U pip - pytest --basetemp={envtmpdir} - diff --git a/travis_pypi_setup.py b/travis_pypi_setup.py deleted file mode 100644 index 5a2f05f..0000000 --- a/travis_pypi_setup.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Update encrypted deploy password in Travis config file.""" - - -from __future__ import print_function -import base64 -import json -import os -from getpass import getpass -import yaml -from cryptography.hazmat.primitives.serialization import load_pem_public_key -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 - - -try: - from urllib import urlopen -except ImportError: - from urllib.request import urlopen - - -GITHUB_REPO = 'release-depot/rtls' -TRAVIS_CONFIG_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), '.travis.yml') - - -def load_key(pubkey): - """Load public RSA key. - - Work around keys with incorrect header/footer format. - - Read more about RSA encryption with cryptography: - https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ - """ - try: - return load_pem_public_key(pubkey.encode(), default_backend()) - except ValueError: - # workaround for https://github.com/travis-ci/travis-api/issues/196 - pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') - return load_pem_public_key(pubkey.encode(), default_backend()) - - -def encrypt(pubkey, password): - """Encrypt password using given RSA public key and encode it with base64. - - The encrypted password can only be decrypted by someone with the - private key (in this case, only Travis). - """ - key = load_key(pubkey) - encrypted_password = key.encrypt(password, PKCS1v15()) - return base64.b64encode(encrypted_password) - - -def fetch_public_key(repo): - """Download RSA public key Travis will use for this repo. - - Travis API docs: http://docs.travis-ci.com/api/#repository-keys - """ - keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) - data = json.loads(urlopen(keyurl).read().decode()) - if 'key' not in data: - errmsg = "Could not find public key for repo: {}.\n".format(repo) - errmsg += "Have you already added your GitHub repo to Travis?" - raise ValueError(errmsg) - return data['key'] - - -def prepend_line(filepath, line): - """Rewrite a file adding a line to its beginning.""" - with open(filepath) as f: - lines = f.readlines() - - lines.insert(0, line) - - with open(filepath, 'w') as f: - f.writelines(lines) - - -def load_yaml_config(filepath): - """Load yaml config file at the given path.""" - with open(filepath) as f: - return yaml.load(f) - - -def save_yaml_config(filepath, config): - """Save yaml config file at the given path.""" - with open(filepath, 'w') as f: - yaml.dump(config, f, default_flow_style=False) - - -def update_travis_deploy_password(encrypted_password): - """Put `encrypted_password` into the deploy section of .travis.yml.""" - config = load_yaml_config(TRAVIS_CONFIG_FILE) - - config['deploy']['password'] = dict(secure=encrypted_password) - - save_yaml_config(TRAVIS_CONFIG_FILE, config) - - line = ('# This file was autogenerated and will overwrite' - ' each time you run travis_pypi_setup.py\n') - prepend_line(TRAVIS_CONFIG_FILE, line) - - -def main(args): - """Add a PyPI password to .travis.yml so that Travis can deploy to PyPI. - - Fetch the Travis public key for the repo, and encrypt the PyPI password - with it before adding, so that only Travis can decrypt and use the PyPI - password. - """ - public_key = fetch_public_key(args.repo) - password = args.password or getpass('PyPI password: ') - update_travis_deploy_password(encrypt(public_key, password.encode())) - print("Wrote encrypted password to .travis.yml -- you're ready to deploy") - - -if '__main__' == __name__: - import argparse - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--repo', default=GITHUB_REPO, - help='GitHub repo (default: %s)' % GITHUB_REPO) - parser.add_argument('--password', - help='PyPI password (will prompt if not provided)') - - args = parser.parse_args() - main(args)