From 91b6ca2b2331cca5c450f650c53d0caaefeea523 Mon Sep 17 00:00:00 2001 From: Anthony Watherston Date: Fri, 6 Sep 2024 10:17:24 +1000 Subject: [PATCH] Document enhancement features (#741) Co-authored-by: Anthony Watherston --- Docs/index.md | 4 + .../operational-scripts-documenting-policy.md | 65 ++++++++++--- .../Out-DocumentationForPolicyAssignments.ps1 | 46 ++++++++- .../Set-AzRoleAssignmentRestMethod.ps1 | 3 + .../Operations/Build-PolicyDocumentation.ps1 | 9 +- .../contoso-allAssignments.jsonc | 10 +- .../contoso-environmentCategories.jsonc | 10 +- .../epac-dev-pipeline-with-adowiki.yml | 97 +++++++++++++++++++ .../epac-tenant-pipeline-with-adowiki.yml | 77 +++++++++++++++ .../templates-ps1-module/documentation.yml | 27 ++++++ .../templates-ps1-scripts/documentation.yml | 26 +++++ 11 files changed, 356 insertions(+), 18 deletions(-) create mode 100644 StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-dev-pipeline-with-adowiki.yml create mode 100644 StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-tenant-pipeline-with-adowiki.yml create mode 100644 StarterKit/Pipelines/AzureDevOps/templates-ps1-module/documentation.yml create mode 100644 StarterKit/Pipelines/AzureDevOps/templates-ps1-scripts/documentation.yml diff --git a/Docs/index.md b/Docs/index.md index 0016d4ff..0bfc3aaf 100644 --- a/Docs/index.md +++ b/Docs/index.md @@ -2,6 +2,10 @@ Enterprise Azure Policy as Code (EPAC for short) is a number of PowerShell scripts which can be used in CI/CD based system or a semi-automated use to deploy Policies, Policy Sets, Policy Assignments, Policy Exemptions and Role Assignments. It also contains operational scripts to simplify operational tasks. +## Latest Updates + +For all EPAC changes and newest updates, please visit our [GitHub Releases Page](https://github.com/Azure/enterprise-azure-policy-as-code/releases). + > [!CAUTION] > Review the Breaking changes in v10.0.0 carefully to avoid issues with your EPAC instance. The changes are [documented here](start-changes.md#breaking-changes-in-v1000). diff --git a/Docs/operational-scripts-documenting-policy.md b/Docs/operational-scripts-documenting-policy.md index 4eb277c8..9e77ec4d 100644 --- a/Docs/operational-scripts-documenting-policy.md +++ b/Docs/operational-scripts-documenting-policy.md @@ -55,7 +55,15 @@ Each file must contain one or both documentation topics. This example file in th { "fileNameStem": "contoso-policy-effects-across-environments", "environmentCategories": [], // when using 'documentAllAssignments', this value will be overwritten - "title": "Contoso Policy effects" + "title": "Contoso Policy effects", + "markdownAdoWiki": true, + "markdownAdoWikiConfig": [ + { + "adoOrganization": "MyOrganization", + "adoProject": "EPAC", + "adoWiki": "EPAC" + } + ] } ] }, @@ -154,20 +162,28 @@ Each file must contain one or both documentation topics. This example file in th "id": "/providers/Microsoft.Management/managementGroups/Contoso-Dev/providers/Microsoft.Authorization/policyAssignments/prod-org" } ] - }, - ], - "documentationSpecifications": [ - { - "fileNameStem": "contoso-policy-effects-across-environments", - "environmentCategories": [ - "prod", - "test", - "dev" - ], - "title": "Contoso Policy effects" } ] }, + "documentationSpecifications": [ + { + "fileNameStem": "contoso-policy-effects-across-environments", + "environmentCategories": [ + "prod", + "test", + "dev" + ], + "title": "Contoso Policy effects", + "markdownAdoWiki": true, + "markdownAdoWikiConfig": [ + { + "adoOrganization": "MyOrganization", + "adoProject": "EPAC", + "adoWiki": "EPAC" + } + ] + } + ], "documentPolicySets": [ { "pacEnvironment": "tenant", @@ -219,6 +235,31 @@ Markdown processors vary slightly. This shipt has settings to tune the output to "markdownAdoWiki": true, // default is false, set to true to format headings for Azure DevOps Wiki and generate a table of contents ``` +### Automating Azure DevOps Wiki Markdown + +- EPAC can be used to automate the population of your Azure DevOps Wiki pages with the generated markdown files. To do this, you must call "Build-PolicyDocumentation" with the parameter "WikiClonePat". The value of the parameter should be the name of the Personal Access Token (PAT) set in your pipeline variable. Example: + +``` +Build-PolicyDocumentation.ps1 -WikiClonePat $(WikiClonePat) +``` +- This PAT only requires "Read & write" permissions for "Code", as it will modify and push these markdown files to your Wiki. For more information please see ["Azure DevOps: Use personal access tokens"](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows) +- In order for your EPAC to reach your Wiki, you must configure the "markdownAdoWikiConfig" property within your policyDeocumention file. + - **adoOrganization**: Name of your ADO Organization + - **adoProject**: Name of your ADO Project + - **adoWiki**: Name of your Wiki (If Wiki was not manually set up, it will be created for you based on the name given here) + +```jsonc +"markdownAdoWikiConfig": [ + { + "adoOrganization": "MyOrganization", + "adoProject": "EPAC", + "adoWiki": "EPAC" + } + ] +``` + +- For a full implementation using an example pipeline, please see ["EPAC GitHub: epac-dev-pipeline-with-adowiki.yml"](https://github.com/Azure/enterprise-azure-policy-as-code/blob/main/StarterKit/Pipelines/GitHubActions/GitHub-Flow-With-ADOWiki/epac-dev-pipeline-with-adowiki.yml) + ### Embedded HTML in Markdown Tables EPAC uses embedded HTML to format Markdown tables. Some Markdown processors, such as SharePoint, do not recognize embedded HTML. Setting `markdownNoEmbeddedHtml` to `true` emits commas `, ` instead of the HTML tag `
`. diff --git a/Scripts/Helpers/Out-DocumentationForPolicyAssignments.ps1 b/Scripts/Helpers/Out-DocumentationForPolicyAssignments.ps1 index 4809355b..633ce2df 100644 --- a/Scripts/Helpers/Out-DocumentationForPolicyAssignments.ps1 +++ b/Scripts/Helpers/Out-DocumentationForPolicyAssignments.ps1 @@ -7,7 +7,8 @@ function Out-DocumentationForPolicyAssignments { $DocumentationSpecification, [hashtable] $AssignmentsByEnvironment, [switch] $IncludeManualPolicies, - [hashtable] $PacEnvironments + [hashtable] $PacEnvironments, + [string] $WikiClonePat ) [string] $fileNameStem = $DocumentationSpecification.fileNameStem @@ -587,5 +588,46 @@ function Out-DocumentationForPolicyAssignments { } #endregion csv - + + #region PushToWiki + if ($DocumentationSpecification.markdownAdoWikiConfig) { + if ($WikiClonePat -eq "") { + Write-Error "PAT Token not found! Please pass as parameter 'WikiClonePat'!" + Exit 1 + } + Write-Information "Attempting push to Azure DevOps Wiki" + # Clone down wiki + git clone "https://$($WikiClonePat):x-oauth-basic@$($DocumentationSpecification.markdownAdoWikiConfig.adoOrganization).visualstudio.com/$($DocumentationSpecification.markdownAdoWikiConfig.adoProject)/_git/$($DocumentationSpecification.markdownAdoWikiConfig.adoWiki).wiki" + # Move into folder + Set-Location -Path "$($DocumentationSpecification.markdownAdoWikiConfig.adoWiki).wiki" + $branch = git branch + $branch = $branch.split(" ")[1] + # Copy main markdown file into wiki + Copy-Item -Path "../$OutputPath/$($DocumentationSpecification.fileNameStem).md" + # Configure dummy email and user (required) + git config user.email "epac-wiki@example.com" + git config user.name "EPAC Wiki" + # Add changes to commit + git add . + # Check if a folder exist that holds the sub pages + if (-not (Test-Path -Path "$($DocumentationSpecification.fileNameStem)")) { + # Create folder if does not exist + New-Item -Path "$($DocumentationSpecification.fileNameStem)" -ItemType Directory + } + # Copy all individual services markdown files + $services = Get-ChildItem -Path "../$OutputPathServices" + # Move into folder + Set-Location -Path "$($DocumentationSpecification.fileNameStem)" + # Remove files that currently exist in file to ensure fresh updates + Get-ChildItem -Path . -File | Remove-Item + # Copy over new individual services markdown files + foreach ($file in $services) { + Copy-Item $file . + } + # Commit and push up to Wiki + git add . + git commit -m "Update wiki with the latest markdown files" + git push origin "$branch" + Set-Location "../../" + } } diff --git a/Scripts/Helpers/RestMethods/Set-AzRoleAssignmentRestMethod.ps1 b/Scripts/Helpers/RestMethods/Set-AzRoleAssignmentRestMethod.ps1 index 39a1f848..1918e7da 100644 --- a/Scripts/Helpers/RestMethods/Set-AzRoleAssignmentRestMethod.ps1 +++ b/Scripts/Helpers/RestMethods/Set-AzRoleAssignmentRestMethod.ps1 @@ -46,5 +46,8 @@ function Set-AzRoleAssignmentRestMethod { $content = $response.Content Write-Warning "Error, continue deployment: $($statusCode) -- $($content)" } + if ($statusCode -eq 403 -and $response.content -match "does not have authorization to perform action") { + Write-Error "Error, Permissions Issue. Please review permissions for service principal at scope $($RoleAssignment.scope)" + } } } diff --git a/Scripts/Operations/Build-PolicyDocumentation.ps1 b/Scripts/Operations/Build-PolicyDocumentation.ps1 index 6ccc468d..de1a0af2 100644 --- a/Scripts/Operations/Build-PolicyDocumentation.ps1 +++ b/Scripts/Operations/Build-PolicyDocumentation.ps1 @@ -55,7 +55,10 @@ param ( [switch] $SuppressConfirmation, [Parameter(Mandatory = $false, HelpMessage = "Include Policies with effect Manual. Default: do not include Polcies with effect Manual.")] - [switch] $IncludeManualPolicies + [switch] $IncludeManualPolicies, + + [Parameter(Mandatory = $false, HelpMessage = "Include if using a PAT token for pushing to ADO Wiki.")] + [string] $WikiClonePat ) # Dot Source Helper Scripts @@ -452,6 +455,7 @@ foreach ($file in $files) { environmentCategories = $envCategoriesArray title = $documentAssignments.documentationSpecifications.title markdownAdoWiki = $documentAssignments.documentationSpecifications.markdownAdoWiki + markdownAdoWikiConfig = if ($null -ne $documentAssignments.documentationSpecifications.markdownAdoWikiConfig) { $documentAssignments.documentationSpecifications.markdownAdoWikiConfig }else { $null } } $documentAssignments.documentationSpecifications = $tempDocumentationSpecifications @@ -500,7 +504,8 @@ foreach ($file in $files) { -DocumentationSpecification $documentationSpecification ` -AssignmentsByEnvironment $assignmentsByEnvironment ` -IncludeManualPolicies:$IncludeManualPolicies ` - -PacEnvironments $pacEnvironments + -PacEnvironments $pacEnvironments ` + -WikiClonePat $WikiClonePat } } } diff --git a/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-allAssignments.jsonc b/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-allAssignments.jsonc index 69a4ef41..1f47ed89 100644 --- a/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-allAssignments.jsonc +++ b/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-allAssignments.jsonc @@ -28,7 +28,15 @@ "prod", "nonprod" ], - "title": "Contoso Policy effects" + "title": "Contoso Policy effects", + "markdownAdoWiki": true, + "markdownAdoWikiConfig": [ + { + "adoOrganization": "MyOrganization", + "adoProject": "EPAC", + "adoWiki": "EPAC" + } + ] } ] } diff --git a/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-environmentCategories.jsonc b/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-environmentCategories.jsonc index 44715e0b..9dfae608 100644 --- a/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-environmentCategories.jsonc +++ b/StarterKit/Definitions-GitHub-Flow/policyDocumentations/contoso-environmentCategories.jsonc @@ -44,7 +44,15 @@ "prod", "nonprod" ], - "title": "Contoso Policy effects" + "title": "Contoso Policy effects", + "markdownAdoWiki": true, + "markdownAdoWikiConfig": [ + { + "adoOrganization": "MyOrganization", + "adoProject": "EPAC", + "adoWiki": "EPAC" + } + ] } ] }, diff --git a/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-dev-pipeline-with-adowiki.yml b/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-dev-pipeline-with-adowiki.yml new file mode 100644 index 00000000..ac91e72e --- /dev/null +++ b/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-dev-pipeline-with-adowiki.yml @@ -0,0 +1,97 @@ +variables: + # This pipeline is used to deploy Policies, Initiative definitions and Assignments into Azure. + PAC_OUTPUT_FOLDER: ./Output + PAC_DEFINITIONS_FOLDER: ./Definitions + + # Use the plain text name of each service connection as a reference + planServiceConnection: "sc-epac-plan" + devServiceConnection: "sc-epac-dev" + + # set the environment selector + pacEnvironmentSelector: epac-dev + +# what to build trigger +trigger: + branches: + include: + - feature/* + paths: + include: + - Definitions + - Pipelines + +pr: none + +pool: + vmImage: "ubuntu-latest" + +stages: + - stage: Plan + displayName: "Plan ${{ variables.pacEnvironmentSelector }}" + jobs: + - job: Plan + steps: + - template: templates/plan.yml + parameters: + serviceConnection: $(planServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + + - stage: Deploy + displayName: "Deploy ${{ variables.pacEnvironmentSelector }}" + dependsOn: Plan + condition: and(not(failed()), not(canceled()), or(eq(dependencies.Plan.outputs['Plan.Plan.deployPolicyChanges'], 'yes'), eq(dependencies.Plan.outputs['Plan.Plan.deployRoleChanges'], 'yes'))) + variables: + PAC_INPUT_FOLDER: "$(Pipeline.Workspace)/plans-${{ variables.pacEnvironmentSelector }}" + localDeployPolicyChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployPolicyChanges']] + localDeployRoleChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployRoleChanges']] + jobs: + - deployment: DeployPolicy + displayName: "Deploy Policy Changes" + environment: PAC-DEV + condition: and(not(failed()), not(canceled()), eq(variables.localDeployPolicyChanges, 'yes')) + strategy: + runOnce: + deploy: + steps: + - template: templates/deploy-policy.yml + parameters: + serviceConnection: $(devServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + - deployment: DeployRoles + displayName: "Deploy Role Changes" + dependsOn: DeployPolicy + environment: PAC-DEV + condition: and(not(failed()), not(canceled()), eq(variables.localDeployRoleChanges, 'yes')) + strategy: + runOnce: + deploy: + steps: + - template: templates/deploy-roles.yml + parameters: + serviceConnection: $(devServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + + - stage: tenantPlan + displayName: "Plan tenant" + dependsOn: + - Deploy + condition: and(not(failed()), not(canceled())) + jobs: + - job: Plan + steps: + - template: templates/plan.yml + parameters: + serviceConnection: $(planServiceConnection) + pacEnvironmentSelector: tenant + + - stage: Documentation + displayName: "Document ${{ variables.pacEnvironmentSelector }}" + condition: always() + jobs: + - job: Documentation + displayName: "Create Documentation" + steps: + - template: templates/documentation.yaml + parameters: + serviceConnection: $(serviceConnectionPlan) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} \ No newline at end of file diff --git a/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-tenant-pipeline-with-adowiki.yml b/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-tenant-pipeline-with-adowiki.yml new file mode 100644 index 00000000..74bfe97b --- /dev/null +++ b/StarterKit/Pipelines/AzureDevOps/GitHub-Flow-With-AdoWiki/epac-tenant-pipeline-with-adowiki.yml @@ -0,0 +1,77 @@ +variables: + # This pipeline is used to deploy Policies, Initiative definitions and Assignments into Azure. + PAC_OUTPUT_FOLDER: ./Output + PAC_DEFINITIONS_FOLDER: ./Definitions + + # Use the plain text name of each service connection as a reference + planServiceConnection: "sc-epac-plan" + deployServiceConnection: "sc-epac-tenant-deploy" + rolesServiceConnection: "sc-epac-tenant-roles" + + # set the environment selector + pacEnvironmentSelector: tenant + +# what to build trigger +trigger: none +pr: none + +pool: + vmImage: "ubuntu-latest" + +stages: + - stage: Plan + displayName: "Plan ${{ variables.pacEnvironmentSelector }}" + jobs: + - job: Plan + steps: + - template: templates/plan.yml + parameters: + serviceConnection: $(planServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + + - stage: Deploy + displayName: "Deploy ${{ variables.pacEnvironmentSelector }}" + dependsOn: Plan + condition: and(not(failed()), not(canceled()), or(eq(dependencies.Plan.outputs['Plan.Plan.deployPolicyChanges'], 'yes'), eq(dependencies.Plan.outputs['Plan.Plan.deployRoleChanges'], 'yes')), contains(variables['Build.SourceBranch'], 'refs/heads/main')) + variables: + PAC_INPUT_FOLDER: "$(Pipeline.Workspace)/plans-${{ variables.pacEnvironmentSelector }}" + localDeployPolicyChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployPolicyChanges']] + localDeployRoleChanges: $[stageDependencies.Plan.Plan.outputs['Plan.deployRoleChanges']] + jobs: + - deployment: DeployPolicy + displayName: "Deploy Policy Changes" + environment: PAC-POLICY + condition: and(not(failed()), not(canceled()), eq(variables.localDeployPolicyChanges, 'yes')) + strategy: + runOnce: + deploy: + steps: + - template: templates/deploy-policy.yml + parameters: + serviceConnection: $(deployServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + - deployment: DeployRoles + displayName: "Deploy Role Changes" + dependsOn: DeployPolicy + environment: PAC-POLICY + condition: and(not(failed()), not(canceled()), eq(variables.localDeployRoleChanges, 'yes')) + strategy: + runOnce: + deploy: + steps: + - template: templates/deploy-roles.yml + parameters: + serviceConnection: $(rolesServiceConnection) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} + + - stage: Documentation + displayName: "Document ${{ variables.pacEnvironmentSelector }}" + condition: always() + jobs: + - job: Documentation + displayName: "Create Documentation" + steps: + - template: templates/documentation.yaml + parameters: + serviceConnection: $(serviceConnectionPlan) + pacEnvironmentSelector: ${{ variables.pacEnvironmentSelector }} \ No newline at end of file diff --git a/StarterKit/Pipelines/AzureDevOps/templates-ps1-module/documentation.yml b/StarterKit/Pipelines/AzureDevOps/templates-ps1-module/documentation.yml new file mode 100644 index 00000000..8000fd0a --- /dev/null +++ b/StarterKit/Pipelines/AzureDevOps/templates-ps1-module/documentation.yml @@ -0,0 +1,27 @@ +parameters: + - name: serviceConnection + type: string + - name: pacEnvironmentSelector + type: string + - name: definitionsRootFolder + type: string + default: Definitions + +steps: + - checkout: self + - pwsh: | + Install-Module EnterprisePolicyAsCode -Force + Install-Module Az.ResourceGraph -Force + - task: AzurePowerShell@5 + name: documentation + displayName: Build Documentation + inputs: + azureSubscription: ${{ parameters.serviceConnection }} + ScriptType: InlineScript + azurePowerShellVersion: LatestVersion + pwsh: true + # Only use WikiClonePat when automatic markdown to Azure DevOps Wiki + Inline: | + Build-PolicyDocumentation -WikiClonePat $(WikiClonePat) + - publish: "$(PAC_OUTPUT_FOLDER)" + artifact: "policy-documentation-${{ parameters.pacEnvironmentSelector }}" \ No newline at end of file diff --git a/StarterKit/Pipelines/AzureDevOps/templates-ps1-scripts/documentation.yml b/StarterKit/Pipelines/AzureDevOps/templates-ps1-scripts/documentation.yml new file mode 100644 index 00000000..cf5e83b5 --- /dev/null +++ b/StarterKit/Pipelines/AzureDevOps/templates-ps1-scripts/documentation.yml @@ -0,0 +1,26 @@ +parameters: + - name: serviceConnection + type: string + - name: pacEnvironmentSelector + type: string + - name: definitionsRootFolder + type: string + default: Definitions + +steps: + - checkout: self + - pwsh: | + Install-Module EnterprisePolicyAsCode -Force + Install-Module Az.ResourceGraph -Force + - task: AzurePowerShell@5 + name: documentation + displayName: Build Documentation + inputs: + azureSubscription: ${{ parameters.serviceConnection }} + ScriptType: InlineScript + azurePowerShellVersion: LatestVersion + pwsh: true + # Only use WikiClonePat when automatic markdown to Azure DevOps Wiki + ScriptPath: "Scripts/Deploy/Build-PolicyDocumentation.ps1" -WikiClonePat $(WikiClonePat) + - publish: "$(PAC_OUTPUT_FOLDER)" + artifact: "policy-documentation-${{ parameters.pacEnvironmentSelector }}" \ No newline at end of file