functions/Invoke-PpacD365AppInstall.ps1
|
<# .SYNOPSIS Installs or updates D365 applications in the environment. .DESCRIPTION Enables the user to install or update D365 applications in the environment as it would be done in Power Platform Admin Center (PPAC). Mimics the behavior of 'pac app-management install-application-package --unique-name': POST https://api.powerplatform.com/appmanagement/environments/{environmentId}/applicationPackages/{uniqueName}/install?api-version=2024-10-01 with body '{"payloadValue":"..."}' (or '{}' when no payload is needed). The same endpoint handles both fresh installs (state 'None') and updates of already installed apps. Returns one result object per requested app with the final operation status. .PARAMETER EnvironmentId The id of the environment that you want to work against .PARAMETER D365App The name, unique name or package name of the D365 application you want to install in the environment. Supports single or multiple application names. .PARAMETER PayloadValue Optional payload value sent as '{"payloadValue":"..."}' in the install request body. Required by some apps with a custom install experience (e.g. Finance and Operations Provisioning App). Omit for regular installs / updates. .PARAMETER PollIntervalSeconds Seconds to wait between each status poll against the operations endpoint. Defaults to 60 seconds. .PARAMETER TimeoutMinutes Maximum time to wait for all installations to reach a terminal state (Succeeded / Failed / Canceled) before giving up polling. Defaults to 120 minutes. Use 0 to wait indefinitely (not recommended for automation). When the timeout expires the cmdlet stops polling, warns, and returns the last known statuses so the caller can decide how to proceed. .EXAMPLE PS C:\> Invoke-PpacD365AppInstall -EnvironmentId "eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6" -D365App "Invoice Capture for Dynamics 365 Finance" This will install the "Invoice Capture for Dynamics 365 Finance" application in the environment. .EXAMPLE PS C:\> Invoke-PpacD365AppInstall -EnvironmentId "eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6" -D365App "msdyn_FnoInvoiceCaptureAnchor" This will install the "Invoice Capture for Dynamics 365 Finance" application in the environment. It will use the unique name of the application (msdyn_FnoInvoiceCaptureAnchor) to find the application and install it. .EXAMPLE PS C:\> Invoke-PpacD365AppInstall -EnvironmentId "eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6" -D365App "MicrosoftOperationsVEAnchor","msdyn_FnoInvoiceCaptureAnchor" This will install the "Microsoft Operations VE" and "Invoice Capture for Dynamics 365 Finance" applications in the environment. It will use the unique name of the D365 applications to find the applications and install them. It will start the installation of both applications, and then wait for the installation process to finish. .NOTES Author: Mötz Jensen (@Splaxi) #> function Invoke-PpacD365AppInstall { [CmdletBinding()] param ( [parameter (Mandatory = $true)] [string] $EnvironmentId, [parameter (Mandatory = $true)] [string[]] $D365App, [parameter (Mandatory = $false)] [string] $PayloadValue, [parameter (Mandatory = $false)] [ValidateRange(5, 3600)] [int] $PollIntervalSeconds = 60, [parameter (Mandatory = $false)] [ValidateRange(0, 4320)] [int] $TimeoutMinutes = 120 ) begin { # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment ` -EnvironmentId $EnvironmentId | ` Select-Object -First 1 if ($null -eq $envObj) { $messageString = "The supplied EnvironmentId: <c='em'>$EnvironmentId</c> didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the <c='em'>Get-BapEnvironment</c> cmdlet." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because environment found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if (Test-PSFFunctionInterrupt) { return } # First we will fetch ALL available apps for the environment $secureTokenPowerApi = (Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" -AsSecureString).Token $tokenPowerApiValue = ConvertFrom-SecureString -AsPlainText -SecureString $secureTokenPowerApi $headersPowerApi = @{ "Authorization" = "Bearer $($tokenPowerApiValue)" } $appsAvailable = Get-PpacD365App ` -EnvironmentId $EnvironmentId } process { if (Test-PSFFunctionInterrupt) { return } [System.Collections.Generic.List[System.Object]] $arrInstallStarted = @() [System.Collections.Generic.List[System.Object]] $arrStatus = @() [System.Collections.Generic.List[System.Object]] $arrFailedStarts = @() $headersPowerApi."Content-Type" = "application/json;charset=utf-8" foreach ($appName in $D365App) { $appToBeInstalled = $appsAvailable | ` Where-Object { $_.PpacD365AppId -eq $appName -or $_.PpacD365AppName -eq $appName -or $_.PpacPackageName -eq $appName } | ` Select-Object -First 1 if ($null -eq $appToBeInstalled) { $messageString = "The combination of the supplied EnvironmentId: <c='em'>$EnvironmentId</c> and PpacD365AppId: <c='em'>$appName</c> didn't return any matching D365App. Please verify that the EnvironmentId & PpacD365AppId is correct - try running the <c='em'>Get-BapEnvironmentD365App</c> cmdlet." Write-PSFMessage -Level Important -Message $messageString Stop-PSFFunction -Message "Stopping because environment and d365app combination was NOT found based on the supplied parameters." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) } if ($PSBoundParameters.ContainsKey('PayloadValue')) { $body = @{ payloadValue = $PayloadValue } | ConvertTo-Json -Compress } else { $body = '{}' } try { $localUri = "https://api.powerplatform.com/appmanagement/environments/{0}/applicationPackages/{1}/install?api-version=2024-10-01" ` -f $envObj.PpacEnvId ` , $appToBeInstalled.PpacPackageName $resIntall = Invoke-RestMethod ` -Method Post ` -Uri $localUri ` -Headers $headersPowerApi ` -Body $body $arrInstallStarted.Add($resIntall) } catch { $startError = $_.Exception.Message if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $startError = $_.ErrorDetails.Message } $appToBeInstalled | Add-Member -MemberType NoteProperty -Name '_InstallStartError' -Value $startError -Force $arrFailedStarts.Add($appToBeInstalled) } } if ($arrFailedStarts.Count -gt 0) { $messageString = "The following packages <c='em'>failed to start</c>:" Write-PSFMessage -Level Important -Message $messageString $arrFailedStarts.ToArray() | Format-Table if ($arrFailedStarts.Count -eq $D365App.Count) { Write-PSFMessage -Level Warning -Message "All package installations failed to start. Returning the failure details." } } $pollDeadline = if ($TimeoutMinutes -gt 0) { (Get-Date).AddMinutes($TimeoutMinutes) } else { [datetime]::MaxValue } $timedOut = $false $pendingCount = 0 $arrStatus = @() if ($arrInstallStarted.Count -gt 0) { do { Start-Sleep -Seconds $PollIntervalSeconds $secureTokenPowerApi = (Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" -AsSecureString).Token $tokenPowerApiValue = ConvertFrom-SecureString -AsPlainText -SecureString $secureTokenPowerApi $headersPowerApi = @{ "Authorization" = "Bearer $($tokenPowerApiValue)" } $arrStatus = @() foreach ($operation in $arrInstallStarted) { $localUri = "https://api.powerplatform.com/appmanagement/environments/{0}/operations/{1}?api-version=2024-10-01" ` -f $envObj.PpacEnvId ` , $operation.lastOperation.operationId $resInstallOperation = Invoke-RestMethod ` -Method Get ` -Uri $localUri ` -Headers $headersPowerApi 4> $null $arrStatus.Add($resInstallOperation) } $pendingCount = @($arrStatus | Where-Object { $_.status -in @('NotStarted', 'Running') }).Count Write-Progress -Id 1 ` -Activity "Installing D365 app(s) in '$($envObj.PpacEnvName)'" ` -Status "$pendingCount of $($arrStatus.Count) operation(s) still active (NotStarted / Running)" ` -PercentComplete $(if ($arrStatus.Count -gt 0) { (($arrStatus.Count - $pendingCount) / $arrStatus.Count) * 100 } else { 100 }) $arrStatus | Format-Table if ($pendingCount -gt 0 -and (Get-Date) -ge $pollDeadline) { $timedOut = $true break } } while ($pendingCount -gt 0) Write-Progress -Id 1 -Completed -Activity "Installing D365 app(s) in '$($envObj.PpacEnvName)'" } if ($timedOut) { $messageString = "Polling timed out after <c='em'>$TimeoutMinutes minute(s)</c> with <c='em'>$pendingCount operation(s)</c> still active. Returning the last known statuses." Write-PSFMessage -Level Warning -Message $messageString } foreach ($install in $arrInstallStarted) { $opId = $install.lastOperation.operationId $final = $arrStatus | Where-Object operationId -eq $opId | Select-Object -First 1 [PSCustomObject]@{ PpacD365AppName = $install.localizedName PpacPackageName = $install.packageUniqueName PackageVersion = $install.packageVersion Status = $final.status StatusMessage = $final.statusMessage ErrorName = $final.error.errorName ErrorMessage = $final.error.message OperationId = $opId TimedOut = $timedOut } } foreach ($failed in $arrFailedStarts) { [PSCustomObject]@{ PpacD365AppName = $failed.PpacD365AppName PpacPackageName = $failed.PpacPackageName PackageVersion = $failed.AvailableVersion Status = 'FailedToStart' StatusMessage = $failed._InstallStartError ErrorName = $null ErrorMessage = $failed._InstallStartError OperationId = $null TimedOut = $false } } } end { } } |