From 900a5617120d14645a90ef26cbcb7fb737f8cf61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:08:08 +0100 Subject: [PATCH 1/7] Feature: Initial cmdlets --- .gitattributes | 2 + .github/FUNDING.yml | 12 + .github/workflows/build.yml | 24 + .github/workflows/validate.yml | 15 + .gitignore | 22 + LICENSE | 6 +- README.md | 113 +- azure-pipelines.yml | 20 + build/vsts-build.ps1 | 131 + build/vsts-prerequisites.ps1 | 25 + build/vsts-validate.ps1 | 7 + d365bap.tools/bin/readme.md | 7 + d365bap.tools/changelog.md | 5 + d365bap.tools/d365bap.tools.psd1 | 98 + d365bap.tools/d365bap.tools.psm1 | 85 + .../en-us/about_d365bap.tools.help.txt | 11 + d365bap.tools/en-us/strings.psd1 | 5 + .../Compare-BapEnvironmentD365App.ps1 | 143 + .../functions/Compare-BapEnvironmentUser.ps1 | 155 ++ .../functions/Get-BapEnvironment.ps1 | 117 + .../Get-BapEnvironmentApplicationUser.ps1 | 88 + .../functions/Get-BapEnvironmentD365App.ps1 | 228 ++ .../functions/Get-BapEnvironmentDetails.ps1 | 77 + .../functions/Get-BapEnvironmentUser.ps1 | 120 + .../Invoke-BapEnvironmentInstallD365App.ps1 | 157 ++ d365bap.tools/functions/readme.md | 7 + .../internal/configurations/configuration.ps1 | 15 + .../internal/configurations/readme.md | 14 + .../functions/Get-EnvironmentLanguage.ps1 | 47 + d365bap.tools/internal/functions/readme.md | 7 + .../internal/scriptblocks/scriptblocks.ps1 | 12 + d365bap.tools/internal/scripts/license.ps1 | 21 + d365bap.tools/internal/scripts/postimport.ps1 | 26 + d365bap.tools/internal/scripts/preimport.ps1 | 14 + d365bap.tools/internal/scripts/strings.ps1 | 8 + d365bap.tools/internal/tepp/assignment.ps1 | 4 + d365bap.tools/internal/tepp/example.tepp.ps1 | 4 + d365bap.tools/internal/tepp/readme.md | 23 + d365bap.tools/readme.md | 17 + .../Compare-BapEnvironmentD365App.Tests.ps1 | 88 + .../Compare-BapEnvironmentUser.Tests.ps1 | 88 + .../functions/Get-BapEnvironment.Tests.ps1 | 49 + ...et-BapEnvironmentApplicationUser.Tests.ps1 | 49 + .../Get-BapEnvironmentD365App.Tests.ps1 | 101 + .../Get-BapEnvironmentDetails.Tests.ps1 | 49 + .../Get-BapEnvironmentUser.Tests.ps1 | 62 + ...oke-BapEnvironmentInstallD365App.Tests.ps1 | 49 + d365bap.tools/tests/functions/readme.md | 7 + .../general/FileIntegrity.Exceptions.ps1 | 51 + .../tests/general/FileIntegrity.Tests.ps1 | 95 + .../tests/general/Help.Exceptions.ps1 | 26 + d365bap.tools/tests/general/Help.Tests.ps1 | 147 + .../tests/general/Manifest.Tests.ps1 | 62 + .../tests/general/PSScriptAnalyzer.Tests.ps1 | 50 + .../tests/general/strings.Exceptions.ps1 | 36 + d365bap.tools/tests/general/strings.Tests.ps1 | 27 + d365bap.tools/tests/pester.ps1 | 113 + d365bap.tools/tests/readme.md | 31 + d365bap.tools/xml/d365bap.tools.Format.ps1xml | 313 +++ d365bap.tools/xml/d365bap.tools.Types.ps1xml | 37 + d365bap.tools/xml/readme.md | 43 + docs/Compare-BapEnvironmentD365App.md | 153 ++ docs/Compare-BapEnvironmentUser.md | 117 + docs/Get-BapEnvironment.md | 97 + docs/Get-BapEnvironmentApplicationUser.md | 87 + docs/Get-BapEnvironmentD365App.md | 243 ++ docs/Get-BapEnvironmentDetails.md | 71 + docs/Get-BapEnvironmentUser.md | 122 + docs/Invoke-BapEnvironmentInstallD365App.md | 131 + install.ps1 | 2431 +++++++++++++++++ library/d365bap.tools/d365bap.tools.sln | 25 + library/d365bap.tools/d365bap.tools/Class1.cs | 8 + .../d365bap.tools/d365bap.tools.csproj | 21 + 73 files changed, 6966 insertions(+), 5 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 azure-pipelines.yml create mode 100644 build/vsts-build.ps1 create mode 100644 build/vsts-prerequisites.ps1 create mode 100644 build/vsts-validate.ps1 create mode 100644 d365bap.tools/bin/readme.md create mode 100644 d365bap.tools/changelog.md create mode 100644 d365bap.tools/d365bap.tools.psd1 create mode 100644 d365bap.tools/d365bap.tools.psm1 create mode 100644 d365bap.tools/en-us/about_d365bap.tools.help.txt create mode 100644 d365bap.tools/en-us/strings.psd1 create mode 100644 d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 create mode 100644 d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 create mode 100644 d365bap.tools/functions/Get-BapEnvironment.ps1 create mode 100644 d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 create mode 100644 d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 create mode 100644 d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 create mode 100644 d365bap.tools/functions/Get-BapEnvironmentUser.ps1 create mode 100644 d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 create mode 100644 d365bap.tools/functions/readme.md create mode 100644 d365bap.tools/internal/configurations/configuration.ps1 create mode 100644 d365bap.tools/internal/configurations/readme.md create mode 100644 d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 create mode 100644 d365bap.tools/internal/functions/readme.md create mode 100644 d365bap.tools/internal/scriptblocks/scriptblocks.ps1 create mode 100644 d365bap.tools/internal/scripts/license.ps1 create mode 100644 d365bap.tools/internal/scripts/postimport.ps1 create mode 100644 d365bap.tools/internal/scripts/preimport.ps1 create mode 100644 d365bap.tools/internal/scripts/strings.ps1 create mode 100644 d365bap.tools/internal/tepp/assignment.ps1 create mode 100644 d365bap.tools/internal/tepp/example.tepp.ps1 create mode 100644 d365bap.tools/internal/tepp/readme.md create mode 100644 d365bap.tools/readme.md create mode 100644 d365bap.tools/tests/functions/Compare-BapEnvironmentD365App.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Compare-BapEnvironmentUser.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Get-BapEnvironment.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentApplicationUser.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentD365App.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentUser.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/Invoke-BapEnvironmentInstallD365App.Tests.ps1 create mode 100644 d365bap.tools/tests/functions/readme.md create mode 100644 d365bap.tools/tests/general/FileIntegrity.Exceptions.ps1 create mode 100644 d365bap.tools/tests/general/FileIntegrity.Tests.ps1 create mode 100644 d365bap.tools/tests/general/Help.Exceptions.ps1 create mode 100644 d365bap.tools/tests/general/Help.Tests.ps1 create mode 100644 d365bap.tools/tests/general/Manifest.Tests.ps1 create mode 100644 d365bap.tools/tests/general/PSScriptAnalyzer.Tests.ps1 create mode 100644 d365bap.tools/tests/general/strings.Exceptions.ps1 create mode 100644 d365bap.tools/tests/general/strings.Tests.ps1 create mode 100644 d365bap.tools/tests/pester.ps1 create mode 100644 d365bap.tools/tests/readme.md create mode 100644 d365bap.tools/xml/d365bap.tools.Format.ps1xml create mode 100644 d365bap.tools/xml/d365bap.tools.Types.ps1xml create mode 100644 d365bap.tools/xml/readme.md create mode 100644 docs/Compare-BapEnvironmentD365App.md create mode 100644 docs/Compare-BapEnvironmentUser.md create mode 100644 docs/Get-BapEnvironment.md create mode 100644 docs/Get-BapEnvironmentApplicationUser.md create mode 100644 docs/Get-BapEnvironmentD365App.md create mode 100644 docs/Get-BapEnvironmentDetails.md create mode 100644 docs/Get-BapEnvironmentUser.md create mode 100644 docs/Invoke-BapEnvironmentInstallD365App.md create mode 100644 install.ps1 create mode 100644 library/d365bap.tools/d365bap.tools.sln create mode 100644 library/d365bap.tools/d365bap.tools/Class1.cs create mode 100644 library/d365bap.tools/d365bap.tools/d365bap.tools.csproj diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a5df2c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5067a11 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c0418fb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +on: + push: + branches: + - master + - main + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\vsts-prerequisites.ps1 + shell: powershell + - name: Validate + run: .\build\vsts-validate.ps1 + shell: powershell + - name: Build + run: .\build\vsts-build.ps1 -ApiKey $env:APIKEY + shell: powershell + env: + APIKEY: ${{ secrets.ApiKey }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..5955876 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,15 @@ +on: [pull_request] + +jobs: + validate: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\vsts-prerequisites.ps1 + shell: powershell + - name: Validate + run: .\build\vsts-validate.ps1 + shell: powershell diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8942f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ + +# ignore the settings folder and files for VSCode and PSS +.vscode/* +*.psproj +*TempPoint* + +# Ignore staging info from Visual Studio +library/d365bap.tools/.vs/* +library/d365bap.tools/d365bap.tools/bin/* +library/d365bap.tools/d365bap.tools/obj/* + +# ignore PowerShell Studio MetaData +d365bap.tools/d365bap.tools.psproj +d365bap.tools/d365bap.tools.psproj.bak +d365bap.tools/d365bap.tools.psprojs +d365bap.tools/d365bap.tools.psproj + +# ignore the TestResults +TestResults/* + +# ignore the publishing Directory +publish/* \ No newline at end of file diff --git a/LICENSE b/LICENSE index c652a2d..0b69cf2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2024 d365collaborative +Copyright (c) 2024 MötzJensen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 0acc7c1..949a0d4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,111 @@ -# d365bap.tools -Tools used for Business Application Platform, One Dynamics One Platform - D365FO + Dataverse +# **d365bap.tools** + +A PowerShell module to handle different management tasks related to Business Application Platform, which consist of Microsoft Dynamics 365 Finance & Operations (D365FO) and Dataverse (PowerPlatform) + +Read more about D365FO on [learn.microsoft.com](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/fin-ops/). + +Read more about Dataverse on [learn.microsoft.com](https://learn.microsoft.com/en-us/power-apps/maker/data-platform/). + +Available on PowerShell Gallery: +[d365bap.tools](https://www.powershellgallery.com/packages/d365bap.tools). + +## Table of contents +* [Getting started](#getting-started) +* [Getting help](#getting-help) +* [Contributing](#contributing) +* [Dependencies](#dependencies) + +## Getting started +### Install the latest module +```PowerShell +Install-Module -Name d365bap.tools +``` + +### Install without administrator privileges +```PowerShell +Install-Module -Name d365bap.tools -Scope CurrentUser +``` +### List all available commands / functions + +```PowerShell +Get-Command -Module d365bap.tools +``` + +### Update the module + +```PowerShell +Update-Module -name d365bap.tools +``` + +### Update the module - force + +```PowerShell +Update-Module -name d365bap.tools -Force +``` + +## Preparing the first execution +The module is leveraging the Az.Account module, to obtain all the needed OAuth 2.0 tokens, for the different REST API's spread across the BAP (Business Application Platform) / ODOP (One Dynamics One Platform). + +So you will need to sign in with an user account with enough permissions / privileges for the different endpoints. This **must be done prior** running any command from the d365bap.tools module. + +``` +Login-AzAccount -TenantId abd... +``` + +Depending on which PowerShell console (v5 / v7+) - you will have different sign-in experiences. There are different ways to utilized the Web Browser experience (Device Authentication), which allows for saved credentials to be utilized while authenticating. + +``` +Login-AzAccount -UseDeviceAuthentication -TenantId abd... +``` + +## Getting help + +[The wiki](https://github.com/d365collaborative/d365bap.tools/wiki) contains more details about installation and also guides to help you with some common tasks. It also contains documentation for all the module's commands. Expand the wiki's `Pages` control at the top of the content sidebar to view and search the list of command documentation pages. + +Another way to learn about the different cmdlets available is to install the tools onto your machine. +You can also visit the **'docs'** folder in this repository (look at the top). Click this link [**docs**](https://github.com/d365collaborative/d365bap.tools/tree/master/docs) to jump straight inside. + +Since the project started we have adopted and extended the comment based help inside each cmdlet / function. This means that every single command contains at least one fully working example on how to run it and what to expect from the cmdlet. + +**Getting help inside the PowerShell console** + +Getting help is as easy as writing **Get-Help CommandName** + +```PowerShell +Get-Help Get-BapEnvironment +``` + +*This will display the available default help.* + +Getting the entire help is as easy as writing **Get-Help CommandName -Full** + +```PowerShell +Get-Help Get-BapEnvironment -Full +``` + +*This will display all available help content there is for the cmdlet / function* + +Getting all the available examples for a given command is as easy as writing **Get-Help CommandName -Examples** + +```PowerShell +Get-Help Get-BapEnvironment -Examples +``` + +*This will display all the available **examples** for the cmdlet / function.* + +We know that when you are learning about new stuff and just want to share your findings with your peers, working with help inside a PowerShell session isn't that great. + +### Web based help and examples +We have implemented **platyPS** (https://github.com/PowerShell/platyPS) to generate markdown files for each cmdlet / function available in the module. These files are hosted here on github for you to consume in your web browser and the give you the look and feel of other documentation sites. + +The generated help markdown files are located inside the **'docs'** folder in this repository. Click this [link](https://github.com/d365collaborative/d365bap.tools/tree/master/docs) to jump straight inside. + +They are also available in the [wiki](https://github.com/d365collaborative/d365bap.tools/wiki) in the list of pages. + +## Contributing + +Want to contribute to the project? We'd love to have you! Visit our [contributing.md](https://github.com/d365collaborative/d365bap.tools/blob/master/contributing.md) for a jump start. + +## Dependencies + +This module depends on other modules. The dependencies are documented in the [dependency graph](https://github.com/d365collaborative/d365bap.tools/network/dependencies) and the Dependencies section of the Package Details of the [package listing](https://www.powershellgallery.com/packages/d365bap.tools) in the PowerShell Gallery. \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..c5fbe3a --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,20 @@ +pool: + vmImage: "windows-latest" +steps: +- task: PowerShell@2 + displayName: Prerequisites + inputs: + targetType: filePath + filePath: './build/vsts-prerequisites.ps1' + +- task: PowerShell@2 + displayName: Validate + inputs: + targetType: filePath + filePath: './build/vsts-validate.ps1' + +- task: PublishTestResults@2 + displayName: 'Publish Test Results **/TEST-*.xml' + inputs: + testResultsFormat: NUnit + condition: always() diff --git a/build/vsts-build.ps1 b/build/vsts-build.ps1 new file mode 100644 index 0000000..ada359c --- /dev/null +++ b/build/vsts-build.ps1 @@ -0,0 +1,131 @@ +<# +This script publishes the module to the gallery. +It expects as input an ApiKey authorized to publish the module. + +Insert any build steps you may need to take before publishing it here. +#> +param ( + $ApiKey, + + $WorkingDirectory, + + $Repository = 'PSGallery', + + [switch] + $LocalRepo, + + [switch] + $SkipPublish, + + [switch] + $AutoVersion, + + [switch] + $Build +) + +#region Handle Working Directory Defaults +if (-not $WorkingDirectory) +{ + if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) + { + $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS + } + else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } +} +if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } +#endregion Handle Working Directory Defaults + +# Build Library +if ($Build) { + dotnet build "$WorkingDirectory\library\d365bap.tools.sln" + if ($LASTEXITCODE -ne 0) { + throw "Failed to build d365bap.tools.dll!" + } +} + +# Prepare publish folder +Write-PSFMessage -Level Important -Message "Creating and populating publishing directory" +$publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force +Copy-Item -Path "$($WorkingDirectory)\d365bap.tools" -Destination $publishDir.FullName -Recurse -Force + +#region Gather text data to compile +$text = @() +$processed = @() + +# Gather Stuff to run before +foreach ($filePath in (& "$($PSScriptRoot)\..\d365bap.tools\internal\scripts\preimport.ps1")) +{ + if ([string]::IsNullOrWhiteSpace($filePath)) { continue } + + $item = Get-Item $filePath + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName +} + +# Gather commands +Get-ChildItem -Path "$($publishDir.FullName)\d365bap.tools\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} +Get-ChildItem -Path "$($publishDir.FullName)\d365bap.tools\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} + +# Gather stuff to run afterwards +foreach ($filePath in (& "$($PSScriptRoot)\..\d365bap.tools\internal\scripts\postimport.ps1")) +{ + if ([string]::IsNullOrWhiteSpace($filePath)) { continue } + + $item = Get-Item $filePath + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName +} +#endregion Gather text data to compile + +#region Update the psm1 file +$fileData = Get-Content -Path "$($publishDir.FullName)\d365bap.tools\d365bap.tools.psm1" -Raw +$fileData = $fileData.Replace('""', '""') +$fileData = $fileData.Replace('""', ($text -join "`n`n")) +[System.IO.File]::WriteAllText("$($publishDir.FullName)\d365bap.tools\d365bap.tools.psm1", $fileData, [System.Text.Encoding]::UTF8) +#endregion Update the psm1 file + +#region Updating the Module Version +if ($AutoVersion) +{ + Write-PSFMessage -Level Important -Message "Updating module version numbers." + try { [version]$remoteVersion = (Find-Module 'd365bap.tools' -Repository $Repository -ErrorAction Stop).Version } + catch + { + Stop-PSFFunction -Message "Failed to access $($Repository)" -EnableException $true -ErrorRecord $_ + } + if (-not $remoteVersion) + { + Stop-PSFFunction -Message "Couldn't find d365bap.tools on repository $($Repository)" -EnableException $true + } + $newBuildNumber = $remoteVersion.Build + 1 + [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\d365bap.tools\d365bap.tools.psd1").ModuleVersion + Update-ModuleManifest -Path "$($publishDir.FullName)\d365bap.tools\d365bap.tools.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" +} +#endregion Updating the Module Version + +#region Publish +if ($SkipPublish) { return } +if ($LocalRepo) +{ + # Dependencies must go first + Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" + New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . + Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: d365bap.tools" + New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\d365bap.tools" -PackagePath . +} +else +{ + # Publish to Gallery + Write-PSFMessage -Level Important -Message "Publishing the d365bap.tools module to $($Repository)" + Publish-Module -Path "$($publishDir.FullName)\d365bap.tools" -NuGetApiKey $ApiKey -Force -Repository $Repository +} +#endregion Publish \ No newline at end of file diff --git a/build/vsts-prerequisites.ps1 b/build/vsts-prerequisites.ps1 new file mode 100644 index 0000000..64a8589 --- /dev/null +++ b/build/vsts-prerequisites.ps1 @@ -0,0 +1,25 @@ +param ( + [string] + $Repository = 'PSGallery' +) + +$modules = @("Pester", "PSFramework", "PSModuleDevelopment", "PSScriptAnalyzer") + +# Automatically add missing dependencies +$data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\d365bap.tools\d365bap.tools.psd1" +foreach ($dependency in $data.RequiredModules) { + if ($dependency -is [string]) { + if ($modules -contains $dependency) { continue } + $modules += $dependency + } + else { + if ($modules -contains $dependency.ModuleName) { continue } + $modules += $dependency.ModuleName + } +} + +foreach ($module in $modules) { + Write-Host "Installing $module" -ForegroundColor Cyan + Install-Module $module -Force -SkipPublisherCheck -Repository $Repository + Import-Module $module -Force -PassThru +} \ No newline at end of file diff --git a/build/vsts-validate.ps1 b/build/vsts-validate.ps1 new file mode 100644 index 0000000..a6df605 --- /dev/null +++ b/build/vsts-validate.ps1 @@ -0,0 +1,7 @@ +# Guide for available variables and working with secrets: +# https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/build/variables?tabs=powershell + +# Needs to ensure things are Done Right and only legal commits to master get built + +# Run internal pester tests +& "$PSScriptRoot\..\d365bap.tools\tests\pester.ps1" \ No newline at end of file diff --git a/d365bap.tools/bin/readme.md b/d365bap.tools/bin/readme.md new file mode 100644 index 0000000..3379e65 --- /dev/null +++ b/d365bap.tools/bin/readme.md @@ -0,0 +1,7 @@ +# bin folder + +The bin folder exists to store binary data. And scripts related to the type system. + +This may include your own C#-based library, third party libraries you want to include (watch the license!), or a script declaring type accelerators (effectively aliases for .NET types) + +For more information on Type Accelerators, see the help on Set-PSFTypeAlias \ No newline at end of file diff --git a/d365bap.tools/changelog.md b/d365bap.tools/changelog.md new file mode 100644 index 0000000..34e5d01 --- /dev/null +++ b/d365bap.tools/changelog.md @@ -0,0 +1,5 @@ +# Changelog +## 1.0.0 (2024-03-02) + - New: Some Stuff + - Upd: Moar Stuff + - Fix: Much Stuff \ No newline at end of file diff --git a/d365bap.tools/d365bap.tools.psd1 b/d365bap.tools/d365bap.tools.psd1 new file mode 100644 index 0000000..48fa056 --- /dev/null +++ b/d365bap.tools/d365bap.tools.psd1 @@ -0,0 +1,98 @@ +@{ + # Script module or binary module file associated with this manifest + RootModule = 'd365bap.tools.psm1' + + # Version number of this module. + ModuleVersion = '0.0.10' + + # ID used to uniquely identify this module + GUID = 'adfc3aa2-1269-4648-a3d6-0342d5ef00bf' + + # Author of this module + Author = 'Mötz Jensen' + + # Company or vendor of this module + CompanyName = 'Essence Solutions' + + # Copyright statement for this module + Copyright = 'Copyright (c) 2024 Mötz Jensen' + + # Description of the functionality provided by this module + Description = 'Tools used for Business Application Platform, One Dynamics One Platform - D365FO + Dataverse' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing + # this module + RequiredModules = @( + @{ ModuleName = 'PSFramework'; ModuleVersion = '1.9.310' } + , @{ ModuleName = 'ImportExcel'; ModuleVersion = '7.8.6' } + , @{ ModuleName = 'Az.Accounts'; ModuleVersion = '2.12.4' } + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @('bin\d365bap.tools.dll') + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @('xml\d365bap.tools.Types.ps1xml') + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = @('xml\d365bap.tools.Format.ps1xml') + + # Functions to export from this module + FunctionsToExport = @( + 'Compare-BapEnvironmentD365App' + , 'Compare-BapEnvironmentUser' + + , 'Get-BapEnvironment' + , 'Get-BapEnvironmentApplicationUser' + + , 'Get-BapEnvironmentD365App' + , 'Get-BapEnvironmentDetails' + + , 'Get-BapEnvironmentUser' + + , 'Invoke-BapEnvironmentInstallD365App' + ) + + # Cmdlets to export from this module + CmdletsToExport = '' + + # Variables to export from this module + VariablesToExport = '' + + # Aliases to export from this module + AliasesToExport = '' + + # List of all modules packaged with this module + ModuleList = @() + + # List of all files packaged with this module + FileList = @() + + # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + #Support for PowerShellGet galleries. + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + } # End of PSData hashtable + + } # End of PrivateData hashtable +} \ No newline at end of file diff --git a/d365bap.tools/d365bap.tools.psm1 b/d365bap.tools/d365bap.tools.psm1 new file mode 100644 index 0000000..49afee1 --- /dev/null +++ b/d365bap.tools/d365bap.tools.psm1 @@ -0,0 +1,85 @@ +$script:ModuleRoot = $PSScriptRoot +$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\d365bap.tools.psd1").ModuleVersion + +# Detect whether at some level dotsourcing was enforced +$script:doDotSource = Get-PSFConfigValue -FullName d365bap.tools.Import.DoDotSource -Fallback $false +if ($d365bap.tools_dotsourcemodule) { $script:doDotSource = $true } + +<# +Note on Resolve-Path: +All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. +This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. +Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. +This is important when testing for paths. +#> + +# Detect whether at some level loading individual module files, rather than the compiled module was enforced +$importIndividualFiles = Get-PSFConfigValue -FullName d365bap.tools.Import.IndividualFiles -Fallback $false +if ($d365bap.tools_importIndividualFiles) { $importIndividualFiles = $true } +if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } +if ("" -eq '') { $importIndividualFiles = $true } + +function Import-ModuleFile +{ + <# + .SYNOPSIS + Loads files into the module on module import. + + .DESCRIPTION + This helper function is used during module initialization. + It should always be dotsourced itself, in order to proper function. + + This provides a central location to react to files being imported, if later desired + + .PARAMETER Path + The path to the file to load + + .EXAMPLE + PS C:\> . Import-ModuleFile -File $function.FullName + + Imports the file stored in $function according to import policy + #> + [CmdletBinding()] + Param ( + [string] + $Path + ) + + $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath + if ($doDotSource) { . $resolvedPath } + else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } +} + +#region Load individual files +if ($importIndividualFiles) +{ + # Execute Preimport actions + foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { + . Import-ModuleFile -Path $path + } + + # Import all internal functions + foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) + { + . Import-ModuleFile -Path $function.FullName + } + + # Import all public functions + foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) + { + . Import-ModuleFile -Path $function.FullName + } + + # Execute Postimport actions + foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { + . Import-ModuleFile -Path $path + } + + # End it here, do not load compiled code below + return +} +#endregion Load individual files + +#region Load compiled code +"" +#endregion Load compiled code \ No newline at end of file diff --git a/d365bap.tools/en-us/about_d365bap.tools.help.txt b/d365bap.tools/en-us/about_d365bap.tools.help.txt new file mode 100644 index 0000000..c629dfa --- /dev/null +++ b/d365bap.tools/en-us/about_d365bap.tools.help.txt @@ -0,0 +1,11 @@ +TOPIC + about_d365bap.tools + +SHORT DESCRIPTION + Explains how to use the d365bap.tools powershell module + +LONG DESCRIPTION + + +KEYWORDS + d365bap.tools \ No newline at end of file diff --git a/d365bap.tools/en-us/strings.psd1 b/d365bap.tools/en-us/strings.psd1 new file mode 100644 index 0000000..7014c33 --- /dev/null +++ b/d365bap.tools/en-us/strings.psd1 @@ -0,0 +1,5 @@ +# This is where the strings go, that are written by +# Write-PSFMessage, Stop-PSFFunction or the PSFramework validation scriptblocks +@{ + 'key' = 'Value' +} \ No newline at end of file diff --git a/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 new file mode 100644 index 0000000..8ca9778 --- /dev/null +++ b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 @@ -0,0 +1,143 @@ + +<# + .SYNOPSIS + Compare environment D365 Apps + + .DESCRIPTION + This enables the user to compare 2 x environments, with one as a source and the other as a destination + + It will only look for installed D365 Apps on the source, and use this as a baseline against the destination + + .PARAMETER SourceEnvironmentId + Environment Id of the source environment that you want to utilized as the baseline for the compare + + .PARAMETER DestinationEnvironmentId + Environment Id of the destination environment that you want to validate against the baseline (source) + + .PARAMETER ShowDiffOnly + Instruct the cmdlet to only output the differences that are not aligned between the source and destination + + .PARAMETER GeoRegion + Instructs the cmdlet which Geo / Region the environment is located + + The default value is: "Emea" + + This is mandatory field from the API specification, we don't have the full list of values at the time of writing + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 + + This will get all installed D365 Apps from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + + Sample output: + PackageId PackageName SourceVersion DestinationVersion AppName + --------- ----------- ------------- ------------------ ------- + ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… + 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… + 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… + 42cc1442-194f-462b-a325-ce5b5f18c02d msdyn_EmailAddressValidation 1.0.0.4 1.0.0.4 Data Valid… + + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -ShowDiffOnly + + This will get all installed D365 Apps from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + It will filter out results, to only include those where the DestinationVersions is different from the SourceVersion. + + Sample output: + PackageId PackageName SourceVersion DestinationVersion AppName + --------- ----------- ------------- ------------------ ------- + ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… + 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… + 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… + 7523d261-f1be-46e7-8e68-f3de16eeabbb DualWriteCoreAnchor 1.0.24022.4 1.0.24011.1 Dual-write… + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Compare-BapEnvironmentD365App { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $SourceEnvironmentId, + + [parameter (mandatory = $true)] + [string] $DestinationEnvironmentId, + + [switch] $ShowDiffOnly, + + [string] $GeoRegion = "Emea", + + [switch] $AsExcelOutput + + ) + + begin { + # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. + $envSourceObj = Get-BapEnvironment -EnvironmentId $SourceEnvironmentId | Select-Object -First 1 + + if ($null -eq $envSourceObj) { + $messageString = "The supplied SourceEnvironmentId: $SourceEnvironmentId didn't return any matching environment details. Please verify that the SourceEnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. + $envDestinationObj = Get-BapEnvironment -EnvironmentId $DestinationEnvironmentId | Select-Object -First 1 + + if ($null -eq $envDestinationObj) { + $messageString = "The supplied DestinationEnvironmentId: $DestinationEnvironmentId didn't return any matching environment details. Please verify that the DestinationEnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + if (Test-PSFFunctionInterrupt) { return } + + $appsSourceEnvironment = Get-BapEnvironmentD365App -EnvironmentId $SourceEnvironmentId -InstallState Installed + $appsDestinationEnvironment = Get-BapEnvironmentD365App -EnvironmentId $DestinationEnvironmentId + } + + process { + if (Test-PSFFunctionInterrupt) { return } + + $resCol = @(foreach ($sourceApp in $($appsSourceEnvironment | Sort-Object -Property ApplicationName )) { + $destinationApp = $appsDestinationEnvironment | Where-Object PackageId -eq $sourceApp.PackageId | Select-Object -First 1 + + $tmp = [Ordered]@{ + PackageId = $sourceApp.PackageId + PackageName = $sourceApp.PackageName + AppName = $sourceApp.AppName + SourceVersion = [System.Version]$sourceApp.InstalledVersion + DestinationVersion = "Missing" + } + + if (-not ($null -eq $destinationApp)) { + $tmp.DestinationVersion = if ($destinationApp.InstalledVersion -eq "N/A") { [System.Version]"0.0.0.0" }else { [System.Version]$destinationApp.InstalledVersion } + } + + ([PSCustomObject]$tmp) | Select-PSFObject -TypeName "D365Bap.Tools.Compare.Package" + } + ) + + if ($ShowDiffOnly) { + $resCol = $resCol | Where-Object { $_.SourceVersion -ne $_.DestinationVersion } + } + + if ($AsExcelOutput) { + $resCol | Export-Excel -NoNumberConversion SourceVersion, DestinationVersion + return + } + + $resCol + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 new file mode 100644 index 0000000..fd02688 --- /dev/null +++ b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 @@ -0,0 +1,155 @@ + +<# + .SYNOPSIS + Compare the environment users + + .DESCRIPTION + This enables the user to compare 2 x environments, with one as a source and the other as a destination + + It will only look for users on the source, and use this as a baseline against the destination + + .PARAMETER SourceEnvironmentId + Environment Id of the source environment that you want to utilized as the baseline for the compare + + .PARAMETER DestinationEnvironmentId + Environment Id of the destination environment that you want to validate against the baseline (source) + + .PARAMETER ShowDiffOnly + Instruct the cmdlet to only output the differences that are not aligned between the source and destination + + .PARAMETER IncludeAppIds + Instruct the cmdlet to also include the users with the ApplicationId property filled + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 + + This will get all system users from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + It will exclude those with ApplicationId filled. + + Sample output: + Email Name AppId SourceId DestinationId + ----- ---- ----- -------- ------------- + aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… + ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… + + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds + + This will get all system users from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + It will include those with ApplicationId filled. + + Sample output: + Email Name AppId SourceId DestinationId + ----- ---- ----- -------- ------------- + aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… + ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… + AIBuilder_StructuredML_Prod_C… AIBuilder_StructuredML_Prod_C… ff8a1ad8-a415-45c1-… 95dc9ca2-8185-… 328db0cc-14c… + AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-… 1831f4dc-4c5… + + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds -ShowDiffOnly + + This will get all system users from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + It will include those with ApplicationId filled. + It will only output the users that is missing in the destionation environment. + + Sample output: + Email Name AppId SourceId DestinationId + ----- ---- ----- -------- ------------- + d365-scm-operationdataservice… d365-scm-operationdataservice… 986556ed-a409-4339-… 5e077e6a-a0c9-… Missing + d365-scm-operationdataservice… d365-scm-operationdataservice… 14e80222-1878-455d-… 183ec023-9ccb-… Missing + def@temp.com Dustin Effect 01e37132-0a44-… Missing + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Compare-BapEnvironmentUser { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $SourceEnvironmentId, + + [parameter (mandatory = $true)] + [string] $DestinationEnvironmentId, + + [switch] $ShowDiffOnly, + + [switch] $IncludeAppIds, + + [switch] $AsExcelOutput + + ) + + begin { + # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. + $envSourceObj = Get-BapEnvironment -EnvironmentId $SourceEnvironmentId | Select-Object -First 1 + + if ($null -eq $envSourceObj) { + $messageString = "The supplied SourceEnvironmentId: $SourceEnvironmentId didn't return any matching environment details. Please verify that the SourceEnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. + $envDestinationObj = Get-BapEnvironment -EnvironmentId $DestinationEnvironmentId | Select-Object -First 1 + + if ($null -eq $envDestinationObj) { + $messageString = "The supplied DestinationEnvironmentId: $DestinationEnvironmentId didn't return any matching environment details. Please verify that the DestinationEnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + if (Test-PSFFunctionInterrupt) { return } + + $usersSourceEnvironment = Get-BapEnvironmentUser -EnvironmentId $SourceEnvironmentId -IncludeAppIds:$IncludeAppIds + $usersDestinationEnvironment = Get-BapEnvironmentUser -EnvironmentId $DestinationEnvironmentId -IncludeAppIds:$IncludeAppIds + } + + process { + if (Test-PSFFunctionInterrupt) { return } + + $resCol = @(foreach ($sourceUser in $($usersSourceEnvironment | Sort-Object -Property Email )) { + if ([System.String]::IsNullOrEmpty($sourceUser.Email)) { continue } + + $destinationUser = $usersDestinationEnvironment | Where-Object Email -eq $sourceUser.Email | Select-Object -First 1 + + $tmp = [Ordered]@{ + Email = $sourceUser.Email + Name = $sourceUser.Name + AppId = $sourceUser.AppId + SourceId = $sourceUser.systemuserid + DestinationId = "Missing" + } + + if (-not ($null -eq $destinationUser)) { + $tmp.DestinationId = $destinationUser.systemuserid + } + + ([PSCustomObject]$tmp) | Select-PSFObject -TypeName "D365Bap.Tools.Compare.User" + } + ) + + if ($ShowDiffOnly) { + $resCol = $resCol | Where-Object { $_.DestinationId -eq "Missing" } + } + + if ($AsExcelOutput) { + $resCol | Export-Excel -NoNumberConversion SourceVersion, DestinationVersion + return + } + + $resCol + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Get-BapEnvironment.ps1 b/d365bap.tools/functions/Get-BapEnvironment.ps1 new file mode 100644 index 0000000..19aeda2 --- /dev/null +++ b/d365bap.tools/functions/Get-BapEnvironment.ps1 @@ -0,0 +1,117 @@ + +<# + .SYNOPSIS + Get environment info + + .DESCRIPTION + This enables the user to query and validate all environments that are available from inside PPAC + + It utilizes the "https://api.bap.microsoft.com" REST API + + .PARAMETER EnvironmentId + The id of the environment that you want to work against + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Get-BapEnvironment + + This will query for ALL available environments. + + Sample output: + PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri + --------- ------------- ----------- ---------- ------------------ + 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 europe new-uat Sandbox https://new-uat.sandbox.operatio… + eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… + d45936a7-0408-4b79-94d1-19e4c6e5a52e europe new-golden Sandbox https://new-golden.sandbox.opera… + Default-e210bc90-e54b-4544-a9b8-b1f… europe New Customer Default + + .EXAMPLE + PS C:\> Get-BapEnvironment -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 + + This will query for the specific environment. + + Sample output: + PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri + --------- ------------- ----------- ---------- ------------------ + eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Get-BapEnvironment { + [CmdletBinding()] + param ( + [string] $EnvironmentId = "*", + + [switch] $AsExcelOutput + ) + + begin { + $tokenBap = Get-AzAccessToken -ResourceUrl "https://service.powerapps.com/" + $headers = @{ + "Authorization" = "Bearer $($tokenBap.Token)" + } + + $resEnvs = Invoke-RestMethod -Method Get -Uri "https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments?api-version=2023-06-01" -Headers $headers | Select-Object -ExpandProperty Value + } + + process { + + $resCol = @( + foreach ($envObj in $resEnvs) { + if (-not ($envObj.Name -like $EnvironmentId)) { continue } + + $res = [ordered]@{} + + $res.Id = $envObj.Name + $res.Region = $envObj.Location + + foreach ($prop in $envObj.Properties.PsObject.Properties) { + if ($prop.Value -is [System.Management.Automation.PSCustomObject]) { + $res."prop_$($prop.Name)" = @( + foreach ($inner in $prop.Value.PsObject.Properties) { + "$($inner.Name)=$($inner.Value)" + }) -join "`r`n" + } + else { + $res."prop_$($prop.Name)" = $prop.Value + } + + } + + ([PSCustomObject]$res) | Select-PSFObject -TypeName "D365Bap.Tools.Environment" -Property "Id as PpacEnvId", + "Region as PpacEnvRegion", + "prop_tenantId as TenantId", + "prop_azureRegion as AzureRegion", + "prop_displayName as PpacEnvName", + @{Name = "DeployedBy"; Expression = { $envObj.Properties.createdBy.userPrincipalName } }, + "prop_provisioningState as PpacProvisioningState", + "prop_environmentSku as PpacEnvSku", + "prop_databaseType as PpacDbType", + @{Name = "LinkedAppLcsEnvId"; Expression = { $envObj.Properties.linkedAppMetadata.id } }, + @{Name = "LinkedAppLcsEnvUri"; Expression = { $envObj.Properties.linkedAppMetadata.url } }, + @{Name = "LinkedMetaPpacOrgId"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.resourceId } }, + @{Name = "LinkedMetaPpacUniqueId"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.uniqueName } }, + @{Name = "LinkedMetaPpacEnvUri"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.instanceUrl -replace "com/", "com" } }, + @{Name = "LinkedMetaPpacEnvLanguage"; Expression = { $envObj.Properties.linkedEnvironmentMetadata.baseLanguage } }, + @{Name = "PpacClusterIsland"; Expression = { $envObj.Properties.cluster.uriSuffix } }, + "*" + } + ) + + if ($AsExcelOutput) { + $resCol | Export-Excel -NoNumberConversion Version, AvailableVersion, InstalledVersion, crmMinversion, crmMaxVersion, Version + return + } + + $resCol + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 b/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 new file mode 100644 index 0000000..edc5549 --- /dev/null +++ b/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 @@ -0,0 +1,88 @@ + +<# + .SYNOPSIS + Get application users from environment + + .DESCRIPTION + Enables the user to fetch all application users from the environment + + Utilizes the built-in "applicationusers" OData entity + + .PARAMETER EnvironmentId + The id of the environment that you want to work against + + This can be obtained from the Get-BapEnvironment cmdlet + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Get-BapEnvironmentApplicationUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 + + This will fetch all ApplicationUsers from the environment. + + Sample output: + AppId AppName ApplicationUserId SolutionId + ----- ------- ----------------- ---------- + b6e52ceb-f771-41ff-bd99-917523b28eaf AIBuilder_StructuredML_Prod_C… 3bafba76-60bf-413d-a4c4-5c49ccabfb12 bf85e0c8-aa47… + 21ceaf7c-054c-43f6-8b14-ef6d04b90a21 AIBuilderProd 560c9a6c-4535-4066-a415-480d1493cf98 bf85e0c8-aa47… + c76313fd-5c6f-4f1f-9869-c884fa7fe226 AppDeploymentOrchestration d88a3535-ebf0-4b2b-ad23-90e686660a64 99aee001-009e… + 29494271-7e38-4433-8bf8-06d335299a17 AriaMdlExporter 8bf8862f-5036-42b0-a4f8-1b638db7896b 99aee001-009e… + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Get-BapEnvironmentApplicationUser { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $EnvironmentId, + + [switch] $AsExcelOutput + ) + + 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + if (Test-PSFFunctionInterrupt) { return } + + $baseUri = $envObj.LinkedMetaPpacEnvUri + $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri + $headersWebApi = @{ + "Authorization" = "Bearer $($tokenWebApi.Token)" + } + + } + + process { + $resAppUsers = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/applicationusers') -Headers $headersWebApi + $resCol = @( + foreach ($appUsrObj in $($resAppUsers.value | Sort-Object -Property applicationname)) { + $appUsrObj | Select-PSFObject -TypeName "D365Bap.Tools.AppUser" -ExcludeProperty "@odata.etag" -Property "applicationid as AppId", + "applicationname as AppName", + * + } + ) + + if ($AsExcelOutput) { + $resCol | Export-Excel + return + } + + $resCol + } + + end { + + } + +} \ No newline at end of file diff --git a/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 new file mode 100644 index 0000000..1660287 --- /dev/null +++ b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 @@ -0,0 +1,228 @@ + +<# + .SYNOPSIS + Get D365 App from the environment + + .DESCRIPTION + This enables the user to analyze and validate the current D365 Apps and their state, on a given environment + + It can show all available D365 Apps - including their InstallState + + It can show only installed D365 Apps + + It can show only installed D365 Apps, with available updates + + .PARAMETER EnvironmentId + The id of the environment that you want to work against + + This can be obtained from the Get-BapEnvironment cmdlet + + .PARAMETER Name + Name of the D365 App / Package that you are looking for + + It supports wildcard searching, which is validated against the following properties: + * AppName / ApplicationName + * PackageName / UniqueName + + .PARAMETER InstallState + Instruct the cmdlet which install states that you want to have included in the output + + The default value is: "All" + + Valid values: + * "All" + * "Installed" + * "None" + + .PARAMETER GeoRegion + Instructs the cmdlet which Geo / Region the environment is located + + The default value is: "Emea" + + This is mandatory field from the API specification, we don't have the full list of values at the time of writing + + .PARAMETER UpdatesOnly + Instruct the cmdlet to only output D365 Apps that has an update available + + Makes it easier to fully automate the update process of a given environment + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 + + This will query the environment for ALL available D365 Apps. + It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + + Sample output: + + PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable + --------- ----------- ---------------- ---------------- --------------- + cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A + ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True + b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A + 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False + 9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A + 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False + + .EXAMPLE + PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed + + This will query the environment for installed only D365 Apps. + It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + + Sample output: + PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable + --------- ----------- ---------------- ---------------- --------------- + ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True + 1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False + 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False + + .EXAMPLE + PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState None + + This will query the environment for NON-installed only D365 Apps. + It will output all details available for the D365 Apps. + + Sample output: + PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable + --------- ----------- ---------------- ---------------- --------------- + cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A + b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A + 9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A + + .EXAMPLE + PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name "*ProviderAnchor*" + + This will query the environment for ALL D365 Apps. + It will filter the output to only those who match the search pattern "*ProviderAnchor*". + It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + + Sample output: + PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable + --------- ----------- ---------------- ---------------- --------------- + c0cb37fd-d7f4-40f2-8592-64ec71a2c508 msft_ConnectorProviderAnchor 9.0.0.1618 9.0.0.1618 False + + .EXAMPLE + PS C:\> Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -UpdatesOnly + + This will query the environment for ALL available D365 Apps. + It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + It will filter the output to only containing those who have an update available. + + Sample output: + PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable + --------- ----------- ---------------- ---------------- --------------- + ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True + + .EXAMPLE + PS C:\> $appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) + PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds + + This will query the environment for installed only D365 Apps. + It will filter the output to only containing those who have an update available. + It will persist the PackageIds for each D365 App, into an array. + It will invoke the installation process using the Invoke-BapEnvironmentInstallD365App cmdlet. + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Get-BapEnvironmentD365App { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $EnvironmentId, + + [string] $Name = "*", + + [ValidateSet("All", "Installed", "None")] + [string] $InstallState = "All", + + [string] $GeoRegion = "Emea", + + [switch] $UpdatesOnly, + + [switch] $AsExcelOutput + ) + + begin { + $tenantId = (Get-AzContext).Tenant.Id + + # 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT 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 + $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" + $headersPowerApi = @{ + "Authorization" = "Bearer $($tokenPowerApi.Token)" + } + + $appsAvailable = Invoke-RestMethod -Method Get -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/applicationPackages?api-version=2022-03-01-preview" -Headers $headersPowerApi | Select-Object -ExpandProperty Value + + # Next we will fetch current installed apps and their details, for the environment + $uriSourceEncoded = [System.Web.HttpUtility]::UrlEncode($envObj.LinkedMetaPpacEnvUri) + $tokenAdminApi = Get-AzAccessToken -ResourceUrl "065d9450-1e87-434e-ac2f-69af271549ed" + $headersAdminApi = @{ + "Authorization" = "Bearer $($tokenAdminApi.Token)" + } + + $appsEnvironment = Invoke-RestMethod -Method Get -Uri "https://api.admin.powerplatform.microsoft.com/api/AppManagement/InstancePackages/instanceId/$tenantId`?instanceUrl=$uriSourceEncoded`&geoType=$GeoRegion" -Headers $headersAdminApi + } + + process { + if (Test-PSFFunctionInterrupt) { return } + + $resCol = @( + foreach ($appObj in $($appsAvailable | Sort-Object -Property ApplicationName)) { + if ((-not ($appObj.ApplicationName -like $Name -or $appObj.ApplicationName -eq $Name)) -and (-not ($appObj.UniqueName -like $Name -or $appObj.UniqueName -eq $Name))) { continue } + if ($InstallState -ne "All" -and $appObj.state -ne $InstallState) { continue } + + $appObj | Add-Member -MemberType NoteProperty -Name CurrentVersion -Value "N/A" + + $currentApp = $appsEnvironment | Where-Object ApplicationId -eq $appObj.ApplicationId | Select-Object -First 1 + if ($currentApp) { + $appObj.CurrentVersion = $currentApp.Version + + $appObj | Add-Member -MemberType NoteProperty -Name IsLatest -Value $($appObj.CurrentVersion -eq $appObj.Version) + $appObj | Add-Member -MemberType NoteProperty -Name UpdateAvail -Value $(-not ($appObj.CurrentVersion -eq $appObj.Version)) + } + + $appObj | Select-PSFObject -TypeName "D365Bap.Tools.Package" -Property "Id as PackageId", + "UniqueName as PackageName", + "Version as AvailableVersion", + "CurrentVersion as InstalledVersion", + "UpdateAvail as UpdateAvailable", + "ApplicationName as AppName", + "state as InstallState", + *, + @{Name = "SupportedCountriesList"; Expression = { $_.supportedCountries -join "," } } + } + ) + + if ($UpdatesOnly) { + $resCol = @($resCol | Where-Object IsLatest -eq $false) + } + + if ($AsExcelOutput) { + $resCol | Export-Excel -NoNumberConversion Version, AvailableVersion, InstalledVersion, crmMinversion, crmMaxVersion, Version + return + } + + $resCol + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 b/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 new file mode 100644 index 0000000..fe8f417 --- /dev/null +++ b/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 @@ -0,0 +1,77 @@ + +<# + .SYNOPSIS + Short description + + .DESCRIPTION + Long description + + .PARAMETER EnvironmentId + Parameter description + + .PARAMETER AsExcelOutput + Parameter description + + .EXAMPLE + An example + + .NOTES + General notes +#> +function Get-BapEnvironmentDetails { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $EnvironmentId, + + [switch] $AsExcelOutput + ) + + begin { + $curUserId = (Get-AzContext).account.id + + # 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + if (Test-PSFFunctionInterrupt) { return } + + $baseUri = $envObj.LinkedMetaPpacEnvUri + $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri + $headersWebApi = @{ + "Authorization" = "Bearer $($tokenWebApi.Token)" + } + + } + + process { + if (Test-PSFFunctionInterrupt) { return } + + # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveAvailableLanguages()" -Headers $headersWebApi + # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveProvisionedLanguages()" -Headers $headersWebApi + + # $resOrg = @(Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi) + # $resOrg.value | ConvertTo-Json -Depth 10 + + $resOrg = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi | Select-Object -ExpandProperty value | Select-Object -First 1 + # $resOrg + + $languages = @(Get-EnvironmentLanguage -BaseUri $baseUri) + + $languages | ConvertTo-Json -Depth 10 + + # $resUser = Invoke-RestMethod -Method Get -Uri $("$baseUri/api/data/v9.2/systemusers?" + '$select=internalemailaddress&$top=1&$filter=internalemailaddress eq' + " '$curUserId'" + '&$expand=user_settings($select=uilanguageid)') -Headers $headersWebApi + $resUser = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/systemusers?$select=internalemailaddress&$top=1&$filter=internalemailaddress eq ''{0}''&$expand=user_settings($select=uilanguageid)' -f $curUserId) -Headers $headersWebApi + + # $resUser.value | ConvertTo-Json -Depth 10 + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 b/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 new file mode 100644 index 0000000..c25c0b6 --- /dev/null +++ b/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 @@ -0,0 +1,120 @@ + +<# + .SYNOPSIS + Get users from environment + + .DESCRIPTION + Enables the user to fetch all users from the environment + + Utilizes the built-in "systemusers" OData entity + + Allows the user to include all users, based on those who has the ApplicationId property filled + + .PARAMETER EnvironmentId + The id of the environment that you want to work against + + This can be obtained from the Get-BapEnvironment cmdlet + + .PARAMETER IncludeAppIds + Instruct the cmdlet to include all users that are available from the "systemusers" OData Entity + + Simply includes those who has the ApplicationId property filled + + .PARAMETER AsExcelOutput + Instruct the cmdlet to output all details directly to an Excel file + + This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + + .EXAMPLE + PS C:\> Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 + + This will fetch all oridinary users from the environment. + + Sample output: + Email Name AppId Systemuserid + ----- ---- ----- ------------ + SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 + INTEGRATION baabe592-2860-4d1a-9365-e95317372498 + aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a + ade@temp.com Alex Denver 39309a5c-7676-4c8a-b702-719fb92c5151 + + .EXAMPLE + PS C:\> Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 + + This will fetch all users from the environment. + It will include the ones with the ApplicationId property filled. + + Sample output: + Email Name AppId Systemuserid + ----- ---- ----- ------------ + SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 + INTEGRATION baabe592-2860-4d1a-9365-e95317372498 + aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a + AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-4c5e-ac84-1831f4dc7d5f + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Get-BapEnvironmentUser { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $EnvironmentId, + + [switch] $IncludeAppIds, + + [switch] $AsExcelOutput + ) + + 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -Message $messageString + Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) + } + + if (Test-PSFFunctionInterrupt) { return } + + $baseUri = $envObj.LinkedMetaPpacEnvUri + $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri + $headersWebApi = @{ + "Authorization" = "Bearer $($tokenWebApi.Token)" + } + + $languages = @(Get-EnvironmentLanguage -BaseUri $baseUri) + } + + process { + $resUsers = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/systemusers?$select=fullname,internalemailaddress,applicationid&$expand=user_settings($select=uilanguageid)') -Headers $headersWebApi + + $resCol = @( + foreach ($usrObj in $($resUsers.value | Sort-Object -Property internalemailaddress)) { + + $usrObj | Add-Member -MemberType NoteProperty -Name "lang" -Value $($languages | Where-Object { ($_.localeid -eq $usrObj.user_settings[0].uilanguageid) -or ($_.BaseLocaleId -eq $usrObj.user_settings[0].uilanguageid) } | Select-Object -First 1 -ExpandProperty code) + $usrObj | Select-PSFObject -TypeName "D365Bap.Tools.User" -ExcludeProperty "@odata.etag" -Property "internalemailaddress as Email", + "fullname as Name", + "applicationid as AppId", + "lang as Language", + * + } + ) + + if (-not $IncludeAppIds) { + $resCol = $resCol | Where-Object applicationid -eq $null + } + + if ($AsExcelOutput) { + $resCol | Export-Excel + return + } + + $resCol + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 b/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 new file mode 100644 index 0000000..ac45b7c --- /dev/null +++ b/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 @@ -0,0 +1,157 @@ + +<# + .SYNOPSIS + Invoke the installation of a D365 App in a given environment + + .DESCRIPTION + This enables the invocation of the installation process against the PowerPlatform API (https://api.powerplatform.com) + + The cmdlet will keep requesting the status of all invoked installations, until they all have a NON "Running" state + + It will request this status every 60 seconds + + .PARAMETER EnvironmentId + The id of the environment that you want to work against + + This can be obtained from the Get-BapEnvironment cmdlet + + .PARAMETER PackageId + The id of the package(s) that you want to have Installed + + It supports id of current packages, with updates available and new D365 apps + + It support an array as input, so it can invoke multiple D365 App installations + + .EXAMPLE + PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId 'be69fc64-7393-4c3c-8908-2a1c2e53aef9','6defa8de-87f9-4478-8f9a-a7d685394e24' + + This will install the 2 x D365 Apps, based on the Ids supplied. + It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. + + Sample output (Install initialized): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + Sample output (Partly succeeded installation): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + Sample output (Completely succeeded installation): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + .EXAMPLE + PS C:\> $appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) + PS C:\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds + + This will find all D365 Apps that has a pending update available. + It will gather the Ids into an array. + It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. + + Sample output (Install initialized): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + Sample output (Partly succeeded installation): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + Sample output (Completely succeeded installation): + status createdDateTime lastActionDateTime error statusMessage operationId + ------ --------------- ------------------ ----- ------------- ----------- + Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 + Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Invoke-BapEnvironmentInstallD365App { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $EnvironmentId, + + [parameter (mandatory = $true)] + [string[]] $PackageId + ) + + begin { + $tenantId = (Get-AzContext).Tenant.Id + + # 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." + Write-PSFMessage -Level Host -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 + $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" + $headersPowerApi = @{ + "Authorization" = "Bearer $($tokenPowerApi.Token)" + } + + $appsAvailable = Get-BapEnvironmentD365App -EnvironmentId $EnvironmentId + } + + process { + if (Test-PSFFunctionInterrupt) { return } + + [System.Collections.Generic.List[System.Object]] $arrInstallStarted = @() + [System.Collections.Generic.List[System.Object]] $arrStatus = @() + + $headersPowerApi."Content-Type" = "application/json;charset=utf-8" + + foreach ($pgkId in $PackageId) { + $appToBeInstalled = $appsAvailable | Where-Object Id -eq $pgkId | Select-Object -First 1 + + if ($null -eq $appToBeInstalled) { + $messageString = "The combination of the supplied EnvironmentId: $EnvironmentId and PackageId: $PackageId didn't return any matching D365App. Please verify that the EnvironmentId & PackageId is correct - try running the Get-BapEnvironmentD365App cmdlet." + Write-PSFMessage -Level Host -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 '<[^>]+>', ''))) + } + + $body = $appToBeInstalled | ConvertTo-Json + $resIntall = Invoke-RestMethod -Method Post -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/applicationPackages/$($appToBeInstalled.uniqueName)/install?api-version=2022-03-01-preview" -Headers $headersPowerApi -Body $body + + $arrInstallStarted.Add($resIntall) + } + + do { + $tokenPowerApi = Get-AzAccessToken -ResourceUrl "https://api.powerplatform.com/" + $headersPowerApi = @{ + "Authorization" = "Bearer $($tokenPowerApi.Token)" + } + + Start-Sleep -Seconds 60 + # Write-PSFMessage -Level Host -Message "Checking for running operations" + + $arrStatus = @() + + foreach ($operation in $arrInstallStarted) { + $resInstallOperation = Invoke-RestMethod -Method Get -Uri "https://api.powerplatform.com/appmanagement/environments/$EnvironmentId/operations/$($operation.lastOperation.operationId)?api-version=2022-03-01-preview" -Headers $headersPowerApi + $arrStatus.Add($resInstallOperation) + } + + $arrStatus | Format-Table + } while ("Running" -in $arrStatus.status) + } + + end { + + } +} \ No newline at end of file diff --git a/d365bap.tools/functions/readme.md b/d365bap.tools/functions/readme.md new file mode 100644 index 0000000..280e32c --- /dev/null +++ b/d365bap.tools/functions/readme.md @@ -0,0 +1,7 @@ +# Functions + +This is the folder where the functions go. + +Depending on the complexity of the module, it is recommended to subdivide them into subfolders. + +The module will pick up all .ps1 files recursively \ No newline at end of file diff --git a/d365bap.tools/internal/configurations/configuration.ps1 b/d365bap.tools/internal/configurations/configuration.ps1 new file mode 100644 index 0000000..47d1fc4 --- /dev/null +++ b/d365bap.tools/internal/configurations/configuration.ps1 @@ -0,0 +1,15 @@ +<# +This is an example configuration file + +By default, it is enough to have a single one of them, +however if you have enough configuration settings to justify having multiple copies of it, +feel totally free to split them into multiple files. +#> + +<# +# Example Configuration +Set-PSFConfig -Module 'd365bap.tools' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" +#> + +Set-PSFConfig -Module 'd365bap.tools' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." +Set-PSFConfig -Module 'd365bap.tools' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." \ No newline at end of file diff --git a/d365bap.tools/internal/configurations/readme.md b/d365bap.tools/internal/configurations/readme.md new file mode 100644 index 0000000..d6c3210 --- /dev/null +++ b/d365bap.tools/internal/configurations/readme.md @@ -0,0 +1,14 @@ +# Configurations + +Through the `PSFramework` you have a simple method that allows you to ... + + - Publish settings + - With onboard documentation + - Input validation + - Scripts that run on change of settings + - That can be discovered and updated by the user + - That can be administrated by policy & DSC + +The configuration system is a bit too complex to describe in a help file, you can however visit us at http://psframework.org for detailed guidance. + +An example can be seen in the attached ps1 file \ No newline at end of file diff --git a/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 b/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 new file mode 100644 index 0000000..d2c033d --- /dev/null +++ b/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 @@ -0,0 +1,47 @@ + +<# + .SYNOPSIS + Get language from Environment + + .DESCRIPTION + Fetches all languages from the environment + + .PARAMETER BaseUri + Base Web API URI for the environment + + Used to construct the correct REST API Url, based on the WebApi / OData endpoint + + .EXAMPLE + PS C:\> Get-EnvironmentLanguage -BaseUri 'https://temp-test.crm4.dynamics.com' + + .NOTES + Author: Mötz Jensen (@Splaxi) +#> +function Get-EnvironmentLanguage { + [CmdletBinding()] + param ( + [parameter (mandatory = $true)] + [string] $BaseUri + ) + + begin { + $tokenWebApi = Get-AzAccessToken -ResourceUrl $BaseUri + $headersWebApi = @{ + "Authorization" = "Bearer $($tokenWebApi.Token)" + } + + $resOrg = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi | Select-Object -ExpandProperty value | Select-Object -First 1 + $resLangs = Invoke-RestMethod -Method Get -Uri "$BaseUri/api/data/v9.2/languagelocale" -Headers $headersWebApi | Select-Object -ExpandProperty value + } + + process { + foreach ($lanObj in $resLangs) { + if ($lanObj.localeid -eq $resOrg.localeid) { + # Could also be "languagecode" - maybe we'll get more info later on + $lanObj | Add-Member -MemberType NoteProperty -Name "BaseLocaleId" -Value 0 + } + } + + $resLangs + } +} \ No newline at end of file diff --git a/d365bap.tools/internal/functions/readme.md b/d365bap.tools/internal/functions/readme.md new file mode 100644 index 0000000..c7074e5 --- /dev/null +++ b/d365bap.tools/internal/functions/readme.md @@ -0,0 +1,7 @@ +# Functions + +This is the folder where the internal functions go. + +Depending on the complexity of the module, it is recommended to subdivide them into subfolders. + +The module will pick up all .ps1 files recursively \ No newline at end of file diff --git a/d365bap.tools/internal/scriptblocks/scriptblocks.ps1 b/d365bap.tools/internal/scriptblocks/scriptblocks.ps1 new file mode 100644 index 0000000..0b90305 --- /dev/null +++ b/d365bap.tools/internal/scriptblocks/scriptblocks.ps1 @@ -0,0 +1,12 @@ +<# +Stored scriptblocks are available in [PsfValidateScript()] attributes. +This makes it easier to centrally provide the same scriptblock multiple times, +without having to maintain it in separate locations. + +It also prevents lengthy validation scriptblocks from making your parameter block +hard to read. + +Set-PSFScriptblock -Name 'd365bap.tools.ScriptBlockName' -Scriptblock { + +} +#> \ No newline at end of file diff --git a/d365bap.tools/internal/scripts/license.ps1 b/d365bap.tools/internal/scripts/license.ps1 new file mode 100644 index 0000000..098fa5b --- /dev/null +++ b/d365bap.tools/internal/scripts/license.ps1 @@ -0,0 +1,21 @@ +New-PSFLicense -Product 'd365bap.tools' -Manufacturer 'MötzJensen' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2024-03-02") -Text @" +Copyright (c) 2024 MötzJensen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +"@ \ No newline at end of file diff --git a/d365bap.tools/internal/scripts/postimport.ps1 b/d365bap.tools/internal/scripts/postimport.ps1 new file mode 100644 index 0000000..81583ec --- /dev/null +++ b/d365bap.tools/internal/scripts/postimport.ps1 @@ -0,0 +1,26 @@ +<# +Add all things you want to run after importing the main function code + +WARNING: ONLY provide paths to files! + +After building the module, this file will be completely ignored, adding anything but paths to files ... +- Will not work after publishing +- Could break the build process +#> + +$moduleRoot = Split-Path (Split-Path $PSScriptRoot) + +# Load Configurations +(Get-ChildItem "$moduleRoot\internal\configurations\*.ps1" -ErrorAction Ignore).FullName + +# Load Scriptblocks +(Get-ChildItem "$moduleRoot\internal\scriptblocks\*.ps1" -ErrorAction Ignore).FullName + +# Load Tab Expansion +(Get-ChildItem "$moduleRoot\internal\tepp\*.tepp.ps1" -ErrorAction Ignore).FullName + +# Load Tab Expansion Assignment +"$moduleRoot\internal\tepp\assignment.ps1" + +# Load License +"$moduleRoot\internal\scripts\license.ps1" \ No newline at end of file diff --git a/d365bap.tools/internal/scripts/preimport.ps1 b/d365bap.tools/internal/scripts/preimport.ps1 new file mode 100644 index 0000000..475843b --- /dev/null +++ b/d365bap.tools/internal/scripts/preimport.ps1 @@ -0,0 +1,14 @@ +<# +Add all things you want to run before importing the main function code. + +WARNING: ONLY provide paths to files! + +After building the module, this file will be completely ignored, adding anything but paths to files ... +- Will not work after publishing +- Could break the build process +#> + +$moduleRoot = Split-Path (Split-Path $PSScriptRoot) + +# Load the strings used in messages +"$moduleRoot\internal\scripts\strings.ps1" \ No newline at end of file diff --git a/d365bap.tools/internal/scripts/strings.ps1 b/d365bap.tools/internal/scripts/strings.ps1 new file mode 100644 index 0000000..0c7a005 --- /dev/null +++ b/d365bap.tools/internal/scripts/strings.ps1 @@ -0,0 +1,8 @@ +<# +This file loads the strings documents from the respective language folders. +This allows localizing messages and errors. +Load psd1 language files for each language you wish to support. +Partial translations are acceptable - when missing a current language message, +it will fallback to English or another available language. +#> +Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'd365bap.tools' -Language 'en-US' \ No newline at end of file diff --git a/d365bap.tools/internal/tepp/assignment.ps1 b/d365bap.tools/internal/tepp/assignment.ps1 new file mode 100644 index 0000000..4fbfc69 --- /dev/null +++ b/d365bap.tools/internal/tepp/assignment.ps1 @@ -0,0 +1,4 @@ +<# +# Example: +Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name d365bap.tools.alcohol +#> \ No newline at end of file diff --git a/d365bap.tools/internal/tepp/example.tepp.ps1 b/d365bap.tools/internal/tepp/example.tepp.ps1 new file mode 100644 index 0000000..c3d285d --- /dev/null +++ b/d365bap.tools/internal/tepp/example.tepp.ps1 @@ -0,0 +1,4 @@ +<# +# Example: +Register-PSFTeppScriptblock -Name "d365bap.tools.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } +#> \ No newline at end of file diff --git a/d365bap.tools/internal/tepp/readme.md b/d365bap.tools/internal/tepp/readme.md new file mode 100644 index 0000000..d8b2e71 --- /dev/null +++ b/d365bap.tools/internal/tepp/readme.md @@ -0,0 +1,23 @@ +# Tab Expansion + +## Description + +Modern Tab Expansion was opened to users with the module `Tab Expansion Plus Plus` (TEPP). + +It allows you to define, what options a user is offered when tabbing through input options. This can save a lot of time for the user and is considered a key element in user experience. + +The `PSFramework` offers a simplified way of offering just this, as the two example files show. + +## Concept + +Custom tab completion is defined in two steps: + + - Define a scriptblock that is run when the user hits `TAB` and provides the strings that are his options. + - Assign that scriptblock to the parameter of a command. You can assign the same scriptblock multiple times. + +## Structure + +Import order matters. In order to make things work with the default scaffold, follow those rules: + + - All scriptfiles _defining_ completion scriptblocks like this: `*.tepp.ps1` + - Put all your completion assignments in `assignment.ps1` \ No newline at end of file diff --git a/d365bap.tools/readme.md b/d365bap.tools/readme.md new file mode 100644 index 0000000..0554a14 --- /dev/null +++ b/d365bap.tools/readme.md @@ -0,0 +1,17 @@ +# PSFModule guidance + +This is a finished module layout optimized for implementing the PSFramework. + +If you don't care to deal with the details, this is what you need to do to get started seeing results: + + - Add the functions you want to publish to `/functions/` + - Update the `FunctionsToExport` node in the module manifest (d365bap.tools.psd1). All functions you want to publish should be in a list. + - Add internal helper functions the user should not see to `/internal/functions/` + + ## Path Warning + + > If you want your module to be compatible with Linux and MacOS, keep in mind that those OS are case sensitive for paths and files. + + `Import-ModuleFile` is preconfigured to resolve the path of the files specified, so it will reliably convert weird path notations the system can't handle. + Content imported through that command thus need not mind the path separator. + If you want to make sure your code too will survive OS-specific path notations, get used to using `Resolve-path` or the more powerful `Resolve-PSFPath`. \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Compare-BapEnvironmentD365App.Tests.ps1 b/d365bap.tools/tests/functions/Compare-BapEnvironmentD365App.Tests.ps1 new file mode 100644 index 0000000..770411d --- /dev/null +++ b/d365bap.tools/tests/functions/Compare-BapEnvironmentD365App.Tests.ps1 @@ -0,0 +1,88 @@ +Describe "Compare-BapEnvironmentD365App Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Compare-BapEnvironmentD365App).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter SourceEnvironmentId' { + $parameter = (Get-Command Compare-BapEnvironmentD365App).Parameters['SourceEnvironmentId'] + $parameter.Name | Should -Be 'SourceEnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter DestinationEnvironmentId' { + $parameter = (Get-Command Compare-BapEnvironmentD365App).Parameters['DestinationEnvironmentId'] + $parameter.Name | Should -Be 'DestinationEnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ShowDiffOnly' { + $parameter = (Get-Command Compare-BapEnvironmentD365App).Parameters['ShowDiffOnly'] + $parameter.Name | Should -Be 'ShowDiffOnly' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter GeoRegion' { + $parameter = (Get-Command Compare-BapEnvironmentD365App).Parameters['GeoRegion'] + $parameter.Name | Should -Be 'GeoRegion' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 2 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Compare-BapEnvironmentD365App).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -SourceEnvironmentId -DestinationEnvironmentId + __AllParameterSets -SourceEnvironmentId -DestinationEnvironmentId -ShowDiffOnly -GeoRegion -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Compare-BapEnvironmentUser.Tests.ps1 b/d365bap.tools/tests/functions/Compare-BapEnvironmentUser.Tests.ps1 new file mode 100644 index 0000000..c188c9b --- /dev/null +++ b/d365bap.tools/tests/functions/Compare-BapEnvironmentUser.Tests.ps1 @@ -0,0 +1,88 @@ +Describe "Compare-BapEnvironmentUser Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Compare-BapEnvironmentUser).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter SourceEnvironmentId' { + $parameter = (Get-Command Compare-BapEnvironmentUser).Parameters['SourceEnvironmentId'] + $parameter.Name | Should -Be 'SourceEnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter DestinationEnvironmentId' { + $parameter = (Get-Command Compare-BapEnvironmentUser).Parameters['DestinationEnvironmentId'] + $parameter.Name | Should -Be 'DestinationEnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ShowDiffOnly' { + $parameter = (Get-Command Compare-BapEnvironmentUser).Parameters['ShowDiffOnly'] + $parameter.Name | Should -Be 'ShowDiffOnly' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter IncludeAppIds' { + $parameter = (Get-Command Compare-BapEnvironmentUser).Parameters['IncludeAppIds'] + $parameter.Name | Should -Be 'IncludeAppIds' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Compare-BapEnvironmentUser).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -SourceEnvironmentId -DestinationEnvironmentId + __AllParameterSets -SourceEnvironmentId -DestinationEnvironmentId -ShowDiffOnly -IncludeAppIds -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Get-BapEnvironment.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironment.Tests.ps1 new file mode 100644 index 0000000..822eacd --- /dev/null +++ b/d365bap.tools/tests/functions/Get-BapEnvironment.Tests.ps1 @@ -0,0 +1,49 @@ +Describe "Get-BapEnvironment Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Get-BapEnvironment).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Get-BapEnvironment).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Get-BapEnvironment).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets - + __AllParameterSets -EnvironmentId -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentApplicationUser.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentApplicationUser.Tests.ps1 new file mode 100644 index 0000000..60a0551 --- /dev/null +++ b/d365bap.tools/tests/functions/Get-BapEnvironmentApplicationUser.Tests.ps1 @@ -0,0 +1,49 @@ +Describe "Get-BapEnvironmentApplicationUser Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Get-BapEnvironmentApplicationUser).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Get-BapEnvironmentApplicationUser).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Get-BapEnvironmentApplicationUser).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -EnvironmentId + __AllParameterSets -EnvironmentId -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentD365App.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentD365App.Tests.ps1 new file mode 100644 index 0000000..2eae462 --- /dev/null +++ b/d365bap.tools/tests/functions/Get-BapEnvironmentD365App.Tests.ps1 @@ -0,0 +1,101 @@ +Describe "Get-BapEnvironmentD365App Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Get-BapEnvironmentD365App).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter Name' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['Name'] + $parameter.Name | Should -Be 'Name' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter InstallState' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['InstallState'] + $parameter.Name | Should -Be 'InstallState' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 2 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter GeoRegion' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['GeoRegion'] + $parameter.Name | Should -Be 'GeoRegion' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 3 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter UpdatesOnly' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['UpdatesOnly'] + $parameter.Name | Should -Be 'UpdatesOnly' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Get-BapEnvironmentD365App).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -EnvironmentId + __AllParameterSets -EnvironmentId -Name -InstallState -GeoRegion -UpdatesOnly -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 new file mode 100644 index 0000000..41f3d28 --- /dev/null +++ b/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 @@ -0,0 +1,49 @@ +Describe "Get-BapEnvironmentDetails Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Get-BapEnvironmentDetails).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -EnvironmentId + __AllParameterSets -EnvironmentId -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentUser.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentUser.Tests.ps1 new file mode 100644 index 0000000..fc1f1ee --- /dev/null +++ b/d365bap.tools/tests/functions/Get-BapEnvironmentUser.Tests.ps1 @@ -0,0 +1,62 @@ +Describe "Get-BapEnvironmentUser Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Get-BapEnvironmentUser).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Get-BapEnvironmentUser).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter IncludeAppIds' { + $parameter = (Get-Command Get-BapEnvironmentUser).Parameters['IncludeAppIds'] + $parameter.Name | Should -Be 'IncludeAppIds' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter AsExcelOutput' { + $parameter = (Get-Command Get-BapEnvironmentUser).Parameters['AsExcelOutput'] + $parameter.Name | Should -Be 'AsExcelOutput' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -EnvironmentId + __AllParameterSets -EnvironmentId -IncludeAppIds -AsExcelOutput + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/Invoke-BapEnvironmentInstallD365App.Tests.ps1 b/d365bap.tools/tests/functions/Invoke-BapEnvironmentInstallD365App.Tests.ps1 new file mode 100644 index 0000000..07d01ca --- /dev/null +++ b/d365bap.tools/tests/functions/Invoke-BapEnvironmentInstallD365App.Tests.ps1 @@ -0,0 +1,49 @@ +Describe "Invoke-BapEnvironmentInstallD365App Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Invoke-BapEnvironmentInstallD365App).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter EnvironmentId' { + $parameter = (Get-Command Invoke-BapEnvironmentInstallD365App).Parameters['EnvironmentId'] + $parameter.Name | Should -Be 'EnvironmentId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter PackageId' { + $parameter = (Get-Command Invoke-BapEnvironmentInstallD365App).Parameters['PackageId'] + $parameter.Name | Should -Be 'PackageId' + $parameter.ParameterType.ToString() | Should -Be System.String[] + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + } + + Describe "Testing parameterset __AllParameterSets" { + <# + __AllParameterSets -EnvironmentId -PackageId + __AllParameterSets -EnvironmentId -PackageId + #> + } + +} \ No newline at end of file diff --git a/d365bap.tools/tests/functions/readme.md b/d365bap.tools/tests/functions/readme.md new file mode 100644 index 0000000..f2b2ef0 --- /dev/null +++ b/d365bap.tools/tests/functions/readme.md @@ -0,0 +1,7 @@ +# Description + +This is where the function tests go. + +Make sure to put them in folders reflecting the actual module structure. + +It is not necessary to differentiate between internal and public functions here. \ No newline at end of file diff --git a/d365bap.tools/tests/general/FileIntegrity.Exceptions.ps1 b/d365bap.tools/tests/general/FileIntegrity.Exceptions.ps1 new file mode 100644 index 0000000..c9082f8 --- /dev/null +++ b/d365bap.tools/tests/general/FileIntegrity.Exceptions.ps1 @@ -0,0 +1,51 @@ +# List of forbidden commands +$global:BannedCommands = @( + 'Write-Host' + 'Write-Verbose' + 'Write-Warning' + 'Write-Error' + 'Write-Output' + 'Write-Information' + 'Write-Debug' + + # Use CIM instead where possible + 'Get-WmiObject' + 'Invoke-WmiMethod' + 'Register-WmiEvent' + 'Remove-WmiObject' + 'Set-WmiInstance' + + # Use Get-WinEvent instead + 'Get-EventLog' + + # User Preference should not be used in automation + 'Clear-Host' # Console Screen belongs to the user + 'Set-Location' # Console path belongs to the user. Use $PSScriptRoot instead. + + # Dynamic Variables are undesirable. Use hashtable instead. + 'Get-Variable' + 'Set-Variable' + 'Clear-Variable' + 'Remove-Variable' + 'New-Variable' + + # Dynamic Code execution should not require this + 'Invoke-Expression' # Consider splatting instead. Yes, you can splat parameters for external applications! +) + +<# + Contains list of exceptions for banned cmdlets. + Insert the file names of files that may contain them. + + Example: + "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') +#> +$global:MayContainCommand = @{ + "Write-Host" = @() + "Write-Verbose" = @() + "Write-Warning" = @() + "Write-Error" = @() + "Write-Output" = @() + "Write-Information" = @() + "Write-Debug" = @() +} \ No newline at end of file diff --git a/d365bap.tools/tests/general/FileIntegrity.Tests.ps1 b/d365bap.tools/tests/general/FileIntegrity.Tests.ps1 new file mode 100644 index 0000000..89e6c9c --- /dev/null +++ b/d365bap.tools/tests/general/FileIntegrity.Tests.ps1 @@ -0,0 +1,95 @@ +$moduleRoot = (Resolve-Path "$global:testroot\..").Path + +. "$global:testroot\general\FileIntegrity.Exceptions.ps1" + +Describe "Verifying integrity of module files" { + BeforeAll { + function Get-FileEncoding + { + <# + .SYNOPSIS + Tests a file for encoding. + + .DESCRIPTION + Tests a file for encoding. + + .PARAMETER Path + The file to test + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [string] + $Path + ) + + if ($PSVersionTable.PSVersion.Major -lt 6) + { + [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path + } + else + { + [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path + } + + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } + elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } + elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } + elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } + else { 'Unknown' } + } + } + + Context "Validating PS1 Script files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) + { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' + } + + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty + } + + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) + + It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { + $parseErrors | Should -BeNullOrEmpty + } + + foreach ($command in $global:BannedCommands) + { + if ($global:MayContainCommand["$command"] -notcontains $file.Name) + { + It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { + $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty + } + } + } + } + } + + Context "Validating help.txt help files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) + { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' + } + + It "[$name] Should have no trailing space" -TestCases @{ file = $file } { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 + } + } + } +} \ No newline at end of file diff --git a/d365bap.tools/tests/general/Help.Exceptions.ps1 b/d365bap.tools/tests/general/Help.Exceptions.ps1 new file mode 100644 index 0000000..f9c9bd7 --- /dev/null +++ b/d365bap.tools/tests/general/Help.Exceptions.ps1 @@ -0,0 +1,26 @@ +# List of functions that should be ignored +$global:FunctionHelpTestExceptions = @( + +) + +<# + List of arrayed enumerations. These need to be treated differently. Add full name. + Example: + + "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]" +#> +$global:HelpTestEnumeratedArrays = @( + +) + +<# + Some types on parameters just fail their validation no matter what. + For those it becomes possible to skip them, by adding them to this hashtable. + Add by following this convention: = @() + Example: + + "Get-DbaCmObject" = @("DoNotUse") +#> +$global:HelpTestSkipParameterType = @{ + +} diff --git a/d365bap.tools/tests/general/Help.Tests.ps1 b/d365bap.tools/tests/general/Help.Tests.ps1 new file mode 100644 index 0000000..804a55d --- /dev/null +++ b/d365bap.tools/tests/general/Help.Tests.ps1 @@ -0,0 +1,147 @@ +<# + .NOTES + The original test this is based upon was written by June Blender. + After several rounds of modifications it stands now as it is, but the honor remains hers. + + Thank you June, for all you have done! + + .DESCRIPTION + This test evaluates the help for all commands in a module. + + .PARAMETER SkipTest + Disables this test. + + .PARAMETER CommandPath + List of paths under which the script files are stored. + This test assumes that all functions have their own file that is named after themselves. + These paths are used to search for commands that should exist and be tested. + Will search recursively and accepts wildcards, make sure only functions are found + + .PARAMETER ModuleName + Name of the module to be tested. + The module must already be imported + + .PARAMETER ExceptionsFile + File in which exceptions and adjustments are configured. + In it there should be two arrays and a hashtable defined: + $global:FunctionHelpTestExceptions + $global:HelpTestEnumeratedArrays + $global:HelpTestSkipParameterType + These can be used to tweak the tests slightly in cases of need. + See the example file for explanations on each of these usage and effect. +#> +[CmdletBinding()] +Param ( + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), + + [string] + $ModuleName = "d365bap.tools", + + [string] + $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" +) +if ($SkipTest) { return } +. $ExceptionsFile + +$includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName +$commandTypes = @('Cmdlet', 'Function') +if ($PSVersionTable.PSEdition -eq 'Desktop' ) { $commandTypes += 'Workflow' } +$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTypes | Where-Object Name -In $includedNames + +## When testing help, remember that help is cached at the beginning of each session. +## To test, restart session. + + +foreach ($command in $commands) { + $commandName = $command.Name + + # Skip all functions that are on the exclusions list + if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } + + # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets + $Help = Get-Help $commandName -ErrorAction SilentlyContinue + + Describe "Test help for $commandName" { + + # If help is not found, synopsis in auto-generated help is the syntax diagram + It "should not be auto-generated" -TestCases @{ Help = $Help } { + $Help.Synopsis | Should -Not -BeLike '*`[``]*' + } + + # Should be a description for every function + It "gets description for $commandName" -TestCases @{ Help = $Help } { + $Help.Description | Should -Not -BeNullOrEmpty + } + + # Should be at least one example + It "gets example code from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + + # Should be at least one example description + It "gets example help from $commandName" -TestCases @{ Help = $Help } { + ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } + + Context "Test parameter help for $commandName" { + + $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'ProgressAction' + + $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common + $parameterNames = $parameters.Name + $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique + foreach ($parameter in $parameters) { + $parameterName = $parameter.Name + $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName + + # Should be a description for every parameter + It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } + + $codeMandatory = $parameter.IsMandatory.toString() + It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { + $parameterHelp.Required | Should -Be $codeMandatory + } + + if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } + + $codeType = $parameter.ParameterType.Name + + if ($parameter.ParameterType.IsEnum) { + # Enumerations often have issues with the typename not being reliably available + $names = $parameter.ParameterType::GetNames($parameter.ParameterType) + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + } + elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { + # Enumerations often have issues with the typename not being reliably available + $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + } + else { + # To avoid calling Trim method on a null object. + $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { + $helpType | Should -be $codeType + } + } + } + foreach ($helpParm in $HelpParameterNames) { + # Shouldn't find extra parameters in help. + It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { + $helpParm -in $parameterNames | Should -Be $true + } + } + } + } +} \ No newline at end of file diff --git a/d365bap.tools/tests/general/Manifest.Tests.ps1 b/d365bap.tools/tests/general/Manifest.Tests.ps1 new file mode 100644 index 0000000..920f427 --- /dev/null +++ b/d365bap.tools/tests/general/Manifest.Tests.ps1 @@ -0,0 +1,62 @@ +Describe "Validating the module manifest" { + $moduleRoot = (Resolve-Path "$global:testroot\..").Path + $manifest = ((Get-Content "$moduleRoot\d365bap.tools.psd1") -join "`n") | Invoke-Expression + Context "Basic resources validation" { + $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" + It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject + $functions | Should -BeNullOrEmpty + } + It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject + $functions | Should -BeNullOrEmpty + } + + It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { + $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" + $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty + } + } + + Context "Individual file validation" { + It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { + Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true + } + + foreach ($format in $manifest.FormatsToProcess) + { + It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { + Test-Path "$moduleRoot\$format" | Should -Be $true + } + } + + foreach ($type in $manifest.TypesToProcess) + { + It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { + Test-Path "$moduleRoot\$type" | Should -Be $true + } + } + + foreach ($assembly in $manifest.RequiredAssemblies) + { + if ($assembly -like "*.dll") { + It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + Test-Path "$moduleRoot\$assembly" | Should -Be $true + } + } + else { + It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { + { Add-Type -AssemblyName $assembly } | Should -Not -Throw + } + } + } + + foreach ($tag in $manifest.PrivateData.PSData.Tags) + { + It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { + $tag -match " " | Should -Be $false + } + } + } +} \ No newline at end of file diff --git a/d365bap.tools/tests/general/PSScriptAnalyzer.Tests.ps1 b/d365bap.tools/tests/general/PSScriptAnalyzer.Tests.ps1 new file mode 100644 index 0000000..4b9b2d2 --- /dev/null +++ b/d365bap.tools/tests/general/PSScriptAnalyzer.Tests.ps1 @@ -0,0 +1,50 @@ +[CmdletBinding()] +Param ( + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") +) + +BeforeDiscovery { + if ($SkipTest) { return } + + $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList + + # Create an array containing the path and basename of all files to test + $commandFiles = $CommandPath | ForEach-Object { + Get-ChildItem -Path $_ -Recurse | Where-Object Name -like "*.ps1" + } | ForEach-Object { + @{ + BaseName = $_.BaseName + FullName = $_.FullName + } + } + + # Create an array contain all rules + $scriptAnalyzerRules = Get-ScriptAnalyzerRule | ForEach-Object { + @{ + RuleName = $_.RuleName + } + } +} + +Describe 'Invoking PSScriptAnalyzer against commandbase' { + + Context "Analyzing " -ForEach $commandFiles { + BeforeAll { + $analysis = Invoke-ScriptAnalyzer -Path $FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess + } + + It "Should pass " -Foreach $scriptAnalyzerRules { + # Test if the rule is present and if so create a string containing more info which will be shown in the details of the test output. If it's empty the test is succesfull as there is no problem with this rule. + $analysis | Where-Object RuleName -EQ $RuleName | Foreach-Object { + # Create a string + "$($_.Severity) at Line $($_.Line) Column $($_.Column) with '$($_.Extent)'" + # Add the data (and supress the output) to the global variable for later use + $null = $global:__pester_data.ScriptAnalyzer.Add($_) + } | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/d365bap.tools/tests/general/strings.Exceptions.ps1 b/d365bap.tools/tests/general/strings.Exceptions.ps1 new file mode 100644 index 0000000..b4c91f2 --- /dev/null +++ b/d365bap.tools/tests/general/strings.Exceptions.ps1 @@ -0,0 +1,36 @@ +$exceptions = @{ } + +<# +A list of entries that MAY be in the language files, without causing the tests to fail. +This is commonly used in modules that generate localized messages straight from C#. +Specify the full key as it is written in the language files, do not prepend the modulename, +as you would have to in C# code. + +Example: +$exceptions['LegalSurplus'] = @( + 'Exception.Streams.FailedCreate' + 'Exception.Streams.FailedDispose' +) +#> +$exceptions['LegalSurplus'] = @( + +) +<# +A list of entries that MAY be used without needing to have text defined. +This is intended for modules (re-)using strings provided by another module +#> +$exceptions['NoTextNeeded'] = @( + 'Validate.FSPath' + 'Validate.FSPath.File' + 'Validate.FSPath.FileOrParent' + 'Validate.FSPath.Folder' + 'Validate.Path' + 'Validate.Path.Container' + 'Validate.Path.Leaf' + 'Validate.TimeSpan.Positive' + 'Validate.Uri.Absolute' + 'Validate.Uri.Absolute.File' + 'Validate.Uri.Absolute.Https' +) + +$exceptions \ No newline at end of file diff --git a/d365bap.tools/tests/general/strings.Tests.ps1 b/d365bap.tools/tests/general/strings.Tests.ps1 new file mode 100644 index 0000000..b5bf667 --- /dev/null +++ b/d365bap.tools/tests/general/strings.Tests.ps1 @@ -0,0 +1,27 @@ +<# +.DESCRIPTION + This test verifies, that all strings that have been used, + are listed in the language files and thus have a message being displayed. + + It also checks, whether the language files have orphaned entries that need cleaning up. +#> + + + +Describe "Testing localization strings" { + $moduleRoot = (Get-Module d365bap.tools).ModuleBase + $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot + $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" + + foreach ($stringEntry in $stringsResults) { + if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry + It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry; exceptions = $exceptions } { + if ($exceptions.LegalSurplus -notcontains $stringEntry.String) { + $stringEntry.Surplus | Should -BeFalse + } + if ($exceptions.NoTextNeeded -notcontains $stringEntry.String) { + $stringEntry.Text | Should -Not -BeNullOrEmpty + } + } + } +} \ No newline at end of file diff --git a/d365bap.tools/tests/pester.ps1 b/d365bap.tools/tests/pester.ps1 new file mode 100644 index 0000000..e3ab2a9 --- /dev/null +++ b/d365bap.tools/tests/pester.ps1 @@ -0,0 +1,113 @@ +param ( + $TestGeneral = $true, + + $TestFunctions = $true, + + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] + [Alias('Show')] + $Output = "None", + + $Include = "*", + + $Exclude = "" +) + +Write-PSFMessage -Level Important -Message "Starting Tests" + +Write-PSFMessage -Level Important -Message "Importing Module" + +$global:testroot = $PSScriptRoot +$global:__pester_data = @{ } + +Remove-Module d365bap.tools -ErrorAction Ignore +Import-Module "$PSScriptRoot\..\d365bap.tools.psd1" +Import-Module "$PSScriptRoot\..\d365bap.tools.psm1" -Force + +# Need to import explicitly so we can use the configuration class +Import-Module Pester + +Write-PSFMessage -Level Important -Message "Creating test result folder" +$null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force + +$totalFailed = 0 +$totalRun = 0 + +$testresults = @() +$config = [PesterConfiguration]::Default +$config.TestResult.Enabled = $true + +#region Run General Tests +if ($TestGeneral) +{ + Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" + foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1")) + { + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } + } + } + } +} +#endregion Run General Tests + +$global:__pester_data.ScriptAnalyzer | Out-Host + +#region Test Commands +if ($TestFunctions) +{ + Write-PSFMessage -Level Important -Message "Proceeding with individual tests" + foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) + { + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $config.Run.Path = $file.FullName + $config.Run.PassThru = $true + $config.Output.Verbosity = $Output + $results = Invoke-Pester -Configuration $config + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { + $testresults += [pscustomobject]@{ + Block = $_.Block + Name = "It $($_.Name)" + Result = $_.Result + Message = $_.ErrorRecord.DisplayErrorMessage + } + } + } + } +} +#endregion Test Commands + +$testresults | Sort-Object Describe, Context, Name, Result, Message | Format-List + +if ($totalFailed -eq 0) { Write-PSFMessage -Level Critical -Message "All $totalRun tests executed without a single failure!" } +else { Write-PSFMessage -Level Critical -Message "$totalFailed tests out of $totalRun tests failed!" } + +if ($totalFailed -gt 0) +{ + throw "$totalFailed / $totalRun tests failed!" +} \ No newline at end of file diff --git a/d365bap.tools/tests/readme.md b/d365bap.tools/tests/readme.md new file mode 100644 index 0000000..43bb2fa --- /dev/null +++ b/d365bap.tools/tests/readme.md @@ -0,0 +1,31 @@ +# Description + +This is the folder, where all the tests go. + +Those are subdivided in two categories: + + - General + - Function + +## General Tests + +General tests are function generic and test for general policies. + +These test scan answer questions such as: + + - Is my module following my style guides? + - Does any of my scripts have a syntax error? + - Do my scripts use commands I do not want them to use? + - Do my commands follow best practices? + - Do my commands have proper help? + +Basically, these allow a general module health check. + +These tests are already provided as part of the template. + +## Function Tests + +A healthy module should provide unit and integration tests for the commands & components it ships. +Only then can be guaranteed, that they will actually perform as promised. + +However, as each such test must be specific to the function it tests, there cannot be much in the way of templates. \ No newline at end of file diff --git a/d365bap.tools/xml/d365bap.tools.Format.ps1xml b/d365bap.tools/xml/d365bap.tools.Format.ps1xml new file mode 100644 index 0000000..4e79f9b --- /dev/null +++ b/d365bap.tools/xml/d365bap.tools.Format.ps1xml @@ -0,0 +1,313 @@ + + + + + + Foo.Bar + + Foo.Bar + + + + + + + + + + + + Foo + + + Bar + + + + + + + + D365Bap.Tools.Package + + D365Bap.Tools.Package + + + + + 36 + + + 30 + + + 19 + + + 19 + + + 15 + + + 30 + + + 15 + + + + + + + PackageId + + + PackageName + + + AvailableVersion + + + InstalledVersion + + + UpdateAvailable + + + AppName + + + InstallState + + + + + + + + D365Bap.Tools.Environment + + D365Bap.Tools.Environment + + + + + 36 + + + 15 + + + 20 + + + 10 + + + 40 + + + 40 + + + + + + + PpacEnvId + + + PpacEnvRegion + + + PpacEnvName + + + PpacEnvSku + + + LinkedAppLcsEnvUri + + + LinkedMetaPpacEnvUri + + + + + + + + D365Bap.Tools.Compare.Package + + D365Bap.Tools.Compare.Package + + + + + 36 + + + 30 + + + 19 + + + 19 + + + 30 + + + + + + + PackageId + + + PackageName + + + SourceVersion + + + DestinationVersion + + + AppName + + + + + + + + D365Bap.Tools.User + + D365Bap.Tools.User + + + + + 30 + + + 30 + + + 20 + + + 36 + + + + + + + Email + + + Name + + + AppId + + + Systemuserid + + + + + + + + D365Bap.Tools.Compare.User + + D365Bap.Tools.Compare.User + + + + + 30 + + + 30 + + + 20 + + + 15 + + + 13 + + + + + + + Email + + + Name + + + AppId + + + SourceId + + + DestinationId + + + + + + + + D365Bap.Tools.AppUser + + D365Bap.Tools.AppUser + + + + + 36 + + + 30 + + + 36 + + + 36 + + + + + + + AppId + + + AppName + + + ApplicationUserId + + + SolutionId + + + + + + + + \ No newline at end of file diff --git a/d365bap.tools/xml/d365bap.tools.Types.ps1xml b/d365bap.tools/xml/d365bap.tools.Types.ps1xml new file mode 100644 index 0000000..d911fcb --- /dev/null +++ b/d365bap.tools/xml/d365bap.tools.Types.ps1xml @@ -0,0 +1,37 @@ + + + + + Deserialized.Foo.Bar + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + Foo.Bar + + + + + + + + Foo.Bar + + + SerializationData + + PSFramework.Serialization.SerializationTypeConverter + GetSerializationData + + + + + PSFramework.Serialization.SerializationTypeConverter + + + \ No newline at end of file diff --git a/d365bap.tools/xml/readme.md b/d365bap.tools/xml/readme.md new file mode 100644 index 0000000..00e28c4 --- /dev/null +++ b/d365bap.tools/xml/readme.md @@ -0,0 +1,43 @@ +# XML + +This is the folder where project XML files go, notably: + + - Format XML + - Type Extension XML + +External help files should _not_ be placed in this folder! + +## Notes on Files and Naming + +There should be only one format file and one type extension file per project, as importing them has a notable impact on import times. + + - The Format XML should be named `d365bap.tools.Format.ps1xml` + - The Type Extension XML should be named `d365bap.tools.Types.ps1xml` + +## Tools + +### New-PSMDFormatTableDefinition + +This function will take an input object and generate format xml for an auto-sized table. + +It provides a simple way to get started with formats. + +### Get-PSFTypeSerializationData + +``` +C# Warning! +This section is only interest if you're using C# together with PowerShell. +``` + +This function generates type extension XML that allows PowerShell to convert types written in C# to be written to file and restored from it without being 'Deserialized'. Also works for jobs or remoting, if both sides have the `PSFramework` module and type extension loaded. + +In order for a class to be eligible for this, it needs to conform to the following rules: + + - Have the `[Serializable]` attribute + - Be public + - Have an empty constructor + - Allow all public properties/fields to be set (even if setting it doesn't do anything) without throwing an exception. + +``` +non-public properties and fields will be lost in this process! +``` \ No newline at end of file diff --git a/docs/Compare-BapEnvironmentD365App.md b/docs/Compare-BapEnvironmentD365App.md new file mode 100644 index 0000000..8679e85 --- /dev/null +++ b/docs/Compare-BapEnvironmentD365App.md @@ -0,0 +1,153 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Compare-BapEnvironmentD365App + +## SYNOPSIS +Compare environment D365 Apps + +## SYNTAX + +``` +Compare-BapEnvironmentD365App [-SourceEnvironmentId] [-DestinationEnvironmentId] + [-ShowDiffOnly] [[-GeoRegion] ] [-AsExcelOutput] [] +``` + +## DESCRIPTION +This enables the user to compare 2 x environments, with one as a source and the other as a destination + +It will only look for installed D365 Apps on the source, and use this as a baseline against the destination + +## EXAMPLES + +### EXAMPLE 1 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 +``` + +This will get all installed D365 Apps from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. + +Sample output: +PackageId PackageName SourceVersion DestinationVersion AppName +--------- ----------- ------------- ------------------ ------- +ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… +1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… +6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… +42cc1442-194f-462b-a325-ce5b5f18c02d msdyn_EmailAddressValidation 1.0.0.4 1.0.0.4 Data Valid… + +### EXAMPLE 2 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -ShowDiffOnly +``` + +This will get all installed D365 Apps from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +It will filter out results, to only include those where the DestinationVersions is different from the SourceVersion. + +Sample output: +PackageId PackageName SourceVersion DestinationVersion AppName +--------- ----------- ------------- ------------------ ------- +ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24012.1005 Agent Prod… +1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24012.1013 appprofile… +6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… +7523d261-f1be-46e7-8e68-f3de16eeabbb DualWriteCoreAnchor 1.0.24022.4 1.0.24011.1 Dual-write… + +## PARAMETERS + +### -SourceEnvironmentId +Environment Id of the source environment that you want to utilized as the baseline for the compare + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DestinationEnvironmentId +Environment Id of the destination environment that you want to validate against the baseline (source) + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ShowDiffOnly +Instruct the cmdlet to only output the differences that are not aligned between the source and destination + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -GeoRegion +Instructs the cmdlet which Geo / Region the environment is located + +The default value is: "Emea" + +This is mandatory field from the API specification, we don't have the full list of values at the time of writing + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: Emea +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/docs/Compare-BapEnvironmentUser.md b/docs/Compare-BapEnvironmentUser.md new file mode 100644 index 0000000..36c9767 --- /dev/null +++ b/docs/Compare-BapEnvironmentUser.md @@ -0,0 +1,117 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Compare-BapEnvironmentUser + +## SYNOPSIS +Short description + +## SYNTAX + +``` +Compare-BapEnvironmentUser [-SourceEnvironmentId] [-DestinationEnvironmentId] [-ShowDiffOnly] + [-IncludeAppIds] [-AsExcelOutput] [] +``` + +## DESCRIPTION +Long description + +## EXAMPLES + +### EXAMPLE 1 +``` +An example +``` + +## PARAMETERS + +### -SourceEnvironmentId +Parameter description + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DestinationEnvironmentId +Parameter description + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ShowDiffOnly +Parameter description + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IncludeAppIds +Parameter description + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Parameter description + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +General notes + +## RELATED LINKS diff --git a/docs/Get-BapEnvironment.md b/docs/Get-BapEnvironment.md new file mode 100644 index 0000000..c0fbc2e --- /dev/null +++ b/docs/Get-BapEnvironment.md @@ -0,0 +1,97 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Get-BapEnvironment + +## SYNOPSIS +Get environment info + +## SYNTAX + +``` +Get-BapEnvironment [[-EnvironmentId] ] [-AsExcelOutput] [] +``` + +## DESCRIPTION +This enables the user to query and validate all environments that are available from inside PPAC + +It utilizes the "https://api.bap.microsoft.com" REST API + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-BapEnvironment +``` + +This will query for ALL available environments. + +Sample output: +PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri +--------- ------------- ----------- ---------- ------------------ +32c6b196-ef52-4c43-93cf-6ecba51e6aa1 europe new-uat Sandbox https://new-uat.sandbox.operatio… +eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… +d45936a7-0408-4b79-94d1-19e4c6e5a52e europe new-golden Sandbox https://new-golden.sandbox.opera… +Default-e210bc90-e54b-4544-a9b8-b1f… europe New Customer Default + +### EXAMPLE 2 +``` +Get-BapEnvironment -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 +``` + +This will query for the specific environment. + +Sample output: +PpacEnvId PpacEnvRegion PpacEnvName PpacEnvSku LinkedAppLcsEnvUri +--------- ------------- ----------- ---------- ------------------ +eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… + +## PARAMETERS + +### -EnvironmentId +The id of the environment that you want to work against + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: * +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/docs/Get-BapEnvironmentApplicationUser.md b/docs/Get-BapEnvironmentApplicationUser.md new file mode 100644 index 0000000..5252760 --- /dev/null +++ b/docs/Get-BapEnvironmentApplicationUser.md @@ -0,0 +1,87 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Get-BapEnvironmentApplicationUser + +## SYNOPSIS +Get application users from environment + +## SYNTAX + +``` +Get-BapEnvironmentApplicationUser [-EnvironmentId] [-AsExcelOutput] [] +``` + +## DESCRIPTION +Enables the user to fetch all application users from the environment + +Utilizes the built-in "applicationusers" OData entity + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-BapEnvironmentApplicationUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 +``` + +This will fetch all ApplicationUsers from the environment. + +Sample output: +AppId AppName ApplicationUserId SolutionId +----- ------- ----------------- ---------- +b6e52ceb-f771-41ff-bd99-917523b28eaf AIBuilder_StructuredML_Prod_C… 3bafba76-60bf-413d-a4c4-5c49ccabfb12 bf85e0c8-aa47… +21ceaf7c-054c-43f6-8b14-ef6d04b90a21 AIBuilderProd 560c9a6c-4535-4066-a415-480d1493cf98 bf85e0c8-aa47… +c76313fd-5c6f-4f1f-9869-c884fa7fe226 AppDeploymentOrchestration d88a3535-ebf0-4b2b-ad23-90e686660a64 99aee001-009e… +29494271-7e38-4433-8bf8-06d335299a17 AriaMdlExporter 8bf8862f-5036-42b0-a4f8-1b638db7896b 99aee001-009e… + +## PARAMETERS + +### -EnvironmentId +The id of the environment that you want to work against + +This can be obtained from the Get-BapEnvironment cmdlet + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/docs/Get-BapEnvironmentD365App.md b/docs/Get-BapEnvironmentD365App.md new file mode 100644 index 0000000..2b4a486 --- /dev/null +++ b/docs/Get-BapEnvironmentD365App.md @@ -0,0 +1,243 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Get-BapEnvironmentD365App + +## SYNOPSIS +Get D365 App from the environment + +## SYNTAX + +``` +Get-BapEnvironmentD365App [-EnvironmentId] [[-Name] ] [[-InstallState] ] + [[-GeoRegion] ] [-UpdatesOnly] [-AsExcelOutput] [] +``` + +## DESCRIPTION +This enables the user to analyze and validate the current D365 Apps and their state, on a given environment + +It can show all available D365 Apps - including their InstallState + +It can show only installed D365 Apps + +It can show only installed D365 Apps, with available updates + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 +``` + +This will query the environment for ALL available D365 Apps. +It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + +Sample output: + +PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable +--------- ----------- ---------------- ---------------- --------------- +cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A +ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True +b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A +1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False +9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A +6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False + +### EXAMPLE 2 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed +``` + +This will query the environment for installed only D365 Apps. +It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + +Sample output: +PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable +--------- ----------- ---------------- ---------------- --------------- +ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True +1c0a1237-9408-4b99-9fec-39696d99287b msdyn_AppProfileManagerAnchor 10.1.24021.1005 10.1.24021.1005 False +6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 1.0.0.1 False + +### EXAMPLE 3 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState None +``` + +This will query the environment for NON-installed only D365 Apps. +It will output all details available for the D365 Apps. + +Sample output: +PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable +--------- ----------- ---------------- ---------------- --------------- +cea6753e-9c74-4aa9-85a1-5869105115d3 msdyn_ExportControlAnchor 1.0.2553.1 N/A +b1676368-b448-4fbd-a238-9b6ddc36be81 SharePointFormProcessing 202209.5.2901.0 N/A +9f4c778b-2f0b-416f-8166-e96da680ffb2 mpa_AwardsAndRecognition 1.0.0.32 N/A + +### EXAMPLE 4 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -Name "*ProviderAnchor*" +``` + +This will query the environment for ALL D365 Apps. +It will filter the output to only those who match the search pattern "*ProviderAnchor*". +It will compare available vs installed D365 Apps, and indicate whether an update is available of not. + +Sample output: +PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable +--------- ----------- ---------------- ---------------- --------------- +c0cb37fd-d7f4-40f2-8592-64ec71a2c508 msft_ConnectorProviderAnchor 9.0.0.1618 9.0.0.1618 False + +### EXAMPLE 5 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -UpdatesOnly +``` + +This will query the environment for ALL available D365 Apps. +It will compare available vs installed D365 Apps, and indicate whether an update is available of not. +It will filter the output to only containing those who have an update available. + +Sample output: +PackageId PackageName AvailableVersion InstalledVersion UpdateAvailable +--------- ----------- ---------------- ---------------- --------------- +ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.1005 9.2.24019.1005 True + +### EXAMPLE 6 +``` +$appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) +``` + +PS C:\\\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds + +This will query the environment for installed only D365 Apps. +It will filter the output to only containing those who have an update available. +It will persist the PackageIds for each D365 App, into an array. +It will invoke the installation process using the Invoke-BapEnvironmentInstallD365App cmdlet. + +## PARAMETERS + +### -EnvironmentId +The id of the environment that you want to work against + +This can be obtained from the Get-BapEnvironment cmdlet + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Name +Name of the D365 App / Package that you are looking for + +It supports wildcard searching, which is validated against the following properties: +* AppName / ApplicationName +* PackageName / UniqueName + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: * +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InstallState +Instruct the cmdlet which install states that you want to have included in the output + +The default value is: "All" + +Valid values: +* "All" +* "Installed" +* "None" + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 3 +Default value: All +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -GeoRegion +Instructs the cmdlet which Geo / Region the environment is located + +The default value is: "Emea" + +This is mandatory field from the API specification, we don't have the full list of values at the time of writing + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: Emea +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -UpdatesOnly +Instruct the cmdlet to only output D365 Apps that has an update available + +Makes it easier to fully automate the update process of a given environment + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/docs/Get-BapEnvironmentDetails.md b/docs/Get-BapEnvironmentDetails.md new file mode 100644 index 0000000..51f100e --- /dev/null +++ b/docs/Get-BapEnvironmentDetails.md @@ -0,0 +1,71 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Get-BapEnvironmentDetails + +## SYNOPSIS +Short description + +## SYNTAX + +``` +Get-BapEnvironmentDetails [-EnvironmentId] [-AsExcelOutput] [] +``` + +## DESCRIPTION +Long description + +## EXAMPLES + +### EXAMPLE 1 +``` +An example +``` + +## PARAMETERS + +### -EnvironmentId +Parameter description + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Parameter description + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +General notes + +## RELATED LINKS diff --git a/docs/Get-BapEnvironmentUser.md b/docs/Get-BapEnvironmentUser.md new file mode 100644 index 0000000..3b248f5 --- /dev/null +++ b/docs/Get-BapEnvironmentUser.md @@ -0,0 +1,122 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Get-BapEnvironmentUser + +## SYNOPSIS +Get users from environment + +## SYNTAX + +``` +Get-BapEnvironmentUser [-EnvironmentId] [-IncludeAppIds] [-AsExcelOutput] [] +``` + +## DESCRIPTION +Enables the user to fetch all users from the environment + +Utilizes the built-in "systemusers" OData entity + +Allows the user to include all users, based on those who has the ApplicationId property filled + +## EXAMPLES + +### EXAMPLE 1 +``` +Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 +``` + +This will fetch all oridinary users from the environment. + +Sample output: +Email Name AppId Systemuserid +----- ---- ----- ------------ +SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 +INTEGRATION baabe592-2860-4d1a-9365-e95317372498 +aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a +ade@temp.com Alex Denver 39309a5c-7676-4c8a-b702-719fb92c5151 + +### EXAMPLE 2 +``` +Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 +``` + +This will fetch all users from the environment. +It will include the ones with the ApplicationId property filled. + +Sample output: +Email Name AppId Systemuserid +----- ---- ----- ------------ +SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 +INTEGRATION baabe592-2860-4d1a-9365-e95317372498 +aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a +AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-4c5e-ac84-1831f4dc7d5f + +## PARAMETERS + +### -EnvironmentId +The id of the environment that you want to work against + +This can be obtained from the Get-BapEnvironment cmdlet + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -IncludeAppIds +Instruct the cmdlet to include all users that are available from the "systemusers" OData Entity + +Simply includes those who has the ApplicationId property filled + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AsExcelOutput +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/docs/Invoke-BapEnvironmentInstallD365App.md b/docs/Invoke-BapEnvironmentInstallD365App.md new file mode 100644 index 0000000..d159818 --- /dev/null +++ b/docs/Invoke-BapEnvironmentInstallD365App.md @@ -0,0 +1,131 @@ +--- +external help file: d365bap.tools-help.xml +Module Name: d365bap.tools +online version: +schema: 2.0.0 +--- + +# Invoke-BapEnvironmentInstallD365App + +## SYNOPSIS +Invoke the installation of a D365 App in a given environment + +## SYNTAX + +``` +Invoke-BapEnvironmentInstallD365App [-EnvironmentId] [-PackageId] [] +``` + +## DESCRIPTION +This enables the invocation of the installation process against the PowerPlatform API (https://api.powerplatform.com) + +The cmdlet will keep requesting the status of all invoked installations, until they all have a NON "Running" state + +It will request this status every 60 seconds + +## EXAMPLES + +### EXAMPLE 1 +``` +Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId 'be69fc64-7393-4c3c-8908-2a1c2e53aef9','6defa8de-87f9-4478-8f9a-a7d685394e24' +``` + +This will install the 2 x D365 Apps, based on the Ids supplied. +It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. + +Sample output (Install initialized): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +Sample output (Partly succeeded installation): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +Sample output (Completely succeeded installation): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +### EXAMPLE 2 +``` +$appIds = @(Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -InstallState Installed -UpdatesOnly | Select-Object -ExpandProperty PackageId) +``` + +PS C:\\\> Invoke-BapEnvironmentInstallD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -PackageId $appIds + +This will find all D365 Apps that has a pending update available. +It will gather the Ids into an array. +It will run the cmdlet and have it get the status of the installation progress until all D365 Apps have been fully installed. + +Sample output (Install initialized): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Running 02/03/2024 13.42.07 02/03/2024 13.42.16 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Running 02/03/2024 13.42.09 02/03/2024 13.42.12 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +Sample output (Partly succeeded installation): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Running 02/03/2024 13.42.09 02/03/2024 13.45.55 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +Sample output (Completely succeeded installation): +status createdDateTime lastActionDateTime error statusMessage operationId +------ --------------- ------------------ ----- ------------- ----------- +Succeeded 02/03/2024 13.42.07 02/03/2024 13.44.48 5c80df7f-d89e-42bd-abeb-98e577ae49f4 +Succeeded 02/03/2024 13.42.09 02/03/2024 13.48.26 6885e0f4-639f-4ebc-b21e-49ce5d5e920d + +## PARAMETERS + +### -EnvironmentId +The id of the environment that you want to work against + +This can be obtained from the Get-BapEnvironment cmdlet + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PackageId +The id of the package(s) that you want to have Installed + +It supports id of current packages, with updates available and new D365 apps + +It support an array as input, so it can invoke multiple D365 App installations + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Author: Mötz Jensen (@Splaxi) + +## RELATED LINKS diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..9472d3f --- /dev/null +++ b/install.ps1 @@ -0,0 +1,2431 @@ +<# + .SYNOPSIS + Installs the d365bap.tools Module from github + + .DESCRIPTION + This script installs the d365bap.tools Module from github. + + It does so by ... + - downloading the specified branch as zip to $env:TEMP + - Unpacking that zip file to a folder in $env:TEMP + - Moving that content to a module folder in either program files (default) or the user profile + + .PARAMETER Branch + The branch to install. Installs master by default. + Unknown branches will terminate the script in error. + + .PARAMETER UserMode + The downloaded module will be moved to the user profile, rather than program files. + + .PARAMETER Scope + By default, the downloaded module will be moved to program files. + Setting this to 'CurrentUser' installs to the userprofile of the current user. + + .PARAMETER Force + The install script will overwrite an existing module. +#> +[CmdletBinding()] +Param ( + [string] + $Branch = "master", + + [switch] + $UserMode, + + [ValidateSet('AllUsers', 'CurrentUser')] + [string] + $Scope = "AllUsers", + + [switch] + $Force +) + +#region Configuration for cloning script +# Name of the module that is being cloned +$ModuleName = "d365bap.tools" + +# Base path to the github repository +$BaseUrl = "https://github.com//d365bap.tools" + +# If the module is in a subfolder of the cloned repository, specify relative path here. Empty string to skip. +$SubFolder = "d365bap.tools" +#endregion Configuration for cloning script + +#region Parameter Calculation +$doUserMode = $false +if ($UserMode) { $doUserMode = $true } +if ($install_CurrentUser) { $doUserMode = $true } +if ($Scope -eq 'CurrentUser') { $doUserMode = $true } + +if ($install_Branch) { $Branch = $install_Branch } +#endregion Parameter Calculation + +#region Utility Functions +function Compress-Archive +{ + <# + .SYNOPSIS + Creates an archive, or zipped file, from specified files and folders. + + .DESCRIPTION + The Compress-Archive cmdlet creates a zipped (or compressed) archive file from one or more specified files or folders. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. An archive file can be compressed by using the compression algorithm specified by the CompressionLevel parameter. + + Because Compress-Archive relies upon the Microsoft .NET Framework API System.IO.Compression.ZipArchive to compress files, the maximum file size that you can compress by using Compress-Archive is currently 2 GB. This is a limitation of the underlying API. + + .PARAMETER Path + Specifies the path or paths to the files that you want to add to the archive zipped file. This parameter can accept wildcard characters. Wildcard characters allow you to add all files in a folder to your zipped archive file. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + + .PARAMETER LiteralPath + Specifies the path or paths to the files that you want to add to the archive zipped file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + + .PARAMETER DestinationPath + Specifies the path to the archive output file. This parameter is required. The specified DestinationPath value should include the desired name of the output zipped file; it specifies either the absolute or relative path to the zipped file. If the file name specified in DestinationPath does not have a .zip file name extension, the cmdlet adds a .zip file name extension. + + .PARAMETER CompressionLevel + Specifies how much compression to apply when you are creating the archive file. Faster compression requires less time to create the file, but can result in larger file sizes. The acceptable values for this parameter are: + + - Fastest. Use the fastest compression method available to decrease processing time; this can result in larger file sizes. + - NoCompression. Do not compress the source files. + - Optimal. Processing time is dependent on file size. + + If this parameter is not specified, the command uses the default value, Optimal. + + .PARAMETER Update + Updates the specified archive by replacing older versions of files in the archive with newer versions of files that have the same names. You can also add this parameter to add files to an existing archive. + + .PARAMETER Force + @{Text=} + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .EXAMPLE + Example 1: Create an archive file + + PS C:\>Compress-Archive -LiteralPath C:\Reference\Draftdoc.docx, C:\Reference\Images\diagram2.vsd -CompressionLevel Optimal -DestinationPath C:\Archives\Draft.Zip + + This command creates a new archive file, Draft.zip, by compressing two files, Draftdoc.docx and diagram2.vsd, specified by the LiteralPath parameter. The compression level specified for this operation is Optimal. + + .EXAMPLE + Example 2: Create an archive with wildcard characters + + PS C:\>Compress-Archive -Path C:\Reference\* -CompressionLevel Fastest -DestinationPath C:\Archives\Draft + + This command creates a new archive file, Draft.zip, in the C:\Archives folder. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. The new archive file contains every file in the C:\Reference folder, because a wildcard character was used in place of specific file names in the Path parameter. The specified compression level is Fastest, which might result in a larger output file, but compresses a large number of files faster. + + .EXAMPLE + Example 3: Update an existing archive file + + PS C:\>Compress-Archive -Path C:\Reference\* -Update -DestinationPath C:\Archives\Draft.Zip + + This command updates an existing archive file, Draft.Zip, in the C:\Archives folder. The command is run to update Draft.Zip with newer versions of existing files that came from the C:\Reference folder, and also to add new files that have been added to C:\Reference since Draft.Zip was initially created. + + .EXAMPLE + Example 4: Create an archive from an entire folder + + PS C:\>Compress-Archive -Path C:\Reference -DestinationPath C:\Archives\Draft + + This command creates an archive from an entire folder, C:\Reference. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. + #> + [CmdletBinding(DefaultParameterSetName = "Path", SupportsShouldProcess = $true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393252")] + param + ( + [parameter (mandatory = $true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $Path, + + [parameter (mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string[]] + $LiteralPath, + + [parameter (mandatory = $true, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [parameter ( + mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateSet("Optimal", "NoCompression", "Fastest")] + [string] + $CompressionLevel = "Optimal", + + [parameter(mandatory = $true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Update = $false, + + [parameter(mandatory = $true, ParameterSetName = "PathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Force = $false + ) + + BEGIN + { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + + $zipFileExtension = ".zip" + + $LocalizedData = ConvertFrom-StringData @' +PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. +ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. +InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. +ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. +ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. +DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. +ArchiveFileIsEmpty=The archive file {0} is empty. +CompressProgressBarText=The archive file '{0}' creation is in progress... +ExpandProgressBarText=The archive file '{0}' expansion is in progress... +AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath patameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. +AddItemtoArchiveFile=Adding '{0}'. +CreateFileAtExpandedPath=Created '{0}'. +InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. +InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. +FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. +DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. +InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. +PreparingToCompressVerboseMessage=Preparing to compress... +PreparingToExpandVerboseMessage=Preparing to expand... +'@ + + #region Utility Functions + function GetResolvedPathHelper + { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + try + { + if ($isLiteralPath) + { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } + else + { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } + catch + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) + { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies + { + + if ($PSEdition -eq "Desktop") + { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath + { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath + { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) + { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + else + { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper + { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) + { + "Fastest" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper + { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) + { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) + { + $sourceFilePaths += $currentPath + } + else + { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) + { + $currentSegmentWeight = 100/[double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) + { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) + { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper + { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper + { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) + { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) + { + $modifiedSourceDirFullName = $sourceDirFullName + } + else + { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } + else + { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) + { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) + { + $subDirFiles.Add($currentContent.FullName) + } + else + { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) + { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper + { + param + ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) + { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try + { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) + { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) + { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } + else + { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) + { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) + { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) + { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + try + { + try + { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } + catch + { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) + { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) + { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) + { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } + finally + { + If ($null -ne $currentFileStream) + { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + If ($null -ne $destStream) + { + $destStream.Dispose() + } + } + } + else + { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) + { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + +<############################################################################################ +# ValidateArchivePathHelper: This is a helper function used to validate the archive file +# path & its file format. The only supported archive file format is .zip +############################################################################################> + function ValidateArchivePathHelper + { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) + { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + else + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + +<############################################################################################ +# ExpandArchiveHelper: This is a helper function used to expand the archive file contents +# to the specified directory. +############################################################################################> + function ExpandArchiveHelper + { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try + { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) + { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) + { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) + { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } + else + { + try + { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) + { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) + { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) + { + if ($force) + { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) + { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) + { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } + else + { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) + { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } + finally + { + If ($null -ne $destStream) + { + $destStream.Dispose() + } + + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + +<############################################################################################ +# ProgressBarHelper: This is a helper function used to display progress message. +# This function is used by both Compress-Archive & Expand-Archive to display archive file +# creation/expansion progress. +############################################################################################> + function ProgressBarHelper + { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) + { + $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + +<############################################################################################ +# CSVHelper: This is a helper function used to append comma after each path specifid by +# the SourcePath array. This helper function is used to display all the user supplied paths +# in the WhatIf message. +############################################################################################> + function CSVHelper + { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) + { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) + { + if ($currentIndex -eq $sourcePath.Count - 1) + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } + else + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } + else + { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + +<############################################################################################ +# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. +############################################################################################> + function ThrowTerminatingErrorHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) + { + $exception = New-object System.IO.IOException $errorMessage + } + else + { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + +<############################################################################################ +# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord +############################################################################################> + function CreateErrorRecordHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) + { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $inputPaths = @() + $destinationParentDir = [system.IO.Path]::GetDirectoryName($DestinationPath) + if ($null -eq $destinationParentDir) + { + $errorMessage = ($LocalizedData.InvalidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + if ($destinationParentDir -eq [string]::Empty) + { + $destinationParentDir = '.' + } + + $achiveFileName = [system.IO.Path]::GetFileName($DestinationPath) + $destinationParentDir = GetResolvedPathHelper $destinationParentDir $false $PSCmdlet + + if ($destinationParentDir.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $DestinationPath, "DestinationPath", "DestinationPath") + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + IsValidFileSystemPath $destinationParentDir | Out-Null + $DestinationPath = Join-Path -Path $destinationParentDir -ChildPath $achiveFileName + + # GetExtension API does not validate for the actual existance of the path. + $extension = [system.IO.Path]::GetExtension($DestinationPath) + + # If user does not specify .Zip extension, we append it. + If ($extension -eq [string]::Empty) + { + $DestinationPathWithOutExtension = $DestinationPath + $DestinationPath = $DestinationPathWithOutExtension + $zipFileExtension + $appendArchiveFileExtensionMessage = ($LocalizedData.AppendArchiveFileExtensionMessage -f $DestinationPathWithOutExtension, $DestinationPath) + Write-Verbose $appendArchiveFileExtensionMessage + } + else + { + # Invalid file extension is specified for the zip file to be created. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + + $archiveFileExist = Test-Path -LiteralPath $DestinationPath -PathType Leaf + + if ($archiveFileExist -and ($Update -eq $false -and $Force -eq $false)) + { + $errorMessage = ($LocalizedData.ZipFileExistError -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # If archive file already exists and if -Update is specified, then we check to see + # if we have write access permission to update the existing archive file. + if ($archiveFileExist -and $Update -eq $true) + { + $item = Get-Item -Path $DestinationPath + if ($item.Attributes.ToString().Contains("ReadOnly")) + { + $errorMessage = ($LocalizedData.ArchiveFileIsReadOnly -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileIsReadOnly" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $DestinationPath + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) + { + $preparingToCompressVerboseMessage = ($LocalizedData.PreparingToCompressVerboseMessage) + Write-Verbose $preparingToCompressVerboseMessage + + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $DestinationPath) + ProgressBarHelper "Compress-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS + { + if ($PsCmdlet.ParameterSetName -eq "Path" -or + $PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "PathWithUpdate") + { + $inputPaths += $Path + } + + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") + { + $inputPaths += $LiteralPath + } + } + END + { + # If archive file already exists and if -Force is specified, we delete the + # existing artchive file and create a brand new one. + if (($PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce") -and $archiveFileExist) + { + Remove-Item -Path $DestinationPath -Force -ErrorAction Stop + } + + # Validate Source Path depeding on parameter set being used. + # The specified source path conatins one or more files or directories that needs + # to be compressed. + $isLiteralPathUsed = $false + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") + { + $isLiteralPathUsed = $true + } + + ValidateDuplicateFileSystemPath $PsCmdlet.ParameterSetName $inputPaths + $resolvedPaths = GetResolvedPathHelper $inputPaths $isLiteralPathUsed $PSCmdlet + IsValidFileSystemPath $resolvedPaths | Out-Null + + $sourcePath = $resolvedPaths; + + # CSVHelper: This is a helper function used to append comma after each path specifid by + # the $sourcePath array. The comma saperated paths are displayed in the -WhatIf message. + $sourcePathInCsvFormat = CSVHelper $sourcePath + if ($pscmdlet.ShouldProcess($sourcePathInCsvFormat)) + { + try + { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + $numberOfItemsArchived = CompressArchiveHelper $sourcePath $DestinationPath $CompressionLevel $Update + + $isArchiveFileProcessingComplete = $true + } + finally + { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + # $numberOfItemsArchived contains the count of number of files or directories add to the archive file. + # If the newly created archive file is empty then we delete it as its not usable. + if (($isArchiveFileProcessingComplete -eq $false) -or + ($numberOfItemsArchived -eq 0)) + { + $DeleteArchiveFileMessage = ($LocalizedData.DeleteArchiveFile -f $DestinationPath) + Write-Verbose $DeleteArchiveFileMessage + + # delete the partial archive file created. + if (Test-Path $DestinationPath) + { + Remove-Item -LiteralPath $DestinationPath -Force -Recurse -ErrorAction SilentlyContinue + } + } + } + } + } +} + +function Expand-Archive +{ + <# + .SYNOPSIS + Extracts files from a specified archive (zipped) file. + + .DESCRIPTION + The Expand-Archive cmdlet extracts files from a specified zipped archive file to a specified destination folder. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. + + .PARAMETER Path + Specifies the path to the archive file. + + .PARAMETER LiteralPath + Specifies the path to an archive file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. Wildcard characters are not supported. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. + + .PARAMETER DestinationPath + Specifies the path to the folder in which you want the command to save extracted files. Enter the path to a folder, but do not specify a file name or file name extension. This parameter is required. + + .PARAMETER Force + Forces the command to run without asking for user confirmation. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .EXAMPLE + Example 1: Extract the contents of an archive + + PS C:\>Expand-Archive -LiteralPath C:\Archives\Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + + .EXAMPLE + Example 2: Extract the contents of an archive in the current folder + + PS C:\>Expand-Archive -Path Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file in the current folder, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + #> + [CmdletBinding( + DefaultParameterSetName = "Path", + SupportsShouldProcess = $true, + HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393253")] + param + ( + [parameter ( + mandatory = $true, + Position = 0, + ParameterSetName = "Path", + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Path, + + [parameter ( + mandatory = $true, + ParameterSetName = "LiteralPath", + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string] + $LiteralPath, + + [parameter (mandatory = $false, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [parameter (mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [switch] + $Force + ) + + BEGIN + { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + + $zipFileExtension = ".zip" + + $LocalizedData = ConvertFrom-StringData @' +PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. +ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. +InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. +ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. +ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. +DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. +ArchiveFileIsEmpty=The archive file {0} is empty. +CompressProgressBarText=The archive file '{0}' creation is in progress... +ExpandProgressBarText=The archive file '{0}' expansion is in progress... +AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath patameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. +AddItemtoArchiveFile=Adding '{0}'. +CreateFileAtExpandedPath=Created '{0}'. +InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. +InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. +FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. +DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. +InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. +PreparingToCompressVerboseMessage=Preparing to compress... +PreparingToExpandVerboseMessage=Preparing to expand... +'@ + + #region Utility Functions + function GetResolvedPathHelper + { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + try + { + if ($isLiteralPath) + { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } + else + { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } + catch + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) + { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies + { + + if ($PSEdition -eq "Desktop") + { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath + { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath + { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) + { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + else + { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper + { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) + { + "Fastest" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper + { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) + { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) + { + $sourceFilePaths += $currentPath + } + else + { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) + { + $currentSegmentWeight = 100/[double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) + { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) + { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper + { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper + { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) + { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) + { + $modifiedSourceDirFullName = $sourceDirFullName + } + else + { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } + else + { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) + { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) + { + $subDirFiles.Add($currentContent.FullName) + } + else + { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) + { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper + { + param + ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) + { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try + { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) + { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) + { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } + else + { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) + { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) + { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) + { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + try + { + try + { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } + catch + { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) + { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) + { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) + { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } + finally + { + If ($null -ne $currentFileStream) + { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + If ($null -ne $destStream) + { + $destStream.Dispose() + } + } + } + else + { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) + { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + +<############################################################################################ +# ValidateArchivePathHelper: This is a helper function used to validate the archive file +# path & its file format. The only supported archive file format is .zip +############################################################################################> + function ValidateArchivePathHelper + { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) + { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + else + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + +<############################################################################################ +# ExpandArchiveHelper: This is a helper function used to expand the archive file contents +# to the specified directory. +############################################################################################> + function ExpandArchiveHelper + { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try + { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) + { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) + { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) + { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } + else + { + try + { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) + { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) + { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) + { + if ($force) + { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) + { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) + { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } + else + { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) + { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } + finally + { + If ($null -ne $destStream) + { + $destStream.Dispose() + } + + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + +<############################################################################################ +# ProgressBarHelper: This is a helper function used to display progress message. +# This function is used by both Compress-Archive & Expand-Archive to display archive file +# creation/expansion progress. +############################################################################################> + function ProgressBarHelper + { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) + { + $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + +<############################################################################################ +# CSVHelper: This is a helper function used to append comma after each path specifid by +# the SourcePath array. This helper function is used to display all the user supplied paths +# in the WhatIf message. +############################################################################################> + function CSVHelper + { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) + { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) + { + if ($currentIndex -eq $sourcePath.Count - 1) + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } + else + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } + else + { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + +<############################################################################################ +# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. +############################################################################################> + function ThrowTerminatingErrorHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) + { + $exception = New-object System.IO.IOException $errorMessage + } + else + { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + +<############################################################################################ +# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord +############################################################################################> + function CreateErrorRecordHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) + { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $isVerbose = $psboundparameters.ContainsKey("Verbose") + $isConfirm = $psboundparameters.ContainsKey("Confirm") + + $isDestinationPathProvided = $true + if ($DestinationPath -eq [string]::Empty) + { + $resolvedDestinationPath = $pwd + $isDestinationPathProvided = $false + } + else + { + $destinationPathExists = Test-Path -Path $DestinationPath -PathType Container + if ($destinationPathExists) + { + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $false $PSCmdlet + if ($resolvedDestinationPath.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidExpandedDirPathError -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDestinationPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # At this point we are sure that the provided path resolves to a valid single path. + # Calling Resolve-Path again to get the underlying provider name. + $suppliedDestinationPath = Resolve-Path -Path $DestinationPath + if ($suppliedDestinationPath.Provider.Name -ne "FileSystem") + { + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + } + else + { + $createdItem = New-Item -Path $DestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop + if ($createdItem -ne $null -and $createdItem.PSProvider.Name -ne "FileSystem") + { + Remove-Item "$DestinationPath" -Force -Recurse -ErrorAction SilentlyContinue + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $true $PSCmdlet + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) + { + $preparingToExpandVerboseMessage = ($LocalizedData.PreparingToExpandVerboseMessage) + Write-Verbose $preparingToExpandVerboseMessage + + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $DestinationPath) + ProgressBarHelper "Expand-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS + { + switch ($PsCmdlet.ParameterSetName) + { + "Path" + { + $resolvedSourcePaths = GetResolvedPathHelper $Path $false $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $Path, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $Path + } + } + "LiteralPath" + { + $resolvedSourcePaths = GetResolvedPathHelper $LiteralPath $true $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $LiteralPath, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $LiteralPath + } + } + } + + ValidateArchivePathHelper $resolvedSourcePaths + + if ($pscmdlet.ShouldProcess($resolvedSourcePaths)) + { + $expandedItems = @() + + try + { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + # The User has not provided a destination path, hence we use '$pwd\ArchiveFileName' as the directory where the + # archive file contents would be expanded. If the path '$pwd\ArchiveFileName' already exists then we use the + # Windows default mechanism of appending a counter value at the end of the directory name where the contents + # would be expanded. + if (!$isDestinationPathProvided) + { + $archiveFile = New-Object System.IO.FileInfo $resolvedSourcePaths + $resolvedDestinationPath = Join-Path -Path $resolvedDestinationPath -ChildPath $archiveFile.BaseName + $destinationPathExists = Test-Path -LiteralPath $resolvedDestinationPath -PathType Container + + if (!$destinationPathExists) + { + New-Item -Path $resolvedDestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop | Out-Null + } + } + + ExpandArchiveHelper $resolvedSourcePaths $resolvedDestinationPath ([ref]$expandedItems) $Force $isVerbose $isConfirm + + $isArchiveFileProcessingComplete = $true + } + finally + { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + if ($isArchiveFileProcessingComplete -eq $false) + { + if ($expandedItems.Count -gt 0) + { + # delete the expanded file/directory as the archive + # file was not completly expanded. + $expandedItems | ForEach-Object { Remove-Item $_ -Force -Recurse } + } + } + } + } + } +} + +function Write-LocalMessage +{ + [CmdletBinding()] + Param ( + [string]$Message + ) + + if (Test-Path function:Write-PSFMessage) { Write-PSFMessage -Level Important -Message $Message } + else { Write-Host $Message } +} +#endregion Utility Functions + +try +{ + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + Write-LocalMessage -Message "Downloading repository from '$($BaseUrl)/archive/$($Branch).zip'" + Invoke-WebRequest -Uri "$($BaseUrl)/archive/$($Branch).zip" -UseBasicParsing -OutFile "$($env:TEMP)\$($ModuleName).zip" -ErrorAction Stop + + Write-LocalMessage -Message "Creating temporary project folder: '$($env:TEMP)\$($ModuleName)'" + $null = New-Item -Path $env:TEMP -Name $ModuleName -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Extracting archive to '$($env:TEMP)\$($ModuleName)'" + Expand-Archive -Path "$($env:TEMP)\$($ModuleName).zip" -DestinationPath "$($env:TEMP)\$($ModuleName)" -ErrorAction Stop + + $basePath = Get-ChildItem "$($env:TEMP)\$($ModuleName)\*" | Select-Object -First 1 + if ($SubFolder) { $basePath = "$($basePath)\$($SubFolder)" } + + # Only needed for PS v5+ but doesn't hurt anyway + $manifest = "$($basePath)\$($ModuleName).psd1" + $manifestData = Invoke-Expression ([System.IO.File]::ReadAllText($manifest)) + $moduleVersion = $manifestData.ModuleVersion + Write-LocalMessage -Message "Download concluded: $($ModuleName) | Branch $($Branch) | Version $($moduleVersion)" + + # Determine output path + $path = "$($env:ProgramFiles)\WindowsPowerShell\Modules\$($ModuleName)" + if ($doUserMode) { $path = "$(Split-Path $profile.CurrentUserAllHosts)\Modules\$($ModuleName)" } + if ($PSVersionTable.PSVersion.Major -ge 5) { $path += "\$moduleVersion" } + + if ((Test-Path $path) -and (-not $Force)) + { + Write-LocalMessage -Message "Module already installed, interrupting installation" + return + } + + Write-LocalMessage -Message "Creating folder: $($path)" + $null = New-Item -Path $path -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Copying files to $($path)" + foreach ($file in (Get-ChildItem -Path $basePath)) + { + Move-Item -Path $file.FullName -Destination $path -ErrorAction Stop + } + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + Write-LocalMessage -Message "Installation of the module $($ModuleName), Branch $($Branch), Version $($moduleVersion) completed successfully!" +} +catch +{ + Write-LocalMessage -Message "Installation of the module $($ModuleName) failed!" + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + throw +} \ No newline at end of file diff --git a/library/d365bap.tools/d365bap.tools.sln b/library/d365bap.tools/d365bap.tools.sln new file mode 100644 index 0000000..280962e --- /dev/null +++ b/library/d365bap.tools/d365bap.tools.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2010 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FD25EA38-C9F8-470B-BB65-7966C71BA37C}") = "d365bap.tools", "d365bap.tools\d365bap.tools.csproj", "{C04AAD0F-651F-4BCE-884B-775FC72AEC85}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C04AAD0F-651F-4BCE-884B-775FC72AEC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C04AAD0F-651F-4BCE-884B-775FC72AEC85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C04AAD0F-651F-4BCE-884B-775FC72AEC85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C04AAD0F-651F-4BCE-884B-775FC72AEC85}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9199E1E5-6EC4-4B29-8DCB-AE70AABD42FF} + EndGlobalSection +EndGlobal diff --git a/library/d365bap.tools/d365bap.tools/Class1.cs b/library/d365bap.tools/d365bap.tools/Class1.cs new file mode 100644 index 0000000..58defa8 --- /dev/null +++ b/library/d365bap.tools/d365bap.tools/Class1.cs @@ -0,0 +1,8 @@ +using System; + +namespace d365bap.tools +{ + public class Class1 + { + } +} diff --git a/library/d365bap.tools/d365bap.tools/d365bap.tools.csproj b/library/d365bap.tools/d365bap.tools/d365bap.tools.csproj new file mode 100644 index 0000000..b859879 --- /dev/null +++ b/library/d365bap.tools/d365bap.tools/d365bap.tools.csproj @@ -0,0 +1,21 @@ + + + + net4.5.2 + + + + ..\..\..\d365bap.tools\bin + ..\..\..\d365bap.tools\bin\d365bap.tools.xml + + + + ..\..\..\d365bap.tools\bin + ..\..\..\d365bap.tools\bin\d365bap.tools.xml + + + + false + + + From f0e9c3e0f3b29f24f5b27fc2b879fcb30214466d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:09:12 +0100 Subject: [PATCH 2/7] Fix: Only working cmdlets --- d365bap.tools/d365bap.tools.psd1 | 1 - .../functions/Get-BapEnvironmentDetails.ps1 | 77 ------------------- 2 files changed, 78 deletions(-) delete mode 100644 d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 diff --git a/d365bap.tools/d365bap.tools.psd1 b/d365bap.tools/d365bap.tools.psd1 index 48fa056..aeb86c8 100644 --- a/d365bap.tools/d365bap.tools.psd1 +++ b/d365bap.tools/d365bap.tools.psd1 @@ -49,7 +49,6 @@ , 'Get-BapEnvironmentApplicationUser' , 'Get-BapEnvironmentD365App' - , 'Get-BapEnvironmentDetails' , 'Get-BapEnvironmentUser' diff --git a/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 b/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 deleted file mode 100644 index fe8f417..0000000 --- a/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 +++ /dev/null @@ -1,77 +0,0 @@ - -<# - .SYNOPSIS - Short description - - .DESCRIPTION - Long description - - .PARAMETER EnvironmentId - Parameter description - - .PARAMETER AsExcelOutput - Parameter description - - .EXAMPLE - An example - - .NOTES - General notes -#> -function Get-BapEnvironmentDetails { - [CmdletBinding()] - param ( - [parameter (mandatory = $true)] - [string] $EnvironmentId, - - [switch] $AsExcelOutput - ) - - begin { - $curUserId = (Get-AzContext).account.id - - # 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." - Write-PSFMessage -Level Host -Message $messageString - Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) - } - - if (Test-PSFFunctionInterrupt) { return } - - $baseUri = $envObj.LinkedMetaPpacEnvUri - $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri - $headersWebApi = @{ - "Authorization" = "Bearer $($tokenWebApi.Token)" - } - - } - - process { - if (Test-PSFFunctionInterrupt) { return } - - # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveAvailableLanguages()" -Headers $headersWebApi - # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveProvisionedLanguages()" -Headers $headersWebApi - - # $resOrg = @(Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi) - # $resOrg.value | ConvertTo-Json -Depth 10 - - $resOrg = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi | Select-Object -ExpandProperty value | Select-Object -First 1 - # $resOrg - - $languages = @(Get-EnvironmentLanguage -BaseUri $baseUri) - - $languages | ConvertTo-Json -Depth 10 - - # $resUser = Invoke-RestMethod -Method Get -Uri $("$baseUri/api/data/v9.2/systemusers?" + '$select=internalemailaddress&$top=1&$filter=internalemailaddress eq' + " '$curUserId'" + '&$expand=user_settings($select=uilanguageid)') -Headers $headersWebApi - $resUser = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/systemusers?$select=internalemailaddress&$top=1&$filter=internalemailaddress eq ''{0}''&$expand=user_settings($select=uilanguageid)' -f $curUserId) -Headers $headersWebApi - - # $resUser.value | ConvertTo-Json -Depth 10 - } - - end { - - } -} \ No newline at end of file From b44e1cd78c12b937dc529a9c3920d9c8509ed700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:09:12 +0100 Subject: [PATCH 3/7] Fix: Only working cmdlets --- d365bap.tools/d365bap.tools.psd1 | 1 - .../functions/Get-BapEnvironmentDetails.ps1 | 77 ------------------- docs/Compare-BapEnvironmentUser.md | 66 +++++++++++++--- docs/Get-BapEnvironmentUser.md | 8 +- 4 files changed, 61 insertions(+), 91 deletions(-) delete mode 100644 d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 diff --git a/d365bap.tools/d365bap.tools.psd1 b/d365bap.tools/d365bap.tools.psd1 index 48fa056..aeb86c8 100644 --- a/d365bap.tools/d365bap.tools.psd1 +++ b/d365bap.tools/d365bap.tools.psd1 @@ -49,7 +49,6 @@ , 'Get-BapEnvironmentApplicationUser' , 'Get-BapEnvironmentD365App' - , 'Get-BapEnvironmentDetails' , 'Get-BapEnvironmentUser' diff --git a/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 b/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 deleted file mode 100644 index fe8f417..0000000 --- a/d365bap.tools/functions/Get-BapEnvironmentDetails.ps1 +++ /dev/null @@ -1,77 +0,0 @@ - -<# - .SYNOPSIS - Short description - - .DESCRIPTION - Long description - - .PARAMETER EnvironmentId - Parameter description - - .PARAMETER AsExcelOutput - Parameter description - - .EXAMPLE - An example - - .NOTES - General notes -#> -function Get-BapEnvironmentDetails { - [CmdletBinding()] - param ( - [parameter (mandatory = $true)] - [string] $EnvironmentId, - - [switch] $AsExcelOutput - ) - - begin { - $curUserId = (Get-AzContext).account.id - - # 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: $EnvironmentId didn't return any matching environment details. Please verify that the EnvironmentId is correct - try running the Get-BapEnvironment cmdlet." - Write-PSFMessage -Level Host -Message $messageString - Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) - } - - if (Test-PSFFunctionInterrupt) { return } - - $baseUri = $envObj.LinkedMetaPpacEnvUri - $tokenWebApi = Get-AzAccessToken -ResourceUrl $baseUri - $headersWebApi = @{ - "Authorization" = "Bearer $($tokenWebApi.Token)" - } - - } - - process { - if (Test-PSFFunctionInterrupt) { return } - - # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveAvailableLanguages()" -Headers $headersWebApi - # Invoke-RestMethod -Method Get -Uri "$baseUri/api/data/v9.2/RetrieveProvisionedLanguages()" -Headers $headersWebApi - - # $resOrg = @(Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi) - # $resOrg.value | ConvertTo-Json -Depth 10 - - $resOrg = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/organizations?$select=organizationid,orgdborgsettings,languagecode,localeid,name') -Headers $headersWebApi | Select-Object -ExpandProperty value | Select-Object -First 1 - # $resOrg - - $languages = @(Get-EnvironmentLanguage -BaseUri $baseUri) - - $languages | ConvertTo-Json -Depth 10 - - # $resUser = Invoke-RestMethod -Method Get -Uri $("$baseUri/api/data/v9.2/systemusers?" + '$select=internalemailaddress&$top=1&$filter=internalemailaddress eq' + " '$curUserId'" + '&$expand=user_settings($select=uilanguageid)') -Headers $headersWebApi - $resUser = Invoke-RestMethod -Method Get -Uri $($baseUri + '/api/data/v9.2/systemusers?$select=internalemailaddress&$top=1&$filter=internalemailaddress eq ''{0}''&$expand=user_settings($select=uilanguageid)' -f $curUserId) -Headers $headersWebApi - - # $resUser.value | ConvertTo-Json -Depth 10 - } - - end { - - } -} \ No newline at end of file diff --git a/docs/Compare-BapEnvironmentUser.md b/docs/Compare-BapEnvironmentUser.md index 36c9767..b098029 100644 --- a/docs/Compare-BapEnvironmentUser.md +++ b/docs/Compare-BapEnvironmentUser.md @@ -8,7 +8,7 @@ schema: 2.0.0 # Compare-BapEnvironmentUser ## SYNOPSIS -Short description +Compare the environment users ## SYNTAX @@ -18,19 +18,65 @@ Compare-BapEnvironmentUser [-SourceEnvironmentId] [-DestinationEnvironm ``` ## DESCRIPTION -Long description +This enables the user to compare 2 x environments, with one as a source and the other as a destination + +It will only look for users on the source, and use this as a baseline against the destination ## EXAMPLES ### EXAMPLE 1 ``` -An example +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 +``` + +This will get all system users from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +It will exclude those with ApplicationId filled. + +Sample output: +Email Name AppId SourceId DestinationId +----- ---- ----- -------- ------------- +aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… +ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… + +### EXAMPLE 2 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds +``` + +This will get all system users from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +It will include those with ApplicationId filled. + +Sample output: +Email Name AppId SourceId DestinationId +----- ---- ----- -------- ------------- +aba@temp.com Austin Baker f85bcd69-ef72-… 5aaac0ec-a91… +ade@temp.com Alex Denver 39309a5c-7676-… 1d521227-43b… +AIBuilder_StructuredML_Prod_C… AIBuilder_StructuredML_Prod_C… ff8a1ad8-a415-45c1-… 95dc9ca2-8185-… 328db0cc-14c… +AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-… 1831f4dc-4c5… + +### EXAMPLE 3 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -IncludeAppIds -ShowDiffOnly ``` +This will get all system users from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +It will include those with ApplicationId filled. +It will only output the users that is missing in the destionation environment. + +Sample output: +Email Name AppId SourceId DestinationId +----- ---- ----- -------- ------------- +d365-scm-operationdataservice… d365-scm-operationdataservice… 986556ed-a409-4339-… 5e077e6a-a0c9-… Missing +d365-scm-operationdataservice… d365-scm-operationdataservice… 14e80222-1878-455d-… 183ec023-9ccb-… Missing +def@temp.com Dustin Effect 01e37132-0a44-… Missing + ## PARAMETERS ### -SourceEnvironmentId -Parameter description +Environment Id of the source environment that you want to utilized as the baseline for the compare ```yaml Type: String @@ -45,7 +91,7 @@ Accept wildcard characters: False ``` ### -DestinationEnvironmentId -Parameter description +Environment Id of the destination environment that you want to validate against the baseline (source) ```yaml Type: String @@ -60,7 +106,7 @@ Accept wildcard characters: False ``` ### -ShowDiffOnly -Parameter description +Instruct the cmdlet to only output the differences that are not aligned between the source and destination ```yaml Type: SwitchParameter @@ -75,7 +121,7 @@ Accept wildcard characters: False ``` ### -IncludeAppIds -Parameter description +Instruct the cmdlet to also include the users with the ApplicationId property filled ```yaml Type: SwitchParameter @@ -90,7 +136,9 @@ Accept wildcard characters: False ``` ### -AsExcelOutput -Parameter description +Instruct the cmdlet to output all details directly to an Excel file + +This makes it easier to deep dive into all the details returned from the API, and makes it possible for the user to persist the current state ```yaml Type: SwitchParameter @@ -112,6 +160,6 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS ## NOTES -General notes +Author: Mötz Jensen (@Splaxi) ## RELATED LINKS diff --git a/docs/Get-BapEnvironmentUser.md b/docs/Get-BapEnvironmentUser.md index 3b248f5..3e27f11 100644 --- a/docs/Get-BapEnvironmentUser.md +++ b/docs/Get-BapEnvironmentUser.md @@ -35,8 +35,8 @@ This will fetch all oridinary users from the environment. Sample output: Email Name AppId Systemuserid ----- ---- ----- ------------ -SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 -INTEGRATION baabe592-2860-4d1a-9365-e95317372498 +SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 +INTEGRATION baabe592-2860-4d1a-9365-e95317372498 aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a ade@temp.com Alex Denver 39309a5c-7676-4c8a-b702-719fb92c5151 @@ -51,8 +51,8 @@ It will include the ones with the ApplicationId property filled. Sample output: Email Name AppId Systemuserid ----- ---- ----- ------------ -SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 -INTEGRATION baabe592-2860-4d1a-9365-e95317372498 +SYSTEM 5d2ff978-a74c-4ba4-8cc2-b4c5a23994f7 +INTEGRATION baabe592-2860-4d1a-9365-e95317372498 aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-4c5e-ac84-1831f4dc7d5f From 1f9544ad95baa6b70d032f50bfc4fff09c20b98f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:23:35 +0100 Subject: [PATCH 4/7] Fix: Remove unit test and docs file for non release cmdlet --- .../Get-BapEnvironmentDetails.Tests.ps1 | 49 ------------- docs/Get-BapEnvironmentDetails.md | 71 ------------------- 2 files changed, 120 deletions(-) delete mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 delete mode 100644 docs/Get-BapEnvironmentDetails.md diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 deleted file mode 100644 index 41f3d28..0000000 --- a/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -Describe "Get-BapEnvironmentDetails Unit Tests" -Tag "Unit" { - BeforeAll { - # Place here all things needed to prepare for the tests - } - AfterAll { - # Here is where all the cleanup tasks go - } - - Describe "Ensuring unchanged command signature" { - It "should have the expected parameter sets" { - (Get-Command Get-BapEnvironmentDetails).ParameterSets.Name | Should -Be '__AllParameterSets' - } - - It 'Should have the expected parameter EnvironmentId' { - $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['EnvironmentId'] - $parameter.Name | Should -Be 'EnvironmentId' - $parameter.ParameterType.ToString() | Should -Be System.String - $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False - } - It 'Should have the expected parameter AsExcelOutput' { - $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['AsExcelOutput'] - $parameter.Name | Should -Be 'AsExcelOutput' - $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter - $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False - } - } - - Describe "Testing parameterset __AllParameterSets" { - <# - __AllParameterSets -EnvironmentId - __AllParameterSets -EnvironmentId -AsExcelOutput - #> - } - -} \ No newline at end of file diff --git a/docs/Get-BapEnvironmentDetails.md b/docs/Get-BapEnvironmentDetails.md deleted file mode 100644 index 51f100e..0000000 --- a/docs/Get-BapEnvironmentDetails.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -external help file: d365bap.tools-help.xml -Module Name: d365bap.tools -online version: -schema: 2.0.0 ---- - -# Get-BapEnvironmentDetails - -## SYNOPSIS -Short description - -## SYNTAX - -``` -Get-BapEnvironmentDetails [-EnvironmentId] [-AsExcelOutput] [] -``` - -## DESCRIPTION -Long description - -## EXAMPLES - -### EXAMPLE 1 -``` -An example -``` - -## PARAMETERS - -### -EnvironmentId -Parameter description - -```yaml -Type: String -Parameter Sets: (All) -Aliases: - -Required: True -Position: 1 -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -AsExcelOutput -Parameter description - -```yaml -Type: SwitchParameter -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: False -Accept pipeline input: False -Accept wildcard characters: False -``` - -### CommonParameters -This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). - -## INPUTS - -## OUTPUTS - -## NOTES -General notes - -## RELATED LINKS From 348c348756b0ef154a9d390dc0c28816d9437f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:23:35 +0100 Subject: [PATCH 5/7] Fix: Remove unit test and docs file for non release cmdlet --- .../Compare-BapEnvironmentD365App.ps1 | 7 ++ .../functions/Compare-BapEnvironmentUser.ps1 | 8 +++ .../functions/Get-EnvironmentLanguage.ps1 | 3 + .../Get-BapEnvironmentDetails.Tests.ps1 | 49 ------------- docs/Get-BapEnvironmentDetails.md | 71 ------------------- 5 files changed, 18 insertions(+), 120 deletions(-) delete mode 100644 d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 delete mode 100644 docs/Get-BapEnvironmentDetails.md diff --git a/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 index 8ca9778..645657f 100644 --- a/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 +++ b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 @@ -58,6 +58,13 @@ 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… 7523d261-f1be-46e7-8e68-f3de16eeabbb DualWriteCoreAnchor 1.0.24022.4 1.0.24011.1 Dual-write… + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput + + This will get all installed D365 Apps from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + Will output all details into an Excel file, that will auto open on your machine. + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 index fd02688..d3cd455 100644 --- a/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 +++ b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 @@ -68,6 +68,14 @@ d365-scm-operationdataservice… d365-scm-operationdataservice… 14e80222-1878-455d-… 183ec023-9ccb-… Missing def@temp.com Dustin Effect 01e37132-0a44-… Missing + .EXAMPLE + PS C:\> Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput + + This will get all system users from the Source Environment. + It will iterate over all of them, and validate against the Destination Environment. + It will exclude those with ApplicationId filled. + Will output all details into an Excel file, that will auto open on your machine. + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 b/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 index d2c033d..54f06af 100644 --- a/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 +++ b/d365bap.tools/internal/functions/Get-EnvironmentLanguage.ps1 @@ -14,6 +14,9 @@ .EXAMPLE PS C:\> Get-EnvironmentLanguage -BaseUri 'https://temp-test.crm4.dynamics.com' + This will fetch all languages from the environment. + Uses the WebAPI / OData endpoint. + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 b/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 deleted file mode 100644 index 41f3d28..0000000 --- a/d365bap.tools/tests/functions/Get-BapEnvironmentDetails.Tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -Describe "Get-BapEnvironmentDetails Unit Tests" -Tag "Unit" { - BeforeAll { - # Place here all things needed to prepare for the tests - } - AfterAll { - # Here is where all the cleanup tasks go - } - - Describe "Ensuring unchanged command signature" { - It "should have the expected parameter sets" { - (Get-Command Get-BapEnvironmentDetails).ParameterSets.Name | Should -Be '__AllParameterSets' - } - - It 'Should have the expected parameter EnvironmentId' { - $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['EnvironmentId'] - $parameter.Name | Should -Be 'EnvironmentId' - $parameter.ParameterType.ToString() | Should -Be System.String - $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False - } - It 'Should have the expected parameter AsExcelOutput' { - $parameter = (Get-Command Get-BapEnvironmentDetails).Parameters['AsExcelOutput'] - $parameter.Name | Should -Be 'AsExcelOutput' - $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter - $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False - } - } - - Describe "Testing parameterset __AllParameterSets" { - <# - __AllParameterSets -EnvironmentId - __AllParameterSets -EnvironmentId -AsExcelOutput - #> - } - -} \ No newline at end of file diff --git a/docs/Get-BapEnvironmentDetails.md b/docs/Get-BapEnvironmentDetails.md deleted file mode 100644 index 51f100e..0000000 --- a/docs/Get-BapEnvironmentDetails.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -external help file: d365bap.tools-help.xml -Module Name: d365bap.tools -online version: -schema: 2.0.0 ---- - -# Get-BapEnvironmentDetails - -## SYNOPSIS -Short description - -## SYNTAX - -``` -Get-BapEnvironmentDetails [-EnvironmentId] [-AsExcelOutput] [] -``` - -## DESCRIPTION -Long description - -## EXAMPLES - -### EXAMPLE 1 -``` -An example -``` - -## PARAMETERS - -### -EnvironmentId -Parameter description - -```yaml -Type: String -Parameter Sets: (All) -Aliases: - -Required: True -Position: 1 -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - -### -AsExcelOutput -Parameter description - -```yaml -Type: SwitchParameter -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: False -Accept pipeline input: False -Accept wildcard characters: False -``` - -### CommonParameters -This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). - -## INPUTS - -## OUTPUTS - -## NOTES -General notes - -## RELATED LINKS From 9b3a83be71a66a2a64c1c9ba5ed2dbcb5f085057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:31:40 +0100 Subject: [PATCH 6/7] Fix: Add missing updates to the doc files --- d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 | 2 +- d365bap.tools/functions/Get-BapEnvironment.ps1 | 2 +- d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 | 2 +- d365bap.tools/functions/Get-BapEnvironmentUser.ps1 | 2 +- docs/Compare-BapEnvironmentD365App.md | 9 +++++++++ docs/Compare-BapEnvironmentUser.md | 10 ++++++++++ docs/Get-BapEnvironment.md | 8 ++++++++ docs/Get-BapEnvironmentApplicationUser.md | 8 ++++++++ docs/Get-BapEnvironmentD365App.md | 9 +++++++++ docs/Get-BapEnvironmentUser.md | 8 ++++++++ 10 files changed, 56 insertions(+), 4 deletions(-) diff --git a/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 index d3cd455..cbe08e0 100644 --- a/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 +++ b/d365bap.tools/functions/Compare-BapEnvironmentUser.ps1 @@ -75,7 +75,7 @@ It will iterate over all of them, and validate against the Destination Environment. It will exclude those with ApplicationId filled. Will output all details into an Excel file, that will auto open on your machine. - + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/functions/Get-BapEnvironment.ps1 b/d365bap.tools/functions/Get-BapEnvironment.ps1 index 979d503..0964cce 100644 --- a/d365bap.tools/functions/Get-BapEnvironment.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironment.ps1 @@ -44,7 +44,7 @@ This will query for ALL available environments. Will output all details into an Excel file, that will auto open on your machine. - + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 index b1d75a2..ea3e241 100644 --- a/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 @@ -133,7 +133,7 @@ This will query the environment for ALL available D365 Apps. It will compare available vs installed D365 Apps, and indicate whether an update is available of not. Will output all details into an Excel file, that will auto open on your machine. - + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 b/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 index f3bd9c4..7a06e59 100644 --- a/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironmentUser.ps1 @@ -57,7 +57,7 @@ This will fetch all oridinary users from the environment. Will output all details into an Excel file, that will auto open on your machine. - + .NOTES Author: Mötz Jensen (@Splaxi) #> diff --git a/docs/Compare-BapEnvironmentD365App.md b/docs/Compare-BapEnvironmentD365App.md index 8679e85..19a7e2d 100644 --- a/docs/Compare-BapEnvironmentD365App.md +++ b/docs/Compare-BapEnvironmentD365App.md @@ -57,6 +57,15 @@ ea8d3b2f-ede2-46b4-900d-ed02c81c44fd AgentProductivityToolsAnchor 9.2.24021.10 6ce2d70e-78bf-4ff6-85ed-1bd63d4ab444 ExportToDataLakeCoreAnchor 1.0.0.1 0.0.0.0 Azure Syna… 7523d261-f1be-46e7-8e68-f3de16eeabbb DualWriteCoreAnchor 1.0.24022.4 1.0.24011.1 Dual-write… +### EXAMPLE 3 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput +``` + +This will get all installed D365 Apps from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -SourceEnvironmentId diff --git a/docs/Compare-BapEnvironmentUser.md b/docs/Compare-BapEnvironmentUser.md index b098029..69e094e 100644 --- a/docs/Compare-BapEnvironmentUser.md +++ b/docs/Compare-BapEnvironmentUser.md @@ -73,6 +73,16 @@ d365-scm-operationdataservice… d365-scm-operationdataservice… 986556ed-a409- d365-scm-operationdataservice… d365-scm-operationdataservice… 14e80222-1878-455d-… 183ec023-9ccb-… Missing def@temp.com Dustin Effect 01e37132-0a44-… Missing +### EXAMPLE 4 +``` +Compare-BapEnvironmentD365App -SourceEnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -DestinationEnvironmentId 32c6b196-ef52-4c43-93cf-6ecba51e6aa1 -AsExcelOutput +``` + +This will get all system users from the Source Environment. +It will iterate over all of them, and validate against the Destination Environment. +It will exclude those with ApplicationId filled. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -SourceEnvironmentId diff --git a/docs/Get-BapEnvironment.md b/docs/Get-BapEnvironment.md index c0fbc2e..85fb4a8 100644 --- a/docs/Get-BapEnvironment.md +++ b/docs/Get-BapEnvironment.md @@ -50,6 +50,14 @@ PpacEnvId PpacEnvRegion PpacEnvName PpacEn --------- ------------- ----------- ---------- ------------------ eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 europe new-test Sandbox https://new-test.sandbox.operati… +### EXAMPLE 3 +``` +Get-BapEnvironment -AsExcelOutput +``` + +This will query for ALL available environments. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -EnvironmentId diff --git a/docs/Get-BapEnvironmentApplicationUser.md b/docs/Get-BapEnvironmentApplicationUser.md index 5252760..e9eab88 100644 --- a/docs/Get-BapEnvironmentApplicationUser.md +++ b/docs/Get-BapEnvironmentApplicationUser.md @@ -38,6 +38,14 @@ b6e52ceb-f771-41ff-bd99-917523b28eaf AIBuilder_StructuredML_Prod_C… 3bafba76-6 c76313fd-5c6f-4f1f-9869-c884fa7fe226 AppDeploymentOrchestration d88a3535-ebf0-4b2b-ad23-90e686660a64 99aee001-009e… 29494271-7e38-4433-8bf8-06d335299a17 AriaMdlExporter 8bf8862f-5036-42b0-a4f8-1b638db7896b 99aee001-009e… +### EXAMPLE 2 +``` +Get-BapEnvironmentApplicationUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput +``` + +This will fetch all ApplicationUsers from the environment. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -EnvironmentId diff --git a/docs/Get-BapEnvironmentD365App.md b/docs/Get-BapEnvironmentD365App.md index 2b4a486..8cbcfc8 100644 --- a/docs/Get-BapEnvironmentD365App.md +++ b/docs/Get-BapEnvironmentD365App.md @@ -117,6 +117,15 @@ It will filter the output to only containing those who have an update available. It will persist the PackageIds for each D365 App, into an array. It will invoke the installation process using the Invoke-BapEnvironmentInstallD365App cmdlet. +### EXAMPLE 7 +``` +Get-BapEnvironmentD365App -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput +``` + +This will query the environment for ALL available D365 Apps. +It will compare available vs installed D365 Apps, and indicate whether an update is available of not. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -EnvironmentId diff --git a/docs/Get-BapEnvironmentUser.md b/docs/Get-BapEnvironmentUser.md index 3e27f11..62312ca 100644 --- a/docs/Get-BapEnvironmentUser.md +++ b/docs/Get-BapEnvironmentUser.md @@ -56,6 +56,14 @@ INTEGRATION aba@temp.com Austin Baker f85bcd69-ef72-45bd-a338-62670a8cef2a AIBuilderProd@onmicrosoft.com AIBuilderProd, # 0a143f2d-2320-4141-… c96f82b8-320f-4c5e-ac84-1831f4dc7d5f +### EXAMPLE 3 +``` +Get-BapEnvironmentUser -EnvironmentId eec2c11a-a4c7-4e1d-b8ed-f62acc9c74c6 -AsExcelOutput +``` + +This will fetch all oridinary users from the environment. +Will output all details into an Excel file, that will auto open on your machine. + ## PARAMETERS ### -EnvironmentId From 47c48bcbecb424c370217d2c8dfec132dde1eb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=B6tz=20Jensen?= Date: Sat, 2 Mar 2024 20:36:09 +0100 Subject: [PATCH 7/7] Fix: Missing output type declaration --- d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 | 4 ++-- d365bap.tools/functions/Get-BapEnvironment.ps1 | 1 + d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 | 1 + d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 | 1 + .../functions/Invoke-BapEnvironmentInstallD365App.ps1 | 2 -- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 index 645657f..e0e88ac 100644 --- a/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 +++ b/d365bap.tools/functions/Compare-BapEnvironmentD365App.ps1 @@ -106,8 +106,8 @@ function Compare-BapEnvironmentD365App { if (Test-PSFFunctionInterrupt) { return } - $appsSourceEnvironment = Get-BapEnvironmentD365App -EnvironmentId $SourceEnvironmentId -InstallState Installed - $appsDestinationEnvironment = Get-BapEnvironmentD365App -EnvironmentId $DestinationEnvironmentId + $appsSourceEnvironment = Get-BapEnvironmentD365App -EnvironmentId $SourceEnvironmentId -InstallState Installed -$GeoRegion $GeoRegion + $appsDestinationEnvironment = Get-BapEnvironmentD365App -EnvironmentId $DestinationEnvironmentId -$GeoRegion $GeoRegion } process { diff --git a/d365bap.tools/functions/Get-BapEnvironment.ps1 b/d365bap.tools/functions/Get-BapEnvironment.ps1 index 0964cce..0a2edb6 100644 --- a/d365bap.tools/functions/Get-BapEnvironment.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironment.ps1 @@ -50,6 +50,7 @@ #> function Get-BapEnvironment { [CmdletBinding()] + [OutputType('System.Object[]')] param ( [string] $EnvironmentId = "*", diff --git a/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 b/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 index a8ff474..04740a2 100644 --- a/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironmentApplicationUser.ps1 @@ -42,6 +42,7 @@ #> function Get-BapEnvironmentApplicationUser { [CmdletBinding()] + [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, diff --git a/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 index ea3e241..26bb77f 100644 --- a/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 +++ b/d365bap.tools/functions/Get-BapEnvironmentD365App.ps1 @@ -139,6 +139,7 @@ #> function Get-BapEnvironmentD365App { [CmdletBinding()] + [OutputType('System.Object[]')] param ( [parameter (mandatory = $true)] [string] $EnvironmentId, diff --git a/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 b/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 index ac45b7c..159051f 100644 --- a/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 +++ b/d365bap.tools/functions/Invoke-BapEnvironmentInstallD365App.ps1 @@ -86,8 +86,6 @@ function Invoke-BapEnvironmentInstallD365App { ) begin { - $tenantId = (Get-AzContext).Tenant.Id - # Make sure all *BapEnvironment* cmdlets will validate that the environment exists prior running anything. $envObj = Get-BapEnvironment -EnvironmentId $EnvironmentId | Select-Object -First 1