diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index df129d8..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3a38870..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,64 +0,0 @@ -# Welcome - -Hello! Thanks for taking an interest in this project and code :) - -Contributions to this project are welcome of course, otherwise it wouldn't reside on GitHub ๐ however there's a few things to be aware of: - -- This is a personal project, it is not maintained by a team or group. -- It might take a long time for the maintainer(s) to reply to issues or review PRs, they will have have a day jobs & might not have looked at the code for a while. -- The code here is likely to not be bullet proof & production grade, there might be a lack of unit tests or other practices missing from the code base. - -# Contributing - -There's several ways of contributing to this project, and effort has been made to make this as easy and transparent as possible, whether it's: - -- Reporting a bug -- Discussing the current state of the code -- Submitting a fix -- Proposing new features -- Becoming a maintainer - -## All code changes happen though pull requests (PRs) - -Pull requests are the best way to propose changes to the codebase (using the standard [Github Flow](https://guides.github.com/introduction/flow/index.html)). - -Some PR guidance: - -- Please keep PRs small and focused on a single feature or change, with discreet commits. Use multiple PRs if need be. -- If you're thinking of adding a feature via a PR please create an issue first where it can be discussed. - -High level steps: - -1. Fork the repo and create your branch from `master` or `main`. -2. If you've changed APIs, update the documentation. -3. Ensure the test suite (if any) passes (run `make lint`). -4. Make sure your code lints (run `make lint`). -5. Issue that pull request! - -## Any contributions you make will be under the MIT Software License - -In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. - -## Report bugs using Github's issues - -This project uses GitHub issues to track public bugs. Report a bug by [opening a new issue](./issues/new/choose) - -## Write bug reports with detail, background, and sample code - -**Great Bug Reports** tend to have: - -- A quick summary and/or background -- Steps to reproduce - - Be specific! - - Give sample code if you can. Even if it's a snippet -- What you expected would happen -- What actually happens -- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - -## Use a consistent coding style - -Run `make lint-fix` in order to format the code fix any formatting & linting issues that might be present. A [Prettier](https://prettier.io/) configuration file is included - -# References - -This document was heavily adapted from the open-source contribution guidelines found in [this gist](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 694825f..0000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2021 Ben Coleman - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index de1a501..25819bd 100644 --- a/README.md +++ b/README.md @@ -1,107 +1 @@ -# Python Flask - Demo Web Application - -This is a simple Python Flask web application. The app provides system information and a realtime monitoring screen with dials showing CPU, memory, IO and process information. - -The app has been designed with cloud native demos & containers in mind, in order to provide a real working application for deployment, something more than "hello-world" but with the minimum of pre-reqs. It is not intended as a complete example of a fully functioning architecture or complex software design. - -Typical uses would be deployment to Kubernetes, demos of Docker, CI/CD (build pipelines are provided), deployment to cloud (Azure) monitoring, auto-scaling - -## Screenshot - - - -# Status - -    - -Live instances: - -[](https://python-demoapp.azurewebsites.net/) -[](https://python-demoapp.kube.benco.io/) - -## Building & Running Locally - -### Pre-reqs - -- Be using Linux, WSL or MacOS, with bash, make etc -- [Python 3.8+](https://www.python.org/downloads/) - for running locally, linting, running tests etc -- [Docker](https://docs.docker.com/get-docker/) - for running as a container, or image build and push -- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux) - for deployment to Azure - -Clone the project to any directory where you do development work - -``` -git clone https://github.com/benc-uk/python-demoapp.git -``` - -### Makefile - -A standard GNU Make file is provided to help with running and building locally. - -```text -help ๐ฌ This help message -lint ๐ Lint & format, will not fix but sets exit code on error -lint-fix ๐ Lint & format, will try to fix errors and modify code -image ๐จ Build container image from Dockerfile -push ๐ค Push container image to registry -run ๐ Run the server locally using Python & Flask -deploy ๐ Deploy to Azure Web App -undeploy ๐ Remove from Azure -test ๐ฏ Unit tests for Flask app -test-report ๐ฏ Unit tests for Flask app (with report output) -test-api ๐ฆ Run integration API tests, server must be running -clean ๐งน Clean up project -``` - -Make file variables and default values, pass these in when calling `make`, e.g. `make image IMAGE_REPO=blah/foo` - -| Makefile Variable | Default | -| ----------------- | ---------------------- | -| IMAGE_REG | ghcr.io | -| IMAGE_REPO | benc-uk/python-demoapp | -| IMAGE_TAG | latest | -| AZURE_RES_GROUP | temp-demoapps | -| AZURE_REGION | uksouth | -| AZURE_SITE_NAME | pythonapp-{git-sha} | - -The app runs under Flask and listens on port 5000 by default, this can be changed with the `PORT` environmental variable. - -# Containers - -Public container image is [available on GitHub Container Registry](https://github.com/users/benc-uk/packages/container/package/python-demoapp) - -Run in a container with: - -```bash -docker run --rm -it -p 5000:5000 ghcr.io/benc-uk/python-demoapp:latest -``` - -Should you want to build your own container, use `make image` and the above variables to customise the name & tag. - -## Kubernetes - -The app can easily be deployed to Kubernetes using Helm, see [deploy/kubernetes/readme.md](deploy/kubernetes/readme.md) for details - -# GitHub Actions CI/CD - -A working set of CI and CD release GitHub Actions workflows are provided `.github/workflows/`, automated builds are run in GitHub hosted runners - -### [GitHub Actions](https://github.com/benc-uk/python-demoapp/actions) - -[](https://github.com/benc-uk/python-demoapp/actions?query=workflow%3A%22CI+Build+App%22) - -[](https://github.com/benc-uk/python-demoapp/actions?query=workflow%3A%22CD+Release+-+AKS%22) - -[](https://github.com/benc-uk/python-demoapp/actions?query=workflow%3A%22CD+Release+-+Webapp%22) - -[](https://github.com/benc-uk/python-demoapp/commits/master) - -## Running in Azure App Service (Linux) - -If you want to deploy to an Azure Web App as a container (aka Linux Web App), a Bicep template is provided in the [deploy](deploy/) directory - -For a super quick deployment, use `make deploy` which will deploy to a resource group, temp-demoapps and use the git ref to create a unique site name - -```bash -make deploy -``` \ No newline at end of file +# cicd-pipeline diff --git a/build/Dockerfile b/build/Dockerfile deleted file mode 100644 index ae0775a..0000000 --- a/build/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3.9-slim-buster - -LABEL Name="Python Flask Demo App" Version=1.4.2 -LABEL org.opencontainers.image.source = "https://github.com/benc-uk/python-demoapp" - -ARG srcDir=src -WORKDIR /app -COPY $srcDir/requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY $srcDir/run.py . -COPY $srcDir/app ./app - -EXPOSE 5000 - -CMD ["gunicorn", "-b", "0.0.0.0:5000", "run:app"] \ No newline at end of file diff --git a/deploy/kubernetes/aks-live.yaml b/deploy/kubernetes/aks-live.yaml deleted file mode 100644 index 1a5f9e2..0000000 --- a/deploy/kubernetes/aks-live.yaml +++ /dev/null @@ -1,15 +0,0 @@ -image: - repository: ghcr.io/benc-uk/python-demoapp - pullPolicy: Always - -service: - targetPort: 5000 - -ingress: - enabled: true - host: python-demoapp.kube.benco.io - annotations: - nginx.ingress.kubernetes.io/ssl-redirect: "true" - tls: - enabled: true - secretName: kube-benco-io-cert diff --git a/deploy/kubernetes/app.sample.yaml b/deploy/kubernetes/app.sample.yaml deleted file mode 100644 index 5731f0b..0000000 --- a/deploy/kubernetes/app.sample.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# -# See this Helm chart for all options -# https://github.com/benc-uk/helm-charts/tree/master/webapp -# - -image: - repository: ghcr.io/benc-uk/python-demoapp - tag: latest - pullPolicy: Always - -service: - targetPort: 5000 - type: LoadBalancer - -# -# If you have an ingress controller set up -# -# ingress: -# enabled: true -# host: changeme.example.net -# tls: -# enabled: true -# secretName: changeme-cert-secret diff --git a/deploy/kubernetes/readme.md b/deploy/kubernetes/readme.md deleted file mode 100644 index e67e885..0000000 --- a/deploy/kubernetes/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -# Kubernetes - -Deployment into Kubernetes is simple using a [generic Helm chart for deploying web apps](https://github.com/benc-uk/helm-charts/tree/master/webapp) - -Make sure you have [Helm installed first](https://helm.sh/docs/intro/install/) - -First add the Helm repo -```bash -helm repo add benc-uk https://benc-uk.github.io/helm-charts -``` - -Make a copy of `app.sample.yaml` to `myapp.yaml` and modify the values to suit your environment. If you're in a real hurry you can use the file as is and make no changes. -```bash -helm install demo benc-uk/webapp --values myapp.yaml -``` \ No newline at end of file diff --git a/deploy/webapp.bicep b/deploy/webapp.bicep deleted file mode 100644 index 36f23ca..0000000 --- a/deploy/webapp.bicep +++ /dev/null @@ -1,33 +0,0 @@ -param location string = resourceGroup().location - -param planName string = 'app-plan-linux' -param planTier string = 'P1v2' - -param webappName string = 'python-demoapp' -param webappImage string = 'ghcr.io/benc-uk/python-demoapp:latest' -param weatherKey string = '' -param releaseInfo string = 'Released on ${utcNow('f')}' - -resource appServicePlan 'Microsoft.Web/serverfarms@2020-10-01' = { - name: planName - location: location - kind: 'linux' - sku: { - name: planTier - } - properties: { - reserved: true - } -} - -resource webApp 'Microsoft.Web/sites@2020-10-01' = { - name: webappName - location: location - properties: { - serverFarmId: appServicePlan.id - siteConfig: { - appSettings: [] - linuxFxVersion: 'DOCKER|${webappImage}' - } - } -} diff --git a/makefile b/makefile deleted file mode 100644 index cebf7c5..0000000 --- a/makefile +++ /dev/null @@ -1,85 +0,0 @@ -# Used by `image`, `push` & `deploy` targets, override as required -IMAGE_REG ?= ghcr.io -IMAGE_REPO ?= benc-uk/python-demoapp -IMAGE_TAG ?= latest - -# Used by `deploy` target, sets Azure webap defaults, override as required -AZURE_RES_GROUP ?= temp-demoapps -AZURE_REGION ?= uksouth -AZURE_SITE_NAME ?= pythonapp-$(shell git rev-parse --short HEAD) - -# Used by `test-api` target -TEST_HOST ?= localhost:5000 - -# Don't change -SRC_DIR := src - -.PHONY: help lint lint-fix image push run deploy undeploy clean test-api .EXPORT_ALL_VARIABLES -.DEFAULT_GOAL := help - -help: ## ๐ฌ This help message - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' - -lint: venv ## ๐ Lint & format, will not fix but sets exit code on error - . $(SRC_DIR)/.venv/bin/activate \ - && black --check $(SRC_DIR) \ - && flake8 src/app/ && flake8 src/run.py - -lint-fix: venv ## ๐ Lint & format, will try to fix errors and modify code - . $(SRC_DIR)/.venv/bin/activate \ - && black $(SRC_DIR) - -image: ## ๐จ Build container image from Dockerfile - docker build . --file build/Dockerfile \ - --tag $(IMAGE_REG)/$(IMAGE_REPO):$(IMAGE_TAG) - -push: ## ๐ค Push container image to registry - docker push $(IMAGE_REG)/$(IMAGE_REPO):$(IMAGE_TAG) - -run: venv ## ๐ Run the server locally using Python & Flask - . $(SRC_DIR)/.venv/bin/activate \ - && python src/run.py - -deploy: ## ๐ Deploy to Azure Web App - az group create --resource-group $(AZURE_RES_GROUP) --location $(AZURE_REGION) -o table - az deployment group create --template-file deploy/webapp.bicep \ - --resource-group $(AZURE_RES_GROUP) \ - --parameters webappName=$(AZURE_SITE_NAME) \ - --parameters webappImage=$(IMAGE_REG)/$(IMAGE_REPO):$(IMAGE_TAG) -o table - @echo "### ๐ Web app deployed to https://$(AZURE_SITE_NAME).azurewebsites.net/" - -undeploy: ## ๐ Remove from Azure - @echo "### WARNING! Going to delete $(AZURE_RES_GROUP) ๐ฒ" - az group delete -n $(AZURE_RES_GROUP) -o table --no-wait - -test: venv ## ๐ฏ Unit tests for Flask app - . $(SRC_DIR)/.venv/bin/activate \ - && pytest -v - -test-report: venv ## ๐ฏ Unit tests for Flask app (with report output) - . $(SRC_DIR)/.venv/bin/activate \ - && pytest -v --junitxml=test-results.xml - -test-api: .EXPORT_ALL_VARIABLES ## ๐ฆ Run integration API tests, server must be running - cd tests \ - && npm install newman \ - && ./node_modules/.bin/newman run ./postman_collection.json --env-var apphost=$(TEST_HOST) - -clean: ## ๐งน Clean up project - rm -rf $(SRC_DIR)/.venv - rm -rf tests/node_modules - rm -rf tests/package* - rm -rf test-results.xml - rm -rf $(SRC_DIR)/app/__pycache__ - rm -rf $(SRC_DIR)/app/tests/__pycache__ - rm -rf .pytest_cache - rm -rf $(SRC_DIR)/.pytest_cache - -# ============================================================================ - -venv: $(SRC_DIR)/.venv/touchfile - -$(SRC_DIR)/.venv/touchfile: $(SRC_DIR)/requirements.txt - python3 -m venv $(SRC_DIR)/.venv - . $(SRC_DIR)/.venv/bin/activate; pip install -Ur $(SRC_DIR)/requirements.txt - touch $(SRC_DIR)/.venv/touchfile diff --git a/src/app/__init__.py b/src/app/__init__.py deleted file mode 100644 index 3bab841..0000000 --- a/src/app/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import Flask - - -def create_app(): - app = Flask(__name__) - with app.app_context(): - from . import views # noqa: E402,F401 - from . import apis # noqa: E402,F401 - return app diff --git a/src/app/apis.py b/src/app/apis.py deleted file mode 100644 index 8543c57..0000000 --- a/src/app/apis.py +++ /dev/null @@ -1,75 +0,0 @@ -from flask import jsonify, current_app as app -import psutil - -olddata = {} -olddata["disk_write"] = 0 -olddata["disk_read"] = 0 -olddata["net_sent"] = 0 -olddata["net_recv"] = 0 - - -# -# This route returns real time process information as a REST API -# -@app.route("/api/process") -def api_process(): - apidata = {} - try: - apidata["processes"] = [] - for proc in psutil.process_iter(): - try: - # pinfo = proc.as_dict(attrs=['pid', 'name', 'num_handles', 'num_threads', 'memory_percent', 'cpu_times']) - pinfo = proc.as_dict( - attrs=["pid", "name", "memory_percent", "num_threads", "cpu_times"] - ) - except psutil.NoSuchProcess: - pass - else: - apidata["processes"].append(pinfo) - except Exception: - pass - - return jsonify(apidata) - - -# -# This route returns real time system metrics as a REST API -# -@app.route("/api/monitor") -def api_monitor(): - apidata = {} - apidata["cpu"] = psutil.cpu_percent(interval=0.9) - apidata["mem"] = psutil.virtual_memory().percent - apidata["disk"] = psutil.disk_usage("/").percent - - try: - netio = psutil.net_io_counters() - apidata["net_sent"] = ( - 0 if olddata["net_sent"] == 0 else netio.bytes_sent - olddata["net_sent"] - ) - olddata["net_sent"] = netio.bytes_sent - apidata["net_recv"] = ( - 0 if olddata["net_recv"] == 0 else netio.bytes_recv - olddata["net_recv"] - ) - olddata["net_recv"] = netio.bytes_recv - except Exception: - apidata["net_sent"] = -1 - apidata["net_recv"] = -1 - - try: - diskio = psutil.disk_io_counters() - apidata["disk_write"] = ( - 0 - if olddata["disk_write"] == 0 - else diskio.write_bytes - olddata["disk_write"] - ) - olddata["disk_write"] = diskio.write_bytes - apidata["disk_read"] = ( - 0 if olddata["disk_read"] == 0 else diskio.read_bytes - olddata["disk_read"] - ) - olddata["disk_read"] = diskio.read_bytes - except Exception: - apidata["disk_write"] = -1 - apidata["disk_read"] = -1 - - return jsonify(apidata) diff --git a/src/app/conftest.py b/src/app/conftest.py deleted file mode 100644 index 9d065eb..0000000 --- a/src/app/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from . import create_app -import pytest - -app = create_app() - - -@pytest.fixture -def client(): - with app.test_client() as client: - yield client diff --git a/src/app/static/css/main.css b/src/app/static/css/main.css deleted file mode 100644 index 21e5885..0000000 --- a/src/app/static/css/main.css +++ /dev/null @@ -1,42 +0,0 @@ - -h1 { - padding-top: 1rem; -} - -.logotext { - font-size: 1.5em !important; -} -.jumbotron { - padding: 2rem !important; -} -.gauges { - width: 100%; - height: 25vw; - margin: 0 auto; -} - -/* Sortable tables */ -table.sortable th { - background-color: #eeeeee; - color:#666666; - font-weight: bold; - cursor: pointer; - padding: 10px; -} - -td { - padding-right: 50px !important; - font-size: 18px; - border-bottom: 1px solid #dddddd; - cursor: default; -} - -.icon { - width: 40px; -} - -.dimmed-box { - background-color: rgba(0,0,0,0.2); - padding: 1rem; - border-radius: 0.3rem; -} \ No newline at end of file diff --git a/src/app/static/img/docker-whale.svg b/src/app/static/img/docker-whale.svg deleted file mode 100644 index dff737c..0000000 --- a/src/app/static/img/docker-whale.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/static/img/favicon.ico b/src/app/static/img/favicon.ico deleted file mode 100644 index 3cf51c6..0000000 Binary files a/src/app/static/img/favicon.ico and /dev/null differ diff --git a/src/app/static/img/flask.png b/src/app/static/img/flask.png deleted file mode 100644 index 022c6de..0000000 Binary files a/src/app/static/img/flask.png and /dev/null differ diff --git a/src/app/static/img/github-2.svg b/src/app/static/img/github-2.svg deleted file mode 100644 index 64bb694..0000000 --- a/src/app/static/img/github-2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/static/img/python.svg b/src/app/static/img/python.svg deleted file mode 100644 index 250b7fa..0000000 --- a/src/app/static/img/python.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/static/js/monitor.js b/src/app/static/js/monitor.js deleted file mode 100644 index 503980e..0000000 --- a/src/app/static/js/monitor.js +++ /dev/null @@ -1,117 +0,0 @@ -var data_memcpu; -var data_disk; -var data_net; -var chart_memcpu; -var chart_disk; -var chart_net; -var options_percent; -var options_io; - -var refresh_sec = 3.0; - -function initCharts() { - data_memcpu = google.visualization.arrayToDataTable([ - ['Label', 'Value'], - ['CPU', 0], - ['Memory', 0], - ]); - data_disk = google.visualization.arrayToDataTable([ - ['Label', 'Value'], - ['Disk read', 0], - ['Disk write', 0], - ]); - data_net = google.visualization.arrayToDataTable([ - ['Label', 'Value'], - ['Net sent', 0], - ['Net recv', 0], - ]); - - options_percent = { - //width: 1200, height: 600, - redFrom: 90, redTo: 100, - yellowFrom: 75, yellowTo: 90, - greenFrom: 0, greenTo: 75, - minorTicks: 5, animation:{ duration: 950, easing: 'inAndOut' } - }; - options_io = { - max: 200, - minorTicks: 10, animation:{ duration: 950, easing: 'inAndOut' } - }; - - chart_memcpu = new google.visualization.Gauge(document.getElementById('chart1')); - chart_disk = new google.visualization.Gauge(document.getElementById('chart2')); - chart_net = new google.visualization.Gauge(document.getElementById('chart3')); - - refreshCharts(); - refreshProcesses(); - setRefresh(refresh_sec); - - $('#refrate').text(refresh_sec); - $('#refslider').val(refresh_sec); - $(document).on('input', '#refslider', function() { - setRefresh($(this).val()) - }); -} - -var proc_timer; -var chart_timer; -function setRefresh(new_secs) { - refresh_sec = parseFloat(new_secs); - $('#refrate').text(refresh_sec); - clearInterval(proc_timer); - clearInterval(chart_timer); - proc_timer = setInterval(function () { - refreshProcesses(); - }, refresh_sec * 1000); - chart_timer = setInterval(function () { - refreshCharts(); - }, refresh_sec * 1000); -} -function refreshCharts() { - $.ajax({ - url: '/api/monitor', - type: 'GET', - dataType: 'json', - success: function (apidata) { - //console.dir(apidata); - data_memcpu.setValue(0, 1, apidata.cpu); - data_memcpu.setValue(1, 1, apidata.mem); - data_disk.setValue(0, 1, apidata.disk_read / (1024000*refresh_sec)); - data_disk.setValue(1, 1, apidata.disk_write / (1024000*refresh_sec)); - data_net.setValue(0, 1, apidata.net_sent / (1024000*refresh_sec)); - data_net.setValue(1, 1, apidata.net_recv / (1024000*refresh_sec)); - - chart_memcpu.draw(data_memcpu, options_percent); - chart_disk.draw(data_disk, options_io); - chart_net.draw(data_net, options_io); - }, - error: function (request, error) { - console.log("API Request: " + JSON.stringify(request)); - } - }); -} - -function refreshProcesses() { - $.ajax({ - url: '/api/process', - type: 'GET', - dataType: 'json', - success: function (apidata) { - $('#process_tab').empty(); - $('#proc_count').text(apidata.processes.length); - for(var p = 0; p < apidata.processes.length; p++) { - $('#process_tab').append('