├── src
├── bundle
│ ├── .gitignore
│ ├── zip_deploy.sh
│ ├── .dockerignore
│ ├── db_migration.sh
│ ├── utils.sh
│ ├── creds.json
│ ├── Dockerfile.tmpl
│ ├── params.json
│ ├── porter-install.sh
│ └── porter.yaml
├── webapp
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── src
│ │ ├── index.css
│ │ ├── App.test.js
│ │ ├── components
│ │ │ ├── Items.test.js
│ │ │ ├── Items.css
│ │ │ └── Items.js
│ │ ├── index.js
│ │ ├── App.js
│ │ ├── App.css
│ │ ├── logo.svg
│ │ └── registerServiceWorker.js
│ ├── server
│ │ ├── views
│ │ │ └── error.jade
│ │ ├── server.js
│ │ ├── routes
│ │ │ ├── index.js
│ │ │ └── item.js
│ │ ├── db
│ │ │ └── db.js
│ │ ├── db_migration
│ │ │ ├── seeds
│ │ │ │ └── 202011-01-seed.js
│ │ │ ├── migrations
│ │ │ │ └── 202011-01.js
│ │ │ ├── migrator.js
│ │ │ └── seed.js
│ │ ├── models
│ │ │ └── item.js
│ │ ├── package.json
│ │ ├── app.js
│ │ ├── test
│ │ │ └── item_test.js
│ │ └── web.config
│ ├── public
│ │ ├── favicon.ico
│ │ ├── manifest.json
│ │ └── index.html
│ ├── .gitignore
│ └── package.json
├── function
│ ├── proxies.json
│ ├── .funcignore
│ ├── tsconfig.json
│ ├── host.json
│ ├── EventGridHttpTrigger
│ │ ├── function.json
│ │ └── index.ts
│ ├── package.json
│ ├── .gitignore
│ └── package-lock.json
├── arm
│ ├── monitoring.bicep
│ ├── plan.bicep
│ ├── postgres.bicep
│ ├── main.bicep
│ ├── function.bicep
│ └── webapp.bicep
└── arc
│ └── lima-setup.sh
├── .devcontainer
├── content
│ ├── local.env
│ ├── connectedk8s-0.3.5-py2.py3-none-any.whl
│ ├── k8s_extension-0.1.0-py2.py3-none-any.whl
│ ├── appservice_kube-0.1.9-py2.py3-none-any.whl
│ ├── customlocation-0.1.0-py2.py3-none-any.whl
│ └── function.local.settings.json
├── docker-compose.yml
├── devcontainer.json
└── Dockerfile
├── docs
├── assets
│ ├── architecture-no-background.png
│ └── architecture-no-background-new.png
└── azure-arc.md
├── .gitignore
├── .vscode
├── app.code-workspace
├── settings.json
├── launch.json
└── tasks.json
├── environments
└── environments.yaml
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── deploy.yaml
│ └── build.yaml
├── notes.md
├── makefile
└── README.md
/src/bundle/.gitignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .cnab/
3 | output/
--------------------------------------------------------------------------------
/src/webapp/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["msjsdiag.debugger-for-chrome"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/webapp/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/function/proxies.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/proxies",
3 | "proxies": {}
4 | }
5 |
--------------------------------------------------------------------------------
/src/function/.funcignore:
--------------------------------------------------------------------------------
1 | *.js.map
2 | *.ts
3 | .git*
4 | .vscode
5 | local.settings.json
6 | test
7 | tsconfig.json
--------------------------------------------------------------------------------
/src/webapp/server/views/error.jade:
--------------------------------------------------------------------------------
1 | block content
2 | h1= message
3 | h2= error.status
4 | pre #{error.stack}
5 |
--------------------------------------------------------------------------------
/src/webapp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/src/webapp/public/favicon.ico
--------------------------------------------------------------------------------
/.devcontainer/content/local.env:
--------------------------------------------------------------------------------
1 | eventGridUrl=http://localhost:7071/api/EventGridHttpTrigger
2 | teamsWebhookUrl=
3 | webApiUrl=http://localhost:3001
--------------------------------------------------------------------------------
/docs/assets/architecture-no-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/docs/assets/architecture-no-background.png
--------------------------------------------------------------------------------
/docs/assets/architecture-no-background-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/docs/assets/architecture-no-background-new.png
--------------------------------------------------------------------------------
/.devcontainer/content/connectedk8s-0.3.5-py2.py3-none-any.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/.devcontainer/content/connectedk8s-0.3.5-py2.py3-none-any.whl
--------------------------------------------------------------------------------
/.devcontainer/content/k8s_extension-0.1.0-py2.py3-none-any.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/.devcontainer/content/k8s_extension-0.1.0-py2.py3-none-any.whl
--------------------------------------------------------------------------------
/.devcontainer/content/appservice_kube-0.1.9-py2.py3-none-any.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/.devcontainer/content/appservice_kube-0.1.9-py2.py3-none-any.whl
--------------------------------------------------------------------------------
/.devcontainer/content/customlocation-0.1.0-py2.py3-none-any.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/HEAD/.devcontainer/content/customlocation-0.1.0-py2.py3-none-any.whl
--------------------------------------------------------------------------------
/src/bundle/zip_deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | uri=`az webapp deployment list-publishing-credentials --ids $1 -o tsv --query scmUri`
4 | curl -X POST --data-binary @$2 $uri/api/zipdeploy?api-version=2020-12-01
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Mac finder
3 | .DS_Store
4 |
5 | # All ndoe modules
6 | node_modules
7 |
8 | # bicep compiled ARM files are ignored
9 | /src/arm/*.json
10 |
11 | # local tmp store
12 | .local/
13 | .azurite/
--------------------------------------------------------------------------------
/src/bundle/.dockerignore:
--------------------------------------------------------------------------------
1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file
2 | # Put files here that you don't want copied into your bundle's invocation image
3 | .gitignore
4 | Dockerfile.tmpl
5 |
--------------------------------------------------------------------------------
/src/bundle/db_migration.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Set up env
4 | export PGDB=$1
5 | export PGHOST=$2
6 | export PGPASSWORD=$3
7 | export PGUSER=$4
8 |
9 | # run the migration script command
10 | npm run migrate_db --prefix=webapi
--------------------------------------------------------------------------------
/src/function/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "dist",
6 | "rootDir": ".",
7 | "sourceMap": true,
8 | "strict": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.devcontainer/content/function.local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "",
5 | "FUNCTIONS_WORKER_RUNTIME": "node",
6 | "teamsWebhookUrl": "",
7 | "webApiUrl": "http://localhost:3001"
8 | }
9 | }
--------------------------------------------------------------------------------
/src/webapp/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/webapp/src/components/Items.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Items from './Items';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/bundle/utils.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo-azure-credentials() {
4 | echo $AZURE_CREDENTIALS
5 | }
6 |
7 | echo-web-api-hostname() {
8 | echo "The web API is located at: https://$1"
9 | }
10 |
11 | # Call requested function and pass arguments as-they-are
12 | "$@"
--------------------------------------------------------------------------------
/src/webapp/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/.vscode/app.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": ".."
5 | },
6 | {
7 | "path": "../src/function"
8 | },
9 | {
10 | "path": "../src/workflow"
11 | },
12 | {
13 | "path": "../src/webapp"
14 | }
15 | ],
16 | "settings": {
17 | "debug.internalConsoleOptions": "neverOpen",
18 | "azurite.silent": true
19 | }
20 | }
--------------------------------------------------------------------------------
/src/function/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | },
11 | "extensionBundle": {
12 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
13 | "version": "[1.*, 2.0.0)"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/bundle/creds.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0-DRAFT+b6c701f",
3 | "name": "webapi-nodejs",
4 | "created": "2021-01-13T14:11:02.294453+01:00",
5 | "modified": "2021-01-13T14:11:02.294453+01:00",
6 | "credentials": [
7 | {
8 | "name": "AZURE_CREDENTIALS",
9 | "source": {
10 | "env": "AZURE_CREDENTIALS"
11 | }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.deploySubpath": "src/function",
3 | "azureFunctions.postDeployTask": "npm install",
4 | "azureFunctions.projectLanguage": "TypeScript",
5 | "azureFunctions.projectRuntime": "~3",
6 | "azureFunctions.projectSubpath": "src/function",
7 | "azureFunctions.preDeployTask": "npm prune",
8 | "debug.internalConsoleOptions": "neverOpen"
9 | }
--------------------------------------------------------------------------------
/src/webapp/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/webapp/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | server/node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /server/build
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/src/webapp/src/components/Items.css:
--------------------------------------------------------------------------------
1 | table {
2 | border-collapse: collapse;
3 | }
4 | th {
5 | background: #ccc;
6 | }
7 |
8 | th, td {
9 | border: 1px solid #ccc;
10 | padding: 8px;
11 | }
12 |
13 | tr:nth-child(even) {
14 | background: #efefef;
15 | }
16 |
17 | tr:hover {
18 | background: #d1d1d1;
19 | }
20 |
21 | .itemsTable {
22 | margin-left: auto;
23 | margin-right: auto;
24 | }
--------------------------------------------------------------------------------
/src/function/EventGridHttpTrigger/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "post"
10 | ]
11 | },
12 | {
13 | "type": "http",
14 | "direction": "out",
15 | "name": "res"
16 | }
17 | ],
18 | "scriptFile": "../dist/EventGridHttpTrigger/index.js"
19 | }
20 |
--------------------------------------------------------------------------------
/src/webapp/server/server.js:
--------------------------------------------------------------------------------
1 | const appInsights = require('applicationinsights');
2 |
3 | if (!process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
4 | console.log(`Found no AI key. AI will not emit data.`);
5 | }
6 | else {
7 | appInsights.setup()
8 | .setSendLiveMetrics(true)
9 | .start();
10 | }
11 |
12 | const go = async () => {
13 |
14 | const App = require("./app");
15 | const app = new App();
16 |
17 | app.start();
18 | };
19 |
20 | go();
--------------------------------------------------------------------------------
/src/function/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "function",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "build": "tsc",
7 | "watch": "tsc -w",
8 | "prestart": "npm run build",
9 | "start": "func start",
10 | "test": "echo \"No tests yet...\""
11 | },
12 | "dependencies": {
13 | "@azure/functions": "^1.0.2-beta2",
14 | "axios": "^0.21.1",
15 | "guid-typescript": "^1.0.9",
16 | "typescript": "^3.3.3",
17 | "@types/node": "^14.14.31"
18 | },
19 | "devDependencies": {}
20 | }
21 |
--------------------------------------------------------------------------------
/src/webapp/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const items = require('./item');
2 | //const order = require('./order');
3 |
4 | const { response } = require('express');
5 |
6 | const Router = require('express-promise-router');
7 | const { models } = require('../db/db');
8 |
9 | const router = new Router();
10 |
11 | router.get('/message', function(req, res, next) {
12 | res.json('Welcome To Azure App Plat ordering system');
13 | });
14 |
15 | module.exports = app => {
16 | app.use('/api/items', items);
17 | app.use('/api', router);
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/src/webapp/server/db/db.js:
--------------------------------------------------------------------------------
1 | const { Sequelize } = require('sequelize');
2 |
3 | var sslOption = true;
4 |
5 | if (process.env.NODE_ENV === 'development') {
6 | sslOption = false;
7 | }
8 |
9 | const db = new Sequelize(process.env.PGDB,
10 | process.env.PGUSER,
11 | process.env.PGPASSWORD,
12 | {
13 | dialect: 'postgres',
14 | host: process.env.PGHOST,
15 | dialectOptions: {
16 | ssl: sslOption
17 | }
18 | });
19 |
20 | const modelDefiners = [
21 | require('../models/item')
22 | ];
23 |
24 | for (const modelDefiner of modelDefiners) {
25 | modelDefiner(db);
26 | }
27 |
28 | module.exports = db;
--------------------------------------------------------------------------------
/environments/environments.yaml:
--------------------------------------------------------------------------------
1 | - name: cloud
2 | deploys:
3 | version: "latest"
4 | config:
5 | AZURE_LOCATION: "westeurope"
6 | AZURE_NAME_PREFIX: "validate-azure"
7 | WEBAPI_NODE_ENV: "production"
8 | KUBE_ENVIRONMENT_ID: ""
9 | CUSTOM_LOCATION_ID: ""
10 | ARC_LOCATION: "eastus"
11 | #- name: onprem
12 | # deploys:
13 | # version: "v0.0.1-latest"
14 | # config:
15 | # AZURE_LOCATION: "eastus"
16 | # AZURE_NAME_PREFIX: "validate-arc-2"
17 | # WEBAPI_NODE_ENV: "production"
18 | # KUBE_ENVIRONMENT_ID: ""
19 | # CUSTOM_LOCATION_ID: ""
20 | # ARC_LOCATION: "eastus"
21 |
--------------------------------------------------------------------------------
/src/webapp/server/db_migration/seeds/202011-01-seed.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize');
2 | const { models, model } = require('../../db/db');
3 |
4 | //const item_names = ["item 1", "item 2"];
5 |
6 | module.exports = {
7 |
8 | up: async () => {
9 | /* await item_names.forEach(element => {
10 | models.item.create({ name: element })
11 | }); */
12 | },
13 |
14 | down: async () => {
15 | /* await item_names.forEach(element => {
16 | models.item.destroy({
17 | where: { name: element }
18 | })
19 | }); */
20 | }
21 | };
--------------------------------------------------------------------------------
/src/arm/monitoring.bicep:
--------------------------------------------------------------------------------
1 | param location string
2 | param name_prefix string
3 |
4 | resource workspace 'Microsoft.OperationalInsights/workspaces@2020-10-01' = {
5 | location : location
6 | name: '${name_prefix}-workspace'
7 | }
8 |
9 | resource insights 'Microsoft.Insights/components@2020-02-02-preview' = {
10 | location : location
11 | name: '${name_prefix}_insights_component'
12 | kind: 'web'
13 | properties: {
14 | Application_Type: 'web'
15 | WorkspaceResourceId: workspace.id
16 | }
17 | }
18 |
19 | output instrumentation_key string = insights.properties.InstrumentationKey
20 | output workspace_id string = insights.properties.WorkspaceResourceId
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: needs-triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/webapp/server/db_migration/migrations/202011-01.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 |
3 | module.exports = {
4 | up: async (query) => {
5 | await query.createTable('items', {
6 | id: {
7 | type: Sequelize.INTEGER,
8 | primaryKey: true,
9 | autoIncrement: true,
10 | allowNull: false
11 | },
12 | orderId: Sequelize.UUID,
13 | status: Sequelize.STRING,
14 | createdAt: Sequelize.DATE,
15 | updatedAt: Sequelize.DATE,
16 | workflowStatus: Sequelize.STRING
17 | }
18 | )
19 | },
20 | down: async (query) => {
21 | await query.dropTable('items');
22 | }
23 | }
--------------------------------------------------------------------------------
/src/webapp/server/db_migration/migrator.js:
--------------------------------------------------------------------------------
1 | const Umzug = require('umzug');
2 | const path = require('path');
3 | const db = require('../db/db');
4 |
5 | const umzug = new Umzug({
6 | storage: 'sequelize',
7 | storageOptions: {
8 | sequelize: db
9 | },
10 | logger: console,
11 |
12 | migrations: {
13 | path: path.join(__dirname, './migrations'),
14 | pattern: /\.js$/,
15 | params: [
16 | db.getQueryInterface()
17 | ]
18 | }
19 | });
20 |
21 | module.exports = {
22 | migrate : async function migrate() {
23 | console.log("Running migrations.");
24 | await umzug.up();
25 | },
26 | rollback : async function rollback() {
27 | console.log("Rollback migrations.");
28 | await umzug.down();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/webapp/server/db_migration/seed.js:
--------------------------------------------------------------------------------
1 | const Umzug = require('umzug');
2 | const path = require('path');
3 | const db = require('../db/db');
4 |
5 | const umzug = new Umzug({
6 | storage: 'sequelize',
7 | storageOptions: {
8 | sequelize: db,
9 | tableName: 'SequelizeMetaSeed'
10 | },
11 | logger: console,
12 |
13 | migrations: {
14 | path: path.join(__dirname, './seeds'),
15 | pattern: /\.js$/,
16 | params: [
17 | db.getQueryInterface()
18 | ]
19 | }
20 | });
21 |
22 | module.exports = {
23 | seed: async function seed() {
24 | console.log("Seeding the database.");
25 | await umzug.up();
26 | },
27 | rollback: async function rollback() {
28 | console.log("Rollback seeds.");
29 | await umzug.down();
30 | }
31 | };
--------------------------------------------------------------------------------
/src/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-react-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "body-parser": "^1.19.0",
7 | "cookie-parser": "^1.4.3",
8 | "debug": "^2.6.9",
9 | "express": "^4.17.1",
10 | "jade": "^1.11.0",
11 | "morgan": "^1.10.0",
12 | "react": "^15.6.1",
13 | "react-dom": "^15.6.1"
14 | },
15 | "devDependencies": {
16 | "concurrently": "^3.5.0",
17 | "nodemon": "^1.12.0",
18 | "react-scripts": "1.0.10"
19 | },
20 | "scripts": {
21 | "start": "concurrently \"nodemon server/server.js\" \"react-scripts start\"",
22 | "build": "react-scripts build && rm -Rf server/build && mv build server",
23 | "test": "react-scripts test --env=jsdom",
24 | "eject": "react-scripts eject"
25 | },
26 | "proxy": "http://localhost:3001"
27 | }
28 |
--------------------------------------------------------------------------------
/src/webapp/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from './logo.svg';
3 | import './App.css';
4 | import Items from './components/Items';
5 |
6 | class App extends Component {
7 | constructor() {
8 | super();
9 | this.state = { message: '' };
10 | }
11 |
12 | componentDidMount() {
13 | fetch('/api/message')
14 | .then(response => response.json())
15 | .then(json => this.setState({ message: json }))
16 | .catch(error => this.setState({ message: 'Welcome'}));
17 | }
18 |
19 | render() {
20 | return (
21 |
22 |
23 |

24 |
{this.state.message}
25 |
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export default App;
34 |
--------------------------------------------------------------------------------
/src/webapp/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "compounds": [
7 | {
8 | "name": "Launch React and Express",
9 | "configurations": ["Launch React", "Launch Express"]
10 | }
11 | ],
12 | "configurations": [
13 | {
14 | "type": "chrome",
15 | "request": "launch",
16 | "name": "Launch React",
17 | "preLaunchTask": "npm: start",
18 | "url": "http://localhost:3000",
19 | "webRoot": "${workspaceRoot}/src"
20 | },
21 | {
22 | "type": "node",
23 | "request": "launch",
24 | "name": "Launch Express",
25 | "program": "${workspaceRoot}/server/server.js"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/src/webapp/server/models/item.js:
--------------------------------------------------------------------------------
1 | const { DataTypes, Sequelize } = require('sequelize');
2 |
3 | module.exports = (sequelize) => {
4 | sequelize.define('item', {
5 | id: {
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | type: DataTypes.INTEGER
10 | },
11 | orderId: {
12 | type:DataTypes.UUID,
13 | defaultValue: Sequelize.UUIDV4
14 | },
15 | status: {
16 | type: DataTypes.STRING,
17 | defaultValue: 'new'
18 | },
19 | createdAt: {
20 | type: DataTypes.DATE,
21 | defaultValue: DataTypes.NOW
22 | },
23 | updatedAt: {
24 | type: DataTypes.DATE,
25 | defaultValue: DataTypes.NOW
26 | },
27 | workflowStatus: {
28 | type: DataTypes.STRING,
29 | defaultValue: 'not started'
30 | }
31 | });
32 |
33 | return 'item';
34 | };
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: needs-triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/webapp/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-react-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@azure/identity": "^1.2.2",
7 | "applicationinsights": "^1.8.9",
8 | "applicationinsights-native-metrics": "0.0.5",
9 | "body-parser": "^1.17.2",
10 | "cookie-parser": "^1.4.3",
11 | "express": "^4.17.1",
12 | "morgan": "^1.8.2",
13 | "debug": "~2.6.3",
14 | "jade": "^1.11.0",
15 | "axios": "^0.21.1",
16 | "express-oas-generator": "^1.0.30",
17 | "express-promise-router": "^4.0.1",
18 | "pg": "^8.5.1",
19 | "pg-hstore": "^2.3.3",
20 | "sequelize": "^6.3.5",
21 | "umzug": "^2.3.0"
22 | },
23 | "devDependencies": {
24 | "chai": "*",
25 | "chai-http": "*",
26 | "mocha": "*"
27 | },
28 | "scripts": {
29 | "start": "node server.js",
30 | "test": "mocha --exit",
31 | "migrate_db": "node -e 'require(\"./db_migration/migrator.js\").migrate()'",
32 | "seed_db": "node -e 'require(\"./db_migration/seed.js\").seed()'"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/notes.md:
--------------------------------------------------------------------------------
1 | # Lima
2 |
3 | ## Create Postgres database in Azure
4 |
5 | PGHOST: {hostname}.postgres.database.azure.com
6 | PGUSER: postgres
7 | PGPASSWORD:
8 | PGDB: postgres
9 |
10 | ## ARM
11 |
12 | 1. Build ARM template
13 | `az bicep build -f main.bicep`
14 | 1. Deploy ARM template
15 | ```bash
16 | az deployment group create -g {rgName} --template-file main.json --parameters location=westeurope name_prefix={prefix} kubeEnvironment_id=""
17 | postgres_adminPassword=""
18 | teamsWebhookUrl=""
19 | ```
20 |
21 | ## Functions and WebApp deployment
22 |
23 | make zip_it
24 |
25 | ./zip_deploy.sh "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{site}" output/app/webapi.zip
26 |
27 | ./zip_deploy.sh "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{site}" output/function/function.zip
28 |
29 | seed the database
30 |
31 | ## In GitHub
32 |
33 | 1. Arc: bool in environment.yaml
34 | 2. Deploy workflow to know whether to pass is_kubeenvironment to arm template
35 | 3. kube_envoronment_id as GitHub secret is still a thing
36 |
--------------------------------------------------------------------------------
/src/webapp/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-intro {
18 | font-size: large;
19 | }
20 |
21 | @keyframes App-logo-spin {
22 | from { transform: rotate(0deg); }
23 | to { transform: rotate(360deg); }
24 | }
25 | .btn {
26 | border: none;
27 | font-family: inherit;
28 | font-size: inherit;
29 | color: inherit;
30 | background: none;
31 | cursor: pointer;
32 | padding: 10px 40px;
33 | display: inline-block;
34 | margin: 15px 30px;
35 | text-transform: uppercase;
36 | letter-spacing: 1px;
37 | font-weight: 300;
38 | outline: none;
39 | position: relative;
40 | -webkit-transition: all 0.3s;
41 | -moz-transition: all 0.3s;
42 | transition: all 0.3s;
43 | }
44 |
45 | /* Button 1 */
46 | .btn-1 {
47 | border: 3px solid #000;
48 | color: #000;
49 | }
50 |
51 | /* Button 1a */
52 | .btn-1a:hover,
53 | .btn-1a:active {
54 | color: #fff;
55 | background: rgb(90, 122, 205);
56 | }
--------------------------------------------------------------------------------
/src/arm/plan.bicep:
--------------------------------------------------------------------------------
1 | param name_prefix string
2 | param location string
3 | param arcLocation string
4 | param customLocationId string
5 | param kubeEnvironmentId string
6 |
7 | var webfarm_name = '${name_prefix}-webfarm-${uniqueString(resourceGroup().id)}'
8 |
9 | resource webapi_farm_azure 'Microsoft.Web/serverfarms@2020-06-01' = if (customLocationId == '') {
10 | name: webfarm_name
11 | location: location
12 | kind: 'linux'
13 | sku: {
14 | name: 'P1V2'
15 | }
16 | properties: {
17 | reserved: true
18 | }
19 | }
20 |
21 | resource webapi_farm_arc 'Microsoft.Web/serverfarms@2020-12-01' = if (customLocationId != '') {
22 | name: concat(webfarm_name, 'arc')
23 | location: arcLocation
24 | kind: 'linux,kubernetes'
25 | sku: {
26 | name: 'K1'
27 | tier: 'Kubernetes'
28 | capacity: 1
29 | }
30 | extendedLocation: {
31 | type: 'CustomLocation'
32 | name: customLocationId
33 | }
34 | properties: {
35 | reserved: true
36 | perSiteScaling: true
37 | isXenon: false
38 | kubeEnvironmentProfile: {
39 | id: kubeEnvironmentId
40 | }
41 | }
42 | }
43 |
44 | output plan_id string = customLocationId == '' ? webapi_farm_azure.id : webapi_farm_arc.id
45 |
--------------------------------------------------------------------------------
/src/bundle/Dockerfile.tmpl:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | ARG BUNDLE_DIR
4 |
5 | RUN apt-get update && apt-get install -y ca-certificates
6 |
7 | # This is a template Dockerfile for the bundle's invocation image
8 | # You can customize it to use different base images, install tools and copy configuration files.
9 | #
10 | # Porter will use it as a template and append lines to it for the mixins
11 | # and to set the CMD appropriately for the CNAB specification.
12 | #
13 | # Add the following line to porter.yaml to instruct Porter to use this template
14 | # dockerfile: Dockerfile.tmpl
15 |
16 | # You can control where the mixin's Dockerfile lines are inserted into this file by moving "# PORTER_MIXINS" line
17 | # another location in this file. If you remove that line, the mixins generated content is appended to this file.
18 | # PORTER_MIXINS
19 |
20 | # Use the BUNDLE_DIR build argument to copy files into the bundle
21 | COPY . $BUNDLE_DIR
22 | RUN chmod +x $BUNDLE_DIR/db_migration.sh
23 | RUN chmod +x $BUNDLE_DIR/zip_deploy.sh
24 | RUN chmod +x $BUNDLE_DIR/utils.sh
25 | RUN /bin/bash -c "az extension add --yes --source https://k8seazurecliextensiondev.blob.core.windows.net/azure-cli-extension/appservice_kube-0.1.8-py2.py3-none-any.whl"
26 |
--------------------------------------------------------------------------------
/src/bundle/params.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0-DRAFT+TODO",
3 | "name": "webapi-nodejs",
4 | "created": "2021-01-13T14:13:54.796881+01:00",
5 | "modified": "2021-01-13T14:13:54.796881+01:00",
6 | "parameters": [
7 | {
8 | "name": "LOCATION",
9 | "source": {
10 | "env": "LOCATION"
11 | }
12 | },
13 | {
14 | "name": "NAME_PREFIX",
15 | "source": {
16 | "env": "NAME_PREFIX"
17 | }
18 | },
19 | {
20 | "name": "POSTGRES_PASSWORD",
21 | "source": {
22 | "env": "POSTGRES_PASSWORD"
23 | }
24 | },
25 | {
26 | "name": "TEAMS_WEBHOOK_URL",
27 | "source": {
28 | "env": "TEAMS_WEBHOOK_URL"
29 | }
30 | },
31 | {
32 | "name": "WEBAPI_NODE_ENV",
33 | "source": {
34 | "env": "WEBAPI_NODE_ENV"
35 | }
36 | },
37 | {
38 | "name": "KUBE_ENVIRONMENT_ID",
39 | "source": {
40 | "env": "KUBE_ENVIRONMENT_ID"
41 | }
42 | },
43 | {
44 | "name": "CUSTOM_LOCATION_ID",
45 | "source": {
46 | "env": "CUSTOM_LOCATION_ID"
47 | }
48 | },
49 | {
50 | "name": "ARC_LOCATION",
51 | "source": {
52 | "env": "ARC_LOCATION"
53 | }
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/src/webapp/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const http = require('http');
3 | const path = require('path');
4 | const bodyParser = require('body-parser');
5 | const mountRoutes = require('./routes');
6 | const db = require('./db/db');
7 | const expressOasGenerator = require('express-oas-generator');
8 |
9 | class App {
10 | app = express();
11 |
12 | constructor() {
13 | expressOasGenerator.handleResponses(this.app, {});
14 | this.app.use(bodyParser.json());
15 | this.app.use(express.static(path.join(__dirname, 'build')));
16 | this.app.set('views', path.join(__dirname, 'views'));
17 | this.app.set('view engine', 'jade');
18 | mountRoutes(this.app);
19 | expressOasGenerator.handleRequests(this.app, {});
20 | };
21 |
22 | start = async () => {
23 | await this.checkDBConnection();
24 |
25 | var port = (process.env.PORT || '3001');
26 | var server = http.createServer(this.app);
27 | server.listen(port, () => console.log(`Server now listening on ${port}`));
28 | };
29 |
30 | checkDBConnection = async () => {
31 | try {
32 | console.log(`Trying to connect to: ${process.env.PGHOST}`);
33 | await db.authenticate();
34 | console.log(`Database connection OK!`);
35 |
36 | } catch (error) {
37 | console.log(`Unable to connect to the database:`);
38 | console.log(error.message);
39 | process.exit(1);
40 | }
41 | };
42 | };
43 |
44 | module.exports = App;
--------------------------------------------------------------------------------
/src/webapp/server/test/item_test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | const should = require('chai').should();
3 |
4 | // Seed data to the database
5 | const migrator = require('../db_migration/migrator');
6 | const db = require('../db/db');
7 | const { model, models } = require('../db/db');
8 |
9 | before(async () => {
10 | await migrator.migrate();
11 |
12 | const item_names = ["test1", "test2"];
13 | await item_names.forEach(element => {
14 | models.item.create({ name: element })
15 | });
16 | });
17 |
18 | // Test that there's data in the database
19 | describe('Get data from database using db.query', () => {
20 | it('it should return two rows', async () => {
21 | var rows = await db.query('SELECT * FROM Items');
22 | rows.length.should.be.above(1);
23 | });
24 | });
25 |
26 | // Test that we get two items back from the webapi
27 | const chaiHttp = require('chai-http');
28 | const http = require('http');
29 | chai.use(chaiHttp);
30 |
31 | const App = require('../app');
32 | const app = new App();
33 | app.start();
34 |
35 | const API = 'http://localhost:3001'
36 |
37 | describe('HTTP call to /GET items', () => {
38 | it('it should GET all two items', (done) => {
39 | chai.request(API)
40 | .get('/api/items')
41 | .end((err, res) => {
42 | res.should.have.status(200);
43 | res.body.should.be.a('array');
44 | res.body.length.should.be.above(1);
45 | done();
46 | });
47 | });
48 | });
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | args:
9 | # [Choice] Node.js version: 14, 12, 10
10 | VARIANT: 14
11 | # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
12 | USER_UID: 1000
13 | USER_GID: 1000
14 |
15 | volumes:
16 | - ..:/workspace:cached
17 |
18 | # Overrides default command so things don't shut down after the process ends.
19 | command: sleep infinity
20 |
21 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
22 | network_mode: service:db
23 |
24 | # Uncomment the next line to use a non-root user for all processes.
25 | # user: node
26 |
27 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
28 | # (Adding the "ports" property to this file will not forward from a Codespace.)
29 |
30 | db:
31 | image: postgres:latest
32 | restart: unless-stopped
33 | volumes:
34 | - postgres-data:/var/lib/postgresql/data
35 | environment:
36 | POSTGRES_PASSWORD: postgres
37 | POSTGRES_USER: postgres
38 | POSTGRES_DB: postgres
39 |
40 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward Postgress locally.
41 | # (Adding the "ports" property to this file will not forward from a Codespace.)
42 |
43 | volumes:
44 | postgres-data:
--------------------------------------------------------------------------------
/src/bundle/porter-install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xeuo pipefail
3 |
4 | PORTER_HOME=~/.porter
5 | PORTER_URL=https://cdn.porter.sh
6 | PORTER_PERMALINK=${PORTER_PERMALINK:-v0.36.0}
7 | PKG_PERMALINK=${PKG_PERMALINK:-latest}
8 | PORTER_TRACE=$(date +%s_%N)
9 | echo "Installing porter to $PORTER_HOME"
10 | echo "PORTER_TRACE: $PORTER_TRACE"
11 |
12 | mkdir -p $PORTER_HOME/runtimes
13 |
14 | curl --http1.1 -v -H "X-Azure-DebugInfo: 1" -A "curl porter_install/$PORTER_PERMALINK porter_trace_$PORTER_TRACE" -fsSLo $PORTER_HOME/porter $PORTER_URL/$PORTER_PERMALINK/porter-linux-amd64
15 | chmod +x $PORTER_HOME/porter
16 | cp $PORTER_HOME/porter $PORTER_HOME/runtimes/porter-runtime
17 | echo Installed `$PORTER_HOME/porter version`
18 |
19 | #$PORTER_HOME/porter mixin install exec --version $PKG_PERMALINK
20 | #$PORTER_HOME/porter mixin install kubernetes --version $PKG_PERMALINK
21 | #$PORTER_HOME/porter mixin install helm --version $PKG_PERMALINK
22 | #$PORTER_HOME/porter mixin install arm --version $PKG_PERMALINK
23 | #$PORTER_HOME/porter mixin install terraform --version $PKG_PERMALINK
24 | #$PORTER_HOME/porter mixin install az --version $PKG_PERMALINK
25 | #$PORTER_HOME/porter mixin install aws --version $PKG_PERMALINK
26 | #$PORTER_HOME/porter mixin install gcloud --version $PKG_PERMALINK
27 |
28 | #$PORTER_HOME/porter plugin install azure --version $PKG_PERMALINK
29 |
30 | echo "Installation complete."
31 | echo "Add porter to your path by adding the following line to your ~/.profile and open a new terminal:"
32 | echo "export PATH=\$PATH:~/.porter"
33 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "compounds": [
4 | {
5 | "name": "Debug Full App",
6 | "configurations": [
7 | "Debug Web App",
8 | "Attach to Node Functions"
9 | ],
10 | "presentation": {
11 | "hidden": false,
12 | "order": 1
13 | }
14 | }
15 | ],
16 | "configurations": [
17 | {
18 | "type": "pwa-node",
19 | "request": "launch",
20 | "name": "Debug Web App",
21 | "program": "${workspaceFolder}/src/webapp/server/server.js",
22 | "internalConsoleOptions": "openOnSessionStart",
23 | "envFile": "${workspaceFolder}/.local/.env",
24 | "preLaunchTask": "make: build",
25 | "serverReadyAction": {
26 | "pattern": "Server now listening on ([0-9]+)",
27 | "uriFormat": "http://localhost:%s",
28 | "action": "openExternally"
29 | }
30 | },
31 | {
32 | "type": "node",
33 | "request": "launch",
34 | "name": "Debug Web Server Tests",
35 | "program": "${workspaceFolder}/src/webapp/server/node_modules/mocha/bin/_mocha",
36 | "args": [
37 | "--timeout",
38 | "999999",
39 | "--colors",
40 | "${workspaceFolder}/src/webapp/server/test"
41 | ],
42 | "console": "integratedTerminal",
43 | "internalConsoleOptions": "openOnSessionStart",
44 | "skipFiles": [
45 | "/**/*.js"
46 | ],
47 | "preLaunchTask": "make: build"
48 | },
49 | {
50 | "name": "Attach to Node Functions",
51 | "type": "node",
52 | "request": "attach",
53 | "port": 9229,
54 | "preLaunchTask": "func: host start"
55 | }
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/src/webapp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Azure App Plat - React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | webapp_dir = src/webapp
2 | function_dir = src/function
3 | workflow_dir = src/workflow
4 |
5 | .PHONY: init
6 | init:
7 | cp .devcontainer/content/function.local.settings.json $(function_dir)/local.settings.json
8 | mkdir -p .local && cp .devcontainer/content/local.env .local/.env
9 | make seed_db
10 | npm install --prefix $(function_dir)
11 | npm run build --prefix $(function_dir)
12 |
13 | .PHONY: test
14 | test : build
15 | npm run test --prefix $(webapp_dir)/server
16 |
17 | .PHONY: start
18 | start : build
19 | npm run start --prefix $(webapp_dir)
20 |
21 | .PHONY: clean
22 | clean :
23 | rm -r $(webapp_dir)/node_modules
24 |
25 | .PHONY: build
26 | build : install
27 | npm run build --prefix $(webapp_dir) & \
28 | npm run build --prefix $(webapp_dir)/server & \
29 | npm run build --prefix $(function_dir)
30 |
31 | .PHONY: install
32 | install :
33 | npm install --prefix $(webapp_dir) & \
34 | npm install --prefix $(webapp_dir)/server & \
35 | npm install --prefix $(function_dir) & \
36 | wait
37 |
38 | .PHONY: migrate_db
39 | migrate_db : build
40 | npm run migrate_db --prefix $(webapp_dir)/server
41 |
42 | .PHONY: seed_db
43 | seed_db : migrate_db
44 | npm run seed_db --prefix $(webapp_dir)/server
45 |
46 | .PHONY: remove_db
47 | remove_db :
48 | dropdb postgres && createdb postgres
49 |
50 | .PHONY: zip_it
51 | zip_it :
52 | cd $(webapp_dir)/server; zip -r ../../../webapi.zip .; cd ../../../
53 | cd $(function_dir); zip -r ../../function.zip .; cd ../../
54 | bicep build src/arm/main.bicep
55 | mkdir -p src/bundle/output/app && mv webapi.zip src/bundle/output/app/webapi.zip -f
56 | mkdir -p src/bundle/output/function && mv function.zip src/bundle/output/function/function.zip -f
57 | mkdir -p src/bundle/output/arm && mv src/arm/main.json src/bundle/output/arm/main.json -f
--------------------------------------------------------------------------------
/src/function/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 |
86 | # TypeScript output
87 | dist
88 | out
89 |
90 | # Azure Functions artifacts
91 | bin
92 | obj
93 | appsettings.json
94 | local.settings.json
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // Update the VARIANT arg in docker-compose.yml to pick a Node.js version: 10, 12, 14
2 | {
3 | "name": "React+Express and PostgreSQL",
4 | "dockerComposeFile": "docker-compose.yml",
5 | "service": "app",
6 | "workspaceFolder": "/workspace",
7 |
8 | // Set env. variable
9 | "remoteEnv": {
10 | "PGHOST" : "db",
11 | "PGPORT" : "5432",
12 | "PGDB" : "postgres",
13 | "PGUSER" : "postgres",
14 | "PGPASSWORD" : "postgres",
15 | "PGUSESSL" : "false",
16 | "NODE_ENV" : "development"
17 | },
18 |
19 | // Set *default* container specific settings.json values on container create.
20 | "settings": {
21 | "terminal.integrated.shell.linux": "/bin/bash",
22 | "sqltools.connections": [{
23 | "name": "Container database",
24 | "driver": "PostgreSQL",
25 | "previewLimit": 50,
26 | "server": "localhost",
27 | "port": 5432,
28 | "database": "postgres",
29 | "username": "postgres",
30 | "password": "postgres"
31 | }],
32 | "yaml.schemas": {
33 | "https://json.schemastore.org/github-workflow": ["/.github/workflows/*"]
34 | }
35 | },
36 |
37 | // Add the IDs of extensions you want installed when the container is created.
38 | "extensions": [
39 | "dbaeumer.vscode-eslint",
40 | "mtxr.sqltools",
41 | "mtxr.sqltools-driver-pg",
42 | "ms-azuretools.vscode-bicep",
43 | "redhat.vscode-yaml",
44 | "ms-kubernetes-tools.porter-vscode",
45 | "ms-azuretools.vscode-azurefunctions",
46 | "ms-dotnettools.csharp",
47 | "humao.rest-client"
48 | ],
49 |
50 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
51 | "forwardPorts": [7071],
52 |
53 | // Use 'postCreateCommand' to run commands after the container is created.
54 | "postCreateCommand": "make init",
55 |
56 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
57 | "remoteUser": "node"
58 | }
59 |
--------------------------------------------------------------------------------
/src/function/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "function",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@azure/functions": {
8 | "version": "1.2.3",
9 | "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.2.3.tgz",
10 | "integrity": "sha512-dZITbYPNg6ay6ngcCOjRUh1wDhlFITS0zIkqplyH5KfKEAVPooaoaye5mUFnR+WP9WdGRjlNXyl/y2tgWKHcRg=="
11 | },
12 | "@types/node": {
13 | "version": "14.14.31",
14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
15 | "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
16 | },
17 | "axios": {
18 | "version": "0.21.1",
19 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
20 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
21 | "requires": {
22 | "follow-redirects": "^1.10.0"
23 | }
24 | },
25 | "follow-redirects": {
26 | "version": "1.13.2",
27 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz",
28 | "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA=="
29 | },
30 | "guid-typescript": {
31 | "version": "1.0.9",
32 | "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
33 | "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
34 | },
35 | "typescript": {
36 | "version": "3.9.7",
37 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
38 | "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw=="
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/arm/postgres.bicep:
--------------------------------------------------------------------------------
1 | param location string
2 | param name_prefix string
3 | param workspace_id string
4 |
5 | param administratorLogin string = 'postgres_admin'
6 | @secure()
7 | param administratorLoginPassword string
8 |
9 | var server_name = '${name_prefix}-postgres-${uniqueString(resourceGroup().id)}'
10 |
11 | resource server 'Microsoft.DBforPostgreSQL/servers@2017-12-01' = {
12 | name: server_name
13 | location: location
14 | properties: {
15 | createMode: 'Default'
16 | administratorLogin: administratorLogin
17 | administratorLoginPassword: administratorLoginPassword
18 | sslEnforcement: 'Enabled'
19 | }
20 | }
21 |
22 | resource database 'Microsoft.DBForPostgreSQL/servers/databases@2017-12-01' = {
23 | name: '${server.name}/my_postgres'
24 | }
25 |
26 | resource firewall_rules 'Microsoft.DBForPostgreSQL/servers/firewallRules@2017-12-01' = {
27 | name: '${server.name}/AllowAny'
28 | properties: {
29 | startIpAddress: '0.0.0.0'
30 | endIpAddress: '255.255.255.255'
31 | }
32 | }
33 |
34 | resource diagnostics 'microsoft.insights/diagnosticSettings@2017-05-01-preview' = {
35 | scope: server
36 | name: 'logAnalytics'
37 | properties:{
38 | workspaceId: workspace_id
39 | logs:[
40 | {
41 | enabled: true
42 | category: 'PostgreSQLLogs'
43 | }
44 | {
45 | enabled: true
46 | category: 'QueryStoreRuntimeStatistics'
47 | }
48 | {
49 | enabled: true
50 | category: 'QueryStoreWaitStatistics'
51 | }
52 | ]
53 | metrics:[
54 | {
55 | enabled: true
56 | category: 'AllMetrics'
57 | }
58 | ]
59 | }
60 | }
61 |
62 | output pg_host string = server.properties.fullyQualifiedDomainName
63 | output pg_user string = administratorLogin
64 | output pg_password string = administratorLoginPassword
65 | output pg_db string = last(split(database.name, '/'))
66 |
--------------------------------------------------------------------------------
/src/webapp/server/routes/item.js:
--------------------------------------------------------------------------------
1 | const { response } = require('express');
2 | const axios = require('axios').default;
3 | const Router = require('express-promise-router');
4 | const { models, Sequelize } = require('../db/db');
5 | const router = new Router();
6 |
7 | const eventGridUrl = process.env.eventGridUrl;
8 |
9 | module.exports = router;
10 |
11 | router.get('/', async (req, res) => {
12 | console.log('Returning all orders');
13 |
14 | const rows = await models.item.findAll({
15 | order: [
16 | ['createdAt', 'DESC']
17 | ]
18 | });
19 |
20 | res.json(rows);
21 | });
22 |
23 | router.get('/:uuid', async (req, res) => {
24 | console.log('Returning specific order');
25 |
26 | const item = await models.item.findOne({ where: { orderId: req.params.uuid }});
27 |
28 | res.json(item);
29 | });
30 |
31 | router.post('/', async (req, res) => {
32 | console.log('Creating order');
33 | const item = await models.item.create();
34 |
35 | console.log(`Triggering Function at ${eventGridUrl}`);
36 | const fnreply = await axios.post(eventGridUrl, item);
37 |
38 | item.workflowStatus = fnreply.data.newWorkflowStatus;
39 | item.status = fnreply.data.newStatus;
40 | console.log(`Workflow status: ${item.workflowStatus} and status: ${item.status}`);
41 |
42 | console.log(`Saving item: ${item.uuid}`);
43 | await item.save();
44 |
45 | console.log(`Saved`);
46 | res.json(item);
47 | });
48 |
49 | router.post('/:uuid', async (req, res) => {
50 | console.log('Updating order');
51 | const item = await models.item.findOne({ where: { orderId: req.params.uuid }});
52 |
53 | item.workflowStatus = 'completed';
54 | item.status = 'received user input';
55 | console.log(`Workflow status: ${item.workflowStatus} and status: ${item.status}`);
56 |
57 | console.log(`Saving item: ${item.uuid}`);
58 | await item.save();
59 |
60 | console.log(`Saved`);
61 | res.json(item);
62 | })
--------------------------------------------------------------------------------
/src/webapp/src/components/Items.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Items.css';
3 |
4 | class Items extends Component {
5 | constructor() {
6 | super();
7 | this.state = {
8 | items: []
9 | }
10 | }
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 | {
33 | this.state.items.length > 0
34 | ?
35 | :
36 | }
37 |
38 | );
39 | }
40 | }
41 |
42 |
43 | export default Items;
44 |
45 |
46 | function ItemsTable(props) {
47 | return (
48 |
49 |
Items Catalogue
50 |
51 |
52 |
53 | | Order Id |
54 | Created At |
55 | Updated At |
56 | Status |
57 | Workflow Status |
58 |
59 | {
60 | props.items.map((item, i) => (
61 |
62 | | {item.orderId} |
63 | {item.createdAt} |
64 | {item.updatedAt} |
65 | {item.status} |
66 | {item.workflowStatus} |
67 |
68 | ))
69 | }
70 |
71 |
72 |
73 | );
74 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "shell",
6 | "command": "make test",
7 | "group": "test",
8 | "label": "make: test"
9 | },
10 | {
11 | "type": "shell",
12 | "command": "make start",
13 | "group": "none",
14 | "label": "make: start"
15 | },
16 | {
17 | "type": "shell",
18 | "command": "make build",
19 | "group": "build",
20 | "label": "make: build"
21 | },
22 | {
23 | "type": "shell",
24 | "command": "make clean",
25 | "group": "none",
26 | "label": "make: clean"
27 | },
28 | {
29 | "type": "shell",
30 | "command": "make migrate_db",
31 | "group": "none",
32 | "label": "make: migrate_db"
33 | },
34 | {
35 | "type": "shell",
36 | "command": "make seed_db",
37 | "group": "none",
38 | "label": "make: seed_db"
39 | },
40 | {
41 | "type": "shell",
42 | "command": "make remove_db",
43 | "group": "none",
44 | "label": "make: remove_db"
45 | },
46 | {
47 | "type": "func",
48 | "command": "host start",
49 | "problemMatcher": "$func-node-watch",
50 | "isBackground": true,
51 | "dependsOn": "function: npm build",
52 | "options": {
53 | "cwd": "${workspaceFolder}/src/function"
54 | }
55 | },
56 | {
57 | "type": "shell",
58 | "label": "function: npm build",
59 | "command": "npm run build",
60 | "dependsOn": "npm install",
61 | "problemMatcher": "$tsc",
62 | "options": {
63 | "cwd": "${workspaceFolder}/src/function"
64 | }
65 | },
66 | {
67 | "type": "shell",
68 | "label": "function: npm install",
69 | "command": "npm install",
70 | "options": {
71 | "cwd": "${workspaceFolder}/src/function"
72 | }
73 | },
74 | {
75 | "type": "shell",
76 | "label": "function: npm prune",
77 | "command": "npm prune --production",
78 | "dependsOn": "npm build",
79 | "problemMatcher": [],
80 | "options": {
81 | "cwd": "${workspaceFolder}/src/function"
82 | }
83 | },
84 | {
85 | "type": "shell",
86 | "label": "webapp: npm start",
87 | "command": "npm start",
88 | "options": {
89 | "cwd": "${workspaceFolder}/src/webapp"
90 | }
91 | },
92 | ]
93 | }
--------------------------------------------------------------------------------
/src/function/EventGridHttpTrigger/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from "@azure/functions"
2 | import axios from "axios";
3 |
4 | const teamsWebhookUrl = process.env.teamsWebhookUrl;
5 | const webApiUrl = process.env.webApiUrl;
6 |
7 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
8 | context.log('HTTP trigger function starting to process a request.');
9 |
10 | if (typeof teamsWebhookUrl !== 'undefined' || typeof webApiUrl !== 'undefined') {
11 | let title = "Hey Ho Let's Go - New Order!!!";
12 | let message = `Incoming order needs manual attention: ${context.req.body.orderId}`;
13 |
14 | context.log('Calling Teams');
15 | let teamsCallStatus = await postMessageToTeams(title, message);
16 | context.log(`teamsCallStatus: ${teamsCallStatus}`);
17 | }
18 | else {
19 | context.log('Not configured to talk to Team');
20 | }
21 |
22 | context.log('HTTP trigger function done processing a request.');
23 |
24 | context.res = {
25 | body: {
26 | newStatus: "awaiting user input",
27 | newWorkflowStatus: "processing"
28 | }
29 | };
30 |
31 | async function postMessageToTeams(title, message) {
32 | const card = {
33 | "@type": "MessageCard",
34 | "@context": "https://schema.org/extensions",
35 | "themeColor": "0078D7",
36 | "title": title,
37 | "text": message,
38 | "potentialAction": [
39 | {
40 | "@type": "HttpPOST",
41 | "name": "It's done",
42 | "isPrimary": true,
43 | "target": `${webApiUrl}/api/items/${context.req.body.orderId}`
44 | },
45 | {
46 | "@type": "OpenUri",
47 | "name": "View order",
48 | "targets": [
49 | {
50 | "os": "default",
51 | "uri": `${webApiUrl}/api/items/${context.req.body.orderId}`
52 | }
53 | ]
54 | }
55 | ]
56 | }
57 | try {
58 | const response = await axios.post(teamsWebhookUrl, card, {
59 | headers: {
60 | 'content-type': 'application/vnd.microsoft.teams.card.o365connector',
61 | 'content-length': `${card.toString().length}`,
62 | },
63 | });
64 | return `${response.status} - ${response.statusText}`;
65 | } catch (err) {
66 | return err;
67 | }
68 | }
69 | };
70 |
71 | export default httpTrigger;
--------------------------------------------------------------------------------
/src/webapp/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/arm/main.bicep:
--------------------------------------------------------------------------------
1 | param location string
2 | param name_prefix string
3 | param kubeEnvironment_id string
4 | param customLocation_id string
5 | param arcLocation string
6 | @secure()
7 | param postgres_adminPassword string
8 | @secure()
9 | param teamsWebhookUrl string
10 | param webapi_node_env string = 'production'
11 |
12 | module monitoring './monitoring.bicep' = {
13 | name: 'monitoring_deploy'
14 | params:{
15 | location: location
16 | name_prefix: name_prefix
17 | }
18 | }
19 |
20 | module postgres './postgres.bicep' = {
21 | name: 'postgres_deploy'
22 | params:{
23 | location: location
24 | name_prefix: name_prefix
25 | workspace_id: monitoring.outputs.workspace_id
26 | administratorLoginPassword: postgres_adminPassword
27 | }
28 | }
29 |
30 | module plan './plan.bicep' = {
31 | name: 'plan_deploy'
32 | params:{
33 | location: location
34 | arcLocation: arcLocation
35 | name_prefix: name_prefix
36 | customLocationId: customLocation_id
37 | kubeEnvironmentId: kubeEnvironment_id
38 | }
39 | }
40 |
41 | module function './function.bicep' = {
42 | name: 'function_deploy'
43 | params:{
44 | location: location
45 | arcLocation: arcLocation
46 | name_prefix: name_prefix
47 | workspace_id: monitoring.outputs.workspace_id
48 | appSettings_insights_key: monitoring.outputs.instrumentation_key
49 | webapp_plan: plan.outputs.plan_id
50 | teamsWebhookUrl: teamsWebhookUrl
51 | customLocationId: customLocation_id
52 | }
53 | }
54 |
55 | module webapi './webapp.bicep' = {
56 | name: 'webapp_deploy'
57 | params:{
58 | location: location
59 | arcLocation: arcLocation
60 | name_prefix: name_prefix
61 | plan_id: plan.outputs.plan_id
62 | workspace_id: monitoring.outputs.workspace_id
63 | customLocationId: customLocation_id
64 | appSettings_pghost: postgres.outputs.pg_host
65 | appSettings_pguser: postgres.outputs.pg_user
66 | appSettings_pgdb: postgres.outputs.pg_db
67 | appSettings_node_env: webapi_node_env
68 | appSettings_pgpassword: postgres_adminPassword
69 | appSettings_insights_key: monitoring.outputs.instrumentation_key
70 | appSettings_eventgridurl: function.outputs.url
71 | }
72 | }
73 |
74 | output webapi_id string = webapi.outputs.webapi_id
75 | output webapi_hostname string = webapi.outputs.webapi_hostname
76 | output function_id string = function.outputs.function_id
77 | output function_hostname string = function.outputs.function_hostname
78 | output postgres_host string = postgres.outputs.pg_host
79 | output postgres_user string = postgres.outputs.pg_user
80 | output postgres_db string = postgres.outputs.pg_db
81 |
--------------------------------------------------------------------------------
/src/webapp/server/web.config:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # Update the VARIANT arg in docker-compose.yml to pick a Node version: 10, 12, 14
2 | ARG VARIANT=12
3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT}
4 |
5 | # Update args in docker-compose.yaml to set the UID/GID of the "node" user.
6 | ARG USER_UID=1000
7 | ARG USER_GID=$USER_UID
8 | RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
9 | groupmod --gid $USER_GID node \
10 | && usermod --uid $USER_UID --gid $USER_GID node \
11 | && chown -R $USER_UID:$USER_GID /home/node \
12 | && chown -R $USER_UID:root /usr/local/share/nvm /usr/local/share/npm-global; \
13 | fi
14 |
15 | # [Optional] Uncomment this section to install additional OS packages.
16 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
17 | # && apt-get -y install --no-install-recommends
18 |
19 | # [Optional] Uncomment if you want to install an additional version of node using nvm
20 | # ARG EXTRA_NODE_VERSION=10
21 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
22 |
23 | # [Optional] Uncomment if you want to install more global node packages
24 | # RUN sudo -u node npm install -g
25 |
26 | RUN sudo apt-key adv --refresh-keys --keyserver keyserver.ubuntu.com
27 |
28 | # install tooling
29 | ## Azure cli
30 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
31 | RUN curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
32 |
33 | ## bicep tools
34 | RUN curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 && chmod +x ./bicep && sudo mv ./bicep /usr/local/bin/bicep
35 |
36 | ## Function runtime
37 | RUN apt-get update \
38 | && export DEBIAN_FRONTEND=noninteractive \
39 | && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
40 | #
41 | # Verify git and needed tools are installed
42 | && apt-get -y install \
43 | git \
44 | openssh-client \
45 | less \
46 | unzip \
47 | iproute2 \
48 | procps \
49 | curl \
50 | apt-transport-https \
51 | gnupg2 \
52 | lsb-release \
53 | libsecret-1-dev
54 |
55 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
56 | RUN sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
57 | RUN sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
58 | RUN sudo apt-get update && apt-get install azure-functions-core-tools-3
59 |
60 | ## .NET Core 3.1 SDK (needed for Logic Apps designer)
61 | RUN sudo apt-get install -y apt-transport-https && \
62 | sudo apt-get update && \
63 | sudo apt-get install -y dotnet-sdk-3.1
64 |
65 | ## psql
66 | RUN sudo apt-get -y install postgresql-client
67 |
68 | ## Porter
69 | RUN curl -L https://cdn.porter.sh/latest/install-linux.sh | bash
70 | RUN sudo mv /root/.porter /home/node/.porter
71 | ENV PATH "$PATH:/home/node/.porter"
72 |
73 | ## Kubectl for ARC-enabled clsuters
74 | RUN sudo az aks install-cli
75 |
76 | ## Helm
77 | RUN curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
78 | RUN sudo apt-get install apt-transport-https --yes
79 | RUN echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
80 | RUN sudo apt-get update
81 | RUN sudo apt-get install helm
--------------------------------------------------------------------------------
/docs/azure-arc.md:
--------------------------------------------------------------------------------
1 | # Deploying to Azure Arc
2 |
3 | This template supports deployment to Azure Arc. The following document lists the requirements to setup Azure Arc to support the template, as well as how you deploy the application to an Arc-enable Kubernetes Cluster.
4 |
5 | - [Deploying to Azure Arc](#deploying-to-azure-arc)
6 | - [Prerequisites](#prerequisites)
7 | - [Regions and resource group support](#regions-and-resource-group-support)
8 | - [Azure Arc enabled App Service](#azure-arc-enabled-app-service)
9 |
10 | ## Prerequisites
11 |
12 | In order to deploy this template to Azure Arc, you need to have the Arc-enabled Kubernetes Cluster created. This template assumes this has already been done. To learn more about configuring an Arc environment for App Service, please see this [blog](https://aka.ms/ArcEnabledAppServices-Build2021-Blog)
13 |
14 | ## Regions and resource group support
15 |
16 | The following is a list of regions supported:
17 |
18 | | Resource | Regions | Other requirements |
19 | | --- | --- | ---- |
20 | | Kubernetes cluster | anywhere | none |
21 | | Kubernetes - Azure Arc | East US | none |
22 | | Log Analytics | anywhere - preferably close to the Kubernetes clusters | none |
23 | | Custom Location | East US or West Europe | none |
24 | | App Service Kubernetes Environment | East US or West Europe | none |
25 | | App Plan | East US or West Europe | none |
26 | | Web Site | East US or West Europe | Has to be in same resource group as the App Plan |
27 |
28 |
29 | ### Azure Arc enabled App Service
30 |
31 | Following these guidelines will deploy the webapi to an ARC-enabled Kubernetes cluster.
32 |
33 | For local development the following is needed:
34 |
35 | 1. Get updated bicep tools
36 | 1. CLI:
37 | 1. Download CLI (7aca810747) https://github.com/Azure/bicep/suites/2816103963/artifacts/62679913
38 | Using nightly.link because GitHub requires authentication to download artifacts --> https://github.com/actions/upload-artifact/issues/51
39 | `curl -L https://nightly.link/Azure/bicep/actions/artifacts/62679913.zip --output bicep.zip`
40 | 1. Check integrity - sha256sum expected: 6179da0ac8e1bebea8f9101cb9f3a40ad1bc06b04355698043d5c83be9f28f15
41 | `echo 6179da0ac8e1bebea8f9101cb9f3a40ad1bc06b04355698043d5c83be9f28f15 bicep.zip | sha256sum --check`
42 | 1. Unzip to path and change permissions
43 | `sudo unzip bicep.zip bicep -d /usr/local/bin && sudo chmod +x /usr/local/bin/bicep`
44 | 1. Check version --> Bicep CLI version 0.3.602 (7aca810747)
45 | `bicep -v`
46 | 1. TBD - VS Code extension and language server
47 |
48 | To deploy from local environment do the following:
49 |
50 | 1. Build the services and create a zip package with the app
51 | `make build && make zip_it`
52 | 1. Build and run the Porter bundle
53 | `porter build && porter install --cred ./creds.json --parameter-set ./params.json`
54 | Ensure all creds and params are in the environment: https://porter.sh/cli/porter/#see-also
55 |
56 | To deploy using the GitHub Actions Workflow, the following is needed in the [environments.yaml](../environments/environments.yaml) file:
57 |
58 | ```
59 | AZURE_LOCATION: "northeurope" #Location of Azure hosted resources, e.g. Azure Monitor
60 | AZURE_NAME_PREFIX: "nodewebapi" #Resource name prefix
61 | WEBAPI_NODE_ENV: "development" #nodeEnv parameter
62 | KUBE_ENVIRONMENT_ID: "" #kubeEnvironmentId to host the webapi on Arc
63 | CUSTOM_LOCATION_ID: "" #customLocationId for the kubeEnvironment
64 | ARC_LOCATION: "" #Location of the Arc resources - e.g. Web App
65 | ```
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to the React.js and serverless accelerator
2 |
3 | 
4 |
5 | This accelerator implements a [node.js (Express) webapp with a React.js frontend](src/webapp), hosted as an [Azure Web app](src/arm/webapp.bicep) backed by a [PostgreSQL database](src/arm/postgres.bicep). Events from the React.js site will invoke background functions implemented as Azure Functions and invoke a webhook in Teams.
6 |
7 | **NOTE:** Check [below](#required-to-get-started) for required secrets and changes to get started
8 |
9 | To get started, simply open the repository in Codespaces and start [debugging](.vscode/launch.json).
10 | This is possible because the accelerator has a development environment [defined in a container (a .devcontainer)](.devcontainer). There are [database migration and seed data processes](src/webapp/db_migration) configured to run [when your devcontainer starts](.devcontainer/devcontainer.json), so expect some data to show up.
11 |
12 | The webapp has a single model and [route implemented](src/webapp/routes/item.js). You can simply go ahead and add your business model and logic to the app.
13 |
14 | Most of the routines you would want to run when developing are implemeted as [make targets](makefile), so simply invoke 'make build' to build the application, or use [VS Code tasks](.vscode/tasks.json) to run 'make test' - and checkout the [testing framework](src/webapp/test) implemented.
15 |
16 | On checkins, the [app builds](.github/workflows/build_bundle.yaml) and a [Porter bundle](src/bundle) is being created and pushed to a [registry]() (To be implemented in [#11](https://github.com/varaderoproject/webapp-nodejs/issues/11)).
17 |
18 | In order to deploy the application and all the required resources, checkin a change to an [environment file]() (To be implemented #11). The [environment file]() also includes all the parameters used for that deployment.
19 |
20 | ## Required to get started
21 |
22 | 1. Setup an ARC-enabled Kubernetes cluster, by following [these instructions](https://github.com/microsoft/Azure-App-Service-on-Azure-Arc/blob/main/docs/getting-started/setup.md). The [src/arc/lima-setup.sh](src/arc/lima-setup.sh) script can help, but check the setup instructions in the above link, as these change frequently.
23 |
24 | 1. Enable CNAB bundle support in GitHub
25 | 1. Follow [this guide](https://docs.github.com/en/free-pro-team@latest/packages/guides/enabling-improved-container-support) to enable support for CNAB on GitHub
26 |
27 | 1. Set following deployment time parameters as GitHub secrets
28 | | Parameter | GitHub Secret | Description |
29 | | --- | --- | --- |
30 | | Teams Webhook | `TEAMS_WEBHOOK_URL` | Create a Team Channel and add an `Incoming Webhook` connector. |
31 | | Package admin | `PACKAGE_ADMIN` | Create a [personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token) with 'write:packages' permissions and save the value to this secret |
32 | | Azure credentials | `AZURE_CREDENTIALS` | `az login -o none && az ad sp create-for-rbac --role contributor --sdk-auth` |
33 | | Postgres admin password | `POSTGRES_DB_ADMIN_PASSWORD` | Configure what postgres database admin password you want to use - [more info](https://docs.microsoft.com/en-us/azure/postgresql/concepts-security#access-management) by saving the password to a [GitHub Secrets](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) name |
34 |
35 | 1. Check the environment.yaml file. For ARC deployments, ensure to include the correct App Service Environment to use. The syntax is commented in the file.
36 |
37 | 1. Run the build workflow, this will eventually kick-off the deployment workflow as well.
38 |
--------------------------------------------------------------------------------
/src/webapp/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/arm/function.bicep:
--------------------------------------------------------------------------------
1 | param location string
2 | param arcLocation string
3 | param name_prefix string
4 | param workspace_id string
5 | param appSettings_insights_key string
6 | param webapp_plan string
7 | param teamsWebhookUrl string
8 | param customLocationId string
9 |
10 | var storage_name = '${uniqueString(resourceGroup().id)}stor'
11 | var function_plan_name = '${name_prefix}-funcplan'
12 | var function_name = '${name_prefix}-function-${uniqueString(resourceGroup().id)}'
13 |
14 | resource storage_account 'Microsoft.Storage/storageAccounts@2019-06-01' = {
15 | name: storage_name
16 | location: location
17 | kind: 'StorageV2'
18 | sku: {
19 | name: 'Standard_LRS'
20 | }
21 | }
22 |
23 | resource function 'Microsoft.Web/sites@2020-06-01' = if(customLocationId == '') {
24 | name: function_name
25 | location: location
26 | kind: 'functionapp,linux'
27 | properties: {
28 | siteConfig: {
29 | linuxFxVersion: 'Node|14'
30 | appSettings: [
31 | {
32 | name: 'AzureWebJobsStorage'
33 | value: 'DefaultEndpointsProtocol=https;AccountName=${storage_account.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage_account.id, storage_account.apiVersion).keys[0].value}'
34 | }
35 | {
36 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
37 | value: 'DefaultEndpointsProtocol=https;AccountName=${storage_account.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage_account.id, storage_account.apiVersion).keys[0].value}'
38 | }
39 | {
40 | name: 'FUNCTIONS_WORKER_RUNTIME'
41 | value: 'node'
42 | }
43 | {
44 | name: 'FUNCTIONS_EXTENSION_VERSION'
45 | value: '~3'
46 | }
47 | {
48 | name: 'WEBSITE_NODE_DEFAULT_VERSION'
49 | value: '~14'
50 | }
51 | {
52 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
53 | value: '${appSettings_insights_key}'
54 | }
55 | {
56 | name: 'teamsWebhookUrl'
57 | value: '${teamsWebhookUrl}'
58 | }
59 | ]
60 | }
61 | serverFarmId: webapp_plan
62 | clientAffinityEnabled: false
63 | }
64 | }
65 |
66 | resource diagnostics 'microsoft.insights/diagnosticSettings@2017-05-01-preview' = if(customLocationId == '') {
67 | scope: function
68 | name: 'logAnalytics'
69 | properties: {
70 | workspaceId: workspace_id
71 | logs: [
72 | {
73 | enabled: true
74 | category: 'FunctionAppLogs'
75 | }
76 | ]
77 | metrics: [
78 | {
79 | enabled: true
80 | category: 'AllMetrics'
81 | }
82 | ]
83 | }
84 | }
85 |
86 | resource functionArc 'Microsoft.Web/sites@2020-12-01' = if(customLocationId != '') {
87 | name: concat(function_name, 'arc')
88 | location: arcLocation
89 | kind: 'kubernetes,functionapp,linux'
90 | extendedLocation: {
91 | type: 'CustomLocation'
92 | name: customLocationId
93 | }
94 | properties: {
95 | siteConfig: {
96 | linuxFxVersion: 'Node|14'
97 | appSettings: [
98 | {
99 | name: 'AzureWebJobsStorage'
100 | value: 'DefaultEndpointsProtocol=https;AccountName=${storage_account.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage_account.id, storage_account.apiVersion).keys[0].value}'
101 | }
102 | {
103 | name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
104 | value: 'DefaultEndpointsProtocol=https;AccountName=${storage_account.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage_account.id, storage_account.apiVersion).keys[0].value}'
105 | }
106 | {
107 | name: 'FUNCTIONS_WORKER_RUNTIME'
108 | value: 'node'
109 | }
110 | {
111 | name: 'FUNCTIONS_EXTENSION_VERSION'
112 | value: '~3'
113 | }
114 | {
115 | name: 'WEBSITE_NODE_DEFAULT_VERSION'
116 | value: '~14'
117 | }
118 | {
119 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
120 | value: '${appSettings_insights_key}'
121 | }
122 | {
123 | name: 'teamsWebhookUrl'
124 | value: '${teamsWebhookUrl}'
125 | }
126 | ]
127 | }
128 | serverFarmId: webapp_plan
129 | clientAffinityEnabled: false
130 | }
131 | }
132 |
133 | resource diagnosticsArc 'microsoft.insights/diagnosticSettings@2017-05-01-preview' = if(customLocationId != '') {
134 | scope: functionArc
135 | name: 'logAnalytics'
136 | properties: {
137 | workspaceId: workspace_id
138 | logs: [
139 | {
140 | enabled: true
141 | category: 'FunctionAppLogs'
142 | }
143 | ]
144 | metrics: [
145 | {
146 | enabled: true
147 | category: 'AllMetrics'
148 | }
149 | ]
150 | }
151 | }
152 |
153 | output function_id string = customLocationId == '' ? function.id : functionArc.id
154 | output function_hostname string = customLocationId == '' ? function.properties.hostNames[0] : functionArc.properties.hostNames[0]
155 | output url string = customLocationId == '' ? '${function.properties.hostNames[0]}/api/EventGridHttpTrigger' : '${functionArc.properties.hostNames[0]}/api/EventGridHttpTrigger'
156 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | workflow_dispatch:
5 | # Trigger the workflow everytime the build workflow ran to completion
6 | workflow_run:
7 | workflows:
8 | - Build
9 | types:
10 | - completed
11 | # Triggers when an environment file has been changed
12 | push:
13 | paths:
14 | - "environments/**"
15 | - ".github/workflows/deploy.yaml"
16 |
17 | jobs:
18 | build_environment_matrix:
19 | name: 'Evaluate and initate deployments'
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v2
24 | - run: git branch
25 | - run: env
26 | - name: Install yq
27 | run: |
28 | sudo wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.5.0/yq_linux_amd64
29 | sudo chmod +x /usr/local/bin/yq
30 | - name: Get yaml to matrix
31 | run: |
32 | echo "::set-output name=AZURE_ENVIRONMENTS::"$(yq e '{"include": .}' ./environments/environments.yaml -j)""
33 | id: check_environment_files
34 | - name: Echo output to log
35 | run: |
36 | echo $AZURE_ENVIRONMENTS
37 | outputs:
38 | matrix: ${{ steps.check_environment_files.outputs.AZURE_ENVIRONMENTS }}
39 |
40 | deploy_bundle:
41 | if: ${{ github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == null }}
42 | name: 'Deploy bundle'
43 | needs: build_environment_matrix
44 | runs-on: ubuntu-latest
45 | strategy:
46 | fail-fast: false
47 | matrix: ${{ fromJSON(needs.build_environment_matrix.outputs.matrix) }}
48 | steps:
49 | - name: Write output to log
50 | run: |
51 | echo "Deploying ${{ matrix.deploys.version }} to ${{ matrix.name }} in ${{ matrix.config.AZURE_LOCATION }}"
52 | - name: Checkout
53 | uses: actions/checkout@v2
54 | - name: Setup Porter
55 | uses: getporter/gh-action@v0.1.3
56 | - name: Login to GitHub Packages OCI Registry
57 | uses: docker/login-action@v1
58 | with:
59 | registry: ghcr.io
60 | username: ${{ github.repository_owner }}
61 | password: ${{ secrets.PACKAGE_ADMIN }}
62 | - name: Get registry and image name
63 | run: |
64 | echo IMAGE_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f 2) >> $GITHUB_ENV
65 | echo BUNDLE_REGISTRY=ghcr.io/$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
66 | working-directory: ./src/bundle
67 | - name: Get incoming bundle digest
68 | run: |
69 | echo BUNDLE_DIGEST_INCOMING=$(porter inspect --reference $BUNDLE_REGISTRY/$IMAGE_NAME:${{ matrix.deploys.version }} -o yaml | awk '$1 == "contentDigest:" {print $2}') >> $GITHUB_ENV
70 | - name: Get incoming config digest
71 | run: |
72 | echo CONFIG_DIGEST_INCOMING=$(echo -n "${{ toJSON(matrix.config) }}" | sha256sum) >> $GITHUB_ENV
73 | - name: Login to Azure
74 | uses: azure/login@v1
75 | with:
76 | creds: ${{ secrets.AZURE_CREDENTIALS }}
77 | - name: Get deployed bundle digest
78 | run: |
79 | RG_NAME=${{ matrix.config.AZURE_NAME_PREFIX }}-reactjs-demo
80 | echo BUNDLE_DIGEST_DEPLOYED=$(az group show --name $RG_NAME | jq .tags.bundle_digest -r) >> $GITHUB_ENV
81 | - name: Get deployed config digest
82 | run: |
83 | RG_NAME=${{ matrix.config.AZURE_NAME_PREFIX }}-reactjs-demo
84 | echo CONFIG_DIGEST_DEPLOYED=$(az group show --name $RG_NAME | jq .tags.config_digest -r) >> $GITHUB_ENV
85 | - name: Output digests to compare to log
86 | run: |
87 | echo Bundle digest:
88 | echo - deployed: $BUNDLE_DIGEST_DEPLOYED
89 | echo - incoming: $BUNDLE_DIGEST_INCOMING
90 | echo Config digest:
91 | echo - deployed: $CONFIG_DIGEST_DEPLOYED
92 | echo - incoming: $CONFIG_DIGEST_INCOMING
93 | - name: Nothing to update
94 | if: (env.BUNDLE_DIGEST_DEPLOYED == env.BUNDLE_DIGEST_INCOMING) && (env.CONFIG_DIGEST_DEPLOYED == env.CONFIG_DIGEST_INCOMING)
95 | run: |
96 | echo "Environment already up to date"
97 | - name: Install
98 | if: (env.BUNDLE_DIGEST_DEPLOYED != env.BUNDLE_DIGEST_INCOMING) || (env.CONFIG_DIGEST_DEPLOYED != env.CONFIG_DIGEST_INCOMING)
99 | run: |
100 | porter install --tag $BUNDLE_REGISTRY/$IMAGE_NAME:${{ matrix.deploys.version }} --cred ./creds.json --parameter-set ./params.json
101 | working-directory: ./src/bundle
102 | env:
103 | LOCATION: ${{ matrix.config.AZURE_LOCATION }}
104 | NAME_PREFIX: ${{ matrix.config.AZURE_NAME_PREFIX }}
105 | POSTGRES_PASSWORD: ${{ secrets.POSTGRES_DB_ADMIN_PASSWORD }}
106 | TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
107 | WEBAPI_NODE_ENV: ${{ matrix.config.WEBAPI_NODE_ENV }}
108 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
109 | ARC_LOCATION: ${{ matrix.config.ARC_LOCATION }}
110 | CUSTOM_LOCATION_ID: ${{ matrix.config.CUSTOM_LOCATION_ID }}
111 | KUBE_ENVIRONMENT_ID: ${{ matrix.config.KUBE_ENVIRONMENT_ID }}
112 |
113 | - name: Update tag
114 | if: (env.BUNDLE_DIGEST_DEPLOYED != env.BUNDLE_DIGEST_INCOMING) || (env.CONFIG_DIGEST_DEPLOYED != env.CONFIG_DIGEST_INCOMING)
115 | run: |
116 | az group update --name ${{ matrix.config.AZURE_NAME_PREFIX }}-reactjs-demo --tags bundle_digest="${{ env.BUNDLE_DIGEST_INCOMING }}" config_digest="${{ env.CONFIG_DIGEST_INCOMING}}"
117 |
--------------------------------------------------------------------------------
/src/arm/webapp.bicep:
--------------------------------------------------------------------------------
1 | param location string
2 | param arcLocation string
3 | param name_prefix string
4 | param workspace_id string
5 | param plan_id string
6 | param customLocationId string
7 |
8 | param appSettings_pghost string
9 | param appSettings_pguser string
10 | @secure()
11 | param appSettings_pgpassword string
12 | param appSettings_pgdb string
13 | param appSettings_node_env string
14 | param appSettings_insights_key string
15 | param appSettings_eventgridurl string
16 |
17 | var webfarm_name = '${name_prefix}-webfarm'
18 | var webapi_name = '${name_prefix}-webapi-${uniqueString(resourceGroup().id)}'
19 |
20 | resource webapi 'Microsoft.Web/sites@2020-06-01' = if(customLocationId == '') {
21 | name: webapi_name
22 | location: location
23 | kind: ''
24 | properties: {
25 | siteConfig: {
26 | linuxFxVersion: 'NODE|14-lts'
27 | appSettings: [
28 | {
29 | name: 'PGHOST'
30 | value: '${appSettings_pghost}'
31 | }
32 | {
33 | name: 'PGUSER'
34 | value: '${appSettings_pguser}@${appSettings_pghost}'
35 | }
36 | {
37 | name: 'PGPASSWORD'
38 | value: '${appSettings_pgpassword}'
39 | }
40 | {
41 | name: 'PGDB'
42 | value: '${appSettings_pgdb}'
43 | }
44 | {
45 | name: 'NODE_ENV'
46 | value: '${appSettings_node_env}'
47 | }
48 | {
49 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
50 | value: '${appSettings_insights_key}'
51 | }
52 | {
53 | name: 'eventGridUrl'
54 | value: 'https://${appSettings_eventgridurl}'
55 | }
56 | ]
57 | }
58 | serverFarmId: plan_id
59 | }
60 | }
61 |
62 | resource diagnostics 'microsoft.insights/diagnosticSettings@2017-05-01-preview' = if(customLocationId == '') {
63 | scope: webapi
64 | name: 'logAnalytics'
65 | properties: {
66 | workspaceId: workspace_id
67 | logs: [
68 | {
69 | enabled: true
70 | category: 'AppServicePlatformLogs'
71 | }
72 | {
73 | enabled: true
74 | category: 'AppServiceIPSecAuditLogs'
75 | }
76 | {
77 | enabled: true
78 | category: 'AppServiceAuditLogs'
79 | }
80 | {
81 | enabled: true
82 | category: 'AppServiceFileAuditLogs'
83 | }
84 | {
85 | enabled: true
86 | category: 'AppServiceAppLogs'
87 | }
88 | {
89 | enabled: true
90 | category: 'AppServiceConsoleLogs'
91 | }
92 | {
93 | enabled: true
94 | category: 'AppServiceHTTPLogs'
95 | }
96 | {
97 | enabled: true
98 | category: 'AppServiceAntivirusScanAuditLogs'
99 | }
100 | ]
101 | metrics: [
102 | {
103 | enabled: true
104 | category: 'AllMetrics'
105 | }
106 | ]
107 | }
108 | }
109 |
110 | resource webapiArc 'Microsoft.Web/sites@2020-12-01' = if(customLocationId != '') {
111 | name: concat(webapi_name, 'arc')
112 | location: arcLocation
113 | kind: 'linux,kubernetes,app'
114 | extendedLocation: {
115 | type: 'CustomLocation'
116 | name: customLocationId
117 | }
118 | properties: {
119 | siteConfig: {
120 | linuxFxVersion: 'NODE|14-lts'
121 | appSettings: [
122 | {
123 | name: 'PGHOST'
124 | value: '${appSettings_pghost}'
125 | }
126 | {
127 | name: 'PGUSER'
128 | value: '${appSettings_pguser}@${appSettings_pghost}'
129 | }
130 | {
131 | name: 'PGPASSWORD'
132 | value: '${appSettings_pgpassword}'
133 | }
134 | {
135 | name: 'PGDB'
136 | value: '${appSettings_pgdb}'
137 | }
138 | {
139 | name: 'NODE_ENV'
140 | value: '${appSettings_node_env}'
141 | }
142 | {
143 | name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
144 | value: '${appSettings_insights_key}'
145 | }
146 | {
147 | name: 'eventGridUrl'
148 | value: 'https://${appSettings_eventgridurl}'
149 | }
150 | ]
151 | }
152 | serverFarmId: plan_id
153 | }
154 | }
155 |
156 | resource diagnosticsArc 'microsoft.insights/diagnosticSettings@2017-05-01-preview' = if(customLocationId != '') {
157 | scope: webapiArc
158 | name: 'logAnalytics'
159 | properties: {
160 | workspaceId: workspace_id
161 | logs: [
162 | {
163 | enabled: true
164 | category: 'AppServicePlatformLogs'
165 | }
166 | {
167 | enabled: true
168 | category: 'AppServiceIPSecAuditLogs'
169 | }
170 | {
171 | enabled: true
172 | category: 'AppServiceAuditLogs'
173 | }
174 | {
175 | enabled: true
176 | category: 'AppServiceFileAuditLogs'
177 | }
178 | {
179 | enabled: true
180 | category: 'AppServiceAppLogs'
181 | }
182 | {
183 | enabled: true
184 | category: 'AppServiceConsoleLogs'
185 | }
186 | {
187 | enabled: true
188 | category: 'AppServiceHTTPLogs'
189 | }
190 | {
191 | enabled: true
192 | category: 'AppServiceAntivirusScanAuditLogs'
193 | }
194 | ]
195 | metrics: [
196 | {
197 | enabled: true
198 | category: 'AllMetrics'
199 | }
200 | ]
201 | }
202 | }
203 |
204 | output webapi_id string = customLocationId == '' ? webapi.id : webapiArc.id
205 | output webapi_hostname string = customLocationId == '' ? webapi.properties.hostNames[0] : webapiArc.properties.hostNames[0]
206 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | workflow_dispatch:
5 | #push:
6 | # paths:
7 | # - "src/**"
8 | # - ".github/workflows/build.yaml"
9 |
10 | jobs:
11 | build_webapp:
12 | name: 'Build WebAPI'
13 | runs-on: ubuntu-latest
14 | services:
15 | db:
16 | image: postgres:latest
17 | volumes:
18 | - postgres-data:/var/lib/postgresql/data
19 | env:
20 | POSTGRES_PASSWORD: postgres
21 | POSTGRES_USER: postgres
22 | POSTGRES_DB: postgres
23 | options: --health-cmd pg_isready
24 | --health-interval 10s
25 | --health-timeout 5s
26 | --health-retries 5
27 | ports:
28 | - 5432:5432
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v2
32 | - name: Use Node.js 14
33 | uses: actions/setup-node@v1
34 | with:
35 | node-version: '14.x'
36 | - name: Test
37 | run: make test
38 | env:
39 | PGHOST: localhost
40 | PGPORT: 5432
41 | PGDATABASE: postgres
42 | PGUSER: postgres
43 | PGPASSWORD: postgres
44 | NODE_ENV: development
45 | - name: Build for production
46 | run: make build
47 | - name: Package webapi
48 | run: (cd src/webapp/server; zip -r ../../../webapi.zip .)
49 | - name: Upload app zip package
50 | uses: actions/upload-artifact@v2
51 | with:
52 | name: app
53 | path: ./webapi.zip
54 | retention-days: 1
55 |
56 | build_function:
57 | name: 'Build Function'
58 | runs-on: ubuntu-latest
59 | steps:
60 | - name: Checkout
61 | uses: actions/checkout@v2
62 | - name: Use Node.js 14
63 | uses: actions/setup-node@v1
64 | with:
65 | node-version: '14.x'
66 | - name: Build for production
67 | run: |
68 | npm install --prefix src/function/
69 | npm run build --prefix src/function/
70 | - name: Test
71 | run: npm test --prefix src/function/
72 | - name: Package function
73 | run: (cd src/function; zip -r ../../function.zip .)
74 | - name: Upload app zip package
75 | uses: actions/upload-artifact@v2
76 | with:
77 | name: function
78 | path: ./function.zip
79 | retention-days: 1
80 |
81 | build_bicep:
82 | name: 'Build Bicep to ARM'
83 | runs-on: ubuntu-latest
84 | steps:
85 | - name: Checkout
86 | uses: actions/checkout@v2
87 | - name: Set up bicep
88 | run: |
89 | # Download CLI (7aca810747) https://github.com/Azure/bicep/suites/2816103963/artifacts/62679913
90 | # Using nightly.link because GitHub requires authentication to download artifacts --> https://github.com/actions/upload-artifact/issues/51
91 | curl -L https://nightly.link/Azure/bicep/actions/artifacts/62679913.zip --output bicep.zip
92 | # Unzip, move to bin and mark it as executable
93 | sudo unzip bicep.zip bicep -d /usr/local/bin && sudo chmod +x /usr/local/bin/bicep
94 | - name: Build bicep
95 | run: bicep build main.bicep
96 | working-directory: ./src/arm
97 | - name: Upload compiled arm template
98 | uses: actions/upload-artifact@v2
99 | with:
100 | name: arm
101 | path: ./src/arm/main.json
102 | retention-days: 1
103 |
104 | build_and_publish_porter_bundle:
105 | name: 'Build and Publish Porter bundle'
106 | runs-on: ubuntu-latest
107 | needs: [build_webapp, build_bicep, build_function]
108 | steps:
109 | - name: Checkout
110 | uses: actions/checkout@v2
111 | - name: Get application artifacts
112 | uses: actions/download-artifact@v2
113 | with:
114 | path: ./src/bundle/output
115 | - name: Display bundle directory
116 | run: ls -R
117 | working-directory: ./src/bundle
118 | - name: Setup Porter
119 | uses: getporter/gh-action@v0.1.3
120 | - name: Prepare bundle metadata (part I)
121 | run: |
122 | echo IMAGE_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f 2) >> $GITHUB_ENV
123 | echo BUNDLE_MAIN_VERSION=$(cat porter.yaml | awk '$1 == "version:" {print $2}') >> $GITHUB_ENV
124 | echo BUNDLE_REGISTRY=ghcr.io/$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
125 | working-directory: ./src/bundle
126 | - name: Prepare bundle metadata (part II)
127 | run: |
128 | echo BUNDLE_VERSION=$BUNDLE_MAIN_VERSION-$GITHUB_SHA >> $GITHUB_ENV
129 | - name: Build Porter bundle
130 | run: porter build --name "$IMAGE_NAME" --version "$BUNDLE_VERSION"
131 | working-directory: ./src/bundle
132 | - name: Login to GitHub Packages OCI Registry
133 | uses: docker/login-action@v1
134 | with:
135 | registry: ghcr.io
136 | username: ${{ github.repository_owner }}
137 | password: ${{ secrets.PACKAGE_ADMIN }}
138 | - name: Porter publish
139 | run: porter publish --registry "$BUNDLE_REGISTRY"
140 | working-directory: ./src/bundle
141 | - name: Create copies for latest reference
142 | run: |
143 | porter copy --source "${BUNDLE_REGISTRY}/${IMAGE_NAME}:${BUNDLE_VERSION}" --destination "${BUNDLE_REGISTRY}/${IMAGE_NAME}:latest"
144 | porter copy --source "${BUNDLE_REGISTRY}/${IMAGE_NAME}:${BUNDLE_VERSION}" --destination "${BUNDLE_REGISTRY}/${IMAGE_NAME}:${BUNDLE_MAIN_VERSION}-latest"
145 | working-directory: ./src/bundle
--------------------------------------------------------------------------------
/src/bundle/porter.yaml:
--------------------------------------------------------------------------------
1 | name: my_github_repo
2 | version: v0.0.1
3 | description: 'Porter bundle for the PaaS vNext demo'
4 | registry: ghcr.io/my_github_username
5 | dockerfile: Dockerfile.tmpl
6 |
7 | parameters:
8 | - name: LOCATION
9 | type: string
10 | description: 'Azure region for the resource group and resources'
11 | - name: NAME_PREFIX
12 | type: string
13 | description: 'Name prefix for Azure resources'
14 | - name: POSTGRES_PASSWORD
15 | type: string
16 | sensitive: true
17 | description: 'GitHub Secret used as password for the Postgres ad admin user'
18 | - name: TEAMS_WEBHOOK_URL
19 | type: string
20 | description: 'Webhook for the Team Channel incoming webhoook'
21 | - name: WEBAPI_NODE_ENV
22 | type: string
23 | description: 'node_environment variable for the webapi'
24 | - name: KUBE_ENVIRONMENT_ID
25 | type: string
26 | description: 'kubeEnvironmentId to host the webapi on Arc'
27 | - name: CUSTOM_LOCATION_ID
28 | type: string
29 | description: 'customLocationId for the kubeEnvironment'
30 | - name: ARC_LOCATION
31 | type: string
32 | description: 'Location of the Arc resources - e.g. Web App'
33 |
34 | credentials:
35 | - name: AZURE_CREDENTIALS
36 | env: AZURE_CREDENTIALS
37 |
38 | outputs:
39 | - name: WEB_API_HOSTNAME
40 | type: string
41 | applyTo:
42 | - install
43 | sensitive: false
44 |
45 | mixins:
46 | - az
47 | - exec
48 |
49 | install:
50 | - exec:
51 | description: 'Extracting deployment parameters...'
52 | command: ./utils.sh
53 | arguments:
54 | - echo-azure-credentials
55 | outputs:
56 | - name: 'AZURE_DEPLOY_CLIENT_ID'
57 | jsonPath: '$.clientId'
58 | - name: 'AZURE_DEPLOY_CLIENT_SECRET'
59 | jsonPath: '$.clientSecret'
60 | - name: 'AZURE_DEPLOY_TENANT_ID'
61 | jsonPath: '$.tenantId'
62 | - name: 'AZURE_DEPLOY_SUBSCRIPTION_ID'
63 | jsonPath: '$.subscriptionId'
64 |
65 | - az:
66 | description: 'Logging into Azure.'
67 | arguments:
68 | - login
69 | flags:
70 | service-principal:
71 | username: '{{ bundle.outputs.AZURE_DEPLOY_CLIENT_ID }}'
72 | password: '{{ bundle.outputs.AZURE_DEPLOY_CLIENT_SECRET }}'
73 | tenant: '{{ bundle.outputs.AZURE_DEPLOY_TENANT_ID }}'
74 | output: table
75 |
76 | - az:
77 | description: 'Setting subscription.'
78 | arguments:
79 | - account
80 | - set
81 | flags:
82 | subscription: '{{ bundle.outputs.AZURE_DEPLOY_SUBSCRIPTION_ID }}'
83 |
84 | - az:
85 | description: 'Creating the Azure resource group if it does not exists.'
86 | arguments:
87 | - group
88 | - create
89 | flags:
90 | name: '{{ bundle.parameters.NAME_PREFIX }}-reactjs-demo'
91 | location: '{{ bundle.parameters.LOCATION }}'
92 |
93 | - az:
94 | description: 'Deploying the ARM template'
95 | arguments:
96 | - deployment
97 | - group
98 | - create
99 | flags:
100 | resource-group: '{{ bundle.parameters.NAME_PREFIX }}-reactjs-demo'
101 | name: '{{ bundle.parameters.NAME_PREFIX }}-deployment'
102 | template-file: 'output/arm/main.json'
103 | parameters: '
104 | location={{ bundle.parameters.LOCATION }}
105 | name_prefix={{ bundle.parameters.NAME_PREFIX }}
106 | postgres_adminPassword={{ bundle.parameters.POSTGRES_PASSWORD }}
107 | teamsWebhookUrl={{ bundle.parameters.TEAMS_WEBHOOK_URL }}
108 | webapi_node_env={{ bundle.parameters.WEBAPI_NODE_ENV }}
109 | kubeEnvironment_id={{ bundle.parameters.KUBE_ENVIRONMENT_ID}}
110 | customLocation_id={{ bundle.parameters.CUSTOM_LOCATION_ID}}
111 | arcLocation={{ bundle.parameters.ARC_LOCATION}}
112 | '
113 | outputs:
114 | - name: 'WEBAPI_ID'
115 | jsonPath: '$.properties.outputs.webapi_id.value'
116 | - name: 'POSTGRES_HOST'
117 | jsonPath: '$.properties.outputs.postgres_host.value'
118 | - name: 'POSTGRES_USER'
119 | jsonPath: '$.properties.outputs.postgres_user.value'
120 | - name: 'POSTGRES_DB'
121 | jsonPath: '$.properties.outputs.postgres_db.value'
122 | - name: 'WEB_API_HOSTNAME'
123 | jsonPath: '$.properties.outputs.webapi_hostname.value'
124 | - name: 'FUNCTION_ID'
125 | jsonPath: '$.properties.outputs.function_id.value'
126 |
127 | - exec:
128 | description: 'Unzip app directory'
129 | command: unzip
130 | arguments:
131 | - '-q'
132 | - output/app/webapi.zip
133 | flags:
134 | d: webapi
135 |
136 | - exec:
137 | command: ./db_migration.sh
138 | description: 'Run database migration script'
139 | arguments:
140 | - '{{ bundle.outputs.POSTGRES_DB }}'
141 | - '{{ bundle.outputs.POSTGRES_HOST }}'
142 | - '{{ bundle.parameters.POSTGRES_PASSWORD }}'
143 | - '{{ bundle.outputs.POSTGRES_USER }}@{{ bundle.outputs.POSTGRES_HOST }}'
144 | suppress-output: false
145 |
146 | - exec:
147 | command: ./zip_deploy.sh
148 | description: 'Deploy the Web API'
149 | arguments:
150 | - '{{ bundle.outputs.WEBAPI_ID }}'
151 | - output/app/webapi.zip
152 | suppress-output: false
153 |
154 | - exec:
155 | command: ./zip_deploy.sh
156 | description: 'Deploy the Function'
157 | arguments:
158 | - '{{ bundle.outputs.FUNCTION_ID }}'
159 | - output/function/function.zip
160 | suppress-output: false
161 |
162 | - az:
163 | description: 'Configuring function with webapi URL'
164 | arguments:
165 | - webapp
166 | - config
167 | - appsettings
168 | - set
169 | flags:
170 | ids: '{{ bundle.outputs.FUNCTION_ID }}'
171 | settings: 'webApiUrl=https://{{ bundle.outputs.WEB_API_HOSTNAME }}'
172 |
173 | - exec:
174 | command: ./utils.sh
175 | description: 'Deployment complete'
176 | arguments:
177 | - echo-web-api-hostname
178 | - '{{ bundle.outputs.WEB_API_HOSTNAME }}'
179 | suppress-output: false
180 |
181 | uninstall:
182 | - az:
183 | description: 'Deleting the entire resource group.'
184 | arguments:
185 | - group
186 | - delete
187 | flags:
188 | name: '{{ bundle.parameters.NAME_PREFIX }}-reactjs-demo'
189 | yes: ''
190 | no-wait: ''
--------------------------------------------------------------------------------
/src/arc/lima-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script can be used to set up an AKS cluster with Azure Arc-enabled Kubernetes and App Service.
4 | # Check this article for the latest info: https://docs.microsoft.com/azure/app-service/manage-create-arc-environment
5 |
6 | GREEN='\033[0;32m'
7 | RED='\033[0;31m'
8 | NC='\033[0m' # No Color
9 |
10 | # Checking for prefix for naming
11 | prefix=$1
12 | # arcLocation is used for the AKS cluster and the ARC cluster resource
13 | arcLocation=${2:-'eastus'}
14 | # k8se location
15 | k8seLocation=${3:-'centraluseuap'}
16 |
17 | if
18 | [[ $prefix == "" ]]
19 | then
20 | printf "${RED}You need to provide a prefix and location for your resources './lima-setup.sh {prefix} ({arcLocation}) ({k8seLocation})'. The prefix is used to name resoures. All resources created by this script also have the prefix as a tag. ${NC}\n"
21 | exit 1
22 | fi
23 |
24 | # Step 0 - Pre-reqs and setup
25 |
26 | printf "${GREEN}Deploying k8se ARC to ${arcLocation} ${NC}\n"
27 |
28 | printf "${GREEN}Log in to Azure ${NC}\n"
29 | az login --use-device-code -o none
30 |
31 | ## Variables
32 | ## Static IP Name for the clsuter
33 | staticIpName="${prefix}-ip"
34 | ## The name of the resource group into which your resources will be provisioned
35 | groupName="${prefix}-lima-rg"
36 | ## Only needed if using AKS; the name of the resource group in which the AKS cluster resides
37 | aksClusterGroupName="${prefix}-aks-cluster-rg"
38 | aksClusterName="${prefix}-aks-cluster"
39 | ## The subscription ID into which your resources will be provisioned
40 | subscriptionId=$(az account show --query id -o tsv)
41 | ## The desired name of your connected cluster resource
42 | clusterName="${prefix}-arc-cluster"
43 | ## The desired name of the extension to be installed in the connected cluster
44 | extensionName="${prefix}-appsvc-ext"
45 | ## The desired name of your custom location
46 | customLocationName="${prefix}-location"
47 | ## The desired name of your Kubernetes environment
48 | kubeEnvironmentName="${prefix}-kube"
49 | ## Workspace name
50 | workspaceName="${prefix}-workspace"
51 |
52 | ## Check installed CLI extensions
53 | printf "${GREEN}Installing Azure-CLI Extensions ${NC}\n"
54 | az extension add --upgrade --yes -n connectedk8s
55 | az extension add --upgrade --yes -n customlocation
56 | az extension add --upgrade --yes -n k8s-extension
57 | az extension add --yes --source "https://aka.ms/appsvc/appservice_kube-latest-py2.py3-none-any.whl"
58 |
59 | az version
60 |
61 | ## Checking that all providers are registrered
62 | printf "${GREEN}Checking if all providers are registrered ${NC}\n"
63 | printf "${GREEN}Regions available for Kubernetes Environments${NC}\n"
64 | az provider show -n Microsoft.Web --query "resourceTypes[?resourceType=='kubeEnvironments'].locations"
65 | printf "${GREEN}Regions available for Connected clusters ${NC}\n"
66 | az provider show -n Microsoft.Kubernetes --query "[registrationState,resourceTypes[?resourceType=='connectedClusters'].locations]"
67 | printf "${GREEN}Regions available for Cluster Extensions ${NC}\n"
68 | az provider show -n Microsoft.KubernetesConfiguration --query "[registrationState,resourceTypes[?resourceType=='extensions'].locations]"
69 | printf "${GREEN}Regions available for Custom Locations ${NC}\n"
70 | az provider show -n Microsoft.ExtendedLocation --query "[registrationState,resourceTypes[?resourceType=='customLocations'].locations]"
71 | printf "${GREEN}Regions available for Web App Kubernetes Environments ${NC}\n"
72 | az provider show -n Microsoft.Web --query "[registrationState,resourceTypes[?resourceType=='kubeEnvironments'].locations]"
73 |
74 | # Section 1 - Creating AKS Cluster
75 |
76 | printf "${GREEN}Creating AKS cluster resource group: ${aksClusterGroupName} in ${arcLocation} ${NC}\n"
77 | az group create -n $aksClusterGroupName -l $arcLocation --tags prefix=$prefix
78 |
79 | printf "${GREEN}Checking if AKS cluster: ${aksClusterName} already exists...${NC}\n"
80 |
81 | if
82 | [[ $(az aks show -g $aksClusterGroupName -n $aksClusterName | jq -r .name) == "${aksClusterName}" ]]
83 | then
84 | echo "Cluster already exists"
85 | else
86 | printf "${GREEN}Creating AKS cluster: ${aksClusterName} in ${arcLocation} ${NC}\n"
87 | az aks create -g $aksClusterGroupName -n $aksClusterName -l $arcLocation --enable-aad --generate-ssh-keys --tags prefix=$prefix
88 | fi
89 |
90 | printf "${GREEN}Getting credentials ${NC}\n"
91 | az aks get-credentials -g $aksClusterGroupName -n $aksClusterName --admin
92 | kubectl get ns
93 |
94 | printf "${GREEN}Creating static IP for the cluster ${NC}\n"
95 | infra_rg=$(az aks show -g $aksClusterGroupName -n $aksClusterName -o tsv --query nodeResourceGroup)
96 |
97 | if
98 | [[ $(az network public-ip show -g $infra_rg -n $staticIpName | jq -r .name) == "${staticIpName}" ]]
99 | then
100 | echo "Static IP already exists"
101 | else
102 | az network public-ip create -g $infra_rg -n $staticIpName --sku STANDARD
103 | fi
104 |
105 | staticIp=$(az network public-ip show -g $infra_rg -n $staticIpName | jq -r .ipAddress)
106 | printf "${GREEN}Ip address: ${staticIp} ${NC}\n"
107 |
108 | # Section 2 - Creating ARC resource
109 |
110 | ## Resource Group
111 | printf "${GREEN}Connecting cluster to ARC in RG: ${groupName} ${NC}\n"
112 | printf "${GREEN}Creating resource group for ARC resource: ${groupName} in ${arcLocation} ${NC}\n"
113 | az group create -n $groupName -l ${arcLocation} --tags prefix=$prefix
114 |
115 | ## Log Analytics workspace
116 | printf "${GREEN}Creating a Log Analytics Workspace for the cluster ${NC}\n"
117 |
118 | if
119 | [[ $(az monitor log-analytics workspace show -g $groupName -n $workspaceName | jq -r .name) == "${workspaceName}" ]]
120 | then
121 | echo "Workspace already exists"
122 | else
123 | az monitor log-analytics workspace create -g $groupName -n $workspaceName -l ${arcLocation}
124 | fi
125 |
126 | logAnalyticsWorkspaceId==$(az monitor log-analytics workspace show --resource-group $groupName --workspace-name $workspaceName -o tsv --query "customerId")
127 | logAnalyticsWorkspaceIdEnc=$(printf %s $logAnalyticsWorkspaceId | base64)
128 | logAnalyticsKey=$(az monitor log-analytics workspace get-shared-keys --resource-group $groupName --workspace-name $workspaceName -o tsv --query "secondarySharedKey")
129 | logAnalyticsKeyEncWithSpace=$(printf %s $logAnalyticsKey | base64)
130 | logAnalyticsKeyEnc=$(echo -n "${logAnalyticsKeyEncWithSpace//[[:space:]]/}")
131 |
132 | ## Installing ARC agent
133 |
134 | printf "${GREEN}Installing ARC agent${NC}\n"
135 |
136 | if
137 | [[ $(az connectedk8s show -g $groupName -n $clusterName | jq -r .name) == ${clusterName} ]]
138 | then
139 | echo "Cluster already connected"
140 | else
141 |
142 | az connectedk8s connect -g $groupName -n $clusterName --tags prefix=$prefix
143 |
144 | ### Looping until cluster is connected
145 | while true
146 | do
147 | printf "${GREEN}\nChecking connectivity... ${NC}\n"
148 | sleep 10
149 | connectivityStatus=$(az connectedk8s show -n $clusterName -g $groupName | jq -r .connectivityStatus)
150 | printf "${GREEN}connectivityStatus: ${connectivityStatus} ${NC}\n"
151 | if
152 | [[ $connectivityStatus == "Failed" ]]
153 | then
154 | exit
155 | elif
156 | [[ $connectivityStatus == "Connected" ]]
157 | then
158 | break
159 | fi
160 | done
161 | fi
162 |
163 | connectedClusterId=$(az connectedk8s show -n $clusterName -g $groupName --query id -o tsv)
164 |
165 | printf "${GREEN}Let's grab the resources in the cluster: ${NC}\n"
166 | kubectl get pods -n azure-arc
167 |
168 | # Step 3 - K8SE setup
169 |
170 | ## K8SE extension installation
171 | printf "${GREEN}Installing the App Service extension on your cluster ${NC}\n"
172 |
173 | if
174 | [[ $(az k8s-extension show --cluster-type connectedClusters -c $clusterName -g $groupName --name $extensionName | jq -r .installState) == "Installed" ]]
175 | then
176 | echo "Extension already installed"
177 | else
178 | az k8s-extension create -g $groupName --name $extensionName \
179 | --cluster-type connectedClusters -c $clusterName \
180 | --extension-type 'Microsoft.Web.Appservice' \
181 | --auto-upgrade-minor-version true \
182 | --scope cluster \
183 | --release-namespace 'appservice-ns' \
184 | --configuration-settings "Microsoft.CustomLocation.ServiceAccount=default" \
185 | --configuration-settings "appsNamespace=appservice-ns" \
186 | --configuration-settings "clusterName=${kubeEnvironmentName}" \
187 | --configuration-settings "loadBalancerIp=${staticIp}" \
188 | --configuration-settings "keda.enabled=true" \
189 | --configuration-settings "buildService.storageClassName=default" \
190 | --configuration-settings "buildService.storageAccessMode=ReadWriteOnce" \
191 | --configuration-settings "envoy.annotations.service.beta.kubernetes.io/azure-load-balancer-resource-group=${aksClusterGroupName}" \
192 | --configuration-settings "logProcessor.appLogs.destination=log-analytics" \
193 | --configuration-settings "customConfigMap=appservice-ns/kube-environment-config" \
194 | --configuration-protected-settings"logProcessor.appLogs.logAnalyticsConfig.customerId=${logAnalyticsWorkspaceIdEnc}" \
195 | --configuration-protected-settings "logProcessor.appLogs.logAnalyticsConfig.sharedKey=${logAnalyticsKeyEnc}"
196 |
197 | ### Looping until extention is installed
198 | while true
199 | do
200 | printf "${GREEN}\nChecking state of extension... ${NC}\n"
201 | sleep 10
202 | installState=$(az k8s-extension show --cluster-type connectedClusters -c $clusterName -g $groupName --name $extensionName | jq -r .installState)
203 | printf "${GREEN}installState: ${installState} ${NC}\n"
204 | if
205 | [[ $installState == "Failed" ]]
206 | then
207 | exit
208 | elif
209 | [[ $installState == "Installed" ]]
210 | then
211 | break
212 | fi
213 | done
214 | fi
215 |
216 | extensionId=$(az k8s-extension show --cluster-type connectedClusters -c $clusterName -g $groupName --name $extensionName --query id -o tsv)
217 |
218 | ## Creating custom location
219 | printf "${GREEN}Creating custom location ${NC}\n"
220 |
221 | if
222 | [[ $(az customlocation show -g $groupName -n $customLocationName | jq -r .provisioningState) == "Succeeded" ]]
223 | then
224 | echo "CustomeLocation already exists"
225 | else
226 | az customlocation create -g $groupName -n $customLocationName \
227 | --host-resource-id $connectedClusterId \
228 | --namespace appservice-ns -c $extensionId
229 |
230 | ### Looping until custom location is provisioned
231 | while true
232 | do
233 | printf "${GREEN}\nChecking state of custom location... ${NC}\n"
234 | sleep 10
235 | customLocationState=$(az customlocation show -g $groupName -n $customLocationName | jq -r .provisioningState)
236 | printf "${GREEN}customLocationState: ${customLocationState} ${NC}\n"
237 | if
238 | [[ $customLocationState == "Failed" ]]
239 | then
240 | exit
241 | elif
242 | [[ $customLocationState == "Succeeded" ]]
243 | then
244 | break
245 | fi
246 | done
247 | fi
248 |
249 | customLocationId=$(az customlocation show -g $groupName -n $customLocationName --query id -o tsv)
250 |
251 | ## Creating Kube-Environment
252 | printf "${GREEN}Creating Kubernetes environment ${NC}\n"
253 |
254 | if
255 | [[ $(az appservice kube show -g $groupName -n $kubeEnvironmentName | jq -r .provisioningState) == "Succeeded" ]]
256 | then
257 | echo "Kube environment already exists"
258 | else
259 | az appservice kube create -g $groupName -n $kubeEnvironmentName \
260 | --custom-location $customLocationId --static-ip "$staticIp" \
261 | --location $k8seLocation
262 |
263 | ### Looping until environment is ready
264 | while true
265 | do
266 | printf "${GREEN}\nChecking state of environment... ${NC}\n"
267 | sleep 10
268 | kubeenvironmentState=$(az appservice kube show -g $groupName -n $kubeEnvironmentName | jq -r .provisioningState)
269 | printf "${GREEN}kubeenvironmentState: ${kubeenvironmentState} ${NC}\n"
270 | if
271 | [[ $kubeenvironmentState == "Failed" ]]
272 | then
273 | exit
274 | elif
275 | [[ $kubeenvironmentState == "Succeeded" ]]
276 | then
277 | break
278 | fi
279 | done
280 | fi
281 |
282 | sleep 10
283 |
284 | printf "${GREEN}Let's check all the resources... Run 'kubectl get pods -n appservice-ns' to check again ${NC}\n"
285 | kubectl get pods -n appservice-ns
286 |
287 | printf "${GREEN}Whooo - congratulations! You made it all the way through - now go deploy apps!!! ${NC}\n"
--------------------------------------------------------------------------------