diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 89badaa..7692c0a 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -22,9 +22,18 @@ permissions: contents: read jobs: - build-dotnet: + build: runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + steps: - name: Checkout uses: actions/checkout@v4 @@ -34,85 +43,79 @@ jobs: with: dotnet-version: '8.x' - - name: Restore NuGet packages - shell: bash - run: | - dotnet restore ./dotnet - - - name: Build .NET Apps - shell: bash - run: | - dotnet build ./dotnet -c Release - - - name: Publish .NET Apps - shell: bash - run: | - dotnet publish ./dotnet/src/ApiApp -c Release -o ./dotnet/publish/ApiApp - dotnet publish ./dotnet/src/WebApp -c Release -o ./dotnet/publish/WebApp - - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: dotnet - path: ./dotnet/publish - - deploy-dotnet: - if: ${{ vars.AZURE_CLIENT_ID != '' }} - runs-on: ubuntu-latest - needs: - - build-dotnet - - env: - AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} - - steps: - - name: Download artifacts - uses: actions/download-artifact@v2 + - name: Setup node.js + uses: actions/setup-node@v4 with: - name: dotnet - path: ./dotnet/publish + node-version: 'lts/Iron' - name: Install SWA CLI shell: bash run: | npm install --global @azure/static-web-apps-cli@latest - - name: Login to Azure + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + + - name: Log in with Azure Developer CLI (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + - name: Login to Azure CLI (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} uses: Azure/login@v2 with: client-id: ${{ env.AZURE_CLIENT_ID }} subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} tenant-id: ${{ env.AZURE_TENANT_ID }} - - name: Get API app name - id: get-apiapp-name - shell: bash + - name: Log in with Azure Developer CLI (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} run: | - export AZURE_WEBAPP_NAME=$(az webapp list -g rg-${{ env.AZURE_ENV_NAME }} --query "[].name | [?contains(@, 'dotnet')]" -o tsv) - echo "AZURE_WEBAPP_NAME=$AZURE_WEBAPP_NAME" >> "$GITHUB_OUTPUT" - - - name: Deploy to Azure - API app - uses: azure/webapps-deploy@v3 + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + env: + AZURE_CREDENTIALS: ${{ env.AZURE_CREDENTIALS }} + + - name: Login to Azure CLI (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} + uses: Azure/login@v2 with: - app-name: ${{ steps.get-apiapp-name.outputs.AZURE_WEBAPP_NAME }} - package: ./dotnet/publish/ApiApp - - - name: Get Static Web app name - id: get-sttapp-name - shell: bash - run: | - export AZURE_STTAPP_NAME=$(az staticwebapp list -g rg-${{ env.AZURE_ENV_NAME }} --query "[].name | [?contains(@, 'dotnet')]" -o tsv) - export AZURE_STTAPP_DEPLOYMENT_KEY=$(az staticwebapp secrets list -g rg-${{ env.AZURE_ENV_NAME }} -n $AZURE_STTAPP_NAME --query "properties.apiKey" -o tsv) - - echo "AZURE_STTAPP_NAME=$AZURE_STTAPP_NAME" >> "$GITHUB_OUTPUT" - echo "::add-mask::$AZURE_STTAPP_DEPLOYMENT_KEY" - echo "AZURE_STTAPP_DEPLOYMENT_KEY=$AZURE_STTAPP_DEPLOYMENT_KEY" >> "$GITHUB_OUTPUT" - - - name: Deploy to Azure - Static Web app - shell: bash - run: | - swa deploy ./dotnet/publish/WebApp/wwwroot --env production --deployment-token ${{ steps.get-sttapp-name.outputs.AZURE_STTAPP_DEPLOYMENT_KEY }} + creds: ${{ env.AZURE_CREDENTIALS }} + + - name: Provision Infrastructure + if: ${{ env.AZURE_CLIENT_ID != '' || env.AZURE_CREDENTIALS != '' }} + run: azd provision --no-prompt + env: + AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ env.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ env.AZURE_SUBSCRIPTION_ID }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ env.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Build Artifacts + if: ${{ env.AZURE_CLIENT_ID != '' || env.AZURE_CREDENTIALS != '' }} + run: azd package --all --no-prompt + env: + AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ env.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ env.AZURE_SUBSCRIPTION_ID }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ env.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Deploy Application + if: ${{ env.AZURE_CLIENT_ID != '' || env.AZURE_CREDENTIALS != '' }} + run: azd deploy --all --no-prompt + env: + AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ env.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ env.AZURE_SUBSCRIPTION_ID }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ env.AZD_INITIAL_ENVIRONMENT_CONFIG }} diff --git a/.github/workflows/register-api.yml b/.github/workflows/register-api.yml index fd2b5d1..53ca12b 100644 --- a/.github/workflows/register-api.yml +++ b/.github/workflows/register-api.yml @@ -19,6 +19,10 @@ on: description: 'The file path relative to the repository root' required: false default: '' + api-management-id: + description: 'The resource ID of the API Management service' + required: false + default: '' permissions: id-token: write @@ -32,8 +36,8 @@ jobs: AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + # AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + # AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} steps: - name: Checkout @@ -42,8 +46,7 @@ jobs: - name: Install APIC extension shell: bash run: | - # az extension add --name apic-extension --allow-preview true --yes - az extension add --source ./infra/scripts/apic_extension-1.0.0b4-py3-none-any.whl --allow-preview true --yes + az extension add --name apic-extension --allow-preview true --yes - name: Login to Azure uses: Azure/login@v2 @@ -59,4 +62,5 @@ jobs: -ResourceId "${{ github.event.inputs.resource-id }}" ` -ResourceGroup "${{ github.event.inputs.resource-group }}" ` -ApiCenterService "${{ github.event.inputs.api-center-service }}" ` - -FileLocation "${{ github.event.inputs.file-location }}" + -FileLocation "${{ github.event.inputs.file-location }}" ` + -ApiManagementId ${{ github.event.inputs.api-management-id }} ` diff --git a/README.md b/README.md index e63e496..b07e2d5 100644 --- a/README.md +++ b/README.md @@ -27,19 +27,23 @@ Throughout this reference sample, we'd like to give developer experiences how to 1. Fork this repository. 1. Clone the forked repository to your local machine. -1. Login to Azure Developer CLI. +1. Log in with the following command. Then, you will be able to use the `azd` cli to quickly provision and deploy the application. ```bash + # Authenticate with Azure Developer CLI azd auth login + + # Authenticate with Azure CLI + az login ``` -1. Provision all resources to Azure and deploy all the apps to those resources. +1. Run `azd up` to provision all the resources to Azure and deploy the code to those resources. ```bash azd up ``` -## Deploy APICenter Analyzer (Optional) +## APICenter Analyzer Integration (Optional) APICenter Analyzer is a tool to lint API specifications on the server-side. If you want to integrate this server-side linting feature, you can install it by following steps. @@ -59,33 +63,132 @@ APICenter Analyzer is a tool to lint API specifications on the server-side. If y ## CI/CD Pipelines -As a default, CI/CD pipelines are disabled. To enable CI/CD pipelines, follow these steps. +If you want to integrate the CI/CD pipeline with GitHub Actions, you can use the following command to create a GitHub repository and push the code to the repository. -1. Configure the CI/CD pipeline. +1. First of all, log in to GitHub. ```bash - azd pipeline config + # Authenticate with GitHub CLI + gh auth login ``` -1. Push your changes to the repository. +1. Run the following commands to update your GitHub repository variables. + + ```bash + # Bash + AZURE_CLIENT_ID=$(./infra/scripts/get-azdvariable.sh --key AZURE_CLIENT_ID) + azd pipeline config --principal-id $AZURE_CLIENT_ID + + # PowerShell + $AZURE_CLIENT_ID = $(./infra/scripts/Get-AzdVariable.ps1 -Key AZURE_CLIENT_ID) + azd pipeline config --principal-id $AZURE_CLIENT_ID + ``` + +1. Now, you're good to go! Push the code to the GitHub repository or manually run the GitHub Actions workflow to get your portal deployed. ## API Registration -You can register APIs to API Center in various ways. +You can register APIs to API Center in various ways. But here, we will show you how to register APIs through Azure CLI and the GitHub Actions workflow. -### Azure Portal +### Through Azure CLI -TBD +#### From local machine -### Azure CLI +You can register an API to API Center from a local machine, run the following commands: -TBD +```bash +# Bash +RESOURCE_GROUP= +APIC_NAME= +API_DOC_FILE_PATH= + +az apic api register -g $RESOURCE_GROUP -s $APIC_NAME --api-location $API_DOC_FILE_PATH + +# PowerShell +$RESOURCE_GROUP = "" +$APIC_NAME = "" +$API_DOC_FILE_PATH = "" + +az apic api register -g $RESOURCE_GROUP -s $APIC_NAME --api-location $API_DOC_FILE_PATH +``` + +> **NOTE**: Replace ``, `` and `` with your values. + +Alternatively, you can run the following script pre-written: + +```bash +# Bash +RESOURCE_GROUP= +APIC_NAME= +API_DOC_FILE_PATH= + +RESOURCE_ID=$(az apic service show -g $RESOURCE_GROUP -s $APIC_NAME --query "id" -o tsv) + +./infra/scripts/new-apiregistration.sh --resource-id $RESOURCE_ID --file-location $API_DOC_FILE_PATH + +# PowerShell +$RESOURCE_GROUP = "" +$APIC_NAME = "" +$API_DOC_FILE_PATH = "" + +$RESOURCE_ID = $(az apic service show -g $RESOURCE_GROUP -s $APIC_NAME --query "id" -o tsv) + +./infra/scripts/New-ApiRegistration.sh -ResourceId $RESOURCE_ID -FileLocation $API_DOC_FILE_PATH +``` + +> **NOTE**: Replace ``, `` and `` with your values. + +#### From API Management + +You can also register APIs to API Center directly importing from API Management. Run the following commands: + +```bash +# Bash +RESOURCE_GROUP= +APIC_NAME= +APIM_NAME= +APIM_ID=$(az resource list --namespace "Microsoft.ApiManagement" --resource-type "service" -g $RESOURCE_GROUP --query "[].id" -o tsv) + +az apic service import-from-apim -g $RESOURCE_GROUP -s $APIC_NAME --source-resource-ids "$APIM_ID/apis/*" + +# PowerShell +$RESOURCE_GROUP = "" +$APIC_NAME = "" +$APIM_NAME = "" +$APIM_ID = az resource list --namespace "Microsoft.ApiManagement" --resource-type "service" -g $RESOURCE_GROUP --query "[].id" -o tsv + +az apic service import-from-apim -g $RESOURCE_GROUP -s $APIC_NAME --source-resource-ids "$APIM_ID/apis/*" +``` + +> **NOTE**: Replace `` `` and `` with your values. + +Alternatively, you can run the following script pre-written: + +```bash +# Bash +RESOURCE_GROUP= + +APIC_ID=$(az resource list --namespace "Microsoft.ApiCenter" --resource-type "services" -g $RESOURCE_GROUP --query "[].id" -o tsv) +APIM_ID=$(az resource list --namespace "Microsoft.ApiManagement" --resource-type "service" -g $RESOURCE_GROUP --query "[].id" -o tsv) + +./infra/scripts/new-apiregistration.sh --resource-id $APIC_ID --api-management-id $APIM_ID + +# PowerShell +$RESOURCE_GROUP = "" + +$APIC_ID = $(az resource list --namespace "Microsoft.ApiCenter" --resource-type "services" -g $RESOURCE_GROUP --query "[].id" -o tsv) +$APIM_ID = $(az resource list --namespace "Microsoft.ApiManagement" --resource-type "service" -g $RESOURCE_GROUP --query "[].id" -o tsv) + +./infra/scripts/New-ApiRegistration.sh -ResourceId $APIC_ID -ApiManagementId $APIM_ID +``` + +> **NOTE**: Replace `` with your values. -### Visual Studio Code +### Through GitHub Actions Workflow TBD -### GitHub Actions +## Custom Metadata Management TBD @@ -101,7 +204,7 @@ TBD TBD -## API Center Portal +## API Center Portal Integration (Optional) TBD diff --git a/azure.yaml b/azure.yaml index 2c45b58..f8e920b 100644 --- a/azure.yaml +++ b/azure.yaml @@ -2,22 +2,87 @@ name: apicenter-reference -# services: -# appservice-dotnet: -# host: appservice -# language: dotnet -# project: ./dotnet/src/ApiApp +workflows: + up: + steps: + - azd: provision + - azd: package + - azd: deploy + +services: + appservice-dotnet: + host: appservice + language: csharp + project: ./dotnet/src/ApiApp # appservice-node: # host: appservice # language: js # project: ./nodejs/src/api -# staticapp-dotnet: -# host: staticwebapp -# language: dotnet -# project: ./dotnet/src/WebApp -# dist: wwwroot + staticapp-dotnet: + host: staticwebapp + language: csharp + project: ./dotnet/src/WebApp + dist: ./dotnet/publish/wwwroot + hooks: + prepackage: + posix: + shell: sh + continueOnError: false + interactive: true + run: dotnet publish -c Release -o ./dotnet/publish + windows: + shell: pwsh + continueOnError: false + interactive: true + run: dotnet publish -c Release -o ./dotnet/publish # staticapp-node: # host: staticwebapp # language: js # project: ./nodejs/src/web # dist: build + +hooks: + preup: + posix: + shell: sh + continueOnError: false + interactive: true + run: infra/hooks/preup.sh + windows: + shell: pwsh + continueOnError: false + interactive: true + run: infra/hooks/preup.ps1 + preprovision: + posix: + shell: sh + continueOnError: false + interactive: true + run: infra/hooks/preprovision.sh + windows: + shell: pwsh + continueOnError: false + interactive: true + run: infra/hooks/preprovision.ps1 + postprovision: + posix: + shell: sh + continueOnError: false + interactive: true + run: infra/hooks/postprovision.sh + windows: + shell: pwsh + continueOnError: false + interactive: true + run: infra/hooks/postprovision.ps1 + predown: + posix: + shell: sh + continueOnError: false + interactive: true + run: infra/hooks/predown.sh + windows: + shell: pwsh + continueOnError: false + interactive: true + run: infra/hooks/predown.ps1 diff --git a/infra/core/gateway/apicenter.bicep b/infra/core/gateway/apicenter.bicep index 72d50a3..af8043b 100644 --- a/infra/core/gateway/apicenter.bicep +++ b/infra/core/gateway/apicenter.bicep @@ -3,11 +3,16 @@ param name string param location string param tags object +param skuName string = 'Free' + // Create an API center service -resource apiCenter 'Microsoft.ApiCenter/services@2024-03-01' = { +resource apiCenter 'Microsoft.ApiCenter/services@2024-03-15-preview' = { name: name location: location tags: tags + sku: { + name: skuName + } identity: { type: 'SystemAssigned' } diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index 7dd42ce..a4d5c4b 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -36,6 +36,9 @@ param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false param ftpsState string = 'FtpsOnly' param healthCheckPath string = '' +param detailedErrorLoggingEnabled bool = true +param httpLoggingEnabled bool = true +param requestTracingEnabled bool = true resource appService 'Microsoft.Web/sites@2022-09-01' = { name: name @@ -58,6 +61,9 @@ resource appService 'Microsoft.Web/sites@2022-09-01' = { cors: { allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) } + detailedErrorLoggingEnabled: detailedErrorLoggingEnabled + httpLoggingEnabled: httpLoggingEnabled + requestTracingEnabled: requestTracingEnabled } clientAffinityEnabled: clientAffinityEnabled httpsOnly: true diff --git a/infra/core/monitor/appservice-diagnosticsettings.bicep b/infra/core/monitor/appservice-diagnosticsettings.bicep new file mode 100644 index 0000000..a136fb2 --- /dev/null +++ b/infra/core/monitor/appservice-diagnosticsettings.bicep @@ -0,0 +1,62 @@ +metadata description = 'Creates a diagnostics settings and integrate it with an App Service instance and Log Analytics Workspace instance.' +param name string + +param logAnalyticsName string +param appServiceName string + +@allowed([ + 'functionapp' + 'appservice' +]) +param kind string = 'appservice' + +var diagnosticLogCategoriesToEnable = kind == 'functionapp' + ? [ + 'FunctionAppLogs' + 'AppServiceAuthenticationLogs' + ] + : [ + 'AppServiceHTTPLogs' + 'AppServiceConsoleLogs' + 'AppServiceAppLogs' + 'AppServiceAuditLogs' + 'AppServiceIPSecAuditLogs' + 'AppServicePlatformLogs' + 'AppServiceAuthenticationLogs' + ] + +var diagnosticMetricsToEnable = [ + 'AllMetrics' +] + +var diagnosticsLogs = [ + for category in diagnosticLogCategoriesToEnable: { + category: category + enabled: true + } +] + +var diagnosticsMetrics = [ + for metric in diagnosticMetricsToEnable: { + category: metric + timeGrain: null + enabled: true + } +] + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = { + name: logAnalyticsName +} +resource appService 'Microsoft.Web/sites@2022-03-01' existing = { + name: appServiceName +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: name + scope: appService + properties: { + workspaceId: logAnalytics.id + metrics: diagnosticsMetrics + logs: diagnosticsLogs + } +} diff --git a/infra/hooks/load_azd_env.ps1 b/infra/hooks/load_azd_env.ps1 new file mode 100644 index 0000000..16bd9e9 --- /dev/null +++ b/infra/hooks/load_azd_env.ps1 @@ -0,0 +1,21 @@ +# Loads the azd .env file into the current environment +# It does the following: +# 1. Loads the azd .env file from the current environment + +Param( + [switch] + [Parameter(Mandatory=$false)] + $ShowMessage +) + +if ($ShowMessage) { + Write-Host "Loading azd .env file from current environment" -ForegroundColor Cyan +} + +foreach ($line in (& azd env get-values)) { + if ($line -match "([^=]+)=(.*)") { + $key = $matches[1] + $value = $matches[2] -replace '^"|"$' + [Environment]::SetEnvironmentVariable($key, $value) + } +} diff --git a/infra/hooks/load_azd_env.sh b/infra/hooks/load_azd_env.sh new file mode 100755 index 0000000..75fb3ec --- /dev/null +++ b/infra/hooks/load_azd_env.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +SHOW_MESSAGE=false + +if [[ $# -eq 0 ]]; then + SHOW_MESSAGE=false +fi + +while [[ "$1" != "" ]]; do + case $1 in + -m | --show-message) + SHOW_MESSAGE=true + ;; + + *) + usage + exit 1 + ;; + esac + + shift +done + +if [[ $SHOW_MESSAGE == true ]]; then + echo -e "\e[36mLoading azd .env file from current environment...\e[0m" +fi + +while IFS='=' read -r key value; do + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + export "$key=$value" +done <$null + +if ([string]::IsNullOrEmpty($EXPIRED_TOKEN)) { + az login --scope https://graph.microsoft.com/.default -o none +} + +if ([string]::IsNullOrEmpty($env:AZURE_SUBSCRIPTION_ID)) { + $ACCOUNT = az account show --query '[id,name]' + Write-Host "You can set the `AZURE_SUBSCRIPTION_ID` environment variable with `azd env set AZURE_SUBSCRIPTION_ID`." + Write-Host $ACCOUNT + + $response = Read-Host "Do you want to use the above subscription? (Y/n) " + $response = if ([string]::IsNullOrEmpty($response)) { "Y" } else { $response } + switch ($response) { + { $_ -match "^[yY](es)?$" } { + # Do nothing + break + } + default { + Write-Host "Listing available subscriptions..." + $SUBSCRIPTIONS = az account list --query 'sort_by([], &name)' --output json + Write-Host "Available subscriptions:" + Write-Host ($SUBSCRIPTIONS | ConvertFrom-Json | ForEach-Object { "{0} {1}" -f $_.name, $_.id } | Format-Table) + $subscription_input = Read-Host "Enter the name or ID of the subscription you want to use: " + $AZURE_SUBSCRIPTION_ID = ($SUBSCRIPTIONS | ConvertFrom-Json | Where-Object { $_.name -eq $subscription_input -or $_.id -eq $subscription_input } | Select-Object -exp id) + if (-not [string]::IsNullOrEmpty($AZURE_SUBSCRIPTION_ID)) { + Write-Host "Setting active subscription to: $AZURE_SUBSCRIPTION_ID" + az account set -s $AZURE_SUBSCRIPTION_ID + } + else { + Write-Host "Subscription not found. Please enter a valid subscription name or ID." + exit 1 + } + break + } + } +} +else { + az account set -s $env:AZURE_SUBSCRIPTION_ID +} diff --git a/infra/hooks/login.sh b/infra/hooks/login.sh new file mode 100755 index 0000000..cfdbba1 --- /dev/null +++ b/infra/hooks/login.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Logs in to Azure through AZD and AZ CLI +# It does the following: +# 1. Checks if the user is logged in to Azure +# 2. Logs in to Azure Developer CLI if the user is not logged in +# 3. Logs in to Azure CLI if the user is not logged in +# 4. Sets the active subscription if the user is logged in +# 5. Prompts the user to select a subscription if the subscription is not set +# 6. Sets the active subscription to the selected subscription +# 7. Exits if the subscription is not found + +set -e + +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + +# AZD LOGIN +# Check if the user is logged in to Azure +login_status=$(azd auth login --check-status) + +# Check if the user is not logged in +if [[ "$login_status" == *"Not logged in"* ]]; then + echo "Not logged in, initiating login process..." + # Command to log in to Azure + azd auth login +fi + +# AZ LOGIN +EXPIRED_TOKEN=$(az ad signed-in-user show --query 'id' -o tsv 2>/dev/null || true) + +if [[ -z "$EXPIRED_TOKEN" ]]; then + az login --scope https://graph.microsoft.com/.default -o none +fi + +if [[ -z "${AZURE_SUBSCRIPTION_ID:-}" ]]; then + ACCOUNT=$(az account show --query '[id,name]') + echo "You can set the \`AZURE_SUBSCRIPTION_ID\` environment variable with \`azd env set AZURE_SUBSCRIPTION_ID\`." + echo $ACCOUNT + + read -r -p "Do you want to use the above subscription? (Y/n) " response + response=${response:-Y} + case "$response" in + [yY][eE][sS]|[yY]) + ;; + *) + echo "Listing available subscriptions..." + SUBSCRIPTIONS=$(az account list --query 'sort_by([], &name)' --output json) + echo "Available subscriptions:" + echo "$SUBSCRIPTIONS" | jq -r '.[] | [.name, .id] | @tsv' | column -t -s $'\t' + read -r -p "Enter the name or ID of the subscription you want to use: " subscription_input + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | jq -r --arg input "$subscription_input" '.[] | select(.name==$input or .id==$input) | .id') + if [[ -n "$AZURE_SUBSCRIPTION_ID" ]]; then + echo "Setting active subscription to: $AZURE_SUBSCRIPTION_ID" + az account set -s $AZURE_SUBSCRIPTION_ID + else + echo "Subscription not found. Please enter a valid subscription name or ID." + exit 1 + fi + ;; + *) + echo "Use the \`az account set\` command to set the subscription you'd like to use and re-run this script." + exit 0 + ;; + esac +else + az account set -s $AZURE_SUBSCRIPTION_ID +fi diff --git a/infra/hooks/postprovision.ps1 b/infra/hooks/postprovision.ps1 new file mode 100644 index 0000000..69d3585 --- /dev/null +++ b/infra/hooks/postprovision.ps1 @@ -0,0 +1,51 @@ +# Runs the post-provision script after the environment is provisioned +# It does the following: +# 1. Creates a service principal +# 2. Adds required permissions to the app +# 3. Sets the environment variables + +Write-Host "Running post-provision script..." + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +# Run only if GITHUB_WORKSPACE is NOT set - this is NOT running in a GitHub Action workflow +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + Write-Host "Registering the application in Azure..." + + # Load the azd environment variables + & "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + + $AZURE_ENV_NAME = $env:AZURE_ENV_NAME + + # Create a service principal + $appId = $env:AZURE_CLIENT_ID + if ([string]::IsNullOrEmpty($appId)) { + $appId = az ad app list --display-name "spn-$AZURE_ENV_NAME" --query "[].appId" -o tsv + if ([string]::IsNullOrEmpty($appId)) { + $appId = az ad app create --display-name spn-$AZURE_ENV_NAME --query "appId" -o tsv + $spnId = az ad sp create --id $appId --query "id" -o tsv + } + } + + $spnId = az ad sp list --display-name "spn-$AZURE_ENV_NAME" --query "[].id" -o tsv + if ([string]::IsNullOrEmpty($spnId)) { + $spnId = az ad sp create --id $appId --query "id" -o tsv + } + + $objectId = az ad app show --id $appId --query "id" -o tsv + + # Add required permissions to the app + $requiredResourceAccess = @( @{ resourceAppId = "c3ca1a77-7a87-4dba-b8f8-eea115ae4573"; resourceAccess = @( @{ type = "Scope"; id = "44327351-3395-414e-882e-7aa4a9c3b25d" } ) } ) + + $payload = @{ requiredResourceAccess = $requiredResourceAccess; } | ConvertTo-Json -Depth 100 -Compress | ConvertTo-Json + + az rest -m PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` + --headers Content-Type=application/json ` + --body $payload + + # Set the environment variables + azd env set AZURE_CLIENT_ID $appId +} else { + Write-Host "Skipping to register the application in Azure..." +} diff --git a/infra/hooks/postprovision.sh b/infra/hooks/postprovision.sh new file mode 100755 index 0000000..df3c292 --- /dev/null +++ b/infra/hooks/postprovision.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Runs the post-provision script after the environment is provisioned +# It does the following: +# 1. Creates a service principal +# 2. Adds required permissions to the app +# 3. Sets the environment variables + +set -e + +echo "Running post-provision script..." + +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + +# Run only if GITHUB_WORKSPACE is NOT set - this is NOT running in a GitHub Action workflow +if [ -z "$GITHUB_WORKSPACE" ]; +then + echo "Registering the application in Azure..." + + # Load the azd environment variables + "$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + + # Create a service principal and assign the required permissions + appId=$AZURE_CLIENT_ID + if [ -z "$appId" ] + then + appId=$(az ad app list --display-name "spn-$AZURE_ENV_NAME" --query "[].appId" -o tsv) + if [ -z "$appId" ] + then + appId=$(az ad app create --display-name "spn-$AZURE_ENV_NAME" --query "appId" -o tsv) + spnId=$(az ad sp create --id $appId --query "id" -o tsv) + fi + fi + + spnId=$(az ad sp list --display-name "spn-$AZURE_ENV_NAME" --query "[].id" -o tsv) + if [ -z "$spnId" ] + then + spnId=$(az ad sp create --id $appId --query "id" -o tsv) + fi + + objectId=$(az ad app show --id $appId --query "id" -o tsv) + + # Add required permissions to the app + requiredResourceAccess="[{\"resourceAppId\": \"c3ca1a77-7a87-4dba-b8f8-eea115ae4573\", \"resourceAccess\": [{\"type\": \"Scope\", \"id\": \"44327351-3395-414e-882e-7aa4a9c3b25d\"}]}]" + + payload=$(jq -n \ + --argjson requiredResourceAccess "$requiredResourceAccess" \ + "{\"requiredResourceAccess\": $requiredResourceAccess}") + + az rest -m PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$objectId" \ + --headers Content-Type=application/json \ + --body "$payload" + + # Set the environment variables + azd env set AZURE_CLIENT_ID $appId +else + echo "Skipping to register the application in Azure..." +fi diff --git a/infra/hooks/predown.ps1 b/infra/hooks/predown.ps1 new file mode 100644 index 0000000..6e7e408 --- /dev/null +++ b/infra/hooks/predown.ps1 @@ -0,0 +1,17 @@ +# Runs the pre-down script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-down script..." + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/predown.sh b/infra/hooks/predown.sh new file mode 100755 index 0000000..5595309 --- /dev/null +++ b/infra/hooks/predown.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Runs the pre-down script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-down script..." + +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/hooks/preprovision.ps1 b/infra/hooks/preprovision.ps1 new file mode 100644 index 0000000..85cebaf --- /dev/null +++ b/infra/hooks/preprovision.ps1 @@ -0,0 +1,16 @@ +# Runs the pre-provision script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-provision script..." + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" -ShowMessage + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/preprovision.sh b/infra/hooks/preprovision.sh new file mode 100755 index 0000000..5ae8a78 --- /dev/null +++ b/infra/hooks/preprovision.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Runs the pre-provision script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-provision script..." + +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" --show-message + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/hooks/preup.ps1 b/infra/hooks/preup.ps1 new file mode 100644 index 0000000..1d1dfe2 --- /dev/null +++ b/infra/hooks/preup.ps1 @@ -0,0 +1,16 @@ +# Runs the pre-up script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-up script..." + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" -ShowMessage + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/preup.sh b/infra/hooks/preup.sh new file mode 100755 index 0000000..e2a4757 --- /dev/null +++ b/infra/hooks/preup.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Runs the pre-up script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-up script..." + +REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" --show-message + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/main.bicep b/infra/main.bicep index c09ab51..fc8e84e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -39,6 +39,7 @@ param useApplicationInsights bool = false // Set in main.parameters.json param logAnalyticsName string = '' // Set in main.parameters.json param applicationInsightsName string = '' // Set in main.parameters.json param applicationInsightsDashboardName string = '' // Set in main.parameters.json +param diagnosticsSettingsName string = '' param appServicePlanName string = '' // Set in main.parameters.json param appServiceSkuName string // Set in main.parameters.json @@ -269,6 +270,18 @@ module appServices './core/host/appservice.bicep' = [for app in apps: { } }] +// Integrate Diagnostics settings +module diagnostics './core/monitor/appservice-diagnosticsettings.bicep' = [for (app, i) in apps: if (useApplicationInsights) { + name: 'diagnostics-${app.name}' + scope: rg + params: { + name: !empty(diagnosticsSettingsName) ? '${diagnosticsSettingsName}-${app.name}' : 'diag-${resourceToken}-${app.name}' + logAnalyticsName: monitoring.outputs.logAnalyticsWorkspaceName + appServiceName: appServices[i].outputs.name + kind: 'appservice' + } +}] + // Provision Static Web Apps for each application module staticApps './core/host/staticwebapp.bicep' = [for app in apps: { name: 'staticapp-${app.name}' diff --git a/infra/scripts/Get-AzdVariable.ps1 b/infra/scripts/Get-AzdVariable.ps1 new file mode 100644 index 0000000..9027d60 --- /dev/null +++ b/infra/scripts/Get-AzdVariable.ps1 @@ -0,0 +1,60 @@ +# This gets environment variables from the current azd context. +Param( + [string] + [Parameter(Mandatory=$false)] + $Key = "", + + [switch] + [Parameter(Mandatory=$false)] + $Help +) + +function Show-Usage { + Write-Output " This gets environment variables from the current azd context + + Usage: $(Split-Path $MyInvocation.ScriptName -Leaf) `` + [-Key ] `` + + [-Help] + + Options: + -Key: Environment variable key. Example: ``AZURE_ENV_NAME`` + + -Help: Show this message. +" + + Exit 0 +} + +# Show usage +$needHelp = $Help -eq $true +if ($needHelp -eq $true) { + Show-Usage + Exit 0 +} + +if ($Key -eq "") { + Write-Host " Key is required." -ForegroundColor Red + Write-Host "" + + Show-Usage + Exit 0 +} + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +pwsh -Command { + Param( + $RepositoryRoot, + $Key + ) + + # Load the azd environment variables + & "$RepositoryRoot/infra/hooks/load_azd_env.ps1" + + $envs = Get-ChildItem -Path env: + + $value = $($envs | Where-Object { $_.Name -eq $Key }).Value + + Write-Output $value +} -args $REPOSITORY_ROOT, $Key diff --git a/infra/scripts/New-ApiRegistration.ps1 b/infra/scripts/New-ApiRegistration.ps1 index cd75f35..d14b0fa 100644 --- a/infra/scripts/New-ApiRegistration.ps1 +++ b/infra/scripts/New-ApiRegistration.ps1 @@ -16,6 +16,10 @@ Param( [Parameter(Mandatory=$false)] $FileLocation = "", + [string] + [Parameter(Mandatory=$false)] + $ApiManagementId = "", + [string] [Parameter(Mandatory=$false)] $ApiVersion = "2024-03-01", @@ -26,13 +30,14 @@ Param( ) function Show-Usage { - Write-Output " This registers API to API Center + Write-Host " This registers API to API Center Usage: $(Split-Path $MyInvocation.ScriptName -Leaf) `` [-ResourceId ] `` [-ResourceGroup ] `` [-ApiCenterService ] `` [-FileLocation ] `` + [-ApiManagementId ] `` [-ApiVersion ] `` [-Help] @@ -42,6 +47,7 @@ function Show-Usage { -ResourceGroup Resource group. It must be provided unless `ResourceId` is provided. -ApiCenterService API Center instance name. It must be provided unless `ResourceId` is provided. -FileLocation File location to register. + -ApiManagementId API Management resource ID. If provided, ``FileLocation`` will be ignored. -ApiVersion REST API version. Default is `2024-03-01`. -Help: Show this message. @@ -58,11 +64,15 @@ if ($needHelp -eq $true) { } if (($ResourceId -eq "") -and ($ResourceGroup -eq "" -or $ApiCenterService -eq "")) { - Write-Output "`ResourceId` must be provided, or both `ResourceGroup` and `ApiCenterService` must be provided" + Write-Host "`ResourceId` must be provided, or both `ResourceGroup` and `ApiCenterService` must be provided" + Exit 0 +} +if ($FileLocation -eq "" -and $ApiManagementId -eq "") { + Write-Host "`FileLocation` must be provided" Exit 0 } -if ($FileLocation -eq "") { - Write-Output "`FileLocation` must be provided" +if ($ApiManagementId -notlike "/subscriptions/*") { + Write-Host "`ApiManagementId` must be a valid resource ID" Exit 0 } @@ -76,7 +86,18 @@ if ($ApiCenterService -eq "") { $REPOSITORY_ROOT = git rev-parse --show-toplevel -$registered = az apic api register ` +if ($ApiManagementId -eq "") { + Write-Host "Registering API from a file: $FileLocation ..." + + $registered = az apic api register ` -g $ResourceGroup ` -s $ApiCenterService ` --api-location "$REPOSITORY_ROOT/$($FileLocation.Replace("\", "/"))" +} else { + Write-Host "Registering API from API Management: $ApiManagementId ..." + + $registered = az apic service import-from-apim ` + -g $ResourceGroup ` + -s $ApiCenterService ` + --source-resource-ids "$APIM_ID/apis/*" +} diff --git a/infra/scripts/apic_extension-1.0.0b4-py3-none-any.whl b/infra/scripts/apic_extension-1.0.0b4-py3-none-any.whl deleted file mode 100644 index f8a7f53..0000000 Binary files a/infra/scripts/apic_extension-1.0.0b4-py3-none-any.whl and /dev/null differ diff --git a/infra/scripts/get-azdvariable.sh b/infra/scripts/get-azdvariable.sh new file mode 100755 index 0000000..903be78 --- /dev/null +++ b/infra/scripts/get-azdvariable.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# This gets environment variables from the current azd context. + +set -e + +function usage() { + cat <