diff --git a/src/internal/classes/AzOpsScope.ps1 b/src/internal/classes/AzOpsScope.ps1 index 1193887e..3f94cd7e 100644 --- a/src/internal/classes/AzOpsScope.ps1 +++ b/src/internal/classes/AzOpsScope.ps1 @@ -390,7 +390,6 @@ return $null } [string] IsResource() { - if ($this.Scope -match $this.regex_managementgroupResource) { return ($this.regex_managementgroupResource.Split($this.Scope) | Select-Object -last 1) } @@ -409,13 +408,18 @@ Should Return Management Group Name #> [string] GetManagementGroup() { - if ($this.GetManagementGroupName()) { foreach ($mgmt in $script:AzOpsAzManagementGroup) { if ($mgmt.Name -eq $this.GetManagementGroupName()) { + $private:mgmtHit = $true return $mgmt.Name } } + if (-not $private:mgmtHit) { + $mgId = $this.Scope -split $this.regex_managementgroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1 + Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroup.NotFound' -StringValues $mgId -FunctionName AzOpsScope -ModuleName AzOps + return $mgId + } } if ($this.Subscription) { foreach ($mgmt in $script:AzOpsAzManagementGroup) { @@ -445,7 +449,8 @@ } else { Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' -StringValues $managementgroupName -FunctionName AzOpsScope -ModuleName AzOps - throw "Management Group not found: $managementgroupName" + $assumeNewResource = "azopsscope-assume-new-resource_$managementgroupName" + return $assumeNewResource.ToLower() } } @@ -455,11 +460,10 @@ [string] GetManagementGroupName() { if ($this.Scope -match $this.regex_managementgroupExtract) { $mgId = $this.Scope -split $this.regex_managementgroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1 - if ($mgId) { $mgDisplayName = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $mgId).Name if ($mgDisplayName) { - #Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.Found.Azure' -StringValues $mgDisplayName -FunctionName AzOpsScope -ModuleName AzOps + Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.Found.Azure' -StringValues $mgDisplayName -FunctionName AzOpsScope -ModuleName AzOps return $mgDisplayName } else { @@ -560,4 +564,4 @@ throw "Unable to determine Resource Scope for: $($this.Scope)" } #endregion Data Accessors -} +} \ No newline at end of file diff --git a/src/internal/functions/New-AzOpsDeployment.ps1 b/src/internal/functions/New-AzOpsDeployment.ps1 index 0f6c759b..61ec633b 100644 --- a/src/internal/functions/New-AzOpsDeployment.ps1 +++ b/src/internal/functions/New-AzOpsDeployment.ps1 @@ -109,7 +109,7 @@ $deploymentCommand = 'New-AzSubscriptionDeployment' } # Management Groups - elseif ($scopeObject.managementGroup) { + elseif ($scopeObject.managementGroup -and (-not ($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) { Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.ManagementGroup.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $parameters.ManagementGroupId = $scopeObject.managementgroup $whatIfCommand = 'Get-AzManagementGroupDeploymentWhatIfResult' @@ -121,6 +121,36 @@ $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult' $deploymentCommand = 'New-AzTenantDeployment' } + # If Management Group resource was not found, validate and prepare for first time deployment of resource + elseif (($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_')) { + $resourceScopeFileContent = Get-Content -Path $addition | ConvertFrom-Json -Depth 100 + $resource = ($resourceScopeFileContent.resources | Where-Object {$_.type -eq 'Microsoft.Management/managementGroups'} | Select-Object -First 1) + $pathDir = (Get-Item -Path $addition).Directory | Resolve-Path -Relative + if ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -ne '.') { + $pathDir = Split-Path -Path $pathDir -Parent + } + $parentDirScopeObject = New-AzOpsScope -Path (Split-Path -Path $pathDir -Parent) | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))} + $parentIdScope = New-AzOpsScope -Scope (($resource).properties.details.parent.id) | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))} + # Validate parent existence with content parent scope, statepath and name match, determines file location match deployment scope + if ($parentDirScopeObject -and $parentIdScope -and $parentDirScopeObject.Scope -eq $parentIdScope.Scope -and $parentDirScopeObject.StatePath -eq $parentIdScope.StatePath -and $parentDirScopeObject.Name -eq $parentIdScope.Name) { + # Validate directory name match resource information + if ((Get-Item -Path $pathDir).Name -eq "$($resource.properties.displayName) ($($resource.name))") { + Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.Root.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject + $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult' + $deploymentCommand = 'New-AzTenantDeployment' + } + # Invalid directory name + else { + Write-PSFMessage -Level Error -String 'New-AzOpsDeployment.Directory.NotFound' -Target $scopeObject -Tag Error -StringValues (Get-Item -Path $pathDir).Name, "$($resource.properties.displayName) ($($resource.name))" + throw + } + } + # Parent missing + else { + Write-PSFMessage -Level Error -String 'New-AzOpsDeployment.Parent.NotFound' -Target $scopeObject -Tag Error -StringValues $addition + throw + } + } else { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Unidentified' -Target $scopeObject -StringValues $scopeObject $scopeFound = $false diff --git a/src/localized/en-us/Strings.psd1 b/src/localized/en-us/Strings.psd1 index 3d63d2df..4e5a22c9 100644 --- a/src/localized/en-us/Strings.psd1 +++ b/src/localized/en-us/Strings.psd1 @@ -17,11 +17,12 @@ 'Assert-AzOpsBicepDependency.Success' = 'Bicep found in current path' # 'Assert-AzOpsBicepDependency.NotFound' = 'Unable to locate bicep binary. Will not be able to deploy bicep templates.' # - 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' = 'Management Group not found: {0}' # $managementgroupName + 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' = 'Management Group path not found: {0}' # $managementgroupName 'AzOpsScope.GetAzOpsResourcePath.NotFound' = 'Unable to determine Resource Scope for: {0}' # $this.Scope 'AzOpsScope.GetAzOpsResourcePath.Retrieving' = 'Getting Resource path for: {0}' # $this.Scope 'AzOpsScope.GetManagementGroupName.Found.Azure' = 'Management Group found in Azure: {0}' # $mgDisplayName - 'AzOpsScope.GetManagementGroupName.NotFound' = 'Management Group not found in Azure. Using directory name instead: {0}' # $mgId + 'AzOpsScope.GetManagementGroup.NotFound' = 'Management Group does not match any existing in Azure. Assume new resource, using directory name: {0}' # $mgId + 'AzOpsScope.GetManagementGroupName.NotFound' = 'Management Group not found in Azure. Trying with directory name instead: {0}' # $mgId 'AzOpsScope.GetSubscription.Found' = 'SubscriptionId found in Azure: {0}' # $sub.Id 'AzOpsScope.GetSubscription.NotFound' = 'SubscriptionId not found in Azure. Using directory name instead: {0}' # $subId 'AzOpsScope.GetSubscriptionDisplayName.Found' = 'Subscription DisplayName found in Azure: {0}' # $sub.displayName @@ -223,6 +224,8 @@ 'New-AzOpsDeployment.WhatIfResults' = 'WhatIf Results: {0}' # $TemplateFilePath 'New-AzOpsDeployment.WhatIfFile' = 'Creating WhatIf Results file' 'New-AzOpsDeployment.SkipDueToWhatIf' = 'Skipping deployment due to WhatIf' # + 'New-AzOpsDeployment.Parent.NotFound' = 'Failed to find parent scope for template {0}' # $addition + 'New-AzOpsDeployment.Directory.NotFound' = 'Directory name {0} does not match expected {1}' # (Get-Item -Path $pathDir).Name, "$($resource.properties.displayName) ($($resource.name))" 'New-AzOpsStateDeployment.EnrollmentAccount.First' = 'No enrollment account defined, using the first account found: {0}' # @($enrollmentAccounts)[0].PrincipalName 'New-AzOpsStateDeployment.EnrollmentAccount.Selected' = 'Using the defined enrollment account {0}' # $cfgEnrollmentAccount diff --git a/src/tests/integration/Repository.Tests.ps1 b/src/tests/integration/Repository.Tests.ps1 index 8a1382ff..af5ced62 100644 --- a/src/tests/integration/Repository.Tests.ps1 +++ b/src/tests/integration/Repository.Tests.ps1 @@ -305,6 +305,11 @@ Describe "Repository" { $script:bicepDeploymentName = "AzOps-{0}-{1}" -f $($script:bicepTemplatePath[0].Name.Replace(".bicep", '')), $deploymentLocationId $script:bicepResourceGroupName = ((Get-Content -Path ($Script:bicepTemplatePath.FullName[1])) | ConvertFrom-Json).parameters.resourceGroupName.value + $script:pushmgmttest1idManagementGroupTemplatePath = Get-Item "$($global:testroot)/templates/pushmgmttest1displayname (pushmgmttest1id)" | Copy-Item -Destination $script:testManagementGroupDirectory -Recurse -PassThru -Force + $script:pushmgmttest1idManagementGroupDeploymentName = "AzOps-{0}-{1}" -f "$($script:pushmgmttest1idManagementGroupTemplatePath[1].Name.Replace(".json", ''))", $deploymentLocationId + $script:pushmgmttest1idName = ((Get-Content -Path ($script:pushmgmttest1idManagementGroupTemplatePath.FullName[1])) | ConvertFrom-Json).resources.name[0] + + $script:pushmgmttest2idManagementGroupTemplatePath = Get-Item "$($global:testroot)/templates/pushmgmttest2displayname (pushmgmttest2id)" | Copy-Item -Destination $script:platformManagementGroupDirectory -Recurse -PassThru -Force #endregion Paths #Test push based on pulled resources @@ -319,7 +324,8 @@ Describe "Repository" { "A`t$script:routeTableFile", "A`t$script:ruleCollectionGroupsFile", "A`t$script:locksFile", - "A`t$($script:bicepTemplatePath.FullName[0])" + "A`t$($script:bicepTemplatePath.FullName[0])", + "A`t$($script:pushmgmttest1idManagementGroupTemplatePath.FullName[1])" ) Invoke-AzOpsPush -ChangeSet $changeSet @@ -930,6 +936,23 @@ Describe "Repository" { } #endregion + #region Deploy Management Group using folder structure and file + It "ManagementGroup deployment using folder structure and file should be successful" { + $script:pushmgmttest1Deployment = Get-AzTenantDeployment -Name $script:pushmgmttest1idManagementGroupDeploymentName + $pushmgmttest1Deployment.ProvisioningState | Should -Be "Succeeded" + } + It "ManagementGroup deployed using folder structure and file should exist" { + $script:pushmgmttest1Mg = Get-AzManagementGroup -GroupName $script:pushmgmttest1idName + $pushmgmttest1Mg.Name | Should -Be $script:pushmgmttest1idName + } + It "ManagementGroup deployed using folder structure and file at folder scope not matching content parent should fail" { + $changeSet = @( + "A`t$($script:pushmgmttest2idManagementGroupTemplatePath.FullName[1])" + ) + {Invoke-AzOpsPush -ChangeSet $changeSet -WhatIf:$true} | Should -Throw + } + #endregion + #region Scope - Policy DeletionDependency It "Deletion of policyDefinitionsFile with assignment dependency should fail" { $changeSet = @( diff --git a/src/tests/templates/pushmgmttest1displayname (pushmgmttest1id)/microsoft.management_managementgroups-pushmgmttest1id.json b/src/tests/templates/pushmgmttest1displayname (pushmgmttest1id)/microsoft.management_managementgroups-pushmgmttest1id.json new file mode 100644 index 00000000..0830e76a --- /dev/null +++ b/src/tests/templates/pushmgmttest1displayname (pushmgmttest1id)/microsoft.management_managementgroups-pushmgmttest1id.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "AzOps" + } + }, + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Management/managementGroups", + "name": "pushmgmttest1id", + "apiVersion": "2021-04-01", + "scope": "/", + "properties": { + "displayName": "pushmgmttest1displayname", + "details": { + "parent": { + "id": "/providers/Microsoft.Management/managementGroups/52fd72ab-b56e-5a52-83a1-1f87365f7998" + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2020-10-01", + "name": "AzOps-microsoft.management_managementgroups-nested", + "location": "[deployment().location]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": {} + } + }, + "dependsOn": [ + "Microsoft.Management/managementGroups/pushmgmttest1id" + ] + } + ], + "outputs": {} +} diff --git a/src/tests/templates/pushmgmttest2displayname (pushmgmttest2id)/microsoft.management_managementgroups-pushmgmttest2id.json b/src/tests/templates/pushmgmttest2displayname (pushmgmttest2id)/microsoft.management_managementgroups-pushmgmttest2id.json new file mode 100644 index 00000000..fdfa16cc --- /dev/null +++ b/src/tests/templates/pushmgmttest2displayname (pushmgmttest2id)/microsoft.management_managementgroups-pushmgmttest2id.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "AzOps" + } + }, + "parameters": {}, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Management/managementGroups", + "name": "pushmgmttest2id", + "apiVersion": "2021-04-01", + "scope": "/", + "properties": { + "displayName": "pushmgmttest2displayname", + "details": { + "parent": { + "id": "/providers/Microsoft.Management/managementGroups/52fd72ab-b56e-5a52-83a1-1f87365f7998" + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2020-10-01", + "name": "AzOps-microsoft.management_managementgroups-nested", + "location": "[deployment().location]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": {} + } + }, + "dependsOn": [ + "Microsoft.Management/managementGroups/pushmgmttest2id" + ] + } + ], + "outputs": {} +}