├── .pipelines ├── azure-pipelines.yml └── pr.yaml ├── LICENSE ├── README.md ├── configurations ├── dev-aca.json ├── dev-apim.json ├── dev.bicepparam ├── dev.json ├── infra-test.json ├── prod.json └── test.json ├── infrastructure ├── aca.bicep ├── apim.bicep ├── core.bicep ├── externalResources.bicep └── main.bicep ├── modules └── private-endpoint.bicep ├── resources └── lambdaStoreSwagger.json └── tests └── infra-pipeline.tests.ps1 /.pipelines/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - main 8 | 9 | pr: none 10 | 11 | pool: 12 | vmImage: ubuntu-latest 13 | 14 | 15 | stages: 16 | - stage: Development 17 | displayName: Development Environment 18 | variables: 19 | resourceGroupName: 'lambda-api-dev' 20 | location: 'westeurope' 21 | configFileName: 'dev.json' 22 | jobs: 23 | - job: 24 | steps: 25 | - task: AzureCLI@2 26 | inputs: 27 | azureSubscription: 'Azure Sponsorship' 28 | scriptType: 'pscore' 29 | scriptLocation: 'inlineScript' 30 | inlineScript: | 31 | az group create --name $(resourceGroupName) --location $(location) 32 | az deployment group create --resource-group $(resourceGroupName) --template-file ./infrastructure/main.bicep --parameters ./configurations/$(configFileName) 33 | - stage: Test 34 | displayName: Test Environment 35 | dependsOn: Development 36 | variables: 37 | resourceGroupName: 'lambda-api-test' 38 | location: 'westeurope' 39 | configFileName: 'test.json' 40 | jobs: 41 | - job: 42 | steps: 43 | - task: AzureCLI@2 44 | inputs: 45 | azureSubscription: 'Azure Sponsorship' 46 | scriptType: 'pscore' 47 | scriptLocation: 'inlineScript' 48 | inlineScript: | 49 | az group create --name $(resourceGroupName) --location $(location) 50 | az deployment group create --resource-group $(resourceGroupName) --template-file ./infrastructure/main.bicep --parameters ./configurations/$(configFileName) 51 | 52 | - stage: Production 53 | displayName: Production Environment 54 | dependsOn: Test 55 | variables: 56 | resourceGroupName: 'lambda-api-prod' 57 | location: 'westeurope' 58 | configFileName: 'prod.json' 59 | jobs: 60 | - deployment: 61 | environment: 'Lambda Toys Production' 62 | strategy: 63 | runOnce: 64 | deploy: 65 | steps: 66 | - task: AzureCLI@2 67 | inputs: 68 | azureSubscription: 'Azure Sponsorship' 69 | scriptType: 'pscore' 70 | scriptLocation: 'inlineScript' 71 | inlineScript: | 72 | az group create --name $(resourceGroupName) --location $(location) 73 | az deployment group create --resource-group $(resourceGroupName) --template-file ./infrastructure/main.bicep --parameters ./configurations/$(configFileName) -------------------------------------------------------------------------------- /.pipelines/pr.yaml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pr: 4 | paths: 5 | include: 6 | - infrastructure 7 | - configurations 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | 14 | # - task: PowerShell@2 15 | # name: CompileTemplate 16 | # displayName: Compile & Lint Template 17 | # inputs: 18 | # targetType: 'inline' 19 | # script: | 20 | # bicep.exe build $(System.DefaultWorkingDirectory)\infrastructure\main.bicep 21 | - task: RunARMTTKTestsXPlat@1 22 | inputs: 23 | templatelocation: '$(System.DefaultWorkingDirectory)/infrastructure/main.bicep' 24 | resultLocation: '$(System.DefaultWorkingDirectory)/results' 25 | allTemplatesMain: false 26 | azureServiceConnection: 'Azure Sponsorship' 27 | 28 | - task: AzureCLI@2 29 | inputs: 30 | azureSubscription: 'Azure Sponsorship' 31 | scriptType: 'pscore' 32 | scriptLocation: 'inlineScript' 33 | inlineScript: | 34 | az group create --name "lambda-api-infra" --location "westeurope" 35 | az deployment group create --resource-group "lambda-api-infra" --template-file ./infrastructure/main.bicep --parameters ./configurations/infra-test.json 36 | 37 | - task: AzurePowerShell@5 38 | displayName: 'Test Infrastructure' 39 | inputs: 40 | azureSubscription: 'Azure Sponsorship' 41 | ScriptType: InlineScript 42 | Inline: | 43 | 44 | Import-Module Pester -Force 45 | Get-Module -Name "Pester" 46 | $configuration = [PesterConfiguration]::Default 47 | $configuration.TestResult.Enabled = $true 48 | $configuration.TestResult.OutputPath ="$(Agent.BuildDirectory)/Test-Infra.XML" 49 | $container = New-PesterContainer -Path "$(System.DefaultWorkingDirectory)/tests/*.tests.ps1" -Data @{ resourceGroup="lambda-api-infra"; prefix="lmbd-infra01" } 50 | $configuration.Run.Container = $container 51 | $configuration.Run.PassThru = $true 52 | $result =Invoke-Pester -Configuration $configuration 53 | exit $result.FailedCount 54 | azurePowerShellVersion: LatestVersion 55 | pwsh: true 56 | - task: PublishTestResults@2 57 | name: PublishTTKTests 58 | displayName: Publish TTK Test Results 59 | inputs: 60 | testResultsFormat: 'NUnit' 61 | testResultsFiles: | 62 | **/*-armttk.xml 63 | $(Agent.BuildDirectory)/Test-Infra.XML 64 | condition: always() 65 | - task: AzureCLI@2 66 | name: DeleteInfra 67 | displayName: Delete Test Infrastructure 68 | inputs: 69 | azureSubscription: 'Azure Sponsorship' 70 | scriptType: 'pscore' 71 | scriptLocation: 'inlineScript' 72 | inlineScript: | 73 | az group delete --name "lambda-api-infra" --yes 74 | condition: always() 75 | 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sam Cogan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-toys-api-infrastructure -------------------------------------------------------------------------------- /configurations/dev-aca.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-dev" 10 | }, 11 | "vNetId": { 12 | "value": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/lambda-api-dev/providers/Microsoft.Network/virtualNetworks/lmbd-dev-vnet" 13 | }, 14 | "containerRegistryName": { 15 | "value": "lmbddevacr" 16 | }, 17 | "containerRegistryUsername": { 18 | "value": "lmbddevacr" 19 | }, 20 | "containerVersion": { 21 | "value": "1.6.0" 22 | }, 23 | "cosmosAccountName": { 24 | "value": "lmbd-dev-cosmos-account" 25 | }, 26 | "cosmosDbName": { 27 | "value": "lmbd-dev-sqldb" 28 | }, 29 | "cosmosContainerName": { 30 | "value": "lmbd-dev-state" 31 | }, 32 | 33 | "containerRegistryPassword": { 34 | "reference": { 35 | "keyVault": { 36 | "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/lambda-api-dev/providers/Microsoft.KeyVault/vaults/lmbd-dev-kv" 37 | }, 38 | "secretName": "acrAdminPassword" 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /configurations/dev-apim.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-dev" 10 | }, 11 | "certKeyVaultName": { 12 | "value": "deploymentkvsc01" 13 | }, 14 | "externalResourcesRg": { 15 | "value": "samcogancore" 16 | }, 17 | "certKeyVaultUrl": { 18 | "value": "https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys" 19 | } 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /configurations/dev.bicepparam: -------------------------------------------------------------------------------- 1 | using '../infrastructure/main.bicep' 2 | 3 | param location = 'westeurope' 4 | param prefix = 'lmbd-dev01' 5 | param containerVersion = '1.7.0' 6 | param certKeyVaultName = 'deploymentkvsc01' 7 | param externalResourcesRg = 'samcogancore' 8 | param certKeyVaultUrl = 'https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys' 9 | param containerRegistryName = 'lambdatoysdev' 10 | param containerRegistryUsername = 'lambdatoysdev' 11 | param existingKeyVaultId = '/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/samcogancore/providers/Microsoft.KeyVault/vaults/deploymentkvsc01' 12 | param secretName = 'lambdaAcrDev' 13 | 14 | -------------------------------------------------------------------------------- /configurations/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-dev01" 10 | }, 11 | "containerVersion": { 12 | "value": "1.7.0" 13 | }, 14 | "certKeyVaultName": { 15 | "value": "deploymentkvsc01" 16 | }, 17 | "externalResourcesRg": { 18 | "value": "samcogancore" 19 | }, 20 | "certKeyVaultUrl": { 21 | "value": "https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys" 22 | }, 23 | "containerRegistryName":{ 24 | "value": "lambdatoysdev" 25 | }, 26 | "containerRegistryUsername" :{ 27 | "value": "lambdatoysdev" 28 | }, 29 | "containerRegistryPassword": { 30 | "reference": { 31 | "keyVault": { 32 | "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/samcogancore/providers/Microsoft.KeyVault/vaults/deploymentkvsc01" 33 | }, 34 | "secretName": "lambdaAcrDev" 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /configurations/infra-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-infra01" 10 | }, 11 | "containerVersion": { 12 | "value": "1.7.0" 13 | }, 14 | "certKeyVaultName": { 15 | "value": "deploymentkvsc01" 16 | }, 17 | "externalResourcesRg": { 18 | "value": "samcogancore" 19 | }, 20 | "certKeyVaultUrl": { 21 | "value": "https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys" 22 | }, 23 | "containerRegistryName":{ 24 | "value": "lambdatoysdev" 25 | }, 26 | "containerRegistryUsername" :{ 27 | "value": "lambdatoysdev" 28 | }, 29 | "containerRegistryPassword": { 30 | "reference": { 31 | "keyVault": { 32 | "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/samcogancore/providers/Microsoft.KeyVault/vaults/deploymentkvsc01" 33 | }, 34 | "secretName": "lambdaAcrDev" 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /configurations/prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-prod01" 10 | }, 11 | "containerVersion": { 12 | "value": "1.7.0" 13 | }, 14 | "certKeyVaultName": { 15 | "value": "deploymentkvsc01" 16 | }, 17 | "externalResourcesRg": { 18 | "value": "samcogancore" 19 | }, 20 | "certKeyVaultUrl": { 21 | "value": "https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys" 22 | }, 23 | "containerRegistryName":{ 24 | "value": "lambdatoysprod" 25 | }, 26 | "containerRegistryUsername" :{ 27 | "value": "lambdatoysprod" 28 | }, 29 | "containerRegistryPassword": { 30 | "reference": { 31 | "keyVault": { 32 | "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/samcogancore/providers/Microsoft.KeyVault/vaults/deploymentkvsc01" 33 | }, 34 | "secretName": "lambdaAcrProd" 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /configurations/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "westeurope" 7 | }, 8 | "prefix": { 9 | "value": "lmbd-test01" 10 | }, 11 | "containerVersion": { 12 | "value": "1.7.0" 13 | }, 14 | "certKeyVaultName": { 15 | "value": "deploymentkvsc01" 16 | }, 17 | "externalResourcesRg": { 18 | "value": "samcogancore" 19 | }, 20 | "certKeyVaultUrl": { 21 | "value": "https://deploymentkvsc01.vault.azure.net/secrets/lambdatoys" 22 | }, 23 | "containerRegistryName":{ 24 | "value": "lambdatoysdev" 25 | }, 26 | "containerRegistryUsername" :{ 27 | "value": "lambdatoysdev" 28 | }, 29 | "containerRegistryPassword": { 30 | "reference": { 31 | "keyVault": { 32 | "id": "/subscriptions/469048f1-92af-4c71-a63b-330ec31d2b82/resourceGroups/samcogancore/providers/Microsoft.KeyVault/vaults/deploymentkvsc01" 33 | }, 34 | "secretName": "lambdaAcrDev" 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /infrastructure/aca.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param prefix string 3 | param vNetName string 4 | param containerRegistryName string 5 | param containerRegistryUsername string 6 | @secure() 7 | param containerRegistryPassword string 8 | param containerVersion string 9 | param cosmosAccountName string 10 | param cosmosDbName string 11 | param cosmosContainerName string 12 | 13 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 14 | name: '${prefix}-la-workspace' 15 | location: location 16 | properties: { 17 | sku: { 18 | name: 'Standard' 19 | } 20 | } 21 | } 22 | resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2021-03-15' existing ={ 23 | name: cosmosAccountName 24 | 25 | } 26 | var cosmosDbKey = cosmosDbAccount.listKeys().primaryMasterKey 27 | 28 | resource env 'Microsoft.App/managedEnvironments@2023-05-01' = { 29 | name: '${prefix}-container-env' 30 | location: location 31 | properties:{ 32 | appLogsConfiguration:{ 33 | destination: 'log-analytics' 34 | logAnalyticsConfiguration:{ 35 | customerId: logAnalyticsWorkspace.properties.customerId 36 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 37 | } 38 | } 39 | vnetConfiguration:{ 40 | infrastructureSubnetId: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName,'acaControlPlaneSubnet') 41 | } 42 | } 43 | resource daprStateStore 'daprComponents@2023-05-01' = { 44 | name: 'statestore' 45 | properties:{ 46 | componentType: 'state.azure.cosmosdb' 47 | version: 'v1' 48 | scopes: [ 49 | 'lambdaapi' 50 | ] 51 | metadata: [ 52 | { 53 | name: 'url' 54 | value: 'https://${cosmosAccountName}.documents.azure.com:443/' 55 | } 56 | { 57 | name: 'database' 58 | value: cosmosDbName 59 | } 60 | { 61 | name: 'collection' 62 | value: cosmosContainerName 63 | } 64 | { 65 | name: 'masterKey' 66 | value: cosmosDbKey 67 | } 68 | ] 69 | } 70 | } 71 | } 72 | 73 | resource apiApp 'Microsoft.App/containerApps@2023-05-01' = { 74 | name:'${prefix}-api-container' 75 | location: location 76 | properties:{ 77 | managedEnvironmentId: env.id 78 | configuration: { 79 | secrets:[ 80 | { 81 | name: 'container-registry-password' 82 | value: containerRegistryPassword 83 | } 84 | ] 85 | registries:[ 86 | { 87 | server: '${containerRegistryName}.azurecr.io' 88 | username: containerRegistryUsername 89 | passwordSecretRef: 'container-registry-password' 90 | } 91 | ] 92 | ingress:{ 93 | external: true 94 | targetPort: 3000 95 | } 96 | dapr: { 97 | enabled: true 98 | appPort: 3000 99 | appId: 'lambdaapi' 100 | } 101 | } 102 | template: { 103 | containers:[ 104 | { 105 | image: '${containerRegistryName}.azurecr.io/hello-k8s-node:${containerVersion}' 106 | name: 'lambdaapi' 107 | resources: { 108 | cpu: 1 109 | memory: '2Gi' 110 | } 111 | 112 | } 113 | ] 114 | scale: { 115 | minReplicas: 1 116 | } 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /infrastructure/apim.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param prefix string 3 | param tier string = 'Consumption' 4 | param capacity int = 0 5 | param externalResourcesRg string 6 | param certKeyVaultName string 7 | param certKeyVaultUrl string 8 | 9 | resource apimUserIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { 10 | name: '${prefix}-apim0-mi' 11 | location: location 12 | } 13 | 14 | module apimExternalResources 'externalResources.bicep' ={ 15 | name:'${prefix}-apim-external' 16 | scope: resourceGroup(externalResourcesRg) 17 | params:{ 18 | zoneName:'lambdatoys.com' 19 | recordName: '${prefix}-apim' 20 | cName: '${prefix}-apim.azure-api.net' 21 | managedIdentityId: apimUserIdentity.properties.principalId 22 | keyVaultName: certKeyVaultName 23 | } 24 | } 25 | 26 | resource apiManagementInstance 'Microsoft.ApiManagement/service@2021-08-01' = { 27 | name: '${prefix}-apim' 28 | location: location 29 | dependsOn:[ 30 | apimExternalResources 31 | ] 32 | sku:{ 33 | capacity: capacity 34 | name: tier 35 | } 36 | identity:{ 37 | type: 'UserAssigned' 38 | userAssignedIdentities:{ 39 | '${apimUserIdentity.id}' : {} 40 | } 41 | } 42 | properties:{ 43 | virtualNetworkType: 'None' 44 | publisherEmail: 'support@lambdatoys.com' 45 | publisherName: 'Lambda Toys' 46 | hostnameConfigurations:[ 47 | { 48 | hostName: '${prefix}-apim.lambdatoys.com' 49 | type: 'Proxy' 50 | certificateSource: 'KeyVault' 51 | keyVaultId: certKeyVaultUrl 52 | identityClientId: apimUserIdentity.properties.clientId 53 | } 54 | ] 55 | } 56 | } 57 | 58 | var base64_api=loadFileAsBase64('../resources/lambdaStoreSwagger.json') 59 | 60 | resource lambdaStoreApi 'Microsoft.ApiManagement/service/apis@2020-12-01' = { 61 | parent: apiManagementInstance 62 | name:'LambdaStore' 63 | properties:{ 64 | format: 'swagger-json' 65 | value: base64ToString(base64_api) 66 | path: 'lambdaToyStore' 67 | } 68 | } 69 | 70 | resource toyProduct 'Microsoft.ApiManagement/service/products@2020-12-01' = { 71 | parent: apiManagementInstance 72 | name: 'toyProduct' 73 | properties: { 74 | displayName: 'Toy product' 75 | description: 'Lambda Toys Ordering Product' 76 | subscriptionRequired: true 77 | approvalRequired: false 78 | subscriptionsLimit: 1 79 | state: 'published' 80 | 81 | } 82 | } 83 | 84 | resource toyProductPolicies 'Microsoft.ApiManagement/service/products/policies@2020-12-01' = { 85 | name: 'policy' 86 | parent: toyProduct 87 | properties: { 88 | value: '\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n' 89 | format: 'xml' 90 | } 91 | } 92 | 93 | resource toyProductApiLink 'Microsoft.ApiManagement/service/products/apis@2020-12-01' = { 94 | name: 'LambdaStore' 95 | parent: toyProduct 96 | dependsOn:[ 97 | lambdaStoreApi 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /infrastructure/core.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param prefix string 3 | param vnetSettings object = { 4 | addressPrefixes: [ 5 | '10.0.0.0/19' 6 | ] 7 | subnets: [ 8 | { 9 | name: 'subnet1' 10 | addressPrefix: '10.0.0.0/21' 11 | } 12 | { 13 | name: 'acaAppSubnet' 14 | addressPrefix: '10.0.8.0/21' 15 | } 16 | { 17 | name: 'acaControlPlaneSubnet' 18 | addressPrefix: '10.0.16.0/21' 19 | } 20 | ] 21 | } 22 | 23 | resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-04-01' = { 24 | name: '${prefix}-default-nsg' 25 | location: location 26 | properties: { 27 | securityRules: [ 28 | { 29 | name: 'allowhttpsinbound' 30 | properties:{ 31 | direction: 'Inbound' 32 | access: 'Allow' 33 | protocol: 'Tcp' 34 | description: 'Allow https traffic into API' 35 | sourceAddressPrefix: '*' 36 | sourcePortRange: '*' 37 | destinationPortRange: '443' 38 | destinationAddressPrefix: '*' 39 | priority: 200 40 | } 41 | } 42 | ] 43 | } 44 | } 45 | 46 | 47 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-04-01' = { 48 | name: '${prefix}-vnet' 49 | location: location 50 | properties: { 51 | addressSpace: { 52 | addressPrefixes: vnetSettings.addressPrefixes 53 | } 54 | subnets: [ for subnet in vnetSettings.subnets: { 55 | name: subnet.name 56 | properties: { 57 | addressPrefix: subnet.addressPrefix 58 | networkSecurityGroup: { 59 | id: networkSecurityGroup.id 60 | } 61 | privateEndpointNetworkPolicies: 'disabled' 62 | } 63 | }] 64 | } 65 | } 66 | 67 | resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = { 68 | name: '${prefix}-cosmos-account' 69 | location: location 70 | kind: 'GlobalDocumentDB' 71 | properties: { 72 | consistencyPolicy: { 73 | defaultConsistencyLevel: 'Session' 74 | } 75 | locations: [ 76 | { 77 | locationName: location 78 | failoverPriority: 0 79 | } 80 | ] 81 | databaseAccountOfferType: 'Standard' 82 | enableAutomaticFailover: false 83 | capabilities: [ 84 | { 85 | name: 'EnableServerless' 86 | } 87 | ] 88 | publicNetworkAccess: 'Disabled' 89 | } 90 | } 91 | 92 | resource sqlDb 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = { 93 | name: '${prefix}-sqldb' 94 | parent: cosmosDbAccount 95 | properties: { 96 | resource: { 97 | id: '${prefix}-sqldb' 98 | } 99 | } 100 | } 101 | 102 | resource sqlContainerName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = { 103 | parent: sqlDb 104 | name: '${prefix}-orders' 105 | properties: { 106 | resource: { 107 | id: '${prefix}-orders' 108 | partitionKey: { 109 | paths: [ 110 | '/id' 111 | ] 112 | } 113 | } 114 | 115 | } 116 | } 117 | 118 | resource stateContainerName 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = { 119 | parent: sqlDb 120 | name: '${prefix}-state' 121 | properties: { 122 | resource: { 123 | id: '${prefix}-state' 124 | partitionKey: { 125 | paths: [ 126 | '/partitionKey' 127 | ] 128 | } 129 | } 130 | 131 | } 132 | } 133 | 134 | module cosmosPrivateLink '../modules/private-endpoint.bicep' ={ 135 | name: 'cosmosPrivateLink' 136 | params:{ 137 | location:location 138 | name: '${prefix}-cosmos' 139 | virtualNetworkId: virtualNetwork.id 140 | subnetId: virtualNetwork.properties.subnets[0].id 141 | zoneName: 'privatelink.documents.azure.com' 142 | subResourceTypes:[ 143 | 'SQL' 144 | ] 145 | resourceId: cosmosDbAccount.id 146 | } 147 | } 148 | 149 | resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { 150 | name: '${prefix}-kv' 151 | location: location 152 | properties: { 153 | enabledForDeployment: true 154 | enabledForTemplateDeployment: true 155 | enabledForDiskEncryption: true 156 | enableRbacAuthorization: true 157 | tenantId: tenant().tenantId 158 | sku: { 159 | name: 'standard' 160 | family: 'A' 161 | } 162 | } 163 | } 164 | 165 | module keyVaultPrivateLink '../modules/private-endpoint.bicep' ={ 166 | name: 'keyVaultPrivateLink' 167 | params:{ 168 | location:location 169 | name: '${prefix}-keyvault' 170 | virtualNetworkId: virtualNetwork.id 171 | subnetId: virtualNetwork.properties.subnets[0].id 172 | zoneName: 'privatelink.vaultcore.azure.net' 173 | subResourceTypes:[ 174 | 'vault' 175 | ] 176 | resourceId: keyVault.id 177 | } 178 | } 179 | 180 | output vNetId string = virtualNetwork.id 181 | output vNetName string = virtualNetwork.name 182 | output SecretKeyVaultName string = keyVault.name 183 | output CosmosAccountName string = cosmosDbAccount.name 184 | output ComosDbName string = sqlDb.name 185 | output CosmosStateContainerName string = stateContainerName.name 186 | output CosmosSqlContainerName string = sqlContainerName.name 187 | -------------------------------------------------------------------------------- /infrastructure/externalResources.bicep: -------------------------------------------------------------------------------- 1 | param zoneName string 2 | param recordName string 3 | param cName string 4 | param keyVaultName string 5 | param managedIdentityId string 6 | 7 | resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' existing = { 8 | name: zoneName 9 | } 10 | 11 | resource dnsRecord 'Microsoft.Network/dnsZones/CNAME@2018-05-01' = { 12 | parent: dnsZone 13 | name: recordName 14 | properties: { 15 | TTL: 3600 16 | CNAMERecord: { 17 | cname: cName 18 | } 19 | } 20 | } 21 | 22 | resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = { 23 | name: keyVaultName 24 | 25 | } 26 | 27 | var roleIds = [ 28 | 'a4417e6f-fecd-4de8-b567-7b0420556985' //Key Vault Certificate Officer 29 | '4633458b-17de-408a-b874-0445c86b69e6' //Key Vault Secret User 30 | ] 31 | 32 | resource kvroleAssignment'Microsoft.Authorization/roleAssignments@2022-04-01' = [ for roleId in roleIds :{ 33 | name: guid(keyVaultName, managedIdentityId, roleId) 34 | scope: keyVault 35 | properties: { 36 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions',roleId) 37 | principalId: managedIdentityId 38 | principalType: 'ServicePrincipal' 39 | } 40 | }] 41 | 42 | 43 | -------------------------------------------------------------------------------- /infrastructure/main.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param prefix string 3 | param vnetSettings object = { 4 | addressPrefixes: [ 5 | '10.0.0.0/19' 6 | ] 7 | subnets: [ 8 | { 9 | name: 'subnet1' 10 | addressPrefix: '10.0.0.0/21' 11 | } 12 | { 13 | name: 'acaAppSubnet' 14 | addressPrefix: '10.0.8.0/21' 15 | } 16 | { 17 | name: 'acaControlPlaneSubnet' 18 | addressPrefix: '10.0.16.0/21' 19 | } 20 | ] 21 | } 22 | param containerVersion string 23 | param tier string = 'Consumption' 24 | param capacity int = 0 25 | param externalResourcesRg string 26 | param certKeyVaultName string 27 | param certKeyVaultUrl string 28 | param containerRegistryName string 29 | param containerRegistryUsername string 30 | #disable-next-line secure-secrets-in-params 31 | param existingKeyVaultId string 32 | param secretName string 33 | 34 | var secretKeyVaultName = split(existingKeyVaultId, '/')[8] 35 | var secretKeyVaultResourceGroup = split(existingKeyVaultId, '/')[4] 36 | var secretKeyVautlSubscriptionId = split(existingKeyVaultId, '/')[2] 37 | 38 | resource kv 'Microsoft.KeyVault/vaults@2023-02-01' existing = { 39 | name: secretKeyVaultName 40 | scope: resourceGroup(secretKeyVautlSubscriptionId, secretKeyVaultResourceGroup) 41 | } 42 | 43 | module core 'core.bicep' = { 44 | name: 'core' 45 | params:{ 46 | location: location 47 | prefix: prefix 48 | vnetSettings: vnetSettings 49 | } 50 | 51 | } 52 | 53 | module aca 'aca.bicep' = { 54 | name: 'aca' 55 | dependsOn:[ 56 | core 57 | ] 58 | params: { 59 | location: location 60 | prefix: prefix 61 | vNetName: core.outputs.vNetName 62 | containerRegistryName: containerRegistryName 63 | containerRegistryUsername: containerRegistryUsername 64 | containerVersion: containerVersion 65 | cosmosAccountName: core.outputs.CosmosAccountName 66 | cosmosContainerName: core.outputs.CosmosStateContainerName 67 | cosmosDbName: core.outputs.ComosDbName 68 | containerRegistryPassword: kv.getSecret(secretName) 69 | } 70 | } 71 | 72 | module apim 'apim.bicep'={ 73 | name: 'apim' 74 | dependsOn:[ 75 | core 76 | ] 77 | params:{ 78 | location: location 79 | prefix: prefix 80 | certKeyVaultName: certKeyVaultName 81 | certKeyVaultUrl: certKeyVaultUrl 82 | externalResourcesRg: externalResourcesRg 83 | capacity: capacity 84 | tier: tier 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /modules/private-endpoint.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param virtualNetworkId string 4 | param subnetId string 5 | param resourceId string 6 | param subResourceTypes array 7 | param zoneName string 8 | 9 | resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 10 | name: zoneName 11 | location: 'global' 12 | } 13 | 14 | resource privateDnsNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 15 | name: '${name}-dns-link' 16 | location: 'global' 17 | parent: privateDnsZone 18 | properties:{ 19 | registrationEnabled: false 20 | virtualNetwork: { 21 | id: virtualNetworkId 22 | } 23 | } 24 | } 25 | 26 | resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { 27 | name: '${name}-pe' 28 | location: location 29 | properties: { 30 | privateLinkServiceConnections: [ 31 | { 32 | name: '${name}-pe' 33 | properties: { 34 | privateLinkServiceId: resourceId 35 | groupIds: subResourceTypes 36 | } 37 | } 38 | ] 39 | subnet: { 40 | 41 | id: subnetId 42 | } 43 | 44 | } 45 | } 46 | 47 | resource privateEndpointDnsLink 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-04-01' = { 48 | name:'${name}-pe-dns' 49 | parent: privateEndpoint 50 | properties: { 51 | privateDnsZoneConfigs: [ 52 | { 53 | name: zoneName 54 | properties: { 55 | privateDnsZoneId: privateDnsZone.id 56 | } 57 | } 58 | ] 59 | } 60 | } 61 | 62 | output privateEndpointId string = privateEndpoint.id 63 | output dnsZoneId string = privateDnsZone.id 64 | -------------------------------------------------------------------------------- /resources/lambdaStoreSwagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server LambdaStore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 5 | "version": "1.0.6", 6 | "title": "Swagger LambdaStore", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "email": "admin@lambdatoys.com" 10 | }, 11 | "license": { 12 | "name": "Apache 2.0", 13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 14 | } 15 | }, 16 | "host": "LambdaStore.swagger.io", 17 | "basePath": "/v2", 18 | "tags": [ 19 | { 20 | "name": "toy", 21 | "description": "Everything about your toys", 22 | "externalDocs": { 23 | "description": "Find out more", 24 | "url": "http://swagger.io" 25 | } 26 | }, 27 | { 28 | "name": "store", 29 | "description": "Access to LambdaStore orders" 30 | }, 31 | { 32 | "name": "user", 33 | "description": "Operations about user", 34 | "externalDocs": { 35 | "description": "Find out more about our store", 36 | "url": "http://swagger.io" 37 | } 38 | } 39 | ], 40 | "schemes": [ 41 | "https", 42 | "http" 43 | ], 44 | "paths": { 45 | "/toy/{toyId}/uploadImage": { 46 | "post": { 47 | "tags": [ 48 | "toy" 49 | ], 50 | "summary": "uploads an image", 51 | "description": "", 52 | "operationId": "uploadFile", 53 | "consumes": [ 54 | "multipart/form-data" 55 | ], 56 | "produces": [ 57 | "application/json" 58 | ], 59 | "parameters": [ 60 | { 61 | "name": "toyId", 62 | "in": "path", 63 | "description": "ID of toy to update", 64 | "required": true, 65 | "type": "integer", 66 | "format": "int64" 67 | }, 68 | { 69 | "name": "additionalMetadata", 70 | "in": "formData", 71 | "description": "Additional data to pass to server", 72 | "required": false, 73 | "type": "string" 74 | }, 75 | { 76 | "name": "file", 77 | "in": "formData", 78 | "description": "file to upload", 79 | "required": false, 80 | "type": "file" 81 | } 82 | ], 83 | "responses": { 84 | "200": { 85 | "description": "successful operation", 86 | "schema": { 87 | "$ref": "#/definitions/ApiResponse" 88 | } 89 | } 90 | }, 91 | "security": [ 92 | { 93 | "LambdaStore_auth": [ 94 | "write:toys", 95 | "read:toys" 96 | ] 97 | } 98 | ] 99 | } 100 | }, 101 | "/toy": { 102 | "post": { 103 | "tags": [ 104 | "toy" 105 | ], 106 | "summary": "Add a new toy to the store", 107 | "description": "", 108 | "operationId": "addtoy", 109 | "consumes": [ 110 | "application/json", 111 | "application/xml" 112 | ], 113 | "produces": [ 114 | "application/json", 115 | "application/xml" 116 | ], 117 | "parameters": [ 118 | { 119 | "in": "body", 120 | "name": "body", 121 | "description": "toy object that needs to be added to the store", 122 | "required": true, 123 | "schema": { 124 | "$ref": "#/definitions/toy" 125 | } 126 | } 127 | ], 128 | "responses": { 129 | "405": { 130 | "description": "Invalid input" 131 | } 132 | }, 133 | "security": [ 134 | { 135 | "LambdaStore_auth": [ 136 | "write:toys", 137 | "read:toys" 138 | ] 139 | } 140 | ] 141 | }, 142 | "put": { 143 | "tags": [ 144 | "toy" 145 | ], 146 | "summary": "Update an existing toy", 147 | "description": "", 148 | "operationId": "updatetoy", 149 | "consumes": [ 150 | "application/json", 151 | "application/xml" 152 | ], 153 | "produces": [ 154 | "application/json", 155 | "application/xml" 156 | ], 157 | "parameters": [ 158 | { 159 | "in": "body", 160 | "name": "body", 161 | "description": "toy object that needs to be added to the store", 162 | "required": true, 163 | "schema": { 164 | "$ref": "#/definitions/toy" 165 | } 166 | } 167 | ], 168 | "responses": { 169 | "400": { 170 | "description": "Invalid ID supplied" 171 | }, 172 | "404": { 173 | "description": "toy not found" 174 | }, 175 | "405": { 176 | "description": "Validation exception" 177 | } 178 | }, 179 | "security": [ 180 | { 181 | "LambdaStore_auth": [ 182 | "write:toys", 183 | "read:toys" 184 | ] 185 | } 186 | ] 187 | } 188 | }, 189 | "/toy/findByStatus": { 190 | "get": { 191 | "tags": [ 192 | "toy" 193 | ], 194 | "summary": "Finds toys by status", 195 | "description": "Multiple status values can be provided with comma separated strings", 196 | "operationId": "findtoysByStatus", 197 | "produces": [ 198 | "application/json", 199 | "application/xml" 200 | ], 201 | "parameters": [ 202 | { 203 | "name": "status", 204 | "in": "query", 205 | "description": "Status values that need to be considered for filter", 206 | "required": true, 207 | "type": "array", 208 | "items": { 209 | "type": "string", 210 | "enum": [ 211 | "available", 212 | "pending", 213 | "sold" 214 | ], 215 | "default": "available" 216 | }, 217 | "collectionFormat": "multi" 218 | } 219 | ], 220 | "responses": { 221 | "200": { 222 | "description": "successful operation", 223 | "schema": { 224 | "type": "array", 225 | "items": { 226 | "$ref": "#/definitions/toy" 227 | } 228 | } 229 | }, 230 | "400": { 231 | "description": "Invalid status value" 232 | } 233 | }, 234 | "security": [ 235 | { 236 | "LambdaStore_auth": [ 237 | "write:toys", 238 | "read:toys" 239 | ] 240 | } 241 | ] 242 | } 243 | }, 244 | "/toy/findByTags": { 245 | "get": { 246 | "tags": [ 247 | "toy" 248 | ], 249 | "summary": "Finds toys by tags", 250 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 251 | "operationId": "findtoysByTags", 252 | "produces": [ 253 | "application/json", 254 | "application/xml" 255 | ], 256 | "parameters": [ 257 | { 258 | "name": "tags", 259 | "in": "query", 260 | "description": "Tags to filter by", 261 | "required": true, 262 | "type": "array", 263 | "items": { 264 | "type": "string" 265 | }, 266 | "collectionFormat": "multi" 267 | } 268 | ], 269 | "responses": { 270 | "200": { 271 | "description": "successful operation", 272 | "schema": { 273 | "type": "array", 274 | "items": { 275 | "$ref": "#/definitions/toy" 276 | } 277 | } 278 | }, 279 | "400": { 280 | "description": "Invalid tag value" 281 | } 282 | }, 283 | "security": [ 284 | { 285 | "LambdaStore_auth": [ 286 | "write:toys", 287 | "read:toys" 288 | ] 289 | } 290 | ], 291 | "deprecated": true 292 | } 293 | }, 294 | "/toy/{toyId}": { 295 | "get": { 296 | "tags": [ 297 | "toy" 298 | ], 299 | "summary": "Find toy by ID", 300 | "description": "Returns a single toy", 301 | "operationId": "gettoyById", 302 | "produces": [ 303 | "application/json", 304 | "application/xml" 305 | ], 306 | "parameters": [ 307 | { 308 | "name": "toyId", 309 | "in": "path", 310 | "description": "ID of toy to return", 311 | "required": true, 312 | "type": "integer", 313 | "format": "int64" 314 | } 315 | ], 316 | "responses": { 317 | "200": { 318 | "description": "successful operation", 319 | "schema": { 320 | "$ref": "#/definitions/toy" 321 | } 322 | }, 323 | "400": { 324 | "description": "Invalid ID supplied" 325 | }, 326 | "404": { 327 | "description": "toy not found" 328 | } 329 | } 330 | 331 | }, 332 | "post": { 333 | "tags": [ 334 | "toy" 335 | ], 336 | "summary": "Updates a toy in the store with form data", 337 | "description": "", 338 | "operationId": "updatetoyWithForm", 339 | "consumes": [ 340 | "application/x-www-form-urlencoded" 341 | ], 342 | "produces": [ 343 | "application/json", 344 | "application/xml" 345 | ], 346 | "parameters": [ 347 | { 348 | "name": "toyId", 349 | "in": "path", 350 | "description": "ID of toy that needs to be updated", 351 | "required": true, 352 | "type": "integer", 353 | "format": "int64" 354 | }, 355 | { 356 | "name": "name", 357 | "in": "formData", 358 | "description": "Updated name of the toy", 359 | "required": false, 360 | "type": "string" 361 | }, 362 | { 363 | "name": "status", 364 | "in": "formData", 365 | "description": "Updated status of the toy", 366 | "required": false, 367 | "type": "string" 368 | } 369 | ], 370 | "responses": { 371 | "405": { 372 | "description": "Invalid input" 373 | } 374 | }, 375 | "security": [ 376 | { 377 | "LambdaStore_auth": [ 378 | "write:toys", 379 | "read:toys" 380 | ] 381 | } 382 | ] 383 | }, 384 | "delete": { 385 | "tags": [ 386 | "toy" 387 | ], 388 | "summary": "Deletes a toy", 389 | "description": "", 390 | "operationId": "deletetoy", 391 | "produces": [ 392 | "application/json", 393 | "application/xml" 394 | ], 395 | "parameters": [ 396 | { 397 | "name": "api_key", 398 | "in": "header", 399 | "required": false, 400 | "type": "string" 401 | }, 402 | { 403 | "name": "toyId", 404 | "in": "path", 405 | "description": "toy id to delete", 406 | "required": true, 407 | "type": "integer", 408 | "format": "int64" 409 | } 410 | ], 411 | "responses": { 412 | "400": { 413 | "description": "Invalid ID supplied" 414 | }, 415 | "404": { 416 | "description": "toy not found" 417 | } 418 | }, 419 | "security": [ 420 | { 421 | "LambdaStore_auth": [ 422 | "write:toys", 423 | "read:toys" 424 | ] 425 | } 426 | ] 427 | } 428 | }, 429 | "/store/order": { 430 | "post": { 431 | "tags": [ 432 | "store" 433 | ], 434 | "summary": "Place an order for a toy", 435 | "description": "", 436 | "operationId": "placeOrder", 437 | "consumes": [ 438 | "application/json" 439 | ], 440 | "produces": [ 441 | "application/json", 442 | "application/xml" 443 | ], 444 | "parameters": [ 445 | { 446 | "in": "body", 447 | "name": "body", 448 | "description": "order placed for purchasing the toy", 449 | "required": true, 450 | "schema": { 451 | "$ref": "#/definitions/Order" 452 | } 453 | } 454 | ], 455 | "responses": { 456 | "200": { 457 | "description": "successful operation", 458 | "schema": { 459 | "$ref": "#/definitions/Order" 460 | } 461 | }, 462 | "400": { 463 | "description": "Invalid Order" 464 | } 465 | } 466 | } 467 | }, 468 | "/store/order/{orderId}": { 469 | "get": { 470 | "tags": [ 471 | "store" 472 | ], 473 | "summary": "Find purchase order by ID", 474 | "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 475 | "operationId": "getOrderById", 476 | "produces": [ 477 | "application/json", 478 | "application/xml" 479 | ], 480 | "parameters": [ 481 | { 482 | "name": "orderId", 483 | "in": "path", 484 | "description": "ID of toy that needs to be fetched", 485 | "required": true, 486 | "type": "integer", 487 | "maximum": 10, 488 | "minimum": 1, 489 | "format": "int64" 490 | } 491 | ], 492 | "responses": { 493 | "200": { 494 | "description": "successful operation", 495 | "schema": { 496 | "$ref": "#/definitions/Order" 497 | } 498 | }, 499 | "400": { 500 | "description": "Invalid ID supplied" 501 | }, 502 | "404": { 503 | "description": "Order not found" 504 | } 505 | } 506 | }, 507 | "delete": { 508 | "tags": [ 509 | "store" 510 | ], 511 | "summary": "Delete purchase order by ID", 512 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 513 | "operationId": "deleteOrder", 514 | "produces": [ 515 | "application/json", 516 | "application/xml" 517 | ], 518 | "parameters": [ 519 | { 520 | "name": "orderId", 521 | "in": "path", 522 | "description": "ID of the order that needs to be deleted", 523 | "required": true, 524 | "type": "integer", 525 | "minimum": 1, 526 | "format": "int64" 527 | } 528 | ], 529 | "responses": { 530 | "400": { 531 | "description": "Invalid ID supplied" 532 | }, 533 | "404": { 534 | "description": "Order not found" 535 | } 536 | } 537 | } 538 | }, 539 | "/store/inventory": { 540 | "get": { 541 | "tags": [ 542 | "store" 543 | ], 544 | "summary": "Returns toy inventories by status", 545 | "description": "Returns a map of status codes to quantities", 546 | "operationId": "getInventory", 547 | "produces": [ 548 | "application/json" 549 | ], 550 | 551 | "responses": { 552 | "200": { 553 | "description": "successful operation", 554 | "schema": { 555 | "type": "object", 556 | "additionalProperties": { 557 | "type": "integer", 558 | "format": "int32" 559 | } 560 | } 561 | } 562 | } 563 | } 564 | }, 565 | "/user/createWithArray": { 566 | "post": { 567 | "tags": [ 568 | "user" 569 | ], 570 | "summary": "Creates list of users with given input array", 571 | "description": "", 572 | "operationId": "createUsersWithArrayInput", 573 | "consumes": [ 574 | "application/json" 575 | ], 576 | "produces": [ 577 | "application/json", 578 | "application/xml" 579 | ], 580 | "parameters": [ 581 | { 582 | "in": "body", 583 | "name": "body", 584 | "description": "List of user object", 585 | "required": true, 586 | "schema": { 587 | "type": "array", 588 | "items": { 589 | "$ref": "#/definitions/User" 590 | } 591 | } 592 | } 593 | ], 594 | "responses": { 595 | "default": { 596 | "description": "successful operation" 597 | } 598 | } 599 | } 600 | }, 601 | "/user/createWithList": { 602 | "post": { 603 | "tags": [ 604 | "user" 605 | ], 606 | "summary": "Creates list of users with given input array", 607 | "description": "", 608 | "operationId": "createUsersWithListInput", 609 | "consumes": [ 610 | "application/json" 611 | ], 612 | "produces": [ 613 | "application/json", 614 | "application/xml" 615 | ], 616 | "parameters": [ 617 | { 618 | "in": "body", 619 | "name": "body", 620 | "description": "List of user object", 621 | "required": true, 622 | "schema": { 623 | "type": "array", 624 | "items": { 625 | "$ref": "#/definitions/User" 626 | } 627 | } 628 | } 629 | ], 630 | "responses": { 631 | "default": { 632 | "description": "successful operation" 633 | } 634 | } 635 | } 636 | }, 637 | "/user/{username}": { 638 | "get": { 639 | "tags": [ 640 | "user" 641 | ], 642 | "summary": "Get user by user name", 643 | "description": "", 644 | "operationId": "getUserByName", 645 | "produces": [ 646 | "application/json", 647 | "application/xml" 648 | ], 649 | "parameters": [ 650 | { 651 | "name": "username", 652 | "in": "path", 653 | "description": "The name that needs to be fetched. Use user1 for testing. ", 654 | "required": true, 655 | "type": "string" 656 | } 657 | ], 658 | "responses": { 659 | "200": { 660 | "description": "successful operation", 661 | "schema": { 662 | "$ref": "#/definitions/User" 663 | } 664 | }, 665 | "400": { 666 | "description": "Invalid username supplied" 667 | }, 668 | "404": { 669 | "description": "User not found" 670 | } 671 | } 672 | }, 673 | "put": { 674 | "tags": [ 675 | "user" 676 | ], 677 | "summary": "Updated user", 678 | "description": "This can only be done by the logged in user.", 679 | "operationId": "updateUser", 680 | "consumes": [ 681 | "application/json" 682 | ], 683 | "produces": [ 684 | "application/json", 685 | "application/xml" 686 | ], 687 | "parameters": [ 688 | { 689 | "name": "username", 690 | "in": "path", 691 | "description": "name that need to be updated", 692 | "required": true, 693 | "type": "string" 694 | }, 695 | { 696 | "in": "body", 697 | "name": "body", 698 | "description": "Updated user object", 699 | "required": true, 700 | "schema": { 701 | "$ref": "#/definitions/User" 702 | } 703 | } 704 | ], 705 | "responses": { 706 | "400": { 707 | "description": "Invalid user supplied" 708 | }, 709 | "404": { 710 | "description": "User not found" 711 | } 712 | } 713 | }, 714 | "delete": { 715 | "tags": [ 716 | "user" 717 | ], 718 | "summary": "Delete user", 719 | "description": "This can only be done by the logged in user.", 720 | "operationId": "deleteUser", 721 | "produces": [ 722 | "application/json", 723 | "application/xml" 724 | ], 725 | "parameters": [ 726 | { 727 | "name": "username", 728 | "in": "path", 729 | "description": "The name that needs to be deleted", 730 | "required": true, 731 | "type": "string" 732 | } 733 | ], 734 | "responses": { 735 | "400": { 736 | "description": "Invalid username supplied" 737 | }, 738 | "404": { 739 | "description": "User not found" 740 | } 741 | } 742 | } 743 | }, 744 | "/user/login": { 745 | "get": { 746 | "tags": [ 747 | "user" 748 | ], 749 | "summary": "Logs user into the system", 750 | "description": "", 751 | "operationId": "loginUser", 752 | "produces": [ 753 | "application/json", 754 | "application/xml" 755 | ], 756 | "parameters": [ 757 | { 758 | "name": "username", 759 | "in": "query", 760 | "description": "The user name for login", 761 | "required": true, 762 | "type": "string" 763 | }, 764 | { 765 | "name": "password", 766 | "in": "query", 767 | "description": "The password for login in clear text", 768 | "required": true, 769 | "type": "string" 770 | } 771 | ], 772 | "responses": { 773 | "200": { 774 | "description": "successful operation", 775 | "headers": { 776 | "X-Expires-After": { 777 | "type": "string", 778 | "format": "date-time", 779 | "description": "date in UTC when token expires" 780 | }, 781 | "X-Rate-Limit": { 782 | "type": "integer", 783 | "format": "int32", 784 | "description": "calls per hour allowed by the user" 785 | } 786 | }, 787 | "schema": { 788 | "type": "string" 789 | } 790 | }, 791 | "400": { 792 | "description": "Invalid username/password supplied" 793 | } 794 | } 795 | } 796 | }, 797 | "/user/logout": { 798 | "get": { 799 | "tags": [ 800 | "user" 801 | ], 802 | "summary": "Logs out current logged in user session", 803 | "description": "", 804 | "operationId": "logoutUser", 805 | "produces": [ 806 | "application/json", 807 | "application/xml" 808 | ], 809 | 810 | "responses": { 811 | "default": { 812 | "description": "successful operation" 813 | } 814 | } 815 | } 816 | }, 817 | "/user": { 818 | "post": { 819 | "tags": [ 820 | "user" 821 | ], 822 | "summary": "Create user", 823 | "description": "This can only be done by the logged in user.", 824 | "operationId": "createUser", 825 | "consumes": [ 826 | "application/json" 827 | ], 828 | "produces": [ 829 | "application/json", 830 | "application/xml" 831 | ], 832 | "parameters": [ 833 | { 834 | "in": "body", 835 | "name": "body", 836 | "description": "Created user object", 837 | "required": true, 838 | "schema": { 839 | "$ref": "#/definitions/User" 840 | } 841 | } 842 | ], 843 | "responses": { 844 | "default": { 845 | "description": "successful operation" 846 | } 847 | } 848 | } 849 | } 850 | }, 851 | "securityDefinitions": { 852 | "api_key": { 853 | "type": "apiKey", 854 | "name": "api_key", 855 | "in": "header" 856 | }, 857 | "LambdaStore_auth": { 858 | "type": "oauth2", 859 | "authorizationUrl": "https://LambdaStore.swagger.io/oauth/authorize", 860 | "flow": "implicit", 861 | "scopes": { 862 | "read:toys": "read your toys", 863 | "write:toys": "modify toys in your account" 864 | } 865 | } 866 | }, 867 | "definitions": { 868 | "ApiResponse": { 869 | "type": "object", 870 | "properties": { 871 | "code": { 872 | "type": "integer", 873 | "format": "int32" 874 | }, 875 | "type": { 876 | "type": "string" 877 | }, 878 | "message": { 879 | "type": "string" 880 | } 881 | } 882 | }, 883 | "Category": { 884 | "type": "object", 885 | "properties": { 886 | "id": { 887 | "type": "integer", 888 | "format": "int64" 889 | }, 890 | "name": { 891 | "type": "string" 892 | } 893 | }, 894 | "xml": { 895 | "name": "Category" 896 | } 897 | }, 898 | "toy": { 899 | "type": "object", 900 | "required": [ 901 | "name", 902 | "photoUrls" 903 | ], 904 | "properties": { 905 | "id": { 906 | "type": "integer", 907 | "format": "int64" 908 | }, 909 | "category": { 910 | "$ref": "#/definitions/Category" 911 | }, 912 | "name": { 913 | "type": "string", 914 | "example": "doggie" 915 | }, 916 | "photoUrls": { 917 | "type": "array", 918 | "xml": { 919 | "wrapped": true 920 | }, 921 | "items": { 922 | "type": "string", 923 | "xml": { 924 | "name": "photoUrl" 925 | } 926 | } 927 | }, 928 | "tags": { 929 | "type": "array", 930 | "xml": { 931 | "wrapped": true 932 | }, 933 | "items": { 934 | "xml": { 935 | "name": "tag" 936 | }, 937 | "$ref": "#/definitions/Tag" 938 | } 939 | }, 940 | "status": { 941 | "type": "string", 942 | "description": "toy status in the store", 943 | "enum": [ 944 | "available", 945 | "pending", 946 | "sold" 947 | ] 948 | } 949 | }, 950 | "xml": { 951 | "name": "toy" 952 | } 953 | }, 954 | "Tag": { 955 | "type": "object", 956 | "properties": { 957 | "id": { 958 | "type": "integer", 959 | "format": "int64" 960 | }, 961 | "name": { 962 | "type": "string" 963 | } 964 | }, 965 | "xml": { 966 | "name": "Tag" 967 | } 968 | }, 969 | "Order": { 970 | "type": "object", 971 | "properties": { 972 | "id": { 973 | "type": "integer", 974 | "format": "int64" 975 | }, 976 | "toyId": { 977 | "type": "integer", 978 | "format": "int64" 979 | }, 980 | "quantity": { 981 | "type": "integer", 982 | "format": "int32" 983 | }, 984 | "shipDate": { 985 | "type": "string", 986 | "format": "date-time" 987 | }, 988 | "status": { 989 | "type": "string", 990 | "description": "Order Status", 991 | "enum": [ 992 | "placed", 993 | "approved", 994 | "delivered" 995 | ] 996 | }, 997 | "complete": { 998 | "type": "boolean" 999 | } 1000 | }, 1001 | "xml": { 1002 | "name": "Order" 1003 | } 1004 | }, 1005 | "User": { 1006 | "type": "object", 1007 | "properties": { 1008 | "id": { 1009 | "type": "integer", 1010 | "format": "int64" 1011 | }, 1012 | "username": { 1013 | "type": "string" 1014 | }, 1015 | "firstName": { 1016 | "type": "string" 1017 | }, 1018 | "lastName": { 1019 | "type": "string" 1020 | }, 1021 | "email": { 1022 | "type": "string" 1023 | }, 1024 | "password": { 1025 | "type": "string" 1026 | }, 1027 | "phone": { 1028 | "type": "string" 1029 | }, 1030 | "userStatus": { 1031 | "type": "integer", 1032 | "format": "int32", 1033 | "description": "User Status" 1034 | } 1035 | }, 1036 | "xml": { 1037 | "name": "User" 1038 | } 1039 | } 1040 | }, 1041 | "externalDocs": { 1042 | "description": "Find out more about Swagger", 1043 | "url": "http://swagger.io" 1044 | } 1045 | } -------------------------------------------------------------------------------- /tests/infra-pipeline.tests.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)] 3 | [string] $resourceGroup, 4 | 5 | [Parameter(Mandatory)] 6 | [string] $prefix 7 | ) 8 | 9 | 10 | Describe "Infrastructure Tests" { 11 | Context "Resource Group Tests" { 12 | It "has created the specific resource group" { 13 | Get-AzResourceGroup -Name $resourceGroup -ErrorAction SilentlyContinue | Should -Not -be $null 14 | } 15 | } 16 | 17 | Context "Virtual Network" { 18 | 19 | BeforeAll { 20 | $vnet = Get-AzVirtualNetwork -Name "$prefix-vnet" -ResourceGroupName $resourceGroup 21 | } 22 | 23 | it "Checks vNet Exists" { 24 | $vnet | Should -Not -be $null 25 | } 26 | 27 | it "Checks that the correct subnets have been created" { 28 | $vnet.subnets.count | Should -be 3 29 | $vnet.subnets.name | Should -be @("subnet1", "acaAppSubnet", "acaControlPlaneSubnet") 30 | } 31 | 32 | it "Checks that the subnets are the correct size" { 33 | $($vnet.Subnets | Where-Object { $_.Name -eq "subnet1"}).AddressPrefix.split('/')[-1] | Should -be "21" 34 | $($vnet.Subnets | Where-Object { $_.Name -eq "acaAppSubnet"}).AddressPrefix.split('/')[-1] | Should -be "21" 35 | $($vnet.Subnets | Where-Object { $_.Name -eq "acaControlPlaneSubnet"}).AddressPrefix.split('/')[-1] | Should -be "21" 36 | } 37 | } 38 | 39 | Context "Cosmos DB" { 40 | 41 | BeforeAll { 42 | $account = Get-AzCosmosDBAccount -Name "$prefix-cosmos-account" -ResourceGroupName $resourceGroup 43 | } 44 | 45 | it "Checks Cosmos Account Exists" { 46 | $account | Should -Not -be $null 47 | } 48 | 49 | it "Checks private endpoint enabled" { 50 | $account.PrivateEndpointConnections.count | Should -BeGreaterThan 0 51 | } 52 | 53 | it "Checks public network access is disabled" { 54 | $account.PublicNetworkAccess | Should -be "disabled" 55 | } 56 | 57 | 58 | } 59 | } --------------------------------------------------------------------------------