diff --git a/utilities/tools/REST2CARML/Add-ModuleFileStructure.ps1 b/utilities/tools/REST2CARML/Add-ModuleFileStructure.ps1 deleted file mode 100644 index 33badf94f7..0000000000 --- a/utilities/tools/REST2CARML/Add-ModuleFileStructure.ps1 +++ /dev/null @@ -1,49 +0,0 @@ - - -function Add-ModuleFileStructure{ - param( - [Parameter(Mandatory)] - [string] $ProviderNamespace, - - [Parameter(Mandatory)] - [string] $ResourceType, - - - - $filesArray=@('deploy.bicep', 'readme.md', 'version.json'), - - $gitHubWorkflowYAMLPath = "github/workflows/", - $azDevOpsModulePipelineYAMLPath = ".azureDevOps/modulePipelines/" - - ) - - try{ - $repoRootPath = (Get-Item $PSScriptRoot).Parent.Parent - $ModulePath = Join-path -path $repoRootPath 'modules' - - $ModuleName = "$ProviderNamespace\$ResourceType" - $ProviderDir = New-Item -Path $ModulePath -Name $ModuleName -ItemType "directory" - - write-Host $ProviderDir - - - if($filesArray){ - foreach($fileName in $filesArray){ - if($fileName){ - write-Host "Creating File: $fileName" - New-Item -Path $ProviderDir -Name $fileName -ItemType "file" - } - } - } - - $workflowYAMLPath = Join-path -path $repoRootPath $gitHubWorkflowYAMLPath - write-Host $workflowYAMLPath - - }catch{ - Write-Host "An error occurred:" - Write-Host $_ - } - -} - - Add-ModuleFileStructure diff --git a/utilities/tools/REST2CARML/Invoke-REST2CARML.ps1 b/utilities/tools/REST2CARML/Invoke-REST2CARML.ps1 index d7256dcfd9..4d6bf8e7c7 100644 --- a/utilities/tools/REST2CARML/Invoke-REST2CARML.ps1 +++ b/utilities/tools/REST2CARML/Invoke-REST2CARML.ps1 @@ -31,7 +31,8 @@ function Invoke-REST2CARML { Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) # Load used functions - . (Join-Path $PSScriptRoot 'Get-ModuleData.ps1') + # . (Join-Path $PSScriptRoot 'Get-ModuleData.ps1') + . (Join-Path $PSScriptRoot 'Set-ModuleFileStructure.ps1') Write-Verbose ('Processing module [{0}/{1}]' -f $ProviderNamespace, $ResourceType) -Verbose } @@ -41,7 +42,7 @@ function Invoke-REST2CARML { # $moduleData = Get-ModuleData -ProviderNamespace $ProviderNamespace -ResourceType $ResourceType if ($PSCmdlet.ShouldProcess(('Module [{0}/{1}] structure' -f $ProviderNamespace, $ResourceType), 'Create/Update')) { - # TODO: Invoke function to create intial module structure & create workflow/pipeline files + Set-ModuleFileStructure -ProviderNamespace $ProviderNamespace -ResourceType $ResourceType } if ($PSCmdlet.ShouldProcess(('Module [{0}/{1}] files' -f $ProviderNamespace, $ResourceType), 'Create/Update')) { @@ -53,3 +54,5 @@ function Invoke-REST2CARML { Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) } } + +Invoke-REST2CARML -ProviderNamespace 'Microsoft.Storage' -ResourceType 'storageAccounts' -Verbose diff --git a/utilities/tools/REST2CARML/Set-ModuleFileStructure.ps1 b/utilities/tools/REST2CARML/Set-ModuleFileStructure.ps1 new file mode 100644 index 0000000000..372283b84b --- /dev/null +++ b/utilities/tools/REST2CARML/Set-ModuleFileStructure.ps1 @@ -0,0 +1,207 @@ +#region Helper functions +<# +.SYNOPSIS +Replace tokens like '<>' in the given file with an actual value + +.DESCRIPTION +Replace tokens like '<>' in the given file with an actual value. Tokens that are replaced: +- <> +- <> +- <> +- <> +- <> +- <> + +.PARAMETER Content +Mandatory. The content to update + +.PARAMETER ProviderNamespace +Mandatory. The Provider Namespace to replaces tokens for + +.PARAMETER ResourceType +Mandatory. The Resource Type to replaces tokens for + +.EXAMPLE +Format-AutomationTemplate -Content "Hello <>-<>" -ProviderNamespace 'Microsoft.KeyVault' -ResourceType 'vaults' + +Update the provided content with different Provider Namespace & Resource Type token variant. Would return 'Hello keyvault-Vaults' +#> +function Format-AutomationTemplate { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Content, + + [Parameter(Mandatory)] + [string] $ProviderNamespace, + + [Parameter(Mandatory)] + [string] $ResourceType + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + $tokens = @{ + providerNamespace = $ProviderNamespace + shortProviderNamespacePascal = ($ProviderNamespace -split '\.')[-1].substring(0, 1).toupper() + ($ProviderNamespace -split '\.')[-1].substring(1) + shortProviderNamespaceLower = ($ProviderNamespace -split '\.')[-1].ToLower() + resourceType = $ResourceType + resourceTypePascal = $ResourceType.substring(0, 1).toupper() + $ResourceType.substring(1) + resourceTypeLower = $ResourceType.ToLower() + } + + foreach ($token in $tokens.Keys) { + $content = $content -replace "<<$token>>", $tokens[$token] + } + + return $content + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} +#endregion + +<# +.SYNOPSIS +Update / create the initial folder structure for a CARML module + +.DESCRIPTION +Update / create the initial folder structure for a CARML module + +.PARAMETER ProviderNamespace +Mandatory. The Provider Namespace to process + +.PARAMETER ResourceType +Mandatory. The Resource Type to process + +.EXAMPLE +Set-ModuleFileStructure -ProviderNamespace 'Microsoft.KeyVault' -ResourceType 'vaults' + +Update / create the folder & file structure for the CARML module 'Microsoft.KeyVault/vaults'. +#> +function Set-ModuleFileStructure { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string] $ProviderNamespace, + + [Parameter(Mandatory)] + [string] $ResourceType + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + $repoRootPath = (Get-Item $PSScriptRoot).Parent.Parent.Parent + } + + process { + # Create folders + # -------------- + $expectedModuleFolderPath = Join-Path $repoRootPath 'modules' $ProviderNamespace $ResourceType + @( + $expectedModuleFolderPath, + (Join-Path $expectedModuleFolderPath '.bicep'), + (Join-Path $expectedModuleFolderPath '.test') + (Join-Path $expectedModuleFolderPath '.test' 'common') + (Join-Path $expectedModuleFolderPath '.test' 'min') + ) | ForEach-Object { + if (-not (Test-Path $_)) { + if ($PSCmdlet.ShouldProcess(('Folder [{0}]' -f ($_ -replace ($repoRootPath -replace '\\', '\\'), '')), 'Create')) { + $null = New-Item -Path $_ -ItemType 'Directory' + } + } else { + Write-Verbose "Folder [$_] already exists." + } + } + + # Create module files + # ------------------- + ## Root files + ### Template file + $templateFilePath = Join-Path $expectedModuleFolderPath 'deploy.bicep' + if (-not (Test-Path $templateFilePath)) { + if ($PSCmdlet.ShouldProcess(('Template file [{0}]' -f ($templateFilePath -replace ($repoRootPath -replace '\\', '\\'), '')), 'Create')) { + $null = New-Item -Path $templateFilePath -ItemType 'File' + } + } else { + Write-Verbose ('Template file [{0}] already exists.' -f ($templateFilePath -replace ($repoRootPath -replace '\\', '\\'), '')) + } + + ### Version file + $versionFilePath = Join-Path $expectedModuleFolderPath 'version.json' + if (-not (Test-Path $versionFilePath)) { + if ($PSCmdlet.ShouldProcess(('Version file [{0}]' -f ($versionFilePath -replace ($repoRootPath -replace '\\', '\\'), '')), 'Create')) { + $versionFileContent = Get-Content (Join-Path $PSScriptRoot 'src' 'moduleVersion.json') -Raw + $null = New-Item -Path $versionFilePath -ItemType 'File' -Value $versionFileContent + } + } else { + Write-Verbose ('Version file [{0}] already exists.' -f ($versionFilePath -replace ($repoRootPath -replace '\\', '\\'), '')) + } + + ### ReadMe file + $readMeFilePath = Join-Path $expectedModuleFolderPath 'readme.md' + if (-not (Test-Path $readMeFilePath)) { + if ($PSCmdlet.ShouldProcess(('ReadMe file [{0}]' -f ($readMeFilePath -replace ($repoRootPath -replace '\\', '\\'), '')), 'Create')) { + $null = New-Item -Path $readMeFilePath -ItemType 'File' + } + } else { + Write-Verbose ('ReadMe file [{0}] already exists.' -f ($readMeFilePath -replace ($repoRootPath -replace '\\', '\\'), '')) + } + + ## .test files + @( + (Join-Path $expectedModuleFolderPath '.test' 'common' 'deploy.bicep') + (Join-Path $expectedModuleFolderPath '.test' 'min' 'deploy.bicep') + ) | ForEach-Object { + if (-not (Test-Path $_)) { + if ($PSCmdlet.ShouldProcess(('File [{0}]' -f ($_ -replace ($repoRootPath -replace '\\', '\\'), '')), 'Create')) { + $null = New-Item -Path $_ -ItemType 'File' + } + } else { + Write-Verbose "File [$_] already exists." + } + } + + # Create/Update DevOps files + # -------------------------- + ## GitHub + $automationFileName = ('ms.{0}.{1}.yml' -f ($ProviderNamespace -split '\.')[-1], $ResourceType).ToLower() + $gitHubWorkflowYAMLPath = Join-Path $repoRootPath '.github' 'workflows' $automationFileName + $workflowFileContent = Get-Content (Join-Path $PSScriptRoot 'src' 'gitHubWorkflowTemplateFile.yml') -Raw + $workflowFileContent = Format-AutomationTemplate -Content $workflowFileContent -ProviderNamespace $ProviderNamespace -ResourceType $ResourceType + if (-not (Test-Path $gitHubWorkflowYAMLPath)) { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Create')) { + $null = New-Item $gitHubWorkflowYAMLPath -ItemType 'File' -Value $workflowFileContent.TrimEnd() + } + } else { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Update')) { + $null = Set-Content -Path $gitHubWorkflowYAMLPath -Value $workflowFileContent.TrimEnd() + } + } + + ## Azure DevOps + $azureDevOpsPipelineYAMLPath = Join-Path $repoRootPath '.azuredevops' 'modulePipelines' $automationFileName + $pipelineFileContent = Get-Content (Join-Path $PSScriptRoot 'src' 'azureDevOpsPipelineTemplateFile.yml') -Raw + $pipelineFileContent = Format-AutomationTemplate -Content $pipelineFileContent -ProviderNamespace $ProviderNamespace -ResourceType $ResourceType + if (-not (Test-Path $azureDevOpsPipelineYAMLPath)) { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Create')) { + $null = New-Item $azureDevOpsPipelineYAMLPath -ItemType 'File' -Value $pipelineFileContent.TrimEnd() + } + } else { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Update')) { + $null = Set-Content -Path $azureDevOpsPipelineYAMLPath -Value $pipelineFileContent.TrimEnd() + } + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml b/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml new file mode 100644 index 0000000000..fca91b6c9c --- /dev/null +++ b/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml @@ -0,0 +1,40 @@ +name: '<> - <>' + +parameters: + - name: removeDeployment + displayName: Remove deployed module + type: boolean + default: true + - name: prerelease + displayName: Publish prerelease module + type: boolean + default: false + +pr: none + +trigger: + batch: true + branches: + include: + - main + paths: + include: + - '/.azuredevops/modulePipelines/ms.<>.<>.yml' + - '/.azuredevops/pipelineTemplates/*.yml' + - '/modules/<>/<>/*' + - '/utilities/pipelines/*' + exclude: + - '/utilities/pipelines/dependencies/*' + - '/**/*.md' + +variables: + - template: '../../settings.yml' + - group: 'PLATFORM_VARIABLES' + - name: modulePath + value: '/modules/<>/<>' + +stages: + - template: /.azuredevops/pipelineTemplates/stages.module.yml + parameters: + removeDeployment: '${{ parameters.removeDeployment }}' + prerelease: '${{ parameters.prerelease }}' diff --git a/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml b/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml new file mode 100644 index 0000000000..cad71a3ef1 --- /dev/null +++ b/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml @@ -0,0 +1,145 @@ +name: '<>: <>' + +on: + workflow_dispatch: + inputs: + removeDeployment: + type: boolean + description: 'Remove deployed module' + required: false + default: true + prerelease: + type: boolean + description: 'Publish prerelease module' + required: false + default: false + push: + branches: + - main + paths: + - '.github/actions/templates/**' + - '.github/workflows/ms.<>.<>.yml' + - 'modules/<>/<>/**' + - 'utilities/pipelines/**' + - '!utilities/pipelines/dependencies/**' + - '!*/**/readme.md' + +env: + variablesPath: 'settings.yml' + modulePath: 'modules/<>/<>' + workflowPath: '.github/workflows/ms.<>.<>.yml' + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + ARM_SUBSCRIPTION_ID: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + ARM_MGMTGROUP_ID: '${{ secrets.ARM_MGMTGROUP_ID }}' + ARM_TENANT_ID: '${{ secrets.ARM_TENANT_ID }}' + TOKEN_NAMEPREFIX: '${{ secrets.TOKEN_NAMEPREFIX }}' + +jobs: + ########################### + # Initialize pipeline # + ########################### + job_initialize_pipeline: + runs-on: ubuntu-20.04 + name: 'Initialize pipeline' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: 'Set input parameters to output variables' + id: get-workflow-param + uses: ./.github/actions/templates/getWorkflowInput + with: + workflowPath: '${{ env.workflowPath}}' + - name: 'Get parameter file paths' + id: get-module-test-file-paths + uses: ./.github/actions/templates/getModuleTestFiles + with: + modulePath: '${{ env.modulePath }}' + outputs: + removeDeployment: ${{ steps.get-workflow-param.outputs.removeDeployment }} + moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }} + + ######################### + # Static validation # + ######################### + job_module_pester_validation: + runs-on: ubuntu-20.04 + name: 'Static validation' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Run tests' + uses: ./.github/actions/templates/validateModulePester + with: + modulePath: '${{ env.modulePath }}' + moduleTestFilePath: '${{ env.moduleTestFilePath }}' + + ############################# + # Deployment validation # + ############################# + job_module_deploy_validation: + runs-on: ubuntu-20.04 + name: 'Deployment validation' + needs: + - job_initialize_pipeline + - job_module_pester_validation + strategy: + fail-fast: false + matrix: + moduleTestFilePaths: ${{ fromJSON(needs.job_initialize_pipeline.outputs.moduleTestFilePaths) }} + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Using test file [${{ matrix.moduleTestFilePaths }}]' + uses: ./.github/actions/templates/validateModuleDeployment + with: + templateFilePath: '${{ env.modulePath }}/${{ matrix.moduleTestFilePaths }}' + location: '${{ env.location }}' + resourceGroupName: '${{ env.resourceGroupName }}' + subscriptionId: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + managementGroupId: '${{ secrets.ARM_MGMTGROUP_ID }}' + removeDeployment: '${{ needs.job_initialize_pipeline.outputs.removeDeployment }}' + + ################## + # Publishing # + ################## + job_publish_module: + name: 'Publishing' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.event.inputs.prerelease == 'true' + runs-on: ubuntu-20.04 + needs: + - job_module_deploy_validation + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Publishing' + uses: ./.github/actions/templates/publishModule + with: + templateFilePath: '${{ env.modulePath }}/deploy.bicep' + templateSpecsRGName: '${{ env.templateSpecsRGName }}' + templateSpecsRGLocation: '${{ env.templateSpecsRGLocation }}' + templateSpecsDescription: '${{ env.templateSpecsDescription }}' + templateSpecsDoPublish: '${{ env.templateSpecsDoPublish }}' + bicepRegistryName: '${{ env.bicepRegistryName }}' + bicepRegistryRGName: '${{ env.bicepRegistryRGName }}' + bicepRegistryRgLocation: '${{ env.bicepRegistryRgLocation }}' + bicepRegistryDoPublish: '${{ env.bicepRegistryDoPublish }}' diff --git a/utilities/tools/REST2CARML/src/moduleVersion.json b/utilities/tools/REST2CARML/src/moduleVersion.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/utilities/tools/REST2CARML/src/moduleVersion.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +}