Skip to content

Commit

Permalink
New policy build workflow (#1725)
Browse files Browse the repository at this point in the history
Co-authored-by: Jack Tracey <[email protected]>
  • Loading branch information
Springstone and jtracey93 authored Aug 14, 2024
1 parent 24ae46a commit 328b900
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 126 deletions.
99 changes: 99 additions & 0 deletions .github/actions-pester/PolicyPesterTestHelper.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,102 @@ function Get-PolicyFiles
return $_
}
}

function Remove-JSONMetadata {

[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[hashtable] $TemplateObject
)
$TemplateObject.Remove('metadata')

# Differantiate case: With user defined types (resources property is hashtable) vs without user defined types (resources property is array)
if ($TemplateObject.resources.GetType().BaseType.Name -eq 'Hashtable') {
# Case: Hashtable
$resourceIdentifiers = $TemplateObject.resources.Keys
for ($index = 0; $index -lt $resourceIdentifiers.Count; $index++) {
if ($TemplateObject.resources[$resourceIdentifiers[$index]].type -eq 'Microsoft.Resources/deployments' -and $TemplateObject.resources[$resourceIdentifiers[$index]].properties.template.GetType().BaseType.Name -eq 'Hashtable') {
$TemplateObject.resources[$resourceIdentifiers[$index]] = Remove-JSONMetadata -TemplateObject $TemplateObject.resources[$resourceIdentifiers[$index]].properties.template
}
}
} else {
# Case: Array
for ($index = 0; $index -lt $TemplateObject.resources.Count; $index++) {
if ($TemplateObject.resources[$index].type -eq 'Microsoft.Resources/deployments' -and $TemplateObject.resources[$index].properties.template.GetType().BaseType.Name -eq 'Hashtable') {
$TemplateObject.resources[$index] = Remove-JSONMetadata -TemplateObject $TemplateObject.resources[$index].properties.template
}
}
}

return $TemplateObject
}

function ConvertTo-OrderedHashtable {

[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $JSONInputObject # Must be string to workaround auto-conversion
)

$JSONObject = ConvertFrom-Json $JSONInputObject -AsHashtable -Depth 99 -NoEnumerate
$orderedLevel = [ordered]@{}

if (-not ($JSONObject.GetType().BaseType.Name -eq 'Hashtable')) {
return $JSONObject # E.g. in primitive data types [1,2,3]
}

foreach ($currentLevelKey in ($JSONObject.Keys | Sort-Object -Culture 'en-US')) {

if ($null -eq $JSONObject[$currentLevelKey]) {
# Handle case in which the value is 'null' and hence has no type
$orderedLevel[$currentLevelKey] = $null
continue
}

switch ($JSONObject[$currentLevelKey].GetType().BaseType.Name) {
{ $PSItem -in @('Hashtable') } {
$orderedLevel[$currentLevelKey] = ConvertTo-OrderedHashtable -JSONInputObject ($JSONObject[$currentLevelKey] | ConvertTo-Json -Depth 99)
}
'Array' {
$arrayOutput = @()

# Case: Array of arrays
$arrayElements = $JSONObject[$currentLevelKey] | Where-Object { $_.GetType().BaseType.Name -eq 'Array' }
foreach ($array in $arrayElements) {
if ($array.Count -gt 1) {
# Only sort for arrays with more than one item. Otherwise single-item arrays are casted
$array = $array | Sort-Object -Culture 'en-US'
}
$arrayOutput += , (ConvertTo-OrderedHashtable -JSONInputObject ($array | ConvertTo-Json -Depth 99))
}

# Case: Array of objects
$hashTableElements = $JSONObject[$currentLevelKey] | Where-Object { $_.GetType().BaseType.Name -eq 'Hashtable' }
foreach ($hashTable in $hashTableElements) {
$arrayOutput += , (ConvertTo-OrderedHashtable -JSONInputObject ($hashTable | ConvertTo-Json -Depth 99))
}

# Case: Primitive data types
$primitiveElements = $JSONObject[$currentLevelKey] | Where-Object { $_.GetType().BaseType.Name -notin @('Array', 'Hashtable') } | ConvertTo-Json -Depth 99 | ConvertFrom-Json -AsHashtable -NoEnumerate -Depth 99
if ($primitiveElements.Count -gt 1) {
$primitiveElements = $primitiveElements | Sort-Object -Culture 'en-US'
}
$arrayOutput += $primitiveElements

if ($array.Count -gt 1) {
# Only sort for arrays with more than one item. Otherwise single-item arrays are casted
$arrayOutput = $arrayOutput | Sort-Object -Culture 'en-US'
}
$orderedLevel[$currentLevelKey] = $arrayOutput
}
Default {
# string/int/etc.
$orderedLevel[$currentLevelKey] = $JSONObject[$currentLevelKey]
}
}
}

return $orderedLevel
}
62 changes: 62 additions & 0 deletions .github/actions-pester/Test-BuildPolicies.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Describe 'UnitTest-BuildPolicies' {

BeforeAll {
Import-Module -Name $PSScriptRoot\PolicyPesterTestHelper.psm1 -Force -Verbose

New-Item -Name "buildout" -Type Directory

# Build the PR policies, initiatives, and role definitions to a temp folder
bicep build ./src/templates/policies.bicep --outfile ./buildout/policies.json
bicep build ./src/templates/initiatives.bicep --outfile ./buildout/initiatives.json
bicep build ./src/templates/roles.bicep --outfile ./buildout/customRoleDefinitions.json
}

Context "Check Policy Builds" {

It "Check policies build done" {
$prFile = "./eslzArm/managementGroupTemplates/policyDefinitions/policies.json"
$buildFile = "./buildout/policies.json"

$buildJson = Remove-JSONMetadata -TemplateObject (Get-Content $buildFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$buildJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $buildJson -Depth 99)

$prJson = Remove-JSONMetadata -TemplateObject (Get-Content $prFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$prJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $prJson -Depth 99)

# Compare files we built to the PR files
(ConvertTo-Json $buildJson -Depth 99) | Should -Be (ConvertTo-Json $prJson -Depth 99) -Because "the [policies.json] should be based on the latest [policies.bicep] file. Please run [` bicep build ./src/templates/policies.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/policies.json `] using the latest Bicep CLI version."
}

It "Check initiatives build done" {
$PRfile = "./eslzArm/managementGroupTemplates/policyDefinitions/initiatives.json"
$buildFile = "./buildout/initiatives.json"

$buildJson = Remove-JSONMetadata -TemplateObject (Get-Content $buildFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$buildJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $buildJson -Depth 99)

$prJson = Remove-JSONMetadata -TemplateObject (Get-Content $prFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$prJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $prJson -Depth 99)

# Compare files we built to the PR files
(ConvertTo-Json $buildJson -Depth 99) | Should -Be (ConvertTo-Json $prJson -Depth 99) -Because "the [initiatives.json] should be based on the latest [initiatives.bicep] file. Please run [` bicep build ./src/templates/initiatives.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/initiatives.json `] using the latest Bicep CLI version."
}

It "Check role definitions build done" {
$PRfile = "./eslzArm/managementGroupTemplates/roleDefinitions/customRoleDefinitions.json"
$buildFile = "./buildout/customRoleDefinitions.json"

$buildJson = Remove-JSONMetadata -TemplateObject (Get-Content $buildFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$buildJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $buildJson -Depth 99)

$prJson = Remove-JSONMetadata -TemplateObject (Get-Content $prFile -Raw | ConvertFrom-Json -Depth 99 -AsHashtable)
$prJson = ConvertTo-OrderedHashtable -JSONInputObject (ConvertTo-Json $prJson -Depth 99)

# Compare files we built to the PR files
(ConvertTo-Json $buildJson -Depth 99) | Should -Be (ConvertTo-Json $prJson -Depth 99) -Because "the [customRoleDefinitions.json] should be based on the latest [customRoleDefinitions.bicep] file. Please run [` bicep build ./src/templates/roles.bicep --outfile ./eslzArm/managementGroupTemplates/roleDefinitions/customRoleDefinitions.json `] using the latest Bicep CLI version."
}
}

AfterAll {
# These are not the droids you are looking for...
}
}
51 changes: 51 additions & 0 deletions .github/workflows/check-policy-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: Check Policy Build

##########################################
# Start the job on PR for all branches #
##########################################

# yamllint disable-line rule:truthy
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
paths:
- "eslzArm/**.json"
- "src/Alz.Tools/**"
- "src/**.json"
- "src/**.bicep"

###############
# Set the Job #
###############

jobs:
check-policy:
name: Check Policy Build
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check build
shell: pwsh
run: |
Import-Module Pester -Force
$pesterConfiguration = @{
Run = @{
Container = New-PesterContainer -Path "./.github/actions-pester/Test-BuildPolicies.Tests.ps1"
PassThru = $true
}
Output = @{
Verbosity = 'Detailed'
}
}
$result = Invoke-Pester -Configuration $pesterConfiguration
exit $result.FailedCount
125 changes: 0 additions & 125 deletions .github/workflows/update-portal.yml

This file was deleted.

8 changes: 7 additions & 1 deletion docs/wiki/ALZ-Contribution-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,13 @@ For a policy set definition, additional code should be added inside of the `load

`loadTextContent('../resources/Microsoft.Authorization/policySetDefinitions/Deploy-Sql-Security.json')`

The policy definition files will be compiled into a `policies.json` file from the `policy.bicep` file which was amended.
~~The policy definition files will be compiled into a `policies.json` file from the `policy.bicep` file which was amended.~~

> Due to security compliance requirements, we've made core changes that mean we no longer automatically build the policies, initiatives and roles templates after changes in the `src` folder are committed. This means that you as a contributor must run the bicep build commands to generate the required outputs as part of your pull request. Depending on the files you've updated these are the commands:
>
> - `bicep build ./src/templates/policies.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/policies.json`
> - `bicep build ./src/templates/initiatives.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/initiatives.json`
> - `bicep build ./src/templates/roles.bicep --outfile ./eslzArm/managementGroupTemplates/roleDefinitions/customRoleDefinitions.json`

Once the policy work has been completed, a pull request should be submitted to the repository:

Expand Down
6 changes: 6 additions & 0 deletions docs/wiki/Whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ Here's what's changed in Enterprise Scale/Azure Landing Zones:

### August 2024

> NOTE TO CONTRIBUTORS: Due to security compliance requirements, we've made core changes that mean we no longer automatically build the policies, initiatives and roles templates after changes in the `src` folder are committed. This means that you as a contributor must run the bicep build commands to generate the required outputs as part of your pull request. Depending on the files you've updated these are the commands (assuming you have bicep installed):
>
> - `bicep build ./src/templates/policies.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/policies.json`
> - `bicep build ./src/templates/initiatives.bicep --outfile ./eslzArm/managementGroupTemplates/policyDefinitions/initiatives.json`
> - `bicep build ./src/templates/roles.bicep --outfile ./eslzArm/managementGroupTemplates/roleDefinitions/customRoleDefinitions.json`
#### Other

- Cleaned up the Log Analytics "solutions" in portal ARM template, as these are no longer required and deployed by ALZ.
Expand Down

0 comments on commit 328b900

Please sign in to comment.