Start-OsDeploy.ps1
Function Start-OsDeploy { Param( [alias('env')] # [Parameter(Mandatory = $true)] # [validatepattern('\A\w\d\Z')] [string]$Environment, [alias('Dir', 'Path')] [string] $ProjectDirectory = (Get-Location | Select-Object -ExpandProperty Path).ToString(), [alias('TF')] [string] $TemplateFile, [string] $TemplateParametersFile, [alias('P')] [string] $Project, [string] $DeploymentName, [string] $StorageAccountName, [string] $ResourceGroupLocation = 'eastus2', [switch] $ValidateOnly, [validateset('All', 'None', 'RequestContent', 'ResponseContent')] [string] $DebugOptions = "None", [switch] $subscriptiondeploy, [switch] $NoUpload, [switch] $FullUpload, [alias('m')] [array] $Modules, [string] $prefix, [alias('rg')] [string] $ResourceGroup, [alias('odir', 'o')] [array] $AddOsirisFilePath, [switch] $WhatIf ) $ErrorActionPreference = 'Stop' # Set-StrictMode -Version 3 if (-not $project) { try { $envJson = Get-Content "$ProjectDirectory\global.parameters.json" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $project = $envJson.parameters.Global.value.AppName } catch { throw "No project name found! No input parameter or project global.parameters.json AppName found." } } if (-not $prefix) { $prefix = Switch -Regex ($ResourceGroupLocation) { 'eastus2' { 'SPZE2' } 'centralus' { 'SPZC1' } default { throw "Location $ResourceGroupLocation is either not supported or not found!" } } } if ($ResourceGroup) { $ResourceGroupName = $ResourceGroup $Environment=$ResourceGroup } else { $ResourceGroupName = ($prefix + '-' + $project + '-' + $Environment).toUpper() } Write-Output "Resource Group: $ResourceGroupName" -ForegroundColor Green # if ($ResourceGroupName -notmatch "^[A-Z][0-9]+-[A-Z][0-9]+-[A-Z][0-9]+$") { # throw "Resource Group name is not formatted correctly: $ResourceGroupName" # } # Run template and parameter json creation functions of Osiris. if (-not $TemplateParametersFile) { New-OsARMParameterTemplate -ProjectDirectory $ProjectDirectory -ResourceGroupLocation $ResourceGroupLocation -Environment $Environment -Modules $Modules $TemplateParametersFile = "$ProjectDirectory\temp\latest-template-deployment.json" } if (-not $TemplateFile) { New-OsARMDeploymentTemplate -ProjectDirectory $ProjectDirectory -ResourceGroupLocation $ResourceGroupLocation -Environment $Environment -Modules $Modules $TemplateFile = "$ProjectDirectory\temp\latest-module-deploy.json" } # if ($WhatIf) { # Write-Host "Template files generated successfully." -ForegroundColor Green # break # } # Check if resource group already exists, if not creates it. if (-not $subscriptiondeploy) { try { Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction Stop | Out-Null } catch { Write-Warning "No resource group found! Creating resource group: $ResourceGroupName" New-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction Stop } } # If no storage account is provided as a parameter then either use the default one for the environment or create one for the environment if not. $StorageContainerName = ($ResourceGroupName + "-staging-$env:USERNAME").ToLower() if (-not $StorageAccountName) { #This ugly regex replace trims the $StorageAccountName to 24 characters or less as required by Azure storage accounts. $StorageAccountName = ($ResourceGroupName + 'staging').ToLower().Replace("-", "") -Replace '(?<=^.{24}).*' Write-Verbose "Storage Account: $StorageAccountName" Write-Verbose "Storage Container: $StorageContainerName" $StorageAccount = Get-AzStorageAccount | Where-Object { $_.StorageAccountName -eq $StorageAccountName } if (-not $StorageAccount) { Write-Warning "No storage account found! Creating a storage account and running a full initial upload of modules to the container. This can take a while!" New-AzStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $ResourceGroupName -Location $ResourceGroupLocation -ErrorAction Stop $StorageAccount = Get-AzStorageAccount | Where-Object { $_.StorageAccountName -eq $StorageAccountName } -ErrorAction Stop $FullUpload = $true } } else { Write-Verbose "Storage Account: $StorageAccountName" Write-Verbose "Storage Container: $StorageContainerName" $StorageAccount = Get-AzStorageAccount | Where-Object { $_.StorageAccountName -eq $StorageAccountName } $FullUpload = $true } if (-not $DeploymentName) { $DeploymentName = (($ResourceGroupName) + '-' + $env:UserName.ToUpper() + '-' + ((Get-Date)).ToString('yyyy-MM-dd-HHmm')) } # Setting up ARM template deployment parameters to pass in with PowerShell. $TemplateParameters = @{ } $TemplateParameters['EnvName'] = $ResourceGroupName $TemplateParameters.Add('TemplateFile', $TemplateFile) $TemplateParameters.Add('TemplateParameterFile', $TemplateParametersFile) Write-Host "Template File: $TemplateFile" -ForegroundColor Green Write-Host "Parameters File: $TemplateParametersFile" -ForegroundColor Green # Generate a 4 hour SAS token for the artifacts location. $TemplateParameters['_artifactsLocationSasToken'] = New-AzStorageContainerSASToken -Container $StorageContainerName -Permission r -Context $StorageAccount.Context -ExpiryTime (Get-Date).AddHours(4) $TemplateParameters['_artifactsLocationSasToken'] = ConvertTo-SecureString $TemplateParameters['_artifactsLocationSasToken'] -AsPlainText -Force $TemplateParameters['_artifactsLocation'] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName if (-not $ValidateOnly) { $TemplateParameters.Add('DeploymentDebugLogLevel', $DebugOptions) } # Deployment script provided template parameters to define a specific project and environment. $TemplateFileJson = Get-Content $TemplateFile -Raw | ConvertFrom-Json $TemplateParamJson = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json $TemplateFileJsonList = $TemplateFileJson.parameters | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name $TemplateParamJsonList = $TemplateParamJson.parameters | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name $FinalParamList = $TemplateFileJsonList | Where-Object { $TemplateParamJsonList -notcontains $_ } Switch -Regex ($FinalParamList) { 'Prefix' { $TemplateParameters['Prefix'] = $Prefix } 'Environment' { $TemplateParameters['Environment'] = $Environment.substring(0, 1) } 'DeploymentID' { $TemplateParameters['DeploymentID'] = $Environment.substring(1, 1) } } Write-Verbose "All Params: $FinalParamList" # Remove parameters for modules that are empty to get rid of clutter. # ForEach ($param in $TemplateParamJsonList) { # if ($TemplateParamJson.parameters.$param.value.GetType().Name -eq "PSCustomObject") { # if (($TemplateParamJson.parameters.$param.value | Get-Member -MemberType NoteProperty).Count -eq 0) { # $TemplateParamJson.parameters = $TemplateParamJson.parameters | Select-Object -Exclude $param # $TemplateFileJson.parameters = $TemplateFileJson.parameters | Select-Object -Exclude $param # } # } # } $TemplateFileJson | ConvertTo-Json -Depth 100 | Out-File $TemplateFile $TemplateParamJson | ConvertTo-Json -Depth 100 | Out-File $TemplateParametersFile # Convert relative paths to absolute paths if needed. $ProjectDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ProjectDirectory)) # Try to upload only the bare minimum files needed for the deployment. if (-not $NoUpload) { $EnvHashesFileName = "$project-$Environment-$ResourceGroupLocation-$env:UserName" if (-not (Test-Path -Path "$ProjectDirectory\temp")) { New-Item -Path "$ProjectDirectory\temp" -ItemType Directory | Out-Null } if (-not (Test-Path -Path "$ProjectDirectory\temp\filehashes")) { New-Item -Path "$ProjectDirectory\temp\filehashes" -ItemType Directory | Out-Null } # It is better to use a try catch block since this is a single API call to Azure. # Compare this to an if statement with a Get storage API call to first check if storage account exists then an Add API call to add the container. try { New-AzStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction Stop Write-Warning "Created new storage container $StorageContainerName" } catch { Write-Verbose "Using existing storage container $StorageContainerName" } # Setup metadata JSON object to verify MD5 file hashes from the last upload depending on if there is an existing file or not. try { $EnvMetadataJson = Get-Content "$ProjectDirectory\temp\filehashes\$EnvHashesFileName.json" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-Verbose "No existing file hashes cache found!" Write-Verbose "Creating new hashes cache file at: $ProjectDirectory\temp\filehashes\$EnvHashesFileName.json" Write-Verbose "Running a full module upload to build initial file hash cache." $EnvMetadataJson = New-Object -TypeName PSCustomObject $EnvMetadataJson | Add-Member -MemberType NoteProperty -Name "filehashes" -Value (New-Object -TypeName PSCustomObject) $FullUpload = $true } if ($FullUpload) { Get-ChildItem "$ProjectDirectory\modules" | ForEach-Object { $ModName = $_.Name Get-ChildItem -File -Recurse $_.FullName | Where-Object { $_.Name -notmatch 'readme|example|metadata' } | ForEach-Object { Write-Output "Uploading: $_" Set-AzStorageBlobContent -Container $StorageContainerName -Context $StorageAccount.Context -File $_.FullName -Blob "modules/$ModName/$($_.Name)" -Force | Out-Null $ModFileHash = Get-FileHash -Algorithm MD5 -Path $_.FullName $EnvMetadataJson.filehashes | Add-Member -MemberType NoteProperty -Name $_.FullName -Value $ModFileHash.Hash -Force } } } else { $script:AllModulesUsedList | ForEach-Object { $ModName = $_ Write-Verbose "ModName: $ModName" Get-ChildItem -File -Recurse "$ProjectDirectory\modules" | Where-Object { $_.Directory -match $ModName -and $_.Name -notmatch 'readme|example' } | ForEach-Object { $ModFilePath = $_ # Get the MD5 hashes of all files to be uploaded to check if they need to be uploaded to the storage container. $ModFileHash = Get-FileHash -Algorithm MD5 -Path $ModFilePath.FullName if ($EnvMetadataJson.filehashes | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -eq $ModFilePath.FullName } -ErrorAction SilentlyContinue) { $ModFileHashUpdated = $EnvMetadataJson.filehashes.($ModFilePath.FullName) -eq $ModFileHash.Hash } else { $ModFileHashUpdated = $false } if (-not $ModFileHashUpdated) { Write-Output "Uploading: $ModFilePath" Set-AzStorageBlobContent -Container $StorageContainerName -Context $StorageAccount.Context -File $_.FullName -Blob "modules/$ModName/$($_.Name)" -Force | Out-Null $EnvMetadataJson.filehashes | Add-Member -MemberType NoteProperty -Name $ModFilePath.FullName -Value $ModFileHash.Hash -Force } else { Write-Verbose "Skipping: $ModFilePath" } } } } $EnvMetadataJson | ConvertTo-Json -Depth 100 | Out-File "$ProjectDirectory\temp\filehashes\$EnvHashesFileName.json" } if ($ValidateOnly) { Test-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName @TemplateParameters -Verbose } else { if (-not $subscriptiondeploy) { if ($WhatIf) { New-AzResourceGroupDeployment -Name $DeploymentName ` -ResourceGroupName $ResourceGroupName ` @TemplateParameters -Force -Verbose -ErrorVariable ErrorMessages -WhatIf } else { New-AzResourceGroupDeployment -Name $DeploymentName ` -ResourceGroupName $ResourceGroupName ` @TemplateParameters -Force -Verbose -ErrorVariable ErrorMessages } } else { New-AzDeployment @TemplateParameters -Name $DeploymentName -Location $ResourceGroupLocation -Verbose -ErrorVariable ErrorMessages } if ($ErrorMessages) { Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) } } } New-Alias -Name OsirisDeploy -Value Start-OsDeploy -Force # This allows the -Modules or -m parameter to dynamically offer tab completition for module and module names. Register-ArgumentCompleter -CommandName Start-OsDeploy -ParameterName Modules -ScriptBlock { param($commandName, $parameterName, $wordToComplete) $ModulesLocation = (Get-Location | Select-Object -ExpandProperty Path).ToString() + '\modules' $foldernames = @() $foldernames += Get-ChildItem $ModulesLocation -Directory | Select-Object -ExpandProperty Name $foldernames | Select-Object -Unique | Sort-Object | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { "$_" } } Register-ArgumentCompleter -CommandName Start-OsDeploy -ParameterName ResourceGroupLocation -ScriptBlock { param($commandName, $parameterName, $wordToComplete) if (-not (Test-Path "$env:TEMP\Osiris\AzLocations.json") -or -not (Get-Item "$env:TEMP\AzLocations.json" | Select-Object -ExpandProperty LastWriteTime ) -gt ((Get-Date).AddDays(-30))) { New-Item -ItemType Directory "$env:TEMP\Osiris" -ErrorAction SilentlyContinue $AzLocations = Get-AzLocation | Select-Object -ExpandProperty Location | Where-Object { $_ -like "*us" } | Sort-Object $AzLocations | ConvertTo-Json -Depth 100 | Out-File "$env:TEMP\AzLocations.json" -Force } $AzLocations = Get-Content "$env:TEMP\Osiris\AzLocations.json" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $AzLocations | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { "$_" } } |