diff --git a/.github/azFunction/AzFunctionCode/.funcignore b/.github/azFunction/AzFunctionCode/.funcignore new file mode 100644 index 000000000..414df2f01 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/.funcignore @@ -0,0 +1,4 @@ +.git* +.vscode +local.settings.json +test \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/README.md b/.github/azFunction/AzFunctionCode/README.md new file mode 100644 index 000000000..ac5b094ac --- /dev/null +++ b/.github/azFunction/AzFunctionCode/README.md @@ -0,0 +1,6 @@ +# Overview +This folder contains the various functions that are contained in the overall Azure Functions. The following functions are present: +1. timerschedule; this is a simple timer trigger which runs every five hours, and triggers the next function through placing a queue item in the startjob queue. +2. getPullRequests; this is a queue based trigger (startjob queue) which gets the latest x closed pull requests from GitHub. PR title, number and state for each is saved in a queue item in the closedPullRequests queue. +3. getSubscriptions; this is a queue based trigger (closedPullRequests queue), which for each pull request looks for an corresponding, active subscription. If the subscription is found subscription name and id is saved in a queue item in the subscriptionsToClose queue. +4. cancelSubscriptions; this is a queue based trigger (subscriptionsToClose queue), which for each subscription tries to cancel the subscription. If succesful subscription name and id is saved in a queue item in the canceledSubscriptions queue. \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/cancelSubscriptions/function.json b/.github/azFunction/AzFunctionCode/cancelSubscriptions/function.json new file mode 100644 index 000000000..958134783 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/cancelSubscriptions/function.json @@ -0,0 +1,18 @@ +{ + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "subscriptionsToClose", + "connection": "AzureWebJobsStorage" + }, + { + "type": "queue", + "direction": "out", + "name": "canceledSubscriptions", + "queueName": "canceledSubscriptions", + "connection": "AzureWebJobsStorage" + } + ] +} diff --git a/.github/azFunction/AzFunctionCode/cancelSubscriptions/readme.md b/.github/azFunction/AzFunctionCode/cancelSubscriptions/readme.md new file mode 100644 index 000000000..f04ce66ea --- /dev/null +++ b/.github/azFunction/AzFunctionCode/cancelSubscriptions/readme.md @@ -0,0 +1,2 @@ +# Overview +This function is triggered by items arriving on the subscriptionsToClose queue as defined in input bindings in the function.json file. Upon triggering the function will cancel the subscription using Azure rest api. If succesful, subscription name and id is placed in the queue canceledSubscriptions on associated storage as specified in output bindings in the function.json file. \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/cancelSubscriptions/run.ps1 b/.github/azFunction/AzFunctionCode/cancelSubscriptions/run.ps1 new file mode 100644 index 000000000..635a10682 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/cancelSubscriptions/run.ps1 @@ -0,0 +1,18 @@ +# Input bindings are passed in via param block. +param($QueueItem, $TriggerMetadata) +# Write out the queue message and insertion time to the information log. +Write-Output "PowerShell queue trigger function processed work item: $QueueItem" +$subscriptionId = $QueueItem.Body.subscriptionId +$subscriptionName = $QueueItem.Body.subscriptionName +Write-Output "Subscription to be canceled is $subscriptionName with id: $subscriptionId" +$cancelUri = "https://management.azure.com/subscriptions/$($subscriptionId)/providers/Microsoft.Subscription/cancel?api-version=2020-09-01" +Invoke-AzRestMethod -Uri $cancelUri -Method POST +$body = @{ + subscriptionName = $subscriptionName + subscriptionId = $subscriptionId +} +Push-OutputBinding -Name canceledSubscriptions -Value ([HttpResponseContext]@{ + Body = $body + }) +Write-Output "Queue item insertion time: $($TriggerMetadata.InsertionTime)" + diff --git a/.github/azFunction/AzFunctionCode/getPullRequests/function.json b/.github/azFunction/AzFunctionCode/getPullRequests/function.json new file mode 100644 index 000000000..f62e476df --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getPullRequests/function.json @@ -0,0 +1,18 @@ +{ + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "startjob", + "connection": "AzureWebJobsStorage" + }, + { + "type": "queue", + "direction": "out", + "name": "pullRequests", + "queueName": "closedPullRequests", + "connection": "AzureWebJobsStorage" + } + ] +} diff --git a/.github/azFunction/AzFunctionCode/getPullRequests/readme.md b/.github/azFunction/AzFunctionCode/getPullRequests/readme.md new file mode 100644 index 000000000..7b99ffa60 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getPullRequests/readme.md @@ -0,0 +1,2 @@ +# Overview +This function is triggered by items arriving on the startjob queue as defined in input bindings in the function.json file. Upon triggering the function will get a number of the latest closed pull requests on the ALZ-Bicep GitHub repo. This is all specified in the run.ps1 file, currently it's set to the last 20 on page 1 (latest). Note that Github API have a maximum of 100 items per page pulled so if a greater number was required, more pages would need to be queried. For each PR the title, PR number and state is placed in the queue closedPullRequests on associated storage as specified in output bindings in the function.json file. \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/getPullRequests/run.ps1 b/.github/azFunction/AzFunctionCode/getPullRequests/run.ps1 new file mode 100644 index 000000000..37d1d12d6 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getPullRequests/run.ps1 @@ -0,0 +1,17 @@ +# Input bindings are passed in via param block. +param($QueueItem, $TriggerMetadata) +# Write out the queue message and insertion time to the information log. +Write-Output "PowerShell queue trigger function processed work item: $QueueItem" +Write-Output "Queue item insertion time: $($TriggerMetadata.InsertionTime)" +$perPageCount = 0 +$closedPrs = Invoke-RestMethod -Method Get -Uri "https://api.github.com/repos/Azure/ALZ-Bicep/pulls?per_page=$perPageCount&state=closed&page=1" +$closedPrs | Select-Object -unique -Property title, number, state | ForEach-Object { + $body = @{ + prTitle = $PSItem.title + prNumber = $PSItem.number + prState = $PSItem.state + } + Push-OutputBinding -Name pullRequests -Value ([HttpResponseContext]@{ + Body = $body + }) +} \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/getSubscriptions/function.json b/.github/azFunction/AzFunctionCode/getSubscriptions/function.json new file mode 100644 index 000000000..695730d39 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getSubscriptions/function.json @@ -0,0 +1,18 @@ +{ + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "closedPullRequests", + "connection": "AzureWebJobsStorage" + }, + { + "type": "queue", + "direction": "out", + "name": "subscriptionsToClose", + "queueName": "subscriptionsToClose", + "connection": "AzureWebJobsStorage" + } + ] +} diff --git a/.github/azFunction/AzFunctionCode/getSubscriptions/readme.md b/.github/azFunction/AzFunctionCode/getSubscriptions/readme.md new file mode 100644 index 000000000..db0417e17 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getSubscriptions/readme.md @@ -0,0 +1,2 @@ +# Overview +This function is triggered by items arriving on the closedPullRequests queue as defined in input bindings in the function.json file. Upon triggering the function will try to get the subscription state based on subscription name using Get-AzSubscription. If subscription exists and is active, subscription name and id is placed in the queue subscriptionsToClose on associated storage as specified in output bindings in the function.json file. \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/getSubscriptions/run.ps1 b/.github/azFunction/AzFunctionCode/getSubscriptions/run.ps1 new file mode 100644 index 000000000..00da33368 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/getSubscriptions/run.ps1 @@ -0,0 +1,31 @@ +# Input bindings are passed in via param block. +param($QueueItem, $TriggerMetadata) +# Write out the queue message and insertion time to the information log. +Write-Output "PowerShell queue trigger function processed work item: $QueueItem" +$prNumber = $QueueItem.Body.prNumber +$subscriptionName = "sub-unit-test-pr-$prNumber" +Write-Output "Subscription to look for is $subscriptionName" +Write-Output "Queue item insertion time: $($TriggerMetadata.InsertionTime)" +#MSI to look for subscripition in current tenant +Import-module Az.Accounts -verbose +$subscription = Get-AzSubscription -SubscriptionName $subscriptionName -ErrorAction SilentlyContinue +If ($subscription) { + Write-Output "found subscription $subscriptionName" + $subscriptionId = $subscription.Id + $subscriptionState = $subscription.State + If ($subscriptionState -eq "Enabled") { + $body = @{ + subscriptionId = $subscriptionId + subscriptionName = $subscriptionName + } + Push-OutputBinding -Name subscriptionsToClose -Value ([HttpResponseContext]@{ + Body = $body + }) + } + Else { + Write-Output "Subscription $subscriptionName is already canceled" + } +} +Else { + Write-Output "Could not find subscription $subscriptionName" +} diff --git a/.github/azFunction/AzFunctionCode/host.json b/.github/azFunction/AzFunctionCode/host.json new file mode 100644 index 000000000..6ae17b864 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/host.json @@ -0,0 +1,18 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + }, + "managedDependency": { + "enabled": true + } +} diff --git a/.github/azFunction/AzFunctionCode/profile.ps1 b/.github/azFunction/AzFunctionCode/profile.ps1 new file mode 100644 index 000000000..1670fc99c --- /dev/null +++ b/.github/azFunction/AzFunctionCode/profile.ps1 @@ -0,0 +1,22 @@ +# Azure Functions profile.ps1 +# +# This profile.ps1 will get executed every "cold start" of your Function App. +# "cold start" occurs when: +# +# * A Function App starts up for the very first time +# * A Function App starts up after being de-allocated due to inactivity +# +# You can define helper functions, run commands, or specify environment variables +# NOTE: any variables defined that are not environment variables will get reset after the first execution + +# Authenticate with Azure PowerShell using MSI. +# Remove this if you are not planning on using MSI or Azure PowerShell. +if ($env:MSI_SECRET) { + Disable-AzContextAutosave -Scope Process | Out-Null + Connect-AzAccount -Identity +} + +# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. +# Enable-AzureRmAlias + +# You can also define functions or aliases that can be referenced in any of your PowerShell functions. diff --git a/.github/azFunction/AzFunctionCode/requirements.psd1 b/.github/azFunction/AzFunctionCode/requirements.psd1 new file mode 100644 index 000000000..e117940f9 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/requirements.psd1 @@ -0,0 +1,8 @@ +# This file enables modules to be automatically managed by the Functions service. +# See https://aka.ms/functionsmanageddependency for additional information. +# +@{ + # For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. + # To use the Az module in your function app, please uncomment the line below. + 'Az.Accounts' = '2.*' +} \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/timerschedule/function.json b/.github/azFunction/AzFunctionCode/timerschedule/function.json new file mode 100644 index 000000000..00d6a0325 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/timerschedule/function.json @@ -0,0 +1,17 @@ +{ + "bindings": [ + { + "name": "Timer", + "type": "timerTrigger", + "direction": "in", + "schedule": "0 0 */2 * * *" + }, + { + "type": "queue", + "direction": "out", + "name": "startJob", + "queueName": "startjob", + "connection": "AzureWebJobsStorage" + } + ] +} diff --git a/.github/azFunction/AzFunctionCode/timerschedule/readme.md b/.github/azFunction/AzFunctionCode/timerschedule/readme.md new file mode 100644 index 000000000..9094437c7 --- /dev/null +++ b/.github/azFunction/AzFunctionCode/timerschedule/readme.md @@ -0,0 +1,2 @@ +# Overview +This trigger runs every x hours (set by cron syntax in the function.json file). When the trigger runs it creates a queue item in the startjob queue on associated storage as defined by output bindings in the function.json file. This queue item is used to trigger subsequent functions. \ No newline at end of file diff --git a/.github/azFunction/AzFunctionCode/timerschedule/run.ps1 b/.github/azFunction/AzFunctionCode/timerschedule/run.ps1 new file mode 100644 index 000000000..ae839a29c --- /dev/null +++ b/.github/azFunction/AzFunctionCode/timerschedule/run.ps1 @@ -0,0 +1,21 @@ +# Input bindings are passed in via param block. +param($Timer) + +# Get the current universal time in the default string format +$currentUTCtime = (Get-Date).ToUniversalTime() + +# The 'IsPastDue' porperty is 'true' when the current function invocation is later than scheduled. +if ($Timer.IsPastDue) { + Write-Output "PowerShell timer is running late!" +} + +# Write an information log with the current time. +Write-Output "PowerShell timer trigger function ran! TIME: $currentUTCtime" +$body = @{ + GitHubRepo = "https://api.github.com/repos/Azure/ALZ-Bicep/pulls" +} +# Associate values to output bindings by calling 'Push-OutputBinding'. +Push-OutputBinding -Name startJob -Value ([HttpResponseContext]@{ + #StatusCode = [HttpStatusCode]::OK + Body = $body + }) diff --git a/.github/azFunction/AzFunctionInfrastructure/main.bicep b/.github/azFunction/AzFunctionInfrastructure/main.bicep new file mode 100644 index 000000000..261a6cccb --- /dev/null +++ b/.github/azFunction/AzFunctionInfrastructure/main.bicep @@ -0,0 +1,112 @@ +@description('Azure region to deploy in') +param parLocation string = resourceGroup().location + +@description('Azure Function App Name') +param parAzFunctionName string = uniqueString(resourceGroup().id) + +@description('Azure Storage Account Name') +param parStorageAccountName string = uniqueString(resourceGroup().id) + +@description('App Service Plan Name') +param parApplicationServicePlanName string = 'FunctionPlan' + +@description('Application Insights Name') +param parApplicationInsightsName string = 'AppInsights' + +resource resStorageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { + name: parStorageAccountName + location: parLocation + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + supportsHttpsTrafficOnly: true + encryption: { + services: { + file: { + keyType: 'Account' + enabled: true + } + blob: { + keyType: 'Account' + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + accessTier: 'Hot' + } +} + +resource resApplicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: parApplicationInsightsName + location: parLocation + kind: 'web' + properties: { + Application_Type: 'web' + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +resource resApplicationServicePlan 'Microsoft.Web/serverfarms@2020-12-01' = { + name: parApplicationServicePlanName + location: parLocation + kind: 'functionapp,linux' + sku: { + name: 'Y1' + } + properties: {} +} + +resource resAzFunction 'Microsoft.Web/sites@2021-03-01' = { + name: parAzFunctionName + location: parLocation + kind: 'functionapp,linux' + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: resApplicationServicePlan.id + enabled: true + reserved: true + isXenon: false + hyperV: false + siteConfig: { + numberOfWorkers: 1 + acrUseManagedIdentityCreds: false + alwaysOn: false + http20Enabled: false + functionAppScaleLimit: 200 + minimumElasticInstanceCount: 0 + appSettings: [ + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${resStorageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(resStorageAccount.id, resStorageAccount.apiVersion).keys[0].value}' + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${resStorageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(resStorageAccount.id, resStorageAccount.apiVersion).keys[0].value}' + } + { + name: 'WEBSITE_CONTENTSHARE' + value: 'denyfunctionb051f2' + } + { + name: 'APPINSIGHTS_INSTRUMENTATIONKEY' + value: resApplicationInsights.properties.InstrumentationKey + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'powerShell' + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + ] + } + httpsOnly: true + } +} diff --git a/.github/azFunction/AzFunctionInfrastructure/rbac.bicep b/.github/azFunction/AzFunctionInfrastructure/rbac.bicep new file mode 100644 index 000000000..1038b4d5d --- /dev/null +++ b/.github/azFunction/AzFunctionInfrastructure/rbac.bicep @@ -0,0 +1,26 @@ +targetScope = 'managementGroup' + +@description('Azure Function App Name') +param parAzFunctionName string + +@description('Resource group for Azure Function App') +param parAzFunctionResourceGroupName string = 'cancelsubscription' + +@description('Subscription Id for Azure Function App') +param parAzFunctionSubscriptionId string + +// Creating a symbolic name for an existing resource +resource resAzFunction 'Microsoft.Web/sites@2021-03-01' existing = { + name: parAzFunctionName + scope: resourceGroup(parAzFunctionSubscriptionId, parAzFunctionResourceGroupName) +} + +module modRoleAssignmentManagementGroup '../../../infra-as-code/bicep/modules/roleAssignments/roleAssignmentManagementGroup.bicep' = { + name: 'Grant-FunctionApp-Owner' + params: { + parAssigneeObjectId: resAzFunction.identity.principalId + parAssigneePrincipalType: 'ServicePrincipal' + parRoleDefinitionId: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + parRoleAssignmentNameGuid: '0f69bde8-3eff-476f-93fb-74210d02dbe5' + } +} diff --git a/.github/azFunction/README.md b/.github/azFunction/README.md new file mode 100644 index 000000000..d1a7c6dd4 --- /dev/null +++ b/.github/azFunction/README.md @@ -0,0 +1,2 @@ +# Overview +This folder contains bicep templates and code to create an Azure function to cancel subscriptions created as part of PR validation testing. This is just for internal ALZ-Bicep environment hygiene and not intended as part of the overall ALZ-Bicep accelerator. \ No newline at end of file diff --git a/.github/azFunction/azure-pipelines/deploy-functions.yml b/.github/azFunction/azure-pipelines/deploy-functions.yml new file mode 100644 index 000000000..c1f0bb7b9 --- /dev/null +++ b/.github/azFunction/azure-pipelines/deploy-functions.yml @@ -0,0 +1,49 @@ +trigger: none + +variables: + - group: csu-bicep-environment + +pool: + vmImage: ubuntu-latest + +steps: +- task: ArchiveFiles@2 + displayName: "Archive files" + inputs: + rootFolderOrFile: "$(System.DefaultWorkingDirectory)/.github/azFunction/AzFunctionCode" + includeRootFolder: false + archiveFile: "$(System.DefaultWorkingDirectory)/build$(Build.BuildId).zip" + +- task: Bash@3 + displayName: Login to Azure + name: git_azlogin + inputs: + targetType: 'inline' + script: | + az login --service-principal --username $(azclilogin) --password $(azclipwd) --tenant $(azclitenant) + +- task: Bash@3 + displayName: Deploy Base Azure Function + name: create_az_function + inputs: + targetType: 'inline' + script: | + az deployment group create --resource-group cancelsubscription --template-file .github/azFunction/AzFunctionInfrastructure/main.bicep --parameters parAzFunctionName=$(cancelsubfunctionname) + +- task: AzureFunctionApp@1 + displayName: "Deploy Functions to base" + inputs: + azureSubscription: 'azserviceconnection' + appType: functionAppLinux + appName: $(cancelsubfunctionname) + package: $(System.DefaultWorkingDirectory)/build$(Build.BuildId).zip + +- task: Bash@3 + displayName: Az CLI create Role Assignment to Tenant root group + name: create_role_assign_tenant + inputs: + targetType: 'inline' + script: | + az deployment mg create --template-file .github/azFunction/AzFunctionInfrastructure/rbac.bicep --parameters parAzFunctionName=$(cancelsubfunctionname) parAzFunctionSubscriptionId=$(cancelsubid) --location $(Location) --management-group-id $(azclitenant) --name setrbac + + diff --git a/.github/workflows/cleanup-on-close-pr.yml b/.github/workflows/cleanup-on-close-pr.yml deleted file mode 100644 index 130e9f76b..000000000 --- a/.github/workflows/cleanup-on-close-pr.yml +++ /dev/null @@ -1,63 +0,0 @@ - -name: Cancel Azure Subscription on PR Close - -# only trigger on pull request closed events -on: - pull_request: - branches: - - main - types: [ closed ] - -env: - SubscriptionName: "sub-unit-test-pr-${{ github.event.number }}" - -jobs: - close_job: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Azure Login - uses: azure/login@v1 - with: - creds: ${{ secrets.ALZ_AZURE_SECRET_UNIT_TESTS }} - enable-AzPSSession: true - - - name: Check if PR has an associated Azure Subscription - shell: pwsh - run: | - Install-Module -Name Az.Resources -Force - $subscription = Get-AzSubscription -SubscriptionName ${{ env.SubscriptionName }} -ErrorAction SilentlyContinue - If($subscription) { - echo "subscriptionId=$($subscription.Id)" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - Write-Information "==> Azure Subscription Found called: ${{ env.SubscriptionName }}" -InformationAction Continue - } - - - name: Remove existing Resource Groups in PR associated Azure Subscription - if: ${{ env.subscriptionId != '' }} - shell: pwsh - run: | - Select-AzSubscription -SubscriptionId ${{ env.subscriptionId }} - $resourceGroups = @() - $resourceGroups += Get-AzResourceGroup - $resourceGroups | Select-Object -Property ResourceGroupName - If ($resourceGroups.Count -gt 0) { - $resourceGroups | ForEach-Object -Parallel { - Write-Information "==>Deleting resource group $($PSItem.ResourceGroupName)" - Remove-AzResourceGroup -Name $PSItem.ResourceGroupName -Force - } - } - - - name: Cancel PR associated Azure Subscription - if: ${{ env.subscriptionId != '' }} - shell: pwsh - run: | - Install-Module -Name Az.Subscription -Force - Write-Information "==> PR #${{ github.event.number }} has been merged" -InformationAction Continue - Write-Information "==> ${{ env.SubscriptionName }} to be deleted" -InformationAction Continue - Update-AzSubscription -SubscriptionId ${{ env.subscriptionId }} -Action 'Cancel' -Debug -Confirm:$false -