├── .devcontainer ├── Dockerfile ├── content │ ├── appservice_kube-0.1.9-py2.py3-none-any.whl │ ├── connectedk8s-0.3.5-py2.py3-none-any.whl │ ├── customlocation-0.1.0-py2.py3-none-any.whl │ ├── function.local.settings.json │ ├── k8s_extension-0.1.0-py2.py3-none-any.whl │ └── local.env ├── devcontainer.json └── docker-compose.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yaml │ └── deploy.yaml ├── .gitignore ├── .vscode ├── app.code-workspace ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── docs ├── assets │ ├── architecture-no-background-new.png │ └── architecture-no-background.png └── azure-arc.md ├── environments └── environments.yaml ├── makefile ├── notes.md └── src ├── arc └── lima-setup.sh ├── arm ├── function.bicep ├── main.bicep ├── monitoring.bicep ├── plan.bicep ├── postgres.bicep └── webapp.bicep ├── bundle ├── .dockerignore ├── .gitignore ├── Dockerfile.tmpl ├── creds.json ├── db_migration.sh ├── params.json ├── porter-install.sh ├── porter.yaml ├── utils.sh └── zip_deploy.sh ├── function ├── .funcignore ├── .gitignore ├── EventGridHttpTrigger │ ├── function.json │ └── index.ts ├── host.json ├── package-lock.json ├── package.json ├── proxies.json └── tsconfig.json └── webapp ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── server ├── app.js ├── db │ └── db.js ├── db_migration │ ├── migrations │ │ └── 202011-01.js │ ├── migrator.js │ ├── seed.js │ └── seeds │ │ └── 202011-01-seed.js ├── models │ └── item.js ├── package-lock.json ├── package.json ├── routes │ ├── index.js │ └── item.js ├── server.js ├── test │ └── item_test.js ├── views │ └── error.jade └── web.config └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Items.css ├── Items.js └── Items.test.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js /.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 -------------------------------------------------------------------------------- /.devcontainer/content/appservice_kube-0.1.9-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/.devcontainer/content/appservice_kube-0.1.9-py2.py3-none-any.whl -------------------------------------------------------------------------------- /.devcontainer/content/connectedk8s-0.3.5-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/.devcontainer/content/connectedk8s-0.3.5-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/3c1909e54a13ec962b55ee878e96d2adc3bd2302/.devcontainer/content/customlocation-0.1.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.devcontainer/content/k8s_extension-0.1.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/.devcontainer/content/k8s_extension-0.1.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /.devcontainer/content/local.env: -------------------------------------------------------------------------------- 1 | eventGridUrl=http://localhost:7071/api/EventGridHttpTrigger 2 | teamsWebhookUrl= 3 | webApiUrl=http://localhost:3001 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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: -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/ -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the React.js and serverless accelerator 2 | 3 | ![Archictecture Overview](./docs/assets/architecture-no-background-new.png) 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 | -------------------------------------------------------------------------------- /docs/assets/architecture-no-background-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/docs/assets/architecture-no-background-new.png -------------------------------------------------------------------------------- /docs/assets/architecture-no-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/docs/assets/architecture-no-background.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .cnab/ 3 | output/ -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 -------------------------------------------------------------------------------- /src/function/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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/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/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/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/function/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["msjsdiag.debugger-for-chrome"] 3 | } 4 | -------------------------------------------------------------------------------- /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/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/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/reactjs-webapp-functions/3c1909e54a13ec962b55ee878e96d2adc3bd2302/src/webapp/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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; -------------------------------------------------------------------------------- /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/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/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 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | }); -------------------------------------------------------------------------------- /src/webapp/server/views/error.jade: -------------------------------------------------------------------------------- 1 | block content 2 | h1= message 3 | h2= error.status 4 | pre #{error.stack} 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | logo 24 |

{this.state.message}

25 |
26 |
27 | 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /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.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/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 | 54 | 55 | 56 | 57 | 58 | 59 | { 60 | props.items.map((item, i) => ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | )) 69 | } 70 | 71 |
Order IdCreated AtUpdated AtStatusWorkflow Status
{item.orderId}{item.createdAt}{item.updatedAt}{item.status}{item.workflowStatus}
72 |
73 | ); 74 | } -------------------------------------------------------------------------------- /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/webapp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/webapp/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------