Browse Source

2021 refresh

pull/4/head
Ben Coleman 5 years ago
parent
commit
30320dba25
  1. 2
      .dockerignore
  2. 2
      .flake8
  3. 2
      .github/.secrets.sample
  4. 26
      .github/act-examples.md
  5. 5
      .github/workflow_dispatch.json
  6. 62
      .github/workflows/cd-release-aks.yaml
  7. 75
      .github/workflows/cd-release-webapp.yaml
  8. 44
      .github/workflows/ci-build.yaml
  9. 19
      .github/workflows/publish.yaml
  10. 5
      .gitignore
  11. 64
      CONTRIBUTING.md
  12. 13
      Dockerfile
  13. 2
      LICENSE
  14. 81
      README.md
  15. 5
      app/__init__.py
  16. 53
      app/templates/base.html
  17. 41
      app/templates/index.html
  18. 49
      app/templates/info.html
  19. 39
      app/templates/monitor.html
  20. 77
      app/views.py
  21. 16
      build/Dockerfile
  22. 0
      deploy/kubernetes/aks-live.yaml
  23. 0
      deploy/kubernetes/app.sample.yaml
  24. 0
      deploy/kubernetes/readme.md
  25. 7
      deploy/webapp.bicep
  26. BIN
      infra/bicep
  27. 78
      makefile
  28. 4
      requirements.txt
  29. 8
      run.py
  30. 6
      src/app/__init__.py
  31. 70
      src/app/apis.py
  32. 0
      src/app/static/css/main.css
  33. 0
      src/app/static/img/docker-whale.svg
  34. 0
      src/app/static/img/favicon.ico
  35. 0
      src/app/static/img/flask.png
  36. 0
      src/app/static/img/github-2.svg
  37. 0
      src/app/static/img/python.svg
  38. 0
      src/app/static/js/monitor.js
  39. 0
      src/app/static/js/sorttable.js
  40. 70
      src/app/templates/base.html
  41. 50
      src/app/templates/index.html
  42. 51
      src/app/templates/info.html
  43. 48
      src/app/templates/monitor.html
  44. 30
      src/app/views.py
  45. 6
      src/requirements.txt
  46. 8
      src/run.py
  47. 157
      tests/postman_collection.json
  48. 107
      workflows-old/cd-release-aks.yaml
  49. 114
      workflows-old/cd-release-webapp.yaml
  50. 78
      workflows-old/ci-build.yaml
  51. 31
      workflows-old/publish.yaml

2
.dockerignore

@ -1,5 +1,3 @@ @@ -1,5 +1,3 @@
.git
*.pyc
__pycache__
infra/bicep
infra/*.json

2
.flake8

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
[flake8]
max-line-length=125

2
.github/.secrets.sample

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
CR_PAT=__CHANGE_ME__
AZURE_CREDENTIALS={"clientId": "", "clientSecret": "", "subscriptionId": "", "tenantId": ""}

26
.github/act-examples.md

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
# Act
Act is an amazing command line local runner for GitHub Actions
https://github.com/nektos/act
Install with
```bash
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
```
To run the workflows for this repo, example commands are given below
The `.secrets` file must be created first, see the sample file for a reference.
### Run CI
```bash
act push --secret-file .github/.secrets --platform ubuntu-latest=ghcr.io/benc-uk/act-runner:python
```
### Run a deployment
```bash
act workflow_dispatch --eventpath .github/workflow_dispatch.json --secret-file .github/.secrets --platform ubuntu-latest=ghcr.io/benc-uk/act-runner:python
```

5
.github/workflow_dispatch.json

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
{
"inputs": {
"IMAGE_TAG": "26-03-2021.1610"
}
}

62
.github/workflows/cd-release-aks.yaml

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
#
# Deploy to Azure Kubernetes Service using Helm
# Using Bicep for infrastructure as code
# Deploy to Azure Kubernetes Service
# Using Helm for parameterized deployment
#
name: CD Release - AKS
@ -29,23 +29,16 @@ jobs: @@ -29,23 +29,16 @@ jobs:
deploy-aks:
name: Deploy to AKS with Helm
runs-on: ubuntu-latest
outputs:
deployment_id: ${{ steps.deploy.outputs.deployment_id }}
environment:
name: AKS - vuego-demoapp
url: https://${{ env.INGRESS_DNS_HOST }}/
steps:
- name: "Checkout"
uses: actions/checkout@v1
- name: "Start deployment"
id: deploy
uses: chrnorm/deployment-action@v1.2.0
with:
ref: ${{ github.event.ref }}
token: ${{ github.token }}
environment: AKS - ${{ env.HELM_RELEASE }}
uses: actions/checkout@v2
- name: "Login to Azure"
uses: azure/login@v1
uses: azure/login@v1.3.0
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
@ -59,17 +52,9 @@ jobs: @@ -59,17 +52,9 @@ jobs:
helm upgrade ${{ env.HELM_RELEASE }} benc-uk/webapp \
--install \
--namespace ${{ env.HELM_NAMESPACE }} \
--values ./kubernetes/aks-live.yaml \
--values deploy/kubernetes/aks-live.yaml \
--set image.tag=${{ github.event.inputs.IMAGE_TAG }},ingress.host=${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-bicep.outputs.deployment_id }}
#
# Post deployment testing stage
#
@ -77,31 +62,18 @@ jobs: @@ -77,31 +62,18 @@ jobs:
name: "Run Deployment Tests"
needs: deploy-aks
runs-on: ubuntu-latest
environment:
name: AKS - vuego-demoapp
url: https://${{ env.INGRESS_DNS_HOST }}/
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Validate site is running"
run: .github/scripts/url-check.sh -u https://${{ env.INGRESS_DNS_HOST }} -s "Python" -t 200
run: .github/scripts/url-check.sh -u https://${{ env.INGRESS_DNS_HOST }} -s "Flask" -t 200
# - name: "Run API tests"
# run: |
# npm install newman --silent
# node_modules/newman/bin/newman.js run src/tests/postman_collection.json --global-var apphost=${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - success"
if: ${{ success() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: success
deployment_id: ${{ needs.deploy-aks.outputs.deployment_id }}
environment_url: https://${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-aks.outputs.deployment_id }}
- name: "Run API tests"
run: |
npm install newman --silent
node_modules/newman/bin/newman.js run tests/postman_collection.json --global-var apphost=${{ env.INGRESS_DNS_HOST }}

75
.github/workflows/cd-release-webapp.yaml

@ -22,60 +22,36 @@ env: @@ -22,60 +22,36 @@ env:
ARM_SUB_ID: 52512f28-c6ed-403e-9569-82a9fb9fec91
ARM_REGION: westeurope
ARM_RES_GROUP: apps
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 # Fixes weird Azure CLI + Bicep + GHA bug
jobs:
#
# Deploy Azure infra (App Service) using Bicep
#
deploy-infra:
environment:
name: App Service - vuego-demoapp
url: https://${{ env.APP_NAME }}.azurewebsites.net/
name: "Deploy Infra"
runs-on: ubuntu-latest
outputs:
deployment_id: ${{ steps.deploy.outputs.deployment_id }}
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Start deployment"
id: deploy
uses: chrnorm/deployment-action@v1.2.0
with:
ref: ${{ github.event.ref }}
token: ${{ github.token }}
environment: App Service - ${{ env.APP_NAME }}
- name: "Run Bicep compiler"
run: |
wget https://github.com/Azure/bicep/releases/download/v0.1.37-alpha/bicep-linux-x64 -qO bicep
chmod +x bicep
./bicep build webapp.bicep
working-directory: ./infra
- name: "Login to Azure"
uses: azure/login@v1
uses: azure/login@v1.3.0
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: "Create resource group"
run: az group create --name ${{ env.ARM_RES_GROUP }} --location ${{ env.ARM_REGION }}
run: az group create --name $ARM_RES_GROUP --location $ARM_REGION
- name: "Deploy resources"
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ env.ARM_SUB_ID }}
resourceGroupName: ${{ env.ARM_RES_GROUP }}
template: ./infra/webapp.json
parameters: webappName=${{ env.APP_NAME }} webappImage=${{ env.IMAGE_REG }}/${{ env.IMAGE_REPO }}:${{ github.event.inputs.IMAGE_TAG }} weatherKey=${{ secrets.WEATHER_API_KEY }}
deploymentName: webapp-deploy-${{ github.run_id }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-bicep.outputs.deployment_id }}
run: |
az deployment group create --template-file deploy/webapp.bicep -g $ARM_RES_GROUP -p webappName=$APP_NAME \
webappImage=$IMAGE_REG/$IMAGE_REPO:${{ github.event.inputs.IMAGE_TAG }} \
releaseInfo="Ref=${{ github.ref }} RunId=${{ github.run_id }}"
#
# Post deployment testing stage
@ -83,32 +59,19 @@ jobs: @@ -83,32 +59,19 @@ jobs:
validate-deployment:
name: "Run Deployment Tests"
needs: deploy-infra
environment:
name: App Service - vuego-demoapp
url: https://${{ env.APP_NAME }}.azurewebsites.net/
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Validate site is running"
run: .github/scripts/url-check.sh -u https://${{ env.APP_NAME }}.azurewebsites.net/ -s "Python" -t 200
run: .github/scripts/url-check.sh -u https://${APP_NAME}.azurewebsites.net/ -s "Flask" -t 200
# - name: "Run API tests"
# run: |
# npm install newman --silent
# node_modules/newman/bin/newman.js run src/tests/postman_collection.json --global-var apphost=${{ env.APP_NAME }}.azurewebsites.net
- name: "End deployment - success"
if: ${{ success() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: success
deployment_id: ${{ needs.deploy-infra.outputs.deployment_id }}
environment_url: https://${{ env.APP_NAME }}.azurewebsites.net/
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-infra.outputs.deployment_id }}
- name: "Run API tests"
run: |
npm install newman --silent
node_modules/newman/bin/newman.js run tests/postman_collection.json --global-var apphost=${APP_NAME}.azurewebsites.net

44
.github/workflows/ci-build.yaml

@ -3,8 +3,8 @@ name: CI Build App @@ -3,8 +3,8 @@ name: CI Build App
on:
push:
branches: [master]
paths-ignore:
- ".github/**"
paths:
- "src/**"
pull_request:
env:
@ -19,21 +19,19 @@ jobs: @@ -19,21 +19,19 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Test/linting stub"
run: echo "Nothing here 😐"
- name: "Run linting"
run: make lint
# - name: "Run tests"
# run: make test-report
# - name: "Upload test results"
# uses: actions/upload-artifact@v2
# # Disabled when running locally with the nektos/act tool
# if: ${{ always() && !env.ACT }}
# with:
# name: test-results
# path: ./src/test-results.xml
# - name: "Report on test results"
# uses: ashley-taylor/junit-report-annotations-action@master
# if: always()
# with:
# access-token: ${{ secrets.GITHUB_TOKEN }}
# path: ./src/test-results.xml
# name: Unit test results
# path: blah
build:
name: "Build & Push Image"
@ -43,23 +41,19 @@ jobs: @@ -43,23 +41,19 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Create datestamp image tag" # Nicer than using github runid, I think
# Nicer than using github runid, I think, will be picked up automatically by make
- name: "Create datestamp image tag"
run: echo "IMAGE_TAG=$(date +%d-%m-%Y.%H%M)" >> $GITHUB_ENV
- name: "Docker build image"
run: docker build . -t $IMAGE_REG/$IMAGE_REPO:$IMAGE_TAG
- name: "Login to GitHub container registry"
if: github.ref == 'refs/heads/master'
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REG }}
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
run: make image
- name: "Docker push image to ${{ env.IMAGE_REG }}"
if: github.ref == 'refs/heads/master'
run: docker push $IMAGE_REG/$IMAGE_REPO
# Only when pushing to default branch (e.g. master or main), then push image to registry
- name: Push to container registry
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: |
echo ${{ secrets.CR_PAT }} | docker login $IMAGE_REG -u $GITHUB_ACTOR --password-stdin
make push
- name: "Trigger AKS release pipeline"
if: github.ref == 'refs/heads/master'

19
.github/workflows/publish.yaml

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
name: Release Versioned Image
on:
workflow_dispatch:
release:
types: [published]
@ -18,14 +17,12 @@ jobs: @@ -18,14 +17,12 @@ jobs:
uses: actions/checkout@v2
- name: "Docker build image with version tag"
run: docker build . -t $IMAGE_REG/$IMAGE_REPO:latest -t $IMAGE_REG/$IMAGE_REPO:${{ github.event.release.tag_name }}
run: |
make image IMAGE_TAG=${{ github.event.release.tag_name }}
make image IMAGE_TAG=latest
- name: "Login to GitHub container registry"
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REG }}
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: "Docker push image to ${{ env.IMAGE_REG }}"
run: docker push $IMAGE_REG/$IMAGE_REPO
- name: "Push to container registry"
run: |
echo ${{ secrets.CR_PAT }} | docker login $IMAGE_REG -u $GITHUB_ACTOR --password-stdin
make push IMAGE_TAG=${{ github.event.release.tag_name }}
make push IMAGE_TAG=latest

5
.gitignore vendored

@ -8,7 +8,6 @@ __pycache__/ @@ -8,7 +8,6 @@ __pycache__/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
@ -99,4 +98,6 @@ venv.bak/ @@ -99,4 +98,6 @@ venv.bak/
/site
# mypy
.mypy_cache/
.mypy_cache/
.secrets
.env

64
CONTRIBUTING.md

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
# 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)

13
Dockerfile

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
FROM python:3.8-buster
LABEL Name="Python Flask Demo App" Version=1.4.0
WORKDIR /demoapp
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY run.py .
COPY app ./app
EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "run:app"]

2
LICENSE

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
Copyright 2019 Ben Coleman
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:

81
README.md

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
# 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.
@ -6,21 +7,74 @@ The app has been designed with cloud native demos & containers in mind, in order @@ -6,21 +7,74 @@ The app has been designed with cloud native demos & containers in mind, in order
Typical uses would be deployment to Kubernetes, demos of Docker, CI/CD (build pipelines are provided), deployment to cloud (Azure) monitoring, auto-scaling
## Screenshot
![screen](https://user-images.githubusercontent.com/14982936/30533171-db17fccc-9c4f-11e7-8862-eb8c148fedea.png)
## Locally via Python
Python 3.6+ is required and has been tested on Windows and Linux (Ubuntu 18.04)
## 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/vuego-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 server and frontend
test-report 🎯 Unit tests for server and frontend (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`
Simply run with `python run.py` the script should start the app, and set Flask listening on port 5000, you can change the port with the environmental variable `PORT`.
Go to `http://localhost:5000` to view the app.
| Makefile Variable | Default |
| ----------------- | ---------------------- |
| IMAGE_REG | ghcr<span>.</span>io |
| IMAGE_REPO | benc-uk/python-demoapp |
| IMAGE_TAG | latest |
| AZURE_RES_GROUP | temp-demoapps |
| AZURE_REGION | uksouth |
| AZURE_SITE_NAME | pythonapp-{git-sha} |
## Running as Docker Container
The image is publicly available on [GitHub Container Registry](https://hub.docker.com/r/bencuk/python-demoapp/) so running it in Docker is simple `docker run --rm -it -p 5000:5000 ghcr.io/benc-uk/python-demoapp`
The app runs under Flask and listens on port 5000 by default, this can be changed with the `PORT` environmental variable.
The container runs on Debian Linux and is configured to use the latest version of Python (3.6.2 at the time of writing)
# Containers
Public container image is [available on GitHub Container Registry](https://github.com/users/benc-uk/packages/container/package/vuego-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
# 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)
@ -33,15 +87,16 @@ A working set of CI and CD release GitHub Actions workflows are provided `.githu @@ -33,15 +87,16 @@ A working set of CI and CD release GitHub Actions workflows are provided `.githu
[![](https://img.shields.io/github/last-commit/benc-uk/python-demoapp)](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 [infrastructure as code](infra/) directory
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
You can also very quickly deploy to Azure App Service directly with the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) and `az webapp up`. Note. `<app-name>` must be globally unique. Change the sku to a larger size, e.g. `P1V2` for a much faster deployment
```
az webapp up --sku F1 --name <app-name>
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
```
## Running in Azure App Service (Windows)
Just don't, it's awful

5
app/__init__.py

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
from flask import Flask
app = Flask(__name__)
from app import views

53
app/templates/base.html

@ -1,53 +0,0 @@ @@ -1,53 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Python DemoApp{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/static/css/main.css" rel="stylesheet" />
<link href="/static/img/favicon.ico" rel="icon" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/united/bootstrap.min.css"
integrity="sha384-JW3PJkbqVWtBhuV/gsuyVVt3m/ecRJjwXC3gCXlTzZZV+zIEEl6AnryAriT7GWYm" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand logotext" href="/">
<img src="static/img/python.svg" width="37" height="40">
&nbsp; Python Demo
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="true" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="btn btn-success btn-lg" href="/info">🧾 Info</a>
</li>
&nbsp;
<li class="nav-item active">
<a class="btn btn-success btn-lg" href="/monitor">🚦 Monitor</a>
</li>
</ul>
</div>
</nav>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<div class="container body-content">{% block content %}{% endblock %}</div>
<span style="float:right">v1.4.0 [Ben Coleman, 2018-2020] &nbsp;&nbsp;&nbsp;</span>
</body>
</html>

41
app/templates/index.html

@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
{% extends "base.html" %}
{% block content %}
<br>
<div class="jumbotron">
<h1><img src="/static/img/flask.png" height="80px" /> Python & Flask Demo App</h1>
<div class="lead">
This is a simple web application written in Python and using Flask. It has been designed with cloud demos &amp;
containers in mind. Demonstrating capabilities such as auto scaling, deployment to Azure or Kubernetes, or anytime
you want something quick and lightweight to run & deploy.
</div>
<br>
<div class="dimmed-box">
<p>
<img src="static/img/github-2.svg" class="icon">
<a href="https://github.com/benc-uk/python-demoapp" class="btn btn-info btn px-4">
GitHub Project
</a>
&nbsp;&nbsp;&nbsp;
<img src="static/img/docker-whale.svg" class="icon">
<a href="https://github.com/users/benc-uk/packages/container/package/python-demoapp" class="btn btn-info btn">
Docker Images
</a>
</p>
<hr>
<p>
<img src="static/img/python.svg" class="icon">
<a class="btn btn-info btn" href="https://azure.microsoft.com/en-gb/develop/python/">
Get started with Azure &amp; Python
</a>
</p>
<br>
<p>Microsoft ❤ Open Source</p>
</div>
</div>
{% endblock %}

49
app/templates/info.html

@ -1,49 +0,0 @@ @@ -1,49 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="">
<h1>🛠 System Information</h1>
<table class="table table-striped table-hover" style="font-size:1.5em">
<tbody>
<tr>
<td><b>Hostname</b></td>
<td>{{ info.plat.node() }}</td>
</tr>
<tr>
<td><b>Boot Time</b></td>
<td>{{ info.boottime }}</td>
</tr>
<tr>
<td><b>OS Platform</b></td>
<td>{{ info.plat.system() }}</td>
</tr>
<tr>
<td><b>OS Version</b></td>
<td>{{ info.plat.version() }}</td>
</tr>
<tr>
<td><b>Python Version</b></td>
<td>{{ info.plat.python_version() }}</td>
</tr>
<tr>
<td><b>Processor &amp; Cores</b></td>
<td>{{ info.cpu.count }} x {{ info.cpu.brand }}</td>
</tr>
<tr>
<td><b>System Memory</b></td>
<td>{{ (info.mem.total / (1024*1024*1024)) | round(0,'ceil') |int }}GB ({{info.mem.percent}}% used)</td>
</tr>
<tr>
<td><b>Network Interfaces</b></td>
<td>
{% for iface, snics in info.net.items() %} {% for snic in snics if (snic.family == 2) %}
<li>{{ iface }} - <b>{{ snic.address }}</b> </li>
{% endfor %} {% endfor %}
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

39
app/templates/monitor.html

@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
{% extends "base.html" %}
{% block content %}
<script src="/static/js/sorttable.js"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script src="/static/js/monitor.js"></script>
<script type="text/javascript">
google.charts.load('current', { 'packages': ['gauge'] });
google.charts.setOnLoadCallback(initCharts);
</script>
<body>
<span style='float:right'>
Refresh Rate: <span id="refrate"></span> secs<input id="refslider" type="range" min="2" max="15" />
</span>
<h2>Running Processes (<span id="proc_count"></span>)</h2>
<div style="height: 200px; overflow: scroll;">
<table class="sortable">
<thead>
<tr>
<th>PID</th>
<th>Name</th>
<th>Mem</th>
<th>CPU Time</th>
<th>Threads</th>
</tr>
</thead>
<tbody id="process_tab">
</tbody>
</table>
</div>
<h2>Performance Monitor</h2>
<div id="chart1" class="gauges"></div>
<div id="chart2" class="gauges"></div>
<div id="chart3" class="gauges"></div>
</body>
{% endblock %}

77
app/views.py

@ -1,77 +0,0 @@ @@ -1,77 +0,0 @@
from flask import url_for, redirect, render_template, flash, g, session, jsonify
from app import app
import cpuinfo
import psutil
import platform
import datetime
@app.route('/')
def index():
return render_template('index.html')
@app.route('/info')
def info():
osinfo = {}
osinfo['plat'] = platform
osinfo['cpu'] = cpuinfo.get_cpu_info()
osinfo['mem'] = psutil.virtual_memory()
osinfo['net'] = psutil.net_if_addrs()
osinfo['boottime'] = datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")
return render_template('info.html', info = osinfo)
@app.route('/monitor')
def monitor():
return render_template('monitor.html')
olddata = {}
olddata['disk_write'] = 0
olddata['disk_read'] = 0
olddata['net_sent'] = 0
olddata['net_recv'] = 0
@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:
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:
apidata['disk_write'] = -1
apidata['disk_read'] = -1
return jsonify(apidata)
@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:
pass
return jsonify(apidata)

16
build/Dockerfile

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
FROM python:3.9-slim-buster
LABEL Name="Python Flask Demo App" Version=1.4.1
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"]

0
kubernetes/aks-live.yaml → deploy/kubernetes/aks-live.yaml

0
kubernetes/app.sample.yaml → deploy/kubernetes/app.sample.yaml

0
kubernetes/readme.md → deploy/kubernetes/readme.md

7
infra/webapp.bicep → deploy/webapp.bicep

@ -6,8 +6,9 @@ param planTier string = 'P1v2' @@ -6,8 +6,9 @@ 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-06-01' = {
resource appServicePlan 'Microsoft.Web/serverfarms@2020-10-01' = {
name: planName
location: location
kind: 'linux'
@ -19,7 +20,7 @@ resource appServicePlan 'Microsoft.Web/serverFarms@2020-06-01' = { @@ -19,7 +20,7 @@ resource appServicePlan 'Microsoft.Web/serverFarms@2020-06-01' = {
}
}
resource webApp 'Microsoft.Web/sites@2018-11-01' = {
resource webApp 'Microsoft.Web/sites@2020-10-01' = {
name: webappName
location: location
properties: {
@ -29,4 +30,4 @@ resource webApp 'Microsoft.Web/sites@2018-11-01' = { @@ -29,4 +30,4 @@ resource webApp 'Microsoft.Web/sites@2018-11-01' = {
linuxFxVersion: 'DOCKER|${webappImage}'
}
}
}
}

BIN
infra/bicep

Binary file not shown.

78
makefile

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
# 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 $(DEPLOY_RES_GROUP) 😲"
az group delete -n $(DEPLOY_RES_GROUP) -o table --no-wait
test: ## 🎯 Unit tests for server and frontend
cd $(SRC_DIR); go test -v | tee server_tests.txt
cd $(SPA_DIR); npm run test
test-report: test ## 🎯 Unit tests for server and frontend (with report output)
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*
# ============================================================================
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

4
requirements.txt

@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
Flask
py-cpuinfo
psutil
gunicorn

8
run.py

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
import os
from app import app
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.jinja_env.auto_reload = True
app.config['TEMPLATES_AUTO_RELOAD'] = True
app.run(host='0.0.0.0', port=port)

6
src/app/__init__.py

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
from flask import Flask
app = Flask(__name__)
from app import views # noqa: E402,F401
from app import apis # noqa: E402,F401

70
src/app/apis.py

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
from flask import jsonify
from app import app
import psutil
olddata = {}
olddata["disk_write"] = 0
olddata["disk_read"] = 0
olddata["net_sent"] = 0
olddata["net_recv"] = 0
@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)
@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)

0
app/static/css/main.css → src/app/static/css/main.css

0
app/static/img/docker-whale.svg → src/app/static/img/docker-whale.svg

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

0
app/static/img/favicon.ico → src/app/static/img/favicon.ico

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

0
app/static/img/flask.png → src/app/static/img/flask.png

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

0
app/static/img/github-2.svg → src/app/static/img/github-2.svg

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 979 B

0
app/static/img/python.svg → src/app/static/img/python.svg

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

0
app/static/js/monitor.js → src/app/static/js/monitor.js

0
app/static/js/sorttable.js → src/app/static/js/sorttable.js

70
src/app/templates/base.html

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Python DemoApp{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/static/css/main.css" rel="stylesheet" />
<link href="/static/img/favicon.ico" rel="icon" />
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/united/bootstrap.min.css"
integrity="sha384-JW3PJkbqVWtBhuV/gsuyVVt3m/ecRJjwXC3gCXlTzZZV+zIEEl6AnryAriT7GWYm"
crossorigin="anonymous"
/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand logotext" href="/">
<img src="static/img/python.svg" width="37" height="40" />
&nbsp; Python Demo
</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="true"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="btn btn-success btn-lg" href="/info">🧾 Info</a>
</li>
&nbsp;
<li class="nav-item active">
<a class="btn btn-success btn-lg" href="/monitor">🚦 Monitor</a>
</li>
</ul>
</div>
</nav>
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"
></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"
></script>
<div class="container body-content">{% block content %}{% endblock %}</div>
<span style="float: right"
>v1.4.1 [Ben Coleman, 2018-2021] &nbsp;&nbsp;&nbsp;</span
>
</body>
</html>

50
src/app/templates/index.html

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
{% extends "base.html" %} {% block content %}
<br />
<div class="jumbotron">
<h1>
<img src="/static/img/flask.png" height="80px" /> Python & Flask Demo App
</h1>
<div class="lead">
This is a simple web application written in Python and using Flask. It has
been designed with cloud demos &amp; containers in mind. Demonstrating
capabilities such as auto scaling, deployment to Azure or Kubernetes, or
anytime you want something quick and lightweight to run & deploy.
</div>
<br />
<div class="dimmed-box">
<p>
<img src="static/img/github-2.svg" class="icon" />
<a
href="https://github.com/benc-uk/python-demoapp"
class="btn btn-info btn px-4"
>
GitHub Project
</a>
&nbsp;&nbsp;&nbsp;
<img src="static/img/docker-whale.svg" class="icon" />
<a
href="https://github.com/users/benc-uk/packages/container/package/python-demoapp"
class="btn btn-info btn"
>
Docker Images
</a>
</p>
<hr />
<p>
<img src="static/img/python.svg" class="icon" />
<a
class="btn btn-info btn"
href="https://azure.microsoft.com/en-gb/develop/python/"
>
Get started with Azure &amp; Python
</a>
</p>
<br />
<p>Microsoft ❤ Open Source</p>
</div>
</div>
{% endblock %}

51
src/app/templates/info.html

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
{% extends "base.html" %} {% block content %}
<div class="">
<h1>🛠 System Information</h1>
<table class="table table-striped table-hover" style="font-size: 1.5em">
<tbody>
<tr>
<td><b>Hostname</b></td>
<td>{{ info.plat.node() }}</td>
</tr>
<tr>
<td><b>Boot Time</b></td>
<td>{{ info.boottime }}</td>
</tr>
<tr>
<td><b>OS Platform</b></td>
<td>{{ info.plat.system() }}</td>
</tr>
<tr>
<td><b>OS Version</b></td>
<td>{{ info.plat.version() }}</td>
</tr>
<tr>
<td><b>Python Version</b></td>
<td>{{ info.plat.python_version() }}</td>
</tr>
<tr>
<td><b>Processor &amp; Cores</b></td>
<td>{{ info.cpu.count }} x {{ info.cpu.brand }}</td>
</tr>
<tr>
<td><b>System Memory</b></td>
<td>
{{ (info.mem.total / (1024*1024*1024)) | round(0,'ceil') |int }}GB
({{info.mem.percent}}% used)
</td>
</tr>
<tr>
<td><b>Network Interfaces</b></td>
<td>
{% for iface, snics in info.net.items() %} {% for snic in snics if
(snic.family == 2) %}
<li>{{ iface }} - <b>{{ snic.address }}</b></li>
{% endfor %} {% endfor %}
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

48
src/app/templates/monitor.html

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
{% extends "base.html" %} {% block content %}
<script src="/static/js/sorttable.js"></script>
<script
type="text/javascript"
src="https://www.gstatic.com/charts/loader.js"
></script>
<script src="/static/js/monitor.js"></script>
<script type="text/javascript">
google.charts.load("current", { packages: ["gauge"] });
google.charts.setOnLoadCallback(initCharts);
</script>
<body>
<span style="float: right">
Refresh Rate: <span id="refrate"></span> secs<input
id="refslider"
type="range"
min="2"
max="15"
/>
</span>
<br />
<h2>👓 Running Processes (<span id="proc_count"></span>)</h2>
<div style="height: 200px; overflow: scroll">
<table class="sortable">
<thead>
<tr>
<th>PID</th>
<th>Name</th>
<th>Mem</th>
<th>CPU Time</th>
<th>Threads</th>
</tr>
</thead>
<tbody id="process_tab"></tbody>
</table>
</div>
<hr />
<h2>🌡 Performance Monitor</h2>
<div id="chart1" class="gauges"></div>
<div id="chart2" class="gauges"></div>
<div id="chart3" class="gauges"></div>
</body>
{% endblock %}

30
src/app/views.py

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
from flask import render_template
from app import app
import cpuinfo
import psutil
import platform
import datetime
@app.route("/")
def index():
return render_template("index.html")
@app.route("/info")
def info():
osinfo = {}
osinfo["plat"] = platform
osinfo["cpu"] = cpuinfo.get_cpu_info()
osinfo["mem"] = psutil.virtual_memory()
osinfo["net"] = psutil.net_if_addrs()
osinfo["boottime"] = datetime.datetime.fromtimestamp(psutil.boot_time()).strftime(
"%Y-%m-%d %H:%M:%S"
)
return render_template("info.html", info=osinfo)
@app.route("/monitor")
def monitor():
return render_template("monitor.html")

6
src/requirements.txt

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
Flask==1.1.2
py-cpuinfo==7.0.0
psutil==5.8.0
gunicorn==20.0.4
black==20.8b1
flake8==3.9.0

8
src/run.py

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import os
from app import app
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.jinja_env.auto_reload = True
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.run(host="0.0.0.0", port=port)

157
tests/postman_collection.json

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
{
"info": {
"_postman_id": "d758a295-d6e7-40ac-a6ba-d9312cf9bbe5",
"name": "Python Demoapp",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Check Home Page",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Home Page: Successful GET request\", function () {",
" pm.response.to.be.ok;",
"});",
"",
"pm.test(\"Home Page: Response valid & HTML body\", function () {",
" pm.response.to.be.withBody;",
" pm.expect(pm.response.headers.get('Content-Type')).to.contain('text/html');",
"});",
"",
"pm.test(\"Home Page: Check content\", function () {",
" pm.expect(pm.response.text()).to.include('Python');",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://{{apphost}}/",
"protocol": "http",
"host": [
"{{apphost}}"
],
"path": [
""
]
}
},
"response": []
},
{
"name": "Check Info Page",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Info Page: Successful GET request\", function () {",
" pm.response.to.be.ok;",
"});",
"",
"pm.test(\"Info Page: Response valid & HTML body\", function () {",
" pm.response.to.be.withBody;",
" pm.expect(pm.response.headers.get('Content-Type')).to.contain('text/html');",
"});",
"",
"pm.test(\"Info Page: Check content\", function () {",
" pm.expect(pm.response.text()).to.include('Network Interfaces');",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://{{apphost}}/info",
"protocol": "http",
"host": [
"{{apphost}}"
],
"path": [
"info"
]
}
},
"response": []
},
{
"name": "Check Process API",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Process API: Successful GET request\", function () {",
" pm.response.to.be.ok;",
"});",
"",
"pm.test(\"Process API: Response valid & JSON body\", function () {",
" pm.response.to.be.withBody;",
" pm.response.to.be.json;",
"});",
"",
"pm.test(\"Process API: Check API response\", function () {",
" var processData = pm.response.json();",
" pm.expect(processData.processes).to.be.an('array')",
" pm.expect(processData.processes[0].name).to.be.an('string')",
" pm.expect(processData.processes[0].memory_percent).to.be.an('number')",
" pm.expect(processData.processes[0].pid).to.be.an('number')",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://{{apphost}}/api/process",
"protocol": "https",
"host": [
"{{apphost}}"
],
"path": [
"api",
"process"
]
}
},
"response": []
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
]
}

107
workflows-old/cd-release-aks.yaml

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
#
# Deploy to Azure Kubernetes Service using Helm
# Using Bicep for infrastructure as code
#
name: CD Release - AKS
on:
workflow_dispatch:
inputs:
IMAGE_TAG:
description: "Image tag to be deployed"
required: true
default: "latest"
# Note. Required secrets: CR_PAT & AZURE_CREDENTIALS
env:
AKS_NAME: benc
AKS_RES_GROUP: aks
HELM_RELEASE: python
HELM_NAMESPACE: demoapps
INGRESS_DNS_HOST: python-demoapp.kube.benco.io
jobs:
#
# Deploy to Kubernetes (AKS)
#
deploy-aks:
name: Deploy to AKS with Helm
runs-on: ubuntu-latest
outputs:
deployment_id: ${{ steps.deploy.outputs.deployment_id }}
steps:
- name: "Checkout"
uses: actions/checkout@v1
- name: "Start deployment"
id: deploy
uses: chrnorm/deployment-action@v1.2.0
with:
ref: ${{ github.event.ref }}
token: ${{ github.token }}
environment: AKS - ${{ env.HELM_RELEASE }}
- name: "Login to Azure"
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: "Get AKS credentials"
run: |
az aks get-credentials -n $AKS_NAME -g $AKS_RES_GROUP
- name: "Helm release"
run: |
helm repo add benc-uk https://benc-uk.github.io/helm-charts
helm upgrade ${{ env.HELM_RELEASE }} benc-uk/webapp \
--install \
--namespace ${{ env.HELM_NAMESPACE }} \
--values ./kubernetes/aks-live.yaml \
--set image.tag=${{ github.event.inputs.IMAGE_TAG }},ingress.host=${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-bicep.outputs.deployment_id }}
#
# Post deployment testing stage
#
validate-deployment:
name: "Run Deployment Tests"
needs: deploy-aks
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Validate site is running"
run: .github/scripts/url-check.sh -u https://${{ env.INGRESS_DNS_HOST }} -s "Python" -t 200
# - name: "Run API tests"
# run: |
# npm install newman --silent
# node_modules/newman/bin/newman.js run src/tests/postman_collection.json --global-var apphost=${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - success"
if: ${{ success() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: success
deployment_id: ${{ needs.deploy-aks.outputs.deployment_id }}
environment_url: https://${{ env.INGRESS_DNS_HOST }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-aks.outputs.deployment_id }}

114
workflows-old/cd-release-webapp.yaml

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
#
# Deploy to Azure App Service as a containerized Web App
# Using Bicep for infrastructure as code
#
name: CD Release - Webapp
on:
workflow_dispatch:
inputs:
IMAGE_TAG:
description: "Image tag to be deployed"
required: true
default: "latest"
# Note. Required secrets: CR_PAT & AZURE_CREDENTIALS
env:
IMAGE_REG: ghcr.io
IMAGE_REPO: benc-uk/python-demoapp
APP_NAME: python-demoapp
ARM_SUB_ID: 52512f28-c6ed-403e-9569-82a9fb9fec91
ARM_REGION: westeurope
ARM_RES_GROUP: apps
jobs:
#
# Deploy Azure infra (App Service) using Bicep
#
deploy-infra:
name: "Deploy Infra"
runs-on: ubuntu-latest
outputs:
deployment_id: ${{ steps.deploy.outputs.deployment_id }}
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Start deployment"
id: deploy
uses: chrnorm/deployment-action@v1.2.0
with:
ref: ${{ github.event.ref }}
token: ${{ github.token }}
environment: App Service - ${{ env.APP_NAME }}
- name: "Run Bicep compiler"
run: |
wget https://github.com/Azure/bicep/releases/download/v0.1.37-alpha/bicep-linux-x64 -qO bicep
chmod +x bicep
./bicep build webapp.bicep
working-directory: ./infra
- name: "Login to Azure"
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: "Create resource group"
run: az group create --name ${{ env.ARM_RES_GROUP }} --location ${{ env.ARM_REGION }}
- name: "Deploy resources"
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ env.ARM_SUB_ID }}
resourceGroupName: ${{ env.ARM_RES_GROUP }}
template: ./infra/webapp.json
parameters: webappName=${{ env.APP_NAME }} webappImage=${{ env.IMAGE_REG }}/${{ env.IMAGE_REPO }}:${{ github.event.inputs.IMAGE_TAG }} weatherKey=${{ secrets.WEATHER_API_KEY }}
deploymentName: webapp-deploy-${{ github.run_id }}
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-bicep.outputs.deployment_id }}
#
# Post deployment testing stage
#
validate-deployment:
name: "Run Deployment Tests"
needs: deploy-infra
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Validate site is running"
run: .github/scripts/url-check.sh -u https://${{ env.APP_NAME }}.azurewebsites.net/ -s "Python" -t 200
# - name: "Run API tests"
# run: |
# npm install newman --silent
# node_modules/newman/bin/newman.js run src/tests/postman_collection.json --global-var apphost=${{ env.APP_NAME }}.azurewebsites.net
- name: "End deployment - success"
if: ${{ success() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: success
deployment_id: ${{ needs.deploy-infra.outputs.deployment_id }}
environment_url: https://${{ env.APP_NAME }}.azurewebsites.net/
- name: "End deployment - failure"
if: ${{ failure() }}
uses: chrnorm/deployment-status@v1.0.0
with:
token: ${{ github.token }}
state: failure
deployment_id: ${{ needs.deploy-infra.outputs.deployment_id }}

78
workflows-old/ci-build.yaml

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
name: CI Build App
on:
push:
branches: [master]
paths-ignore:
- ".github/**"
pull_request:
env:
IMAGE_REG: ghcr.io
IMAGE_REPO: benc-uk/python-demoapp
jobs:
test:
name: "Tests & Linting"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Test/linting stub"
run: echo "Nothing here 😐"
# - name: "Upload test results"
# uses: actions/upload-artifact@v2
# with:
# name: test-results
# path: ./src/test-results.xml
# - name: "Report on test results"
# uses: ashley-taylor/junit-report-annotations-action@master
# if: always()
# with:
# access-token: ${{ secrets.GITHUB_TOKEN }}
# path: ./src/test-results.xml
# name: Unit test results
build:
name: "Build & Push Image"
needs: test
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Create datestamp image tag" # Nicer than using github runid, I think
run: echo "IMAGE_TAG=$(date +%d-%m-%Y.%H%M)" >> $GITHUB_ENV
- name: "Docker build image"
run: docker build . -t $IMAGE_REG/$IMAGE_REPO:$IMAGE_TAG
- name: "Login to GitHub container registry"
if: github.ref == 'refs/heads/master'
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REG }}
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: "Docker push image to ${{ env.IMAGE_REG }}"
if: github.ref == 'refs/heads/master'
run: docker push $IMAGE_REG/$IMAGE_REPO
- name: "Trigger AKS release pipeline"
if: github.ref == 'refs/heads/master'
uses: benc-uk/workflow-dispatch@v1
with:
workflow: "CD Release - AKS"
token: ${{ secrets.CR_PAT }}
inputs: '{ "IMAGE_TAG": "${{ env.IMAGE_TAG }}" }'
- name: "Trigger Azure web app release pipeline"
if: github.ref == 'refs/heads/master'
uses: benc-uk/workflow-dispatch@v1
with:
workflow: "CD Release - Webapp"
token: ${{ secrets.CR_PAT }}
inputs: '{ "IMAGE_TAG": "${{ env.IMAGE_TAG }}" }'

31
workflows-old/publish.yaml

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
name: Release Versioned Image
on:
workflow_dispatch:
release:
types: [published]
env:
IMAGE_REG: ghcr.io
IMAGE_REPO: benc-uk/python-demoapp
jobs:
publish-image:
name: "Build & Publish"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Docker build image with version tag"
run: docker build . -t $IMAGE_REG/$IMAGE_REPO:latest -t $IMAGE_REG/$IMAGE_REPO:${{ github.event.release.tag_name }}
- name: "Login to GitHub container registry"
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REG }}
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: "Docker push image to ${{ env.IMAGE_REG }}"
run: docker push $IMAGE_REG/$IMAGE_REPO
Loading…
Cancel
Save