From bbb51377d884f8f2770847147b485cd96059cb4e Mon Sep 17 00:00:00 2001 From: ahelland Date: Tue, 5 Dec 2023 19:40:53 +0100 Subject: [PATCH] Initial commit --- devCenter/deploy.azcli | 5 + devCenter/main.bicep | 102 +++++ devCenter/main.bicepparam | 8 + .../ContainerEnvironment/dnsRecord.bicep | 20 + .../ContainerEnvironment/dnsZone.bicep | 34 ++ environments/ContainerEnvironment/main.bicep | 145 +++++++ environments/ContainerEnvironment/main.json | 354 ++++++++++++++++++ .../ContainerEnvironment/manifest.yaml | 30 ++ environments/Sandbox/main.bicep | 1 + environments/Sandbox/main.json | 12 + environments/Sandbox/manifest.yaml | 6 + modules/devcenters/devcenter/README.md | 40 ++ modules/devcenters/devcenter/main.bicep | 69 ++++ modules/devcenters/devcenter/main.json | 143 +++++++ .../devcenters/devcenter/test/main.test.bicep | 33 ++ modules/devcenters/devcenter/version.json | 7 + .../devcenters/network-connection/README.md | 35 ++ .../devcenters/network-connection/main.bicep | 31 ++ .../devcenters/network-connection/main.json | 74 ++++ .../network-connection/test/main.test.bicep | 27 ++ .../network-connection/version.json | 7 + modules/devcenters/project/README.md | 40 ++ modules/devcenters/project/main.bicep | 67 ++++ modules/devcenters/project/main.json | 136 +++++++ .../devcenters/project/test/main.test.bicep | 36 ++ modules/devcenters/project/version.json | 7 + 26 files changed, 1469 insertions(+) create mode 100644 devCenter/deploy.azcli create mode 100644 devCenter/main.bicep create mode 100644 devCenter/main.bicepparam create mode 100644 environments/ContainerEnvironment/dnsRecord.bicep create mode 100644 environments/ContainerEnvironment/dnsZone.bicep create mode 100644 environments/ContainerEnvironment/main.bicep create mode 100644 environments/ContainerEnvironment/main.json create mode 100644 environments/ContainerEnvironment/manifest.yaml create mode 100644 environments/Sandbox/main.bicep create mode 100644 environments/Sandbox/main.json create mode 100644 environments/Sandbox/manifest.yaml create mode 100644 modules/devcenters/devcenter/README.md create mode 100644 modules/devcenters/devcenter/main.bicep create mode 100644 modules/devcenters/devcenter/main.json create mode 100644 modules/devcenters/devcenter/test/main.test.bicep create mode 100644 modules/devcenters/devcenter/version.json create mode 100644 modules/devcenters/network-connection/README.md create mode 100644 modules/devcenters/network-connection/main.bicep create mode 100644 modules/devcenters/network-connection/main.json create mode 100644 modules/devcenters/network-connection/test/main.test.bicep create mode 100644 modules/devcenters/network-connection/version.json create mode 100644 modules/devcenters/project/README.md create mode 100644 modules/devcenters/project/main.bicep create mode 100644 modules/devcenters/project/main.json create mode 100644 modules/devcenters/project/test/main.test.bicep create mode 100644 modules/devcenters/project/version.json diff --git a/devCenter/deploy.azcli b/devCenter/deploy.azcli new file mode 100644 index 0000000..60b60b1 --- /dev/null +++ b/devCenter/deploy.azcli @@ -0,0 +1,5 @@ +# To validate the deployment without creating resources +az deployment sub what-if --location westeurope --name DevCenterStack --template-file main.bicep --parameters .\main.bicepparam + +# To deploy the Dev Center as a deployment stack +az stack sub create --name DevCenterStack --location westeurope --template-file main.bicep --parameters .\main.bicepparam --deny-settings-mode none \ No newline at end of file diff --git a/devCenter/main.bicep b/devCenter/main.bicep new file mode 100644 index 0000000..6d4e347 --- /dev/null +++ b/devCenter/main.bicep @@ -0,0 +1,102 @@ +targetScope = 'subscription' + +param location string + +@description('Tags retrieved from parameter file.') +param resourceTags object = {} +@description('Name of DevBox definition.') +param definitionName string = 'DevBox-8-32' +@description('DevBox definition SKU.') +param definitionSKU string = 'general_i_8c32gb256ssd_v2' +@description('DevBox definition storage type.') +param definitionStorageType string = 'ssd_256gb' + +resource rg_devc 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-devcenter' + location: location + tags: resourceTags +} + +param vnetName string = 'core-vnet-weu' +module vnet 'br/public:network/virtual-network:1.1.3' = { + scope: rg_devc + name: 'core-vnet-weu' + params: { + name: vnetName + location: location + addressPrefixes: [ + '10.1.0.0/16' + ] + subnets: [ + { + name: 'snet-devbox-01' + addressPrefix: '10.1.1.0/24' + privateEndpointNetworkPolicies: 'Enabled' + } + { + name: 'snet-cae-01' + addressPrefix: '10.1.2.0/24' + privateEndpointNetworkPolicies: 'Enabled' + delegations: [ + { + name: 'Microsoft.App.environments' + properties: { + serviceName: 'Microsoft.App/environments' + } + type: 'Microsoft.Network/virtualNetworks/subnets/delegations' + } + ] + } + ] + } +} + +module devCenter '../modules/devcenter/main.bicep' = { + scope: rg_devc + name: 'devcenter' + params: { + location: location + devCenterName: 'devCenter' + definitionName: definitionName + definitionSKU: definitionSKU + definitionStorageType: definitionStorageType + image: 'microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2' + networkConnectionId: networkConnection.outputs.id + } +} + +module devProject '../modules/project/main.bicep' = { + scope: rg_devc + name: 'devProject' + params: { + devBoxDefinitionName: definitionName + devCenterId: devCenter.outputs.devCenterId + devPoolName: 'devBoxPool' + location: location + networkConnectionName: devCenter.outputs.devCenterAttachedNetwork + projectName: 'devProject' + deploymentTargetId: subscription().id + } +} + +//Add permissions for the dev environment identity to modify the vnet +var networkContributorRole = resourceId('Microsoft.Authorization/roleAssignments','4d97b98b-1d4f-4787-a291-c67834d212e7') +resource networkRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(rg_devc.id,devCenter.name,networkContributorRole) + properties: { + principalId: devProject.outputs.devEnvironmentManagedId + roleDefinitionId: networkContributorRole + principalType: 'ServicePrincipal' + } +} + +//Connect the Dev Center to the custom vnet +module networkConnection '../modules/network-connection/main.bicep' = { + scope: rg_devc + name: 'devcenter-network-connection' + params: { + connectionName: 'devcenter-network-connection' + location: location + snetId: vnet.outputs.subnetResourceIds[0] + } +} diff --git a/devCenter/main.bicepparam b/devCenter/main.bicepparam new file mode 100644 index 0000000..8bc717e --- /dev/null +++ b/devCenter/main.bicepparam @@ -0,0 +1,8 @@ +using './main.bicep' + +param resourceTags = { + IaC: 'Bicep' + Source: 'GitHub' +} + +param location = 'westeurope' diff --git a/environments/ContainerEnvironment/dnsRecord.bicep b/environments/ContainerEnvironment/dnsRecord.bicep new file mode 100644 index 0000000..0ed20a4 --- /dev/null +++ b/environments/ContainerEnvironment/dnsRecord.bicep @@ -0,0 +1,20 @@ +param zone string +param recordName string +param ipAddress string + +resource dnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { + name: zone +} + +resource record 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { + parent: dnsZone + name: recordName + properties: { + ttl: 3600 + aRecords: [ + { + ipv4Address: ipAddress + } + ] + } +} diff --git a/environments/ContainerEnvironment/dnsZone.bicep b/environments/ContainerEnvironment/dnsZone.bicep new file mode 100644 index 0000000..2780824 --- /dev/null +++ b/environments/ContainerEnvironment/dnsZone.bicep @@ -0,0 +1,34 @@ +metadata name = 'DNS Zone Private' +metadata description = 'Creates a private DNS Zone hosted in Azure DNS.' +metadata owner = 'ahelland' + +@description('Tags retrieved from parameter file.') +param resourceTags object = {} + +@description('The name of the DNS zone to be created. Must have at least 2 segments, e.g. hostname.org') +param zoneName string + +@description('Enable auto-registration for virtual network.') +param registrationEnabled bool +@description('The name of vnet to connect the zone to (for naming of link). Null if registrationEnabled is false.') +param vnetName string? +@description('Vnet to link up with. Null if registrationEnabled is false.') +param vnetId string? + +resource zone 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: zoneName + location: 'global' + tags: resourceTags + + resource vnet 'virtualNetworkLinks@2020-06-01' = if (!empty(vnetName)) { + name: '${vnetName}-link' + location: 'global' + properties: { + registrationEnabled: registrationEnabled + virtualNetwork: { + id: vnetId + } + } + + } +} diff --git a/environments/ContainerEnvironment/main.bicep b/environments/ContainerEnvironment/main.bicep new file mode 100644 index 0000000..1b41b05 --- /dev/null +++ b/environments/ContainerEnvironment/main.bicep @@ -0,0 +1,145 @@ +metadata name = 'Container Environment - Azure' +metadata description = 'Deploys a Container Environment in Azure.' +metadata owner = 'ahelland' + +@description('Name of Container Environment') +param name string +@description('Location for Container Environment') +param location string +@description('Tags retrieved from parameter file.') +param resourceTags object = {} + +@description('Should the Container Environment be connected to a custom virtual network? Enabling this also requires a valid value for snetId.') +param vnetInternal bool = true +@description('If vnet integration is enabled which subnet should the container environment be connected to?') +param snetId string + +//Include Log Analytics in module to avoid passing clientSecret via outputs +resource loganalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: 'log-analytics-${name}' + location: location + tags: resourceTags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +resource containerenvironment 'Microsoft.App/managedEnvironments@2023-05-02-preview' = { + name: 'container-environment-${name}' + location: location + tags: resourceTags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: loganalytics.properties.customerId + sharedKey: loganalytics.listKeys().primarySharedKey + } + } + vnetConfiguration: { + internal: vnetInternal ? true : false + //If vnetInternal == false, snetId is assumed to be null. + infrastructureSubnetId: snetId + } + peerAuthentication: { + mtls: { + enabled: true + } + } + workloadProfiles: [ + { + workloadProfileType: 'Consumption' + name: 'Consumption' + } + ] + } +} + +//Split the subnetId into individual parts to build the vnetId part +//Note: this is a quick hack type implementation +var vnetComponents = split(snetId,'/') +var vnetId = '/${vnetComponents[1]}/${vnetComponents[2]}/${vnetComponents[3]}/${vnetComponents[4]}/${vnetComponents[5]}/${vnetComponents[6]}/${vnetComponents[7]}/${vnetComponents[8]}' + +module dnsZone 'dnsZone.bicep' = { + name: '${containerenvironment.name}-dns' + params: { + resourceTags: resourceTags + registrationEnabled: false + zoneName: containerenvironment.properties.defaultDomain + vnetName: 'cae' + vnetId: vnetId + } +} + +resource helloApp 'Microsoft.App/containerApps@2023-05-02-preview' = { + name: 'hello' + location: location + properties: { + managedEnvironmentId: containerenvironment.id + environmentId: containerenvironment.id + workloadProfileName: 'Consumption' + configuration: { + activeRevisionsMode: 'Single' + ingress: { + external: true + targetPort: 80 + exposedPort: 0 + transport: 'Auto' + traffic: [ + { + weight: 100 + latestRevision: true + } + ] + allowInsecure: false + } + } + template: { + revisionSuffix: '' + containers: [ + { + image: 'mcr.microsoft.com/k8se/quickstart:latest' + name: 'simple-hello-world-container' + resources: { + cpu: json('0.25') + memory: '0.5Gi' + } + } + ] + scale: { + minReplicas: 0 + maxReplicas: 10 + } + } + } + identity: { + type: 'None' + } +} + +module aRecord 'dnsRecord.bicep' = { + name: helloApp.name + params: { + ipAddress: containerenvironment.properties.staticIp + recordName: helloApp.name + zone: containerenvironment.properties.defaultDomain + } + dependsOn: [ + dnsZone + ] +} + +@description('Id of Container Environment') +output id string = containerenvironment.id +@description('The static IP of the environment.') +output staticIp string = containerenvironment.properties.staticIp +@description('The default domain.') +output defaultDomain string = containerenvironment.properties.defaultDomain +@description('Verification id for creating DNS records') +output verificationId string = containerenvironment.properties.customDomainConfiguration.customDomainVerificationId diff --git a/environments/ContainerEnvironment/main.json b/environments/ContainerEnvironment/main.json new file mode 100644 index 0000000..99044af --- /dev/null +++ b/environments/ContainerEnvironment/main.json @@ -0,0 +1,354 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "1145367959530505544" + }, + "name": "Container Environment - Azure", + "description": "Deploys a Container Environment in Azure.", + "owner": "ahelland" + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of Container Environment" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for Container Environment" + } + }, + "resourceTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags retrieved from parameter file." + } + }, + "vnetInternal": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Should the Container Environment be connected to a custom virtual network? Enabling this also requires a valid value for snetId." + } + }, + "snetId": { + "type": "string", + "metadata": { + "description": "If vnet integration is enabled which subnet should the container environment be connected to? (Requires dedicated /23 subnet.)" + } + } + }, + "variables": { + "vnetComponents": "[split(parameters('snetId'), '/')]", + "vnetId": "[format('/{0}/{1}/{2}/{3}/{4}/{5}/{6}/{7}', variables('vnetComponents')[1], variables('vnetComponents')[2], variables('vnetComponents')[3], variables('vnetComponents')[4], variables('vnetComponents')[5], variables('vnetComponents')[6], variables('vnetComponents')[7], variables('vnetComponents')[8])]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[format('log-analytics-{0}', parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('resourceTags')]", + "properties": { + "retentionInDays": 30, + "features": { + "searchVersion": 1 + }, + "sku": { + "name": "PerGB2018" + } + } + }, + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-02-preview", + "name": "[format('container-environment-{0}', parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('resourceTags')]", + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', format('log-analytics-{0}', parameters('name'))), '2022-10-01').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format('log-analytics-{0}', parameters('name'))), '2022-10-01').primarySharedKey]" + } + }, + "vnetConfiguration": { + "internal": "[if(parameters('vnetInternal'), true(), false())]", + "infrastructureSubnetId": "[parameters('snetId')]" + }, + "peerAuthentication": { + "mtls": { + "enabled": true + } + }, + "workloadProfiles": [ + { + "workloadProfileType": "Consumption", + "name": "Consumption" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-analytics-{0}', parameters('name')))]" + ] + }, + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-05-02-preview", + "name": "hello", + "location": "[parameters('location')]", + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]", + "environmentId": "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]", + "workloadProfileName": "Consumption", + "configuration": { + "activeRevisionsMode": "Single", + "ingress": { + "external": true, + "targetPort": 80, + "exposedPort": 0, + "transport": "Auto", + "traffic": [ + { + "weight": 100, + "latestRevision": true + } + ], + "allowInsecure": false + } + }, + "template": { + "revisionSuffix": "", + "containers": [ + { + "image": "mcr.microsoft.com/k8se/quickstart:latest", + "name": "simple-hello-world-container", + "resources": { + "cpu": "[json('0.25')]", + "memory": "0.5Gi" + } + } + ], + "scale": { + "minReplicas": 0, + "maxReplicas": 10 + } + } + }, + "identity": { + "type": "None" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-dns', format('container-environment-{0}', parameters('name')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "resourceTags": { + "value": "[parameters('resourceTags')]" + }, + "registrationEnabled": { + "value": false + }, + "zoneName": { + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').defaultDomain]" + }, + "vnetName": { + "value": "cae" + }, + "vnetId": { + "value": "[variables('vnetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "14337871963777644428" + }, + "name": "DNS Zone Private", + "description": "Creates a private DNS Zone hosted in Azure DNS.", + "owner": "ahelland" + }, + "parameters": { + "resourceTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags retrieved from parameter file." + } + }, + "zoneName": { + "type": "string", + "metadata": { + "description": "The name of the DNS zone to be created. Must have at least 2 segments, e.g. hostname.org" + } + }, + "registrationEnabled": { + "type": "bool", + "metadata": { + "description": "Enable auto-registration for virtual network." + } + }, + "vnetName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The name of vnet to connect the zone to (for naming of link). Null if registrationEnabled is false." + } + }, + "vnetId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Vnet to link up with. Null if registrationEnabled is false." + } + } + }, + "resources": { + "zone::vnet": { + "condition": "[not(empty(parameters('vnetName')))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), format('{0}-link', parameters('vnetName')))]", + "location": "global", + "properties": { + "registrationEnabled": "[parameters('registrationEnabled')]", + "virtualNetwork": { + "id": "[parameters('vnetId')]" + } + }, + "dependsOn": [ + "zone" + ] + }, + "zone": { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('resourceTags')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "hello", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "ipAddress": { + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').staticIp]" + }, + "recordName": { + "value": "hello" + }, + "zone": { + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').defaultDomain]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "6659999053734156704" + } + }, + "parameters": { + "zone": { + "type": "string" + }, + "recordName": { + "type": "string" + }, + "ipAddress": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/A", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zone'), parameters('recordName'))]", + "properties": { + "ttl": 3600, + "aRecords": [ + { + "ipv4Address": "[parameters('ipAddress')]" + } + ] + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-dns', format('container-environment-{0}', parameters('name'))))]", + "[resourceId('Microsoft.App/containerApps', 'hello')]" + ] + } + ], + "outputs": { + "id": { + "type": "string", + "metadata": { + "description": "Id of Container Environment" + }, + "value": "[resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name')))]" + }, + "staticIp": { + "type": "string", + "metadata": { + "description": "The static IP of the environment." + }, + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').staticIp]" + }, + "defaultDomain": { + "type": "string", + "metadata": { + "description": "The default domain." + }, + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').defaultDomain]" + }, + "verificationId": { + "type": "string", + "metadata": { + "description": "Verification id for creating DNS records" + }, + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('container-environment-{0}', parameters('name'))), '2023-05-02-preview').customDomainConfiguration.customDomainVerificationId]" + } + } +} \ No newline at end of file diff --git a/environments/ContainerEnvironment/manifest.yaml b/environments/ContainerEnvironment/manifest.yaml new file mode 100644 index 0000000..819fee3 --- /dev/null +++ b/environments/ContainerEnvironment/manifest.yaml @@ -0,0 +1,30 @@ +name: ContainerEnvironment +version: 1.0.0 +summary: Basic Container Environment +description: Deploys a Container App Environment connected to subnet +runner: ARM +templatePath: main.json +parameters: +- id: 'location' + name: 'location' + description: 'location' + default: "[resourceGroup().location]" + type: string + required: false +- id: 'name' + name: 'name' + description: 'Name of environment' + default: "" + type: string + required: true +- id: 'vnetInternal' + name: 'vnetInternal' + description: "Should the environment be connected to a vnet?" + type: boolean + required: false +- id: "snetId" + name: 'snetId' + description: 'subnetId' + default: "" + type: string + required: false \ No newline at end of file diff --git a/environments/Sandbox/main.bicep b/environments/Sandbox/main.bicep new file mode 100644 index 0000000..4d83d29 --- /dev/null +++ b/environments/Sandbox/main.bicep @@ -0,0 +1 @@ +// Empty! diff --git a/environments/Sandbox/main.json b/environments/Sandbox/main.json new file mode 100644 index 0000000..7865574 --- /dev/null +++ b/environments/Sandbox/main.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "2698412200108083870" + } + }, + "resources": [] +} \ No newline at end of file diff --git a/environments/Sandbox/manifest.yaml b/environments/Sandbox/manifest.yaml new file mode 100644 index 0000000..f68cd69 --- /dev/null +++ b/environments/Sandbox/manifest.yaml @@ -0,0 +1,6 @@ +name: Sandbox +version: 1.0.0 +summary: Empty sandbox environment +description: Deploys an empty sandbox environment +runner: ARM +templatePath: main.json \ No newline at end of file diff --git a/modules/devcenters/devcenter/README.md b/modules/devcenters/devcenter/README.md new file mode 100644 index 0000000..f5a04ef --- /dev/null +++ b/modules/devcenters/devcenter/README.md @@ -0,0 +1,40 @@ +# Dev Center + +Dev Center + +## Details + +{{ Add detailed information about the module. }} + +## Parameters + +| Name | Type | Required | Description | +| :---------------------- | :------: | :------: | :-------------------------------------------------------------------------- | +| `location` | `string` | Yes | Specifies the location for resources. | +| `devCenterName` | `string` | Yes | Name of DevCenter | +| `networkConnectionId` | `string` | Yes | Network connection id for the network the Dev Center should be attached to. | +| `resourceTags` | `object` | No | Tags retrieved from parameter file. | +| `image` | `string` | Yes | DevBox definition image id. | +| `definitionName` | `string` | No | Name of DevBox definition. | +| `definitionSKU` | `string` | No | DevBox definition SKU. | +| `definitionStorageType` | `string` | No | DevBox definition storage type. | + +## Outputs + +| Name | Type | Description | +| :------------------------- | :------: | :--------------------------------------------------- | +| `devCenterId` | `string` | Id of DevCenter. | +| `devCenterAttachedNetwork` | `string` | Name of the attached network. | +| `devCenterManagedId` | `string` | Id of the system-managed identity of the Dev Center. | + +## Examples + +### Example 1 + +```bicep +``` + +### Example 2 + +```bicep +``` \ No newline at end of file diff --git a/modules/devcenters/devcenter/main.bicep b/modules/devcenters/devcenter/main.bicep new file mode 100644 index 0000000..8cfdb87 --- /dev/null +++ b/modules/devcenters/devcenter/main.bicep @@ -0,0 +1,69 @@ +metadata name = 'Dev Center' +metadata description = 'Dev Center' +metadata owner = 'ahelland' + +@description('Specifies the location for resources.') +param location string +@description('Name of DevCenter') +param devCenterName string +@description('Network connection id for the network the Dev Center should be attached to.') +param networkConnectionId string + +@description('Tags retrieved from parameter file.') +param resourceTags object = {} +@description('DevBox definition image id.') +param image string +@description('Name of DevBox definition.') +param definitionName string = 'DevBox-8-32' +@description('DevBox definition SKU.') +param definitionSKU string = 'general_i_8c32gb256ssd_v2' +@description('DevBox definition storage type.') +param definitionStorageType string = 'ssd_256gb' + +resource DevCenter 'Microsoft.DevCenter/devcenters@2023-10-01-preview' = { + name: devCenterName + location: location + tags: resourceTags + identity: { + type: 'SystemAssigned' + } + properties: { } +} + +//Add a default environment type +resource devEnvironment 'Microsoft.DevCenter/devcenters/environmentTypes@2023-04-01' = { + name: 'dev' + parent: DevCenter + properties: {} +} + +resource network 'Microsoft.DevCenter/devcenters/attachednetworks@2023-04-01' = { + name: '${devCenterName}-network' + parent: DevCenter + properties: { + networkConnectionId: networkConnectionId + } +} + +resource DevBoxDefinition 'Microsoft.DevCenter/devcenters/devboxdefinitions@2023-10-01-preview' = { + parent: DevCenter + name: definitionName + location: location + properties: { + imageReference: { + id: '${DevCenter.id}/galleries/default/images/${image}' + } + sku: { + name: definitionSKU + } + osStorageType: definitionStorageType + hibernateSupport: 'Enabled' + } +} + +@description('Id of DevCenter.') +output devCenterId string = DevCenter.id +@description('Name of the attached network.') +output devCenterAttachedNetwork string = network.name +@description('Id of the system-managed identity of the Dev Center.') +output devCenterManagedId string = DevCenter.identity.principalId diff --git a/modules/devcenters/devcenter/main.json b/modules/devcenters/devcenter/main.json new file mode 100644 index 0000000..da03349 --- /dev/null +++ b/modules/devcenters/devcenter/main.json @@ -0,0 +1,143 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "11861028144094318088" + }, + "name": "Dev Center", + "description": "Dev Center", + "owner": "ahelland" + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Specifies the location for resources." + } + }, + "devCenterName": { + "type": "string", + "metadata": { + "description": "Name of DevCenter" + } + }, + "networkConnectionId": { + "type": "string", + "metadata": { + "description": "Network connection id for the network the Dev Center should be attached to." + } + }, + "resourceTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags retrieved from parameter file." + } + }, + "image": { + "type": "string", + "metadata": { + "description": "DevBox definition image id." + } + }, + "definitionName": { + "type": "string", + "defaultValue": "DevBox-8-32", + "metadata": { + "description": "Name of DevBox definition." + } + }, + "definitionSKU": { + "type": "string", + "defaultValue": "general_i_8c32gb256ssd_v2", + "metadata": { + "description": "DevBox definition SKU." + } + }, + "definitionStorageType": { + "type": "string", + "defaultValue": "ssd_256gb", + "metadata": { + "description": "DevBox definition storage type." + } + } + }, + "resources": [ + { + "type": "Microsoft.DevCenter/devcenters", + "apiVersion": "2023-10-01-preview", + "name": "[parameters('devCenterName')]", + "location": "[parameters('location')]", + "tags": "[parameters('resourceTags')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": {} + }, + { + "type": "Microsoft.DevCenter/devcenters/environmentTypes", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', parameters('devCenterName'), 'dev')]", + "properties": {}, + "dependsOn": [ + "[resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName'))]" + ] + }, + { + "type": "Microsoft.DevCenter/devcenters/attachednetworks", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', parameters('devCenterName'), format('{0}-network', parameters('devCenterName')))]", + "properties": { + "networkConnectionId": "[parameters('networkConnectionId')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName'))]" + ] + }, + { + "type": "Microsoft.DevCenter/devcenters/devboxdefinitions", + "apiVersion": "2023-10-01-preview", + "name": "[format('{0}/{1}', parameters('devCenterName'), parameters('definitionName'))]", + "location": "[parameters('location')]", + "properties": { + "imageReference": { + "id": "[format('{0}/galleries/default/images/{1}', resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName')), parameters('image'))]" + }, + "sku": { + "name": "[parameters('definitionSKU')]" + }, + "osStorageType": "[parameters('definitionStorageType')]", + "hibernateSupport": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName'))]" + ] + } + ], + "outputs": { + "devCenterId": { + "type": "string", + "metadata": { + "description": "Id of DevCenter." + }, + "value": "[resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName'))]" + }, + "devCenterAttachedNetwork": { + "type": "string", + "metadata": { + "description": "Name of the attached network." + }, + "value": "[format('{0}-network', parameters('devCenterName'))]" + }, + "devCenterManagedId": { + "type": "string", + "metadata": { + "description": "Id of the system-managed identity of the Dev Center." + }, + "value": "[reference(resourceId('Microsoft.DevCenter/devcenters', parameters('devCenterName')), '2023-10-01-preview', 'full').identity.principalId]" + } + } +} \ No newline at end of file diff --git a/modules/devcenters/devcenter/test/main.test.bicep b/modules/devcenters/devcenter/test/main.test.bicep new file mode 100644 index 0000000..56a28da --- /dev/null +++ b/modules/devcenters/devcenter/test/main.test.bicep @@ -0,0 +1,33 @@ +targetScope = 'subscription' + +param location string = 'westeurope' + +param resourceTags object = { + value: { + IaC: 'Bicep' + Environment: 'Test' + } +} + +resource rg_devc 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-devcenter' + location: location + tags: resourceTags +} + +resource NetworkConnection 'Microsoft.DevCenter/networkConnections@2023-10-01-preview' existing = { + scope: rg_devc + name: 'devcenter-network-connection' +} + +module devCenter '../main.bicep' = { + scope: rg_devc + name: 'devcenter' + params: { + resourceTags: resourceTags + devCenterName: 'devCenter-01' + location: location + image: 'microsoftvisualstudio_visualstudioplustools_vs-2022-ent-general-win11-m365-gen2' + networkConnectionId: NetworkConnection.id + } +} diff --git a/modules/devcenters/devcenter/version.json b/modules/devcenters/devcenter/version.json new file mode 100644 index 0000000..a830c3d --- /dev/null +++ b/modules/devcenters/devcenter/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "0.10", + "pathFilters": [ + "./main.json" + ] +} \ No newline at end of file diff --git a/modules/devcenters/network-connection/README.md b/modules/devcenters/network-connection/README.md new file mode 100644 index 0000000..4c566d9 --- /dev/null +++ b/modules/devcenters/network-connection/README.md @@ -0,0 +1,35 @@ +# Dev Box Network Connection + +Dev Box Network Connection + +## Details + +{{ Add detailed information about the module. }} + +## Parameters + +| Name | Type | Required | Description | +| :--------------- | :------: | :------: | :------------------------------------ | +| `location` | `string` | Yes | Specifies the location for resources. | +| `resourceTags` | `object` | No | Tags retrieved from parameter file. | +| `connectionName` | `string` | Yes | Name of Network Connection | +| `snetId` | `string` | Yes | Subnet for network connection. | + +## Outputs + +| Name | Type | Description | +| :--------------- | :------: | :-------------------------- | +| `id` | `string` | Id of network connection. | +| `connectionName` | `string` | Name of network connection. | + +## Examples + +### Example 1 + +```bicep +``` + +### Example 2 + +```bicep +``` \ No newline at end of file diff --git a/modules/devcenters/network-connection/main.bicep b/modules/devcenters/network-connection/main.bicep new file mode 100644 index 0000000..e973865 --- /dev/null +++ b/modules/devcenters/network-connection/main.bicep @@ -0,0 +1,31 @@ +metadata name = 'Dev Box Network Connection' +metadata description = 'Dev Box Network Connection' +metadata owner = 'ahelland' + +@description('Specifies the location for resources.') +param location string +@description('Tags retrieved from parameter file.') +param resourceTags object = {} +@description('Name of Network Connection') +param connectionName string +@description('Subnet for network connection.') +param snetId string + +resource NetworkConnection 'Microsoft.DevCenter/networkConnections@2023-10-01-preview' = { + name: connectionName + location: location + tags: resourceTags + properties: { + domainJoinType: 'AzureADJoin' + subnetId: snetId + domainName: '' + organizationUnit: '' + domainUsername: '' + networkingResourceGroupName: 'NI_${connectionName}_westeurope' + } +} + +@description('Id of network connection.') +output id string = NetworkConnection.id +@description('Name of network connection.') +output connectionName string = NetworkConnection.name diff --git a/modules/devcenters/network-connection/main.json b/modules/devcenters/network-connection/main.json new file mode 100644 index 0000000..7e0a668 --- /dev/null +++ b/modules/devcenters/network-connection/main.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "2707311748966755923" + }, + "name": "Dev Box Network Connection", + "description": "Dev Box Network Connection", + "owner": "ahelland" + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Specifies the location for resources." + } + }, + "resourceTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags retrieved from parameter file." + } + }, + "connectionName": { + "type": "string", + "metadata": { + "description": "Name of Network Connection" + } + }, + "snetId": { + "type": "string", + "metadata": { + "description": "Subnet for network connection." + } + } + }, + "resources": [ + { + "type": "Microsoft.DevCenter/networkConnections", + "apiVersion": "2023-10-01-preview", + "name": "[parameters('connectionName')]", + "location": "[parameters('location')]", + "tags": "[parameters('resourceTags')]", + "properties": { + "domainJoinType": "AzureADJoin", + "subnetId": "[parameters('snetId')]", + "domainName": "", + "organizationUnit": "", + "domainUsername": "", + "networkingResourceGroupName": "[format('NI_{0}_westeurope', parameters('connectionName'))]" + } + } + ], + "outputs": { + "id": { + "type": "string", + "metadata": { + "description": "Id of network connection." + }, + "value": "[resourceId('Microsoft.DevCenter/networkConnections', parameters('connectionName'))]" + }, + "connectionName": { + "type": "string", + "metadata": { + "description": "Name of network connection." + }, + "value": "[parameters('connectionName')]" + } + } +} \ No newline at end of file diff --git a/modules/devcenters/network-connection/test/main.test.bicep b/modules/devcenters/network-connection/test/main.test.bicep new file mode 100644 index 0000000..8cbf267 --- /dev/null +++ b/modules/devcenters/network-connection/test/main.test.bicep @@ -0,0 +1,27 @@ +targetScope = 'subscription' + +param location string = 'westeurope' + +param resourceTags object = { + value: { + IaC: 'Bicep' + Environment: 'Test' + } +} + +resource rg_devc 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-devcenter' + location: location + tags: resourceTags +} + +module networkConnection '../main.bicep' = { + scope: rg_devc + name: 'devConnection' + params: { + resourceTags: resourceTags + connectionName: 'devConnection' + location: location + snetId: 'snet-devbox-01' + } +} diff --git a/modules/devcenters/network-connection/version.json b/modules/devcenters/network-connection/version.json new file mode 100644 index 0000000..a830c3d --- /dev/null +++ b/modules/devcenters/network-connection/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "0.10", + "pathFilters": [ + "./main.json" + ] +} \ No newline at end of file diff --git a/modules/devcenters/project/README.md b/modules/devcenters/project/README.md new file mode 100644 index 0000000..7ca90be --- /dev/null +++ b/modules/devcenters/project/README.md @@ -0,0 +1,40 @@ +# Dev Box Project + +Dev Box Project with Pool. + +## Details + +{{ Add detailed information about the module. }} + +## Parameters + +| Name | Type | Required | Description | +| :---------------------- | :------: | :------: | :------------------------------------------------ | +| `location` | `string` | Yes | Specifies the location for resources. | +| `resourceTags` | `object` | No | Tags retrieved from parameter file. | +| `devCenterId` | `string` | Yes | Id of the DevCenter to attach project to. | +| `projectName` | `string` | Yes | Name of project. | +| `devPoolName` | `string` | Yes | Name of DevBox pool. | +| `networkConnectionName` | `string` | Yes | Name of network connection to attach to. | +| `devBoxDefinitionName` | `string` | Yes | Name of DevBox definition. | +| `licenseType` | `string` | No | License Type of DevBox. | +| `localAdministrator` | `string` | No | Status of local admin account. | +| `deploymentTargetId` | `string` | Yes | SubscriptionId the environment will be mapped to. | + +## Outputs + +| Name | Type | Description | +| :------------------------ | :------: | :--------------------------------------------------------- | +| `devEnvironmentManagedId` | `string` | Id of the system-managed identity for the dev environment. | + +## Examples + +### Example 1 + +```bicep +``` + +### Example 2 + +```bicep +``` \ No newline at end of file diff --git a/modules/devcenters/project/main.bicep b/modules/devcenters/project/main.bicep new file mode 100644 index 0000000..1e9d470 --- /dev/null +++ b/modules/devcenters/project/main.bicep @@ -0,0 +1,67 @@ +metadata name = 'Dev Box Project' +metadata description = 'Dev Box Project with Pool.' +metadata owner = 'ahelland' + +@description('Specifies the location for resources.') +param location string +@description('Tags retrieved from parameter file.') +param resourceTags object = {} +@description('Id of the DevCenter to attach project to.') +param devCenterId string +@description('Name of project.') +param projectName string +@description('Name of DevBox pool.') +param devPoolName string +@description('Name of network connection to attach to.') +param networkConnectionName string +@description('Name of DevBox definition.') +param devBoxDefinitionName string +@description('License Type of DevBox.') +param licenseType string = 'Windows_Client' +@allowed([ + 'Enabled' + 'Disabled' +]) +@description('Status of local admin account.') +param localAdministrator string = 'Enabled' +@description('SubscriptionId the environment will be mapped to.') +param deploymentTargetId string + +resource Project 'Microsoft.DevCenter/projects@2023-10-01-preview' = { + name: projectName + tags: resourceTags + location: location + properties: { + devCenterId: devCenterId + } +} + +//Add a Dev environment +resource devEnvironment 'Microsoft.DevCenter/projects/environmentTypes@2023-04-01' = { + name: 'dev' + location: location + parent: Project + identity: { + type: 'SystemAssigned' + } + properties: { + deploymentTargetId: deploymentTargetId + status: 'Enabled' + } +} + +resource DevPool 'Microsoft.DevCenter/projects/pools@2023-10-01-preview' = { + parent: Project + name: devPoolName + tags: resourceTags + location: location + properties: { + devBoxDefinitionName: devBoxDefinitionName + networkConnectionName: networkConnectionName + licenseType: licenseType + localAdministrator: localAdministrator + } +} + +@description('Id of the system-managed identity for the dev environment.') +output devEnvironmentManagedId string = devEnvironment.identity.principalId diff --git a/modules/devcenters/project/main.json b/modules/devcenters/project/main.json new file mode 100644 index 0000000..eaae96c --- /dev/null +++ b/modules/devcenters/project/main.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "11066982034529870583" + }, + "name": "Dev Box Project", + "description": "Dev Box Project with Pool.", + "owner": "ahelland" + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Specifies the location for resources." + } + }, + "resourceTags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags retrieved from parameter file." + } + }, + "devCenterId": { + "type": "string", + "metadata": { + "description": "Id of the DevCenter to attach project to." + } + }, + "projectName": { + "type": "string", + "metadata": { + "description": "Name of project." + } + }, + "devPoolName": { + "type": "string", + "metadata": { + "description": "Name of DevBox pool." + } + }, + "networkConnectionName": { + "type": "string", + "metadata": { + "description": "Name of network connection to attach to." + } + }, + "devBoxDefinitionName": { + "type": "string", + "metadata": { + "description": "Name of DevBox definition." + } + }, + "licenseType": { + "type": "string", + "defaultValue": "Windows_Client", + "metadata": { + "description": "License Type of DevBox." + } + }, + "localAdministrator": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Status of local admin account." + } + }, + "deploymentTargetId": { + "type": "string", + "metadata": { + "description": "SubscriptionId the environment will be mapped to." + } + } + }, + "resources": [ + { + "type": "Microsoft.DevCenter/projects", + "apiVersion": "2023-10-01-preview", + "name": "[parameters('projectName')]", + "tags": "[parameters('resourceTags')]", + "location": "[parameters('location')]", + "properties": { + "devCenterId": "[parameters('devCenterId')]" + } + }, + { + "type": "Microsoft.DevCenter/projects/environmentTypes", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', parameters('projectName'), 'dev')]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "deploymentTargetId": "[parameters('deploymentTargetId')]", + "status": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.DevCenter/projects', parameters('projectName'))]" + ] + }, + { + "type": "Microsoft.DevCenter/projects/pools", + "apiVersion": "2023-10-01-preview", + "name": "[format('{0}/{1}', parameters('projectName'), parameters('devPoolName'))]", + "tags": "[parameters('resourceTags')]", + "location": "[parameters('location')]", + "properties": { + "devBoxDefinitionName": "[parameters('devBoxDefinitionName')]", + "networkConnectionName": "[parameters('networkConnectionName')]", + "licenseType": "[parameters('licenseType')]", + "localAdministrator": "[parameters('localAdministrator')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.DevCenter/projects', parameters('projectName'))]" + ] + } + ], + "outputs": { + "devEnvironmentManagedId": { + "type": "string", + "metadata": { + "description": "Id of the system-managed identity for the dev environment." + }, + "value": "[reference(resourceId('Microsoft.DevCenter/projects/environmentTypes', parameters('projectName'), 'dev'), '2023-04-01', 'full').identity.principalId]" + } + } +} \ No newline at end of file diff --git a/modules/devcenters/project/test/main.test.bicep b/modules/devcenters/project/test/main.test.bicep new file mode 100644 index 0000000..6fc8a94 --- /dev/null +++ b/modules/devcenters/project/test/main.test.bicep @@ -0,0 +1,36 @@ +targetScope = 'subscription' + +param location string = 'westeurope' + +param resourceTags object = { + value: { + IaC: 'Bicep' + Environment: 'Test' + } +} + +resource rg_devc 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-devcenter' + location: location + tags: resourceTags +} + +resource devCenter 'Microsoft.DevCenter/devcenters@2023-04-01' existing = { + scope: rg_devc + name: 'devCenter-01' +} + +module DevProject '../main.bicep' = { + scope: rg_devc + name: 'devProject' + params: { + resourceTags: resourceTags + devBoxDefinitionName: 'DevBox-8-32' + devCenterId: devCenter.id + devPoolName: 'devPool-01' + location: location + networkConnectionName: 'devConnection' + projectName: 'iac' + deploymentTargetId: subscription().id + } +} diff --git a/modules/devcenters/project/version.json b/modules/devcenters/project/version.json new file mode 100644 index 0000000..a830c3d --- /dev/null +++ b/modules/devcenters/project/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "0.10", + "pathFilters": [ + "./main.json" + ] +} \ No newline at end of file