diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..af40948 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,69 @@ +version: 2.1 +jobs: + tests: + docker: + - image: circleci/python:3.7.4 + steps: + - checkout + - run: + name: Create venv + command: | + virtualenv flaskops + - run: + name: Install python dependencies + command: | + . flaskops/bin/activate + pip install --no-cache-dir -r requirements.txt + - run: + name: Run Linter + command: | + . flaskops/bin/activate + pylint --load-plugins pylint_flask --fail-under=7 app.py tests.py + - run: + name: Run Unit tests + command: | + . flaskops/bin/activate + python tests.py + - store_artifacts: + path: test-reports/ + destination: tr:${CIRCLE_SHA1} + - store_test_results: + path: test-reports/ + build: + machine: true + steps: + - checkout + - run: + name: Build Docker Image + command: | + docker build --build-arg secret_salt=${SECRET_SALT} -t registry.heroku.com/${HEROKU_APP_NAME}/web . + - run: + name: Integration Tests + command: | + docker run --rm -it registry.heroku.com/${HEROKU_APP_NAME}/web python -m pytest -vv + - run: + name: Push Docker Image to Heroku Registry + command: | + echo $HEROKU_API_KEY | docker login --username=_ --password-stdin registry.heroku.com + docker push registry.heroku.com/${HEROKU_APP_NAME}/web + deploy: + machine: true + steps: + - checkout + - run: + name: Deploy to Heroku + command: | + bash .circleci/setup-heroku.sh + echo $HEROKU_API_KEY | docker login --username=_ --password-stdin registry.heroku.com + heroku container:release web --app ${HEROKU_APP_NAME} +workflows: + version: 2 + flask-ops-cicd: + jobs: + - tests + - build: + requires: + - tests + - deploy: + requires: + - build diff --git a/.circleci/setup-heroku.sh b/.circleci/setup-heroku.sh new file mode 100644 index 0000000..2ae7ef7 --- /dev/null +++ b/.circleci/setup-heroku.sh @@ -0,0 +1,15 @@ +#!/bin/bash +git remote add heroku https://git.heroku.com/circleci-demo-python-flask.git +wget https://cli-assets.heroku.com/branches/stable/heroku-linux-amd64.tar.gz +sudo mkdir -p /usr/local/lib /usr/local/bin +sudo tar -xvzf heroku-linux-amd64.tar.gz -C /usr/local/lib +sudo ln -s /usr/local/lib/heroku/bin/heroku /usr/local/bin/heroku + +cat > ~/.netrc << EOF +machine api.heroku.com + login $HEROKU_LOGIN + password $HEROKU_API_KEY +machine git.heroku.com + login $HEROKU_LOGIN + password $HEROKU_API_KEY +EOF diff --git a/.gitignore b/.gitignore index b6e4761..abeac14 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# Pycharm +.idea/ + # Distribution / packaging .Python build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..247ef0e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.7 +ARG secret_salt +ENV SALT=$secret_salt +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +RUN mkdir /flask-app +WORKDIR /flask-app +COPY requirements.txt /flask-app/ +EXPOSE 5000 +RUN pip install --upgrade pip +RUN pip install -r requirements.txt +COPY . /flask-app/ +CMD ["bash", "start.sh"] diff --git a/app.py b/app.py index 512a98e..67ed165 100644 --- a/app.py +++ b/app.py @@ -1,29 +1,31 @@ +""" ### ACME CORP ### Customer session key generator. Protects PII. ### / 2020 +""" +import hashlib +import os +import secrets +import string from flask import Flask -import string -import secrets -import hashlib - app = Flask(__name__) +SUPER_SECRET_SALT = os.environ.get('SALT') -# !! DON'T EXPOSE THIS KEY !! -super_secret_salt = "68874260280172957479" def encrypt_string(hash_string): sha_signature = \ hashlib.sha256(hash_string.encode()).hexdigest() return sha_signature -# Generate a simple key -def generate(): + +def generate(salt): rand_device = string.ascii_letters + string.digits - return encrypt_string((''.join(secrets.choice(rand_device) for i in range(8))).join(super_secret_salt)) + return encrypt_string((''.join(secrets.choice(rand_device) for i in range(8))).join(salt)) + @app.route('/') def signature(): - return generate() + return generate(SUPER_SECRET_SALT) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..17b49c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +pylint-flask +pytest \ No newline at end of file diff --git a/start.sh b/start.sh index b0b168a..95bad94 100644 --- a/start.sh +++ b/start.sh @@ -1,4 +1,9 @@ #!/bin/bash export FLASK_APP=app.py -flask run +if [ -z "$PORT" ] +then + flask run --host=0.0.0.0 --port=5000 +else + flask run --host=0.0.0.0 --port="$PORT" +fi diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..5d373c3 --- /dev/null +++ b/tests.py @@ -0,0 +1,27 @@ +""" +Unit tests for flask-ops application +""" +import unittest + +from app import encrypt_string + + +class TestApp(unittest.TestCase): + """ + Test app.py + """ + def test_encrypt_string_empty(self): + self.assertEqual(encrypt_string(''), + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + + def test_encrypt_string_lower_block_size(self): + self.assertEqual(encrypt_string('qwerty'), + '65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5') + + def test_encrypt_string_equal_block_size(self): + self.assertEqual(encrypt_string('a' * 64), + 'ffe054fe7ae0cb6dc65c3af9b61d5209f439851db43d0ba5997337df154668eb') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0f6e6fd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,25 @@ +import pytest +from flask import Flask + +from app import generate + + +@pytest.fixture() +def app(): + app = Flask(__name__) + + test_salt = "124325246546379565" + + @app.route('/') + def signature(): + return generate(test_salt) + return app + + +@pytest.fixture() +def request_qty(): + return 1 + +@pytest.fixture +def client(app): + return app.test_client() diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..a2fbbf6 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,15 @@ +import pytest + + +def test_health(app, client): + res = client.get('/') + assert res.status_code == 200 + + +@pytest.mark.parametrize('request_qty', [1, 10, 20, 50]) +def test_uniq_responses(app, client, request_qty): + responses = set() + for request in range(request_qty): + res = client.get('/') + responses.add(res.data) + assert len(responses) == request_qty