├── image
├── src
│ ├── .env
│ ├── dev-patches
│ │ ├── DebugOn
│ │ │ └── Web.config.xdt
│ │ ├── CustomErrorsOff
│ │ │ └── Web.config.xdt
│ │ ├── DevEnvOn
│ │ │ └── Web.config.xdt
│ │ ├── OptimizeCompilationsOn
│ │ │ └── Web.config.xdt
│ │ ├── HttpErrorsDetailed
│ │ │ └── Web.config.xdt
│ │ ├── XdbOff
│ │ │ └── App_Config
│ │ │ │ └── Environment
│ │ │ │ └── XdbOff.config
│ │ ├── RobotDetectionOff
│ │ │ └── App_Config
│ │ │ │ └── Environment
│ │ │ │ └── RobotDetectionOff.config
│ │ ├── DeviceDetectionOff
│ │ │ └── App_Config
│ │ │ │ └── Environment
│ │ │ │ └── DeviceDetectionOff.config
│ │ ├── DiagnosticsOff
│ │ │ └── App_Config
│ │ │ │ └── Environment
│ │ │ │ └── DiagnosticsOff.config
│ │ └── InitMessagesOff
│ │ │ └── App_Config
│ │ │ └── Environment
│ │ │ └── InitMessagesOff.config
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── scripts
│ │ ├── Install-ConfigurationFolder.ps1
│ │ ├── Get-PatchFolders.ps1
│ │ ├── Invoke-XdtTransform.ps1
│ │ ├── Deploy-TdsWdpPackages.ps1
│ │ └── Watch-Directory.ps1
│ └── entrypoints
│ │ ├── worker
│ │ └── Development.ps1
│ │ └── iis
│ │ └── Development.ps1
├── .vscode
│ ├── settings.json
│ ├── launch.json
│ └── tasks.json
├── test
│ ├── TestUtils.ps1
│ └── scripts
│ │ ├── Get-PatchFolders.Tests.ps1
│ │ ├── Install-ConfigurationFolder.Tests.ps1
│ │ ├── Watch-Directory.Tests.ps1
│ │ └── Invoke-XdtTransform.Tests.ps1
└── build
│ ├── azure-pipelines.yml
│ ├── azure-pipelines-ltsc2019.yml
│ ├── azure-pipelines-ltsc2022.yml
│ └── azure-pipelines-ltsc2025.yml
├── .gitignore
├── .gitattributes
├── powershell
├── .vscode
│ ├── settings.json
│ ├── launch.json
│ └── tasks.json
├── src
│ ├── SitecoreDockerTools.nuspec
│ ├── Private
│ │ ├── Get-EnvFileContent.ps1
│ │ ├── Utilities.ps1
│ │ └── RSAKeyTool.ps1
│ ├── SitecoreDockerTools.psm1
│ ├── Public
│ │ ├── Create-PfxFile.ps1
│ │ ├── Load-Certificate.ps1
│ │ ├── Remove-HostsEntry.ps1
│ │ ├── Add-HostsEntry.ps1
│ │ ├── ConvertTo-CompressedBase64String.ps1
│ │ ├── Get-SitecoreCertificateAsBase64String.ps1
│ │ ├── Get-EnvFileVariable.ps1
│ │ ├── Set-EnvFileVariable.ps1
│ │ ├── Create-SqlServerCertificate.ps1
│ │ ├── Import-Certificate.ps1
│ │ ├── Write-SitecoreDockerWelcome.ps1
│ │ └── Get-SitecoreRandomString.ps1
│ └── SitecoreDockerTools.psd1
├── test
│ ├── TestRunner.ps1
│ ├── Public
│ │ ├── SitecoreDockerTools.Tests.ps1
│ │ ├── Import-Certificate.Tests.ps1
│ │ ├── ConvertTo-CompressedBase64String.Tests.ps1
│ │ ├── Create-PfxFile.Tests.ps1
│ │ ├── Create-SqlServerCertificate.Tests.ps1
│ │ ├── Get-SitecoreCertificateAsBase64String.Tests.ps1
│ │ ├── Get-EnvFileVariable.Tests.ps1
│ │ ├── Load-Certificate.Tests.ps1
│ │ ├── Get-SitecoreRandomString.Tests.ps1
│ │ ├── Add-HostsEntry.Tests.ps1
│ │ ├── Remove-HostsEntry.Tests.ps1
│ │ ├── Set-EnvFileVariable.Tests.ps1
│ │ └── Get-SitecoreSelfSignedCertificate.Tests.ps1
│ ├── Private
│ │ ├── Utilities.Tests.ps1
│ │ ├── Get-EnvFileContent.Tests.ps1
│ │ └── RSAKeyTool.Tests.ps1
│ └── TestUtils.ps1
└── build
│ └── azure-pipelines.yml
├── LICENSE
└── README.md
/image/src/.env:
--------------------------------------------------------------------------------
1 | REGISTRY=
2 | VERSION=
3 | BASE_IMAGE=
4 | BUILD_IMAGE=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | *.log
3 | obj
4 | bin
5 | *.exe
6 | *.dll
7 | Packages/
8 | *.user
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behavior to automatically normalize line endings.
2 | * text=auto
--------------------------------------------------------------------------------
/image/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // When enabled, will trim trailing whitespace when you save a file.
3 | "files.trimTrailingWhitespace": true
4 | }
5 |
--------------------------------------------------------------------------------
/powershell/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // When enabled, will trim trailing whitespace when you save a file.
3 | "files.trimTrailingWhitespace": true
4 | }
5 |
--------------------------------------------------------------------------------
/image/src/dev-patches/DebugOn/Web.config.xdt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/src/dev-patches/CustomErrorsOff/Web.config.xdt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/src/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | tools:
5 | image: ${REGISTRY}sitecore-docker-tools-assets:${VERSION:-latest}
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | args:
10 | BASE_IMAGE: ${BASE_IMAGE}
11 | BUILD_IMAGE: ${BUILD_IMAGE}
--------------------------------------------------------------------------------
/image/src/dev-patches/DevEnvOn/Web.config.xdt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/image/src/dev-patches/OptimizeCompilationsOn/Web.config.xdt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/image/src/dev-patches/HttpErrorsDetailed/Web.config.xdt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/powershell/src/SitecoreDockerTools.nuspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SitecoreDockerTools
5 | $VERSION$
6 | Sitecore Corporation A/S
7 | PowerShell extensions for Docker-based Sitecore development
8 | Copyright (C) by Sitecore A/S
9 |
10 |
--------------------------------------------------------------------------------
/image/src/dev-patches/XdbOff/App_Config/Environment/XdbOff.config:
--------------------------------------------------------------------------------
1 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/powershell/test/TestRunner.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [Parameter(HelpMessage="The block of tests to run in the scope of the module")]
3 | [ScriptBlock]$TestScope
4 | )
5 |
6 | if (Get-Module SitecoreDockerTools -ErrorAction SilentlyContinue) {
7 | Remove-Module SitecoreDockerTools -Force
8 | }
9 | Import-Module $PSScriptRoot\..\src\SitecoreDockerTools.psd1 -Force -Scope Global -ErrorAction Stop
10 |
11 | InModuleScope SitecoreDockerTools $TestScope
--------------------------------------------------------------------------------
/image/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "PowerShell: Interactive Session",
9 | "type": "PowerShell",
10 | "request": "launch",
11 | "cwd": ""
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/powershell/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "PowerShell: Interactive Session",
9 | "type": "PowerShell",
10 | "request": "launch",
11 | "cwd": ""
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/image/src/dev-patches/RobotDetectionOff/App_Config/Environment/RobotDetectionOff.config:
--------------------------------------------------------------------------------
1 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/powershell/src/Private/Get-EnvFileContent.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | function Get-EnvFileContent {
4 | Param (
5 | [Parameter(Mandatory = $true)]
6 | [ValidateScript( { Test-Path $_ -IsValid })]
7 | [string]
8 | $File
9 | )
10 |
11 | if (!(Test-Path -Path $File)) {
12 | throw "$File not found."
13 | }
14 | try {
15 | return (Get-Content $File -Raw).Replace("\", "\\") | ConvertFrom-StringData
16 | }
17 | catch {
18 | throw "Error processing $File"
19 | }
20 | }
--------------------------------------------------------------------------------
/powershell/src/SitecoreDockerTools.psm1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | #Requires -RunAsAdministrator
4 |
5 | # Get Functions
6 | $private = @(Get-ChildItem -Path (Join-Path $PSScriptRoot Private) -Include *.ps1 -File -Recurse)
7 | $public = @(Get-ChildItem -Path (Join-Path $PSScriptRoot Public) -Include *.ps1 -File -Recurse)
8 |
9 | # Dot source to scope
10 | # Private must be sourced first - usage in public functions during load
11 | ($private + $public) | ForEach-Object {
12 | try {
13 | . $_.FullName
14 | }
15 | catch {
16 | Write-Warning $_.Exception.Message
17 | }
18 | }
--------------------------------------------------------------------------------
/image/test/TestUtils.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [Parameter(HelpMessage="The block of tests to run in the scope of the module")]
3 | [ScriptBlock]$TestScope = $null
4 | )
5 |
6 | Function Test-ParamIsMandatory {
7 | param(
8 | [Parameter(Mandatory=$true)]
9 | [string]$CommandName,
10 | [Parameter(Mandatory=$true)]
11 | [string]$Parameter,
12 | [string]$SetName = ''
13 | )
14 |
15 | $cmd = Get-Command $CommandName
16 | $attr = $cmd.Parameters.$Parameter.Attributes | Where-Object { $_.TypeId -eq [System.Management.Automation.ParameterAttribute] }
17 | if($SetName) {
18 | $attr = $attr | Where-Object { $_.ParameterSetName -eq $SetName }
19 | }
20 | $attr.Mandatory
21 | }
--------------------------------------------------------------------------------
/image/src/dev-patches/DeviceDetectionOff/App_Config/Environment/DeviceDetectionOff.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/image/src/dev-patches/DiagnosticsOff/App_Config/Environment/DiagnosticsOff.config:
--------------------------------------------------------------------------------
1 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/powershell/test/Public/SitecoreDockerTools.Tests.ps1:
--------------------------------------------------------------------------------
1 | $ModuleScriptName = 'SitecoreDockerTools.psm1'
2 | $ModuleManifestName = 'SitecoreDockerTools.psd1'
3 | $ModuleScriptPath = "$PSScriptRoot\..\..\src\$ModuleScriptName"
4 | $ModuleManifestPath = "$PSScriptRoot\..\..\src\$ModuleManifestName"
5 |
6 | if (!(Get-Module PSScriptAnalyzer -ErrorAction SilentlyContinue)) {
7 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force
8 | }
9 |
10 | Describe 'Module Tests' {
11 | It 'imports successfully' {
12 | { Import-Module -Name $ModuleScriptPath -ErrorAction Stop } | Should -Not -Throw
13 | }
14 |
15 | It 'passes default PSScriptAnalyzer rules' {
16 | Invoke-ScriptAnalyzer -Path $ModuleScriptPath | Should -BeNullOrEmpty
17 | }
18 | }
19 |
20 | Describe 'Module Manifest Tests' {
21 | It 'passes Test-ModuleManifest' {
22 | Write-Host $ModuleManifestPath
23 | Test-ModuleManifest -Path $ModuleManifestPath | Should -Not -BeNullOrEmpty
24 | $? | Should -Be $true
25 | }
26 | }
--------------------------------------------------------------------------------
/image/src/dev-patches/InitMessagesOff/App_Config/Environment/InitMessagesOff.config:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sitecore
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/powershell/test/Public/Import-Certificate.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Import-LoadedCertificate' {
5 | It 'requires $Certificate and $StoreName and $StoreLocation' {
6 | $result = Test-ParamIsMandatory -Command Import-LoadedCertificate -Parameter Certificate
7 | $result | Should Be $true
8 | $result = Test-ParamIsMandatory -Command Import-LoadedCertificate -Parameter StoreName
9 | $result | Should Be $true
10 | $result = Test-ParamIsMandatory -Command Import-LoadedCertificate -Parameter StoreLocation
11 | $result | Should Be $true
12 | }
13 | }
14 |
15 | Describe 'Import-CertificateForSigning' {
16 | It 'requires $SignerCertificate and $SignerCertificatePassword' {
17 | $result = Test-ParamIsMandatory -Command Import-CertificateForSigning -Parameter SignerCertificate
18 | $result | Should Be $true
19 | $result = Test-ParamIsMandatory -Command Import-CertificateForSigning -Parameter SignerCertificatePassword
20 | $result | Should Be $true
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/image/src/Dockerfile:
--------------------------------------------------------------------------------
1 | # escape=`
2 |
3 | ARG BASE_IMAGE
4 | ARG BUILD_IMAGE
5 |
6 | FROM ${BUILD_IMAGE} as build
7 |
8 | SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]
9 |
10 | # Create working directories
11 | RUN New-Item -Path 'C:\\temp' -ItemType 'Directory' -Force | Out-Null; `
12 | New-Item -Path 'C:\\tools' -ItemType 'Directory' -Force | Out-Null; `
13 | New-Item -Path 'C:\\tools\\bin' -ItemType 'Directory' -Force | Out-Null;
14 |
15 | # Install NuGet
16 | ADD https://dist.nuget.org/win-x86-commandline/v5.2.0/nuget.exe /temp/
17 |
18 | # Install Microsoft XDT assembly
19 | RUN & 'C:\\temp\\nuget.exe' install 'Microsoft.Web.Xdt' -Version '3.0.0' -OutputDirectory 'C:\\temp'; `
20 | Copy-Item -Path 'C:\\temp\\Microsoft.Web.Xdt*\\lib\\netstandard2.0\\*.dll' -Destination 'C:\\tools\\bin'; `
21 | Remove-Item -Path (Get-Item -Path 'C:\\temp\\Microsoft.Web.Xdt*\\').FullName -Recurse -Force;
22 |
23 | # Add entrypoints and scripts and patches
24 | COPY /entrypoints/ /tools/entrypoints/
25 | COPY /scripts/ /tools/scripts/
26 | COPY /dev-patches/ /tools/dev-patches/
27 |
28 | FROM ${BASE_IMAGE}
29 |
30 | # Copy resulting tools
31 | COPY --from=build /tools/ /tools/
--------------------------------------------------------------------------------
/image/src/scripts/Install-ConfigurationFolder.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Applies a directory of Sitecore configuration patches to a destination path / web root.
4 | .DESCRIPTION
5 | Applies a directory of Sitecore configuration patches to a destination path / web root. Any .config
6 | files will be copied. The structure of the directory should match the structure of the intended destination in the web root.
7 | .PARAMETER Path
8 | Specifies the path of the target web root.
9 | .PARAMETER ConfigurationPath
10 | Specifies the path of the configuration patch collection.
11 | .EXAMPLE
12 | PS C:\> .\Install-ConfigurationFolder.ps1 -Path 'C:\inetpub\wwwroot' -PatchPath 'C:\tools\dev-patches\CustomErrorsOff'
13 | .INPUTS
14 | None
15 | .OUTPUTS
16 | None
17 | #>
18 | [CmdletBinding()]
19 | Param (
20 | [Parameter(Mandatory = $true)]
21 | [ValidateScript( { Test-Path $_ -PathType Container })]
22 | [string]$Path,
23 |
24 | [Parameter(Mandatory = $true)]
25 | [ValidateScript( { Test-Path $_ -PathType Container })]
26 | [string]$PatchPath
27 | )
28 |
29 | # We need to iterate children, otherwise Copy-Item includes the folder itself when copying
30 | Get-ChildItem $PatchPath | Copy-Item -Destination $Path -Filter *.config -Recurse -Force -Container
--------------------------------------------------------------------------------
/powershell/test/Private/Utilities.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'WriteLines' {
5 |
6 | BeforeEach {
7 | Push-Location $TestDrive
8 | }
9 |
10 | AfterEach {
11 | Pop-Location
12 | }
13 |
14 | It 'throws on invalid path' {
15 | { WriteLines -File "z:\fail.txt" } | Should -Throw
16 | }
17 |
18 | It 'throws on unrooted path' {
19 | { WriteLines -File ".\file.txt" } | Should -Throw
20 | }
21 |
22 | It 'throws if lock retry limit has been reached' {
23 | $random = Get-Random
24 | $file = Join-Path -Path $TestDrive -ChildPath $random
25 | New-Item -Path $file
26 | $fileLock = [System.IO.File]::Open($file, 'Open', 'ReadWrite', 'None')
27 |
28 | { WriteLines -File $file -Content "Test" -Retries 5} | Should -Throw
29 |
30 | $fileLock.Close()
31 | }
32 |
33 | It 'writes content to file'{
34 | $file = Join-Path -Path $TestDrive -ChildPath "normalfile.txt"
35 | $content = "ABC123"
36 |
37 | WriteLines -File $file -Content $content
38 |
39 | $result = Get-Content -Path $file
40 | $result | Should -Be $content
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/powershell/src/Private/Utilities.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | function WriteLines
4 | {
5 | Param (
6 | [Parameter(Mandatory = $true)]
7 | [ValidateScript({ Test-Path $_ -IsValid })]
8 | [ValidateScript({ [System.IO.Path]::IsPathRooted($_) })]
9 | [string]
10 | $File,
11 |
12 | [string[]]
13 | $Content,
14 |
15 | [System.Text.Encoding]
16 | $Encoding = [System.Text.Encoding]::UTF8,
17 |
18 | [int]
19 | $Retries = 10
20 | )
21 |
22 | $enc = $Encoding
23 | $crlf = $enc.GetBytes([Environment]::NewLine)
24 | $tries = 0
25 | $fileLock = $false
26 |
27 | if (!(Test-Path -Path $File)) {
28 | New-Item -Path $File
29 | }
30 |
31 | do {
32 | try {
33 | $fileLock = [System.IO.File]::Open($File, 'Open', 'ReadWrite', 'None')
34 | }
35 | catch {
36 | Write-Warning -Message "Failed to get lock on file. $File"
37 | $tries++
38 | Start-Sleep -Milliseconds 100
39 | }
40 | } until ($fileLock -or ($tries -eq $Retries))
41 |
42 | if ($tries -eq $Retries) {
43 | throw "Unable to get lock on file $File after $Retries attempt(s)."
44 | }
45 |
46 | $fileLock.SetLength(0)
47 |
48 | foreach ($line in $Content) {
49 | $newLine = $enc.GetBytes($line)
50 | $fileLock.Write($newLine, 0, $newLine.Length)
51 | $fileLock.Write($crlf, 0, $crlf.Length)
52 | }
53 |
54 | $fileLock.Close()
55 | }
--------------------------------------------------------------------------------
/image/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // Available variables which can be used inside of strings.
2 | // ${workspaceRoot}: the root folder of the team
3 | // ${file}: the current opened file
4 | // ${relativeFile}: the current opened file relative to workspaceRoot
5 | // ${fileBasename}: the current opened file's basename
6 | // ${fileDirname}: the current opened file's dirname
7 | // ${fileExtname}: the current opened file's extension
8 | // ${cwd}: the current working directory of the spawned process
9 | {
10 | // See https://go.microsoft.com/fwlink/?LinkId=733558
11 | // for the documentation about the tasks.json format
12 | "version": "2.0.0",
13 |
14 | // Start PowerShell
15 | "windows": {
16 | "command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe",
17 | //"command": "${env:ProgramFiles}/PowerShell/6.0.0/powershell.exe",
18 | "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass" ]
19 | },
20 |
21 | // Associate with test task runner
22 | "tasks": [
23 | {
24 | "label": "Test",
25 | "type": "shell",
26 | "group": {
27 | "kind": "test",
28 | "isDefault": true
29 | },
30 | "args": [
31 | "Write-Host 'Invoking Pester...';",
32 | "Invoke-Pester -Script test -PesterOption @{IncludeVSCodeMarker=$true};",
33 | "Write-Host 'Completed Test task in task runner.';"
34 | ],
35 | "problemMatcher": "$pester"
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/powershell/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // Available variables which can be used inside of strings.
2 | // ${workspaceRoot}: the root folder of the team
3 | // ${file}: the current opened file
4 | // ${relativeFile}: the current opened file relative to workspaceRoot
5 | // ${fileBasename}: the current opened file's basename
6 | // ${fileDirname}: the current opened file's dirname
7 | // ${fileExtname}: the current opened file's extension
8 | // ${cwd}: the current working directory of the spawned process
9 | {
10 | // See https://go.microsoft.com/fwlink/?LinkId=733558
11 | // for the documentation about the tasks.json format
12 | "version": "2.0.0",
13 |
14 | // Start PowerShell
15 | "windows": {
16 | "command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe",
17 | //"command": "${env:ProgramFiles}/PowerShell/6.0.0/powershell.exe",
18 | "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass" ]
19 | },
20 |
21 | // Associate with test task runner
22 | "tasks": [
23 | {
24 | "label": "Test",
25 | "type": "shell",
26 | "group": {
27 | "kind": "test",
28 | "isDefault": true
29 | },
30 | "args": [
31 | "Write-Host 'Invoking Pester...';",
32 | "Invoke-Pester -Script test -PesterOption @{IncludeVSCodeMarker=$true};",
33 | "Write-Host 'Completed Test task in task runner.';"
34 | ],
35 | "problemMatcher": "$pester"
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/powershell/src/Public/Create-PfxFile.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Exports a certificate to a PFX file.
6 | .DESCRIPTION
7 | Exports the specified certificate to a PFX file at the given path, using the provided password to protect the PFX file.
8 | .PARAMETER Certificate
9 | The certificate to be exported.
10 | .PARAMETER OutCertPath
11 | The file path where the PFX file will be saved.
12 | .PARAMETER Password
13 | The password to protect the PFX file.
14 | .INPUTS
15 | None. You cannot pipe objects to Create-PfxFile.
16 | .OUTPUTS
17 | None. Create-PfxFile does not generate any output.
18 | .EXAMPLE
19 | PS C:\> $cert = Get-Item Cert:\CurrentUser\My\THUMBPRINT
20 | PS C:\> $password = ConvertTo-SecureString -String "password" -AsPlainText -Force
21 | PS C:\> Create-PfxFile -Certificate $cert -OutCertPath "C:\path\to\certificate.pfx" -Password $password
22 | #>
23 | function Create-PfxFile{
24 | param (
25 | [Parameter(Mandatory = $true)]
26 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
27 | $Certificate,
28 |
29 | [Parameter(Mandatory = $true)]
30 | [string]$OutCertPath,
31 |
32 | [Parameter(Mandatory = $true)]
33 | [SecureString]$Password
34 | )
35 |
36 | Write-Information -MessageData "Exporting '$($Certificate.Thumbprint)' certificate into Pfx." -InformationAction Continue
37 |
38 | $pfxContent = $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $Password)
39 |
40 | [System.IO.File]::WriteAllBytes($OutCertPath, $pfxContent)
41 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Load-Certificate.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Loads a certificate from a specified path.
6 | .DESCRIPTION
7 | Loads a certificate from the specified file path. If a password is provided, it will be used to decrypt the certificate.
8 | .PARAMETER CertPath
9 | The file path to the certificate to be loaded.
10 | .PARAMETER CertPassword
11 | The password for the certificate, if it is encrypted. This parameter is optional.
12 | .INPUTS
13 | None. You cannot pipe objects to Load-Certificate.
14 | .OUTPUTS
15 | System.Security.Cryptography.X509Certificates.X509Certificate2. The loaded certificate.
16 | .EXAMPLE
17 | PS C:\> Load-Certificate -CertPath "C:\path\to\certificate.pfx"
18 | PS C:\> Load-Certificate -CertPath "C:\path\to\certificate.pfx" -CertPassword (ConvertTo-SecureString -String "password" -AsPlainText -Force)
19 | #>
20 | function Load-Certificate {
21 | param(
22 | [Parameter(Mandatory = $true)]
23 | [string]
24 | $CertPath,
25 |
26 | [Parameter()]
27 | [SecureString]
28 | $CertPassword = $null
29 | )
30 |
31 | Write-Information -MessageData "Loading '$CertPath' certificate." -InformationAction Continue
32 |
33 | # Reading into memory first as a workaround for
34 | # https://github.com/dotnet/runtime/issues/27826
35 | $certContent = [System.IO.File]::ReadAllBytes($CertPath)
36 |
37 | if ($CertPassword -ne $null) {
38 | $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($certContent, $CertPassword)
39 | }else {
40 | $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$certContent)
41 | }
42 |
43 | return $cert
44 | }
--------------------------------------------------------------------------------
/image/src/scripts/Get-PatchFolders.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Gets all the subfolders that exist under the provided path which are listed in the SITECORE_DEVELOPMENT_TRANSFORMS environment variable.
4 | .DESCRIPTION
5 | Splits the folder names in SITECORE_DEVELOPMENT_TRANSFORMS and finds matching patch folders under the provided path.
6 | .PARAMETER Path
7 | Specifies the path to search.
8 | .EXAMPLE
9 | PS C:\> .\Get-PatchFolders.ps1 -Path c:\tools\dev-patches
10 | .INPUTS
11 | None
12 | .OUTPUTS
13 | None
14 | #>
15 | Function Get-PatchFolders {
16 | Param (
17 | [Parameter(Mandatory = $true)]
18 | [ValidateScript( { Test-Path $_ -PathType Container })]
19 | [string]$Path
20 | )
21 |
22 | $folders = @()
23 |
24 | # Example: SITECORE_DEVELOPMENT_PATCHES=CustomErrorsOn,DebugOn,OptimizeCompilationsOn
25 | $folderNames = $env:SITECORE_DEVELOPMENT_PATCHES
26 | if (-not $folderNames) {
27 | return $folders
28 | }
29 |
30 | $illegalCharacters = [System.IO.Path]::GetInvalidFileNameChars()
31 | $folderNames.Split(",") | ForEach-Object {
32 | $patchFolder = $_
33 |
34 | $validName = $true
35 | $illegalCharacters | ForEach-Object {
36 | if ($patchFolder.IndexOf($_) -gt 0) {
37 | Write-Host "** Sitecore development patch folder name $patchFolder is invalid"
38 | $validName = $false
39 | return
40 | }
41 | }
42 | if (-not $validName) {
43 | return
44 | }
45 |
46 | $folder = Join-Path $Path $patchFolder
47 | if (-not (Test-Path $folder)) {
48 | Write-Host "** Sitecore development patch folder $patchFolder not found in $Path"
49 | return
50 | }
51 | $folders += (Get-Item $folder)
52 | }
53 | $folders
54 | }
--------------------------------------------------------------------------------
/image/src/entrypoints/worker/Development.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [Parameter(Mandatory = $false)]
4 | [hashtable]$WatchDirectoryParameters
5 | )
6 |
7 | # Setup
8 | $ErrorActionPreference = "Stop"
9 | $InformationPreference = "Continue"
10 | $timeFormat = "HH:mm:ss:fff"
11 | $executable = "C:\\service\\$($env:WORKER_EXECUTABLE_NAME_ENV)"
12 |
13 | # Print start message
14 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: starting..."
15 |
16 | # Check to see if we should start the Watch-Directory.ps1 script
17 | $watchDirectoryJobName = "Watch-Directory.ps1"
18 | $useWatchDirectory = $null -ne $WatchDirectoryParameters -bor (Test-Path -Path "C:\deploy" -PathType "Container") -eq $true
19 |
20 | if ($useWatchDirectory)
21 | {
22 | # Setup default parameters if none is supplied
23 | if ($null -eq $WatchDirectoryParameters)
24 | {
25 | $WatchDirectoryParameters = @{ Path = "C:\deploy"; Destination = "C:\service"; }
26 | }
27 | $WatchDirectoryParameters["Executable"] = $executable
28 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: '$watchDirectoryJobName' validating..."
29 |
30 | # First a trial-run to catch any parameter validation / setup errors
31 | $WatchDirectoryParameters["WhatIf"] = $true
32 | & "C:\tools\scripts\Watch-Directory.ps1" @WatchDirectoryParameters
33 | $WatchDirectoryParameters["WhatIf"] = $false
34 |
35 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: '$watchDirectoryJobName' started."
36 |
37 | & "C:\tools\scripts\Watch-Directory.ps1" @WatchDirectoryParameters
38 | }
39 | else
40 | {
41 | Write-Host ("$(Get-Date -Format $timeFormat): Development ENTRYPOINT: Skipping start of '$watchDirectoryJobName'. To enable you should mount a directory into '$watchFolder'.")
42 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: ready!"
43 | & "$executable"
44 | }
--------------------------------------------------------------------------------
/powershell/test/TestUtils.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [Parameter(HelpMessage="The block of tests to run in the scope of the module")]
3 | [ScriptBlock]$TestScope = $null
4 | )
5 |
6 | Function Test-ParamIsMandatory {
7 | param(
8 | [Parameter(Mandatory=$true)]
9 | [string]$CommandName,
10 | [Parameter(Mandatory=$true)]
11 | [string]$Parameter,
12 | [string]$SetName = ''
13 | )
14 |
15 | $cmd = Get-Command $CommandName
16 | $attr = $cmd.Parameters.$Parameter.Attributes | Where-Object { $_.TypeId -eq [System.Management.Automation.ParameterAttribute] }
17 | if($SetName) {
18 | $attr = $attr | Where-Object { $_.ParameterSetName -eq $SetName }
19 | }
20 | $attr.Mandatory
21 | }
22 |
23 | Function Test-ParamValidateSet {
24 | param(
25 | [Parameter(Mandatory=$true)]
26 | [string]$CommandName,
27 | [Parameter(Mandatory=$true)]
28 | [string]$Parameter,
29 | [string[]]$Values
30 | )
31 |
32 | $cmd = Get-Command $CommandName
33 | $attr = $cmd.Parameters.$Parameter.Attributes | Where-Object { $_.TypeId -eq [System.Management.Automation.ValidateSetAttribute] }
34 |
35 | Compare-Sets $attr.ValidValues $Values
36 | }
37 |
38 | Function Compare-Sets {
39 | param(
40 | [Parameter(Mandatory=$true)]
41 | [psobject[]]$Left,
42 | [Parameter(Mandatory=$true)]
43 | [psobject[]]$Right
44 | )
45 |
46 | $results = Compare-Object $Left $Right
47 | if($results){
48 | $formatter = {
49 | $obj = New-Object psobject -Property @{
50 | State = if($_.SideIndicator.Contains('>')) { 'Missing' } else { 'Extra' }
51 | Value = $_.InputObject
52 | }
53 |
54 | Write-Host "$($Obj.State) => $($obj.Value)"
55 | }
56 |
57 | $results | ForEach-Object $formatter
58 | return $false
59 | }
60 |
61 | $true
62 | }
--------------------------------------------------------------------------------
/powershell/test/Private/Get-EnvFileContent.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Get-EnvFileContent' {
5 |
6 | BeforeEach {
7 | Push-Location $TestDrive
8 | }
9 |
10 | AfterEach {
11 | Pop-Location
12 | }
13 |
14 | It 'throws on invalid path' {
15 | { Get-EnvFileContent -File "z:\fail.txt" } | Should -Throw
16 | }
17 |
18 | It 'throws on unrooted path' {
19 | { Get-EnvFileContent -File ".\file.txt" } | Should -Throw
20 | }
21 |
22 | It 'reads content from file the absolute path' {
23 | $envFile = Join-Path $TestDrive '.env'
24 | $content = @(
25 | 'VAR1=VAL1',
26 | 'VAR2=VAL2',
27 | 'VAR3=VAL3'
28 | )
29 | Set-Content $envFile -Value $content
30 |
31 | $compare = @{
32 | VAR1 = 'VAL1'
33 | VAR2 = 'VAL2'
34 | VAR3 = 'VAL3'
35 | }
36 |
37 | $result = Get-EnvFileContent -File $envFile
38 | $result.Count | Should -Be $compare.Keys.Count
39 | $result.Get_Item('VAR1') | Should -Be $compare.Get_Item('VAR1')
40 | }
41 |
42 | It 'reads content from file using the relative path' {
43 | $envFile = '.env'
44 | $content = @(
45 | 'VAR1=VAL1',
46 | 'VAR2=VAL2',
47 | 'VAR3=VAL3'
48 | )
49 | Set-Content $envFile -Value $content
50 |
51 | $compare = @{
52 | VAR1 = 'VAL1'
53 | VAR2 = 'VAL2'
54 | VAR3 = 'VAL3'
55 | }
56 |
57 | $result = Get-EnvFileContent -File $envFile
58 | $result.Count | Should -Be $compare.Keys.Count
59 | $result.Get_Item('VAR1') | Should -Be $compare.Get_Item('VAR1')
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Remove-HostsEntry.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Removes a host entry from the system hosts file.
6 | .DESCRIPTION
7 | Removes a host entry with the specified Hostname from the system hosts file (if it exist).
8 | A backup of the current hosts file is taken before updating.
9 | .PARAMETER Hostname
10 | The hostname to remove.
11 | .INPUTS
12 | None. You cannot pipe objects to Remove-HostsEntry.
13 | .OUTPUTS
14 | None. Remove-HostsEntry does not generate any output.
15 | .EXAMPLE
16 | PS C:\> Remove-HostsEntry 'my.host.name'
17 | #>
18 | function Remove-HostsEntry
19 | {
20 | Param (
21 | [Parameter(Mandatory = $true, Position = 0)]
22 | [ValidateNotNullOrEmpty()]
23 | [string]
24 | $Hostname,
25 |
26 | [string]
27 | $Path = (Join-Path -Path $env:windir -ChildPath "system32\drivers\etc\hosts")
28 | )
29 |
30 | if (-not (Test-Path $Path)) {
31 | Write-Warning "No hosts file found, hosts have not been updated"
32 | return
33 | }
34 |
35 | # Create backup
36 | Copy-Item $Path "$Path.backup"
37 | Write-Verbose "Created backup of hosts file to $Path.backup"
38 |
39 | # Build regex match pattern
40 | $pattern = '^[0-9a-f.:]+\s+' + [Regex]::Escape($HostName) + '\s*$'
41 |
42 | $hostsContent = @(Get-Content -Path $Path -Encoding UTF8)
43 |
44 | if (-not $hostsContent) {
45 | Write-Verbose "The hosts file is empty, hosts have not been updated"
46 | return
47 | }
48 |
49 | # Check if exists
50 | $updatedHostsContent = $hostsContent | Select-String -Pattern $pattern -NotMatch
51 | if ($null -ne $updatedHostsContent -and @(Compare-Object -ReferenceObject $hostsContent -DifferenceObject $updatedHostsContent).Count -eq 0) {
52 | Write-Verbose "No existing host entry found for hostname '$HostName'"
53 | return
54 | }
55 |
56 | # Remove it
57 | WriteLines -File $Path -Content $updatedHostsContent -Encoding ([System.Text.Encoding]::UTF8)
58 | Write-Verbose -Message "Host entry for hostname '$HostName' has been removed"
59 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Add-HostsEntry.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Adds a host entry to the system hosts file.
6 | .DESCRIPTION
7 | Adds a host entry with the specified Hostname and IPAddress to the system hosts file (if it does not already exist).
8 | A backup of the current hosts file is taken before updating.
9 | .PARAMETER Hostname
10 | The hostname to use for the entry.
11 | .PARAMETER IPAddress
12 | The IP address to use for the entry. Default is 127.0.0.1.
13 | .INPUTS
14 | None. You cannot pipe objects to Add-HostsEntry.
15 | .OUTPUTS
16 | None. Add-HostsEntry does not generate any output.
17 | .EXAMPLE
18 | PS C:\> Add-HostsEntry 'my.host.name'
19 | #>
20 | function Add-HostsEntry
21 | {
22 | Param (
23 | [Parameter(Mandatory = $true, Position = 0)]
24 | [ValidateNotNullOrEmpty()]
25 | [string]
26 | $Hostname,
27 |
28 | [string]
29 | [ValidateNotNullOrEmpty()]
30 | $IPAddress = "127.0.0.1",
31 |
32 | [string]
33 | $Path = (Join-Path -Path $env:windir -ChildPath "system32\drivers\etc\hosts")
34 | )
35 |
36 | if (-not (Test-Path $Path)) {
37 | Write-Warning "No hosts file found, hosts have not been updated"
38 | return
39 | }
40 |
41 | # Create backup
42 | Copy-Item $Path "$Path.backup"
43 | Write-Verbose "Created backup of hosts file to $Path.backup"
44 |
45 | # Build regex match pattern
46 | $pattern = '^' + [Regex]::Escape($IPAddress) + '\s+' + [Regex]::Escape($HostName) + '\s*$'
47 |
48 | $hostsContent = @(Get-Content -Path $Path -Encoding UTF8)
49 |
50 | # Check if exists
51 | $existingEntries = $hostsContent -match $pattern
52 | if ($existingEntries.Count -gt 0) {
53 | Write-Verbose "Existing host entry found for $IPAddress with hostname '$HostName'"
54 | return
55 | }
56 |
57 | # Add it
58 | $hostsContent += "$IPAddress`t$HostName"
59 | WriteLines -File $Path -Content $hostsContent -Encoding ([System.Text.Encoding]::UTF8)
60 | Write-Verbose "Host entry for $IPAddress with hostname '$HostName' has been added"
61 | }
--------------------------------------------------------------------------------
/powershell/src/Public/ConvertTo-CompressedBase64String.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Converts file contents to GZip compressed, Base64 encoded value.
6 | .DESCRIPTION
7 | Converts file contents to GZip compressed, Base64 encoded value.
8 | .PARAMETER Stream
9 | Specifies the file stream. Either Stream or Path is required.
10 | .PARAMETER Path
11 | Specifies the file path. Either Stream or Path is required.
12 | .EXAMPLE
13 | PS C:\> ConvertTo-CompressedBase64String -Path C:\file.txt
14 | .EXAMPLE
15 | PS C:\> [System.IO.File]::OpenRead('C:\file.txt') | ConvertTo-CompressedBase64String
16 | .INPUTS
17 | System.IO.FileStream. You can pipe in the Stream parameter.
18 | .OUTPUTS
19 | System.String. The GZip compressed, Base64 encoded string.
20 | #>
21 | function ConvertTo-CompressedBase64String
22 | {
23 | [CmdletBinding(DefaultParameterSetName = 'FromPath')]
24 | Param (
25 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'FromStream')]
26 | [System.IO.FileStream]
27 | $Stream,
28 |
29 | [Parameter(Mandatory = $true, ParameterSetName = 'FromPath')]
30 | [ValidateScript({ Test-Path $_ -PathType Leaf })]
31 | [string]
32 | $Path
33 | )
34 |
35 | $encodedString = $null
36 | if ($PSCmdlet.ParameterSetName -eq 'FromPath') {
37 | $Stream = [System.IO.File]::OpenRead($Path)
38 | }
39 |
40 | try {
41 | $memory = [System.IO.MemoryStream]::new()
42 | $gzip = [System.IO.Compression.GZipStream]::new($memory, [System.IO.Compression.CompressionLevel]::Optimal, $false)
43 | $Stream.CopyTo($gzip)
44 | $gzip.Close()
45 |
46 | # base64 encode the gzipped content
47 | $encodedString = [System.Convert]::ToBase64String($memory.ToArray())
48 | }
49 | finally {
50 | # cleanup
51 | if ($null -ne $gzip) {
52 | $gzip.Dispose()
53 | $gzip = $null
54 | }
55 |
56 | if ($null -ne $memory) {
57 | $memory.Dispose()
58 | $memory = $null
59 | }
60 |
61 | $Stream.Dispose()
62 | $Stream = $null
63 | }
64 |
65 | return $encodedString
66 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Get-SitecoreCertificateAsBase64String.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Generates a new self-signed certificate in its Base64 encoded form.
6 | .DESCRIPTION
7 | Generates a new self-signed certificate and returns the certificate in its password-protected, Base64 encoded form.
8 | .PARAMETER Password
9 | Specifies the password to be used for securing the certificate.
10 | .PARAMETER DnsName
11 | Specifies the DnsName to use for the certificate. Uses "localhost" by default.
12 | .EXAMPLE
13 | PS C:\> Get-SitecoreCertificateAsBase64String -DnsName "localhost" -Password (ConvertTo-SecureString -String "Password12345" -Force -AsPlainText)
14 | .INPUTS
15 | System.SecureString. You can pipe in the Password parameter.
16 | .OUTPUTS
17 | System.String. The Base64 encoded string.
18 | #>
19 | function Get-SitecoreCertificateAsBase64String
20 | {
21 | Param (
22 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
23 | [securestring]
24 | $Password,
25 |
26 | [ValidateNotNullOrEmpty()]
27 | [string[]]
28 | $DnsName = "localhost",
29 |
30 | [Parameter()]
31 | [ValidateSet(512, 1024, 2048, 4096)]
32 | [int]
33 | $KeyLength = 2048
34 | )
35 |
36 | $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
37 | [X500DistinguishedName]::new("CN=$DnsName"),
38 | [System.Security.Cryptography.RSA]::Create($KeyLength),
39 | [System.Security.Cryptography.HashAlgorithmName]::SHA256,
40 | [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
41 |
42 | $basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $false)
43 | $certRequest.CertificateExtensions.Add($basicConstraints)
44 | $subjectKeyIdentifier = [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($certRequest.PublicKey, $false)
45 | $certRequest.CertificateExtensions.Add($subjectKeyIdentifier)
46 |
47 | $certificate = $certRequest.CreateSelfSigned([datetime]::Now, [datetime]::Now.AddYears(5))
48 | $certificateBytes = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $Password)
49 |
50 | return [System.Convert]::ToBase64String($certificateBytes)
51 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Get-EnvFileVariable.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Gets the value of a variable in a Docker environment (.env) file.
6 | .DESCRIPTION
7 | Gets the value of a variable in a Docker environment (.env) file.
8 | Assumes .env file is in the current directory by default.
9 | .PARAMETER Variable
10 | Specifies the variable name.
11 | .PARAMETER Path
12 | Specifies the Docker environment (.env) file path. Assumes .env file is in the current directory by default.
13 | .EXAMPLE
14 | PS C:\> Get-EnvFileVariable -Variable VAR1
15 | .EXAMPLE
16 | PS C:\> Get-EnvFileVariable "VAR1"
17 | .EXAMPLE
18 | PS C:\> Get-EnvFileVariable -Variable VAR1 -Path .\src\.env
19 | .INPUTS
20 | System.String.
21 | .OUTPUTS
22 | System.String. Value of variable. System.Exception if key is not present
23 | #>
24 | function Get-EnvFileVariable {
25 | Param (
26 | [Parameter(Mandatory = $true, Position = 0)]
27 | [ValidateNotNullOrEmpty()]
28 | [string]
29 | $Variable,
30 |
31 | [string]
32 | $Path = ".\.env"
33 | )
34 |
35 | if (!(Test-Path $Path)) {
36 | throw "The environment file $Path does not exist"
37 | }
38 |
39 | $variables = Get-EnvFileContent $Path
40 | try {
41 | if ($variables.ContainsKey($variable)) {
42 | $rawVariable = $variables.Get_Item($variable)
43 | if (IsLiteral -Value $rawVariable) {
44 | #strip out the start/end single quotes if it's a literal value and un-escape.
45 | return $rawVariable.Substring(1, $rawVariable.Length - 2).Replace("''", "'")
46 | }
47 | return $rawVariable
48 | }
49 | else {
50 | throw "Unable to find value for $Variable in $Path"
51 | }
52 | }
53 | catch {
54 | throw "Unable to find value for $Variable in $Path"
55 | }
56 | }
57 |
58 | function IsLiteral {
59 | param(
60 | [Parameter(Mandatory = $true)]
61 | [string]
62 | $Value
63 | )
64 | if(!$Value.StartsWith("'")){ return $false }
65 | #Is it a literal value? Test for an odd number of starting 's to avoid escaped values and ending with '
66 | $nonQuoteIndex = [regex]::Match($Value, "[^']")
67 | return ($nonQuoteIndex.Success -and $nonQuoteIndex.Index % 2 -eq 1 -and $Value.EndsWith("'"))
68 | }
--------------------------------------------------------------------------------
/powershell/test/Public/ConvertTo-CompressedBase64String.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | function GetExpectedResult([string] $path) {
5 | $stream = [System.IO.File]::OpenRead($path)
6 | $memory = [System.IO.MemoryStream]::new()
7 | $gzip = [System.IO.Compression.GZipStream]::new($memory, [System.IO.Compression.CompressionLevel]::Optimal, $false)
8 | $stream.CopyTo($gzip)
9 | $gzip.Close()
10 | $expectedResult = [System.Convert]::ToBase64String($memory.ToArray())
11 |
12 | $gzip.Dispose()
13 | $gzip = $null
14 | $memory.Dispose()
15 | $memory = $null
16 | $stream.Dispose()
17 | $stream = $null
18 |
19 | return $expectedResult
20 | }
21 |
22 | Describe 'ConvertTo-CompressedBase64String' {
23 |
24 | BeforeAll {
25 | $testPath = Join-Path $TestDrive 'test.txt'
26 | Set-Content $testPath -value "Lorem ipsum dolor sit amet."
27 | $expectedResult = GetExpectedResult $testPath
28 | }
29 |
30 | It 'requires $Path or $Stream' {
31 | $result = Test-ParamIsMandatory -Command ConvertTo-CompressedBase64String -Parameter Path -SetName 'FromPath'
32 | $result | Should -Be $true
33 | $result = Test-ParamIsMandatory -Command ConvertTo-CompressedBase64String -Parameter Stream -SetName 'FromStream'
34 | $result | Should -Be $true
35 | }
36 |
37 | It 'throws if $Path is $null' {
38 | { ConvertTo-CompressedBase64String -Path $null } | Should -Throw
39 | }
40 |
41 | It 'throws if $Path is empty' {
42 | { ConvertTo-CompressedBase64String -Path "" } | Should -Throw
43 | }
44 |
45 | It 'throws if $Path is invalid' {
46 | { ConvertTo-CompressedBase64String -Path "NO:\\NO" } | Should -Throw
47 | }
48 |
49 | It 'throws if $Path is a folder' {
50 | { ConvertTo-CompressedBase64String -Path $TestDrive } | Should -Throw
51 | }
52 |
53 | It 'returns compressed base64 string from $Path' {
54 | $result = ConvertTo-CompressedBase64String -Path $testPath
55 | $result | Should -Be $expectedResult
56 | }
57 |
58 | It 'returns compressed base64 string from $Stream' {
59 | $result = [System.IO.File]::OpenRead($testPath) | ConvertTo-CompressedBase64String
60 | $result | Should -Be $expectedResult
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Set-EnvFileVariable.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Sets a variable in a Docker environment (.env) file.
6 | .DESCRIPTION
7 | Sets a variable in a Docker environment (.env) file.
8 | Assumes .env file is in the current directory by default.
9 | .PARAMETER Variable
10 | Specifies the variable name.
11 | .PARAMETER Value
12 | Specifies the variable value.
13 | .PARAMETER AsLiteral
14 | Specifies whether the Value should be written as a literal (i.e wrapped in single quotes)
15 | .PARAMETER Path
16 | Specifies the Docker environment (.env) file path. Assumes .env file is in the current directory by default.
17 | .EXAMPLE
18 | PS C:\> Set-EnvFileVariable -Variable VAR1 -Value "value one"
19 | .EXAMPLE
20 | PS C:\> "value one" | Set-EnvFileVariable "VAR1"
21 | .EXAMPLE
22 | PS C:\> Set-EnvFileVariable -Variable VAR1 -Value "value one" -Path .\src\.env
23 | .EXAMPLE
24 | PS C:\> Set-EnvFileVariable -Variable VAR1 -Value "literal $tring" -AsLiteral
25 | .INPUTS
26 | System.String. You can pipe in the Value parameter.
27 | .OUTPUTS
28 | None.
29 | #>
30 | function Set-EnvFileVariable
31 | {
32 | Param (
33 | [Parameter(Mandatory = $true, Position = 0)]
34 | [ValidateNotNullOrEmpty()]
35 | [string]
36 | $Variable,
37 |
38 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
39 | [ValidateNotNull()]
40 | [AllowEmptyString()]
41 | [string]
42 | $Value,
43 |
44 | [switch]
45 | $AsLiteral = $false,
46 |
47 | [string]
48 | $Path = ".\.env"
49 | )
50 |
51 | if (!(Test-Path $Path)) {
52 | throw "The environment file $Path does not exist"
53 | }
54 |
55 | if ($AsLiteral){
56 | # Escape any ' to avoid terminating the value unexpectedly
57 | $Value = "'$($Value.Replace("'", "''"))'"
58 | }
59 |
60 | $found = $false
61 |
62 | $lines = @(Get-Content $Path -Encoding UTF8 | ForEach-Object {
63 | if ($_ -imatch "^$Variable=.*") {
64 | # Escape any '$' to prevent being used as a regex substitution
65 | $Value = $Value.Replace('$', '$$')
66 | $_ -ireplace "^$Variable=.*", "$Variable=$Value"
67 | $found = $true
68 | }
69 | else {
70 | $_
71 | }
72 | })
73 |
74 | if (!$found) {
75 | $lines += "$Variable=$Value"
76 | }
77 |
78 | WriteLines -File (Resolve-Path $Path) -Content $lines -Encoding ([System.Text.Encoding]::UTF8)
79 | }
80 |
81 | # For backward compatibility
82 | New-Alias -Name Set-DockerComposeEnvFileVariable -Value Set-EnvFileVariable
--------------------------------------------------------------------------------
/image/src/entrypoints/iis/Development.ps1:
--------------------------------------------------------------------------------
1 | [CmdletBinding()]
2 | param(
3 | [Parameter(Mandatory = $false)]
4 | [hashtable]$WatchDirectoryParameters
5 | )
6 |
7 | # Setup
8 | $ErrorActionPreference = "Stop"
9 | $InformationPreference = "Continue"
10 | $timeFormat = "HH:mm:ss:fff"
11 |
12 | # Print start message
13 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: starting..."
14 |
15 | # Check to see if we should start the Watch-Directory.ps1 script
16 | $watchDirectoryJobName = "Watch-Directory.ps1"
17 | $useWatchDirectory = $null -ne $WatchDirectoryParameters -bor (Test-Path -Path "C:\deploy" -PathType "Container") -eq $true
18 |
19 | if ($useWatchDirectory)
20 | {
21 | # Setup default parameters if none is supplied
22 | if ($null -eq $WatchDirectoryParameters)
23 | {
24 | $WatchDirectoryParameters = @{ Path = "C:\deploy"; Destination = "C:\inetpub\wwwroot"; }
25 | }
26 |
27 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: '$watchDirectoryJobName' validating..."
28 |
29 | # First a trial-run to catch any parameter validation / setup errors
30 | $WatchDirectoryParameters["WhatIf"] = $true
31 | & "C:\tools\scripts\Watch-Directory.ps1" @WatchDirectoryParameters
32 | $WatchDirectoryParameters["WhatIf"] = $false
33 |
34 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: '$watchDirectoryJobName' starting..."
35 |
36 | # Start Watch-Directory.ps1 in background
37 | Start-Job -Name $watchDirectoryJobName -ArgumentList $WatchDirectoryParameters -ScriptBlock {
38 | param([hashtable]$params)
39 |
40 | & "C:\tools\scripts\Watch-Directory.ps1" @params
41 |
42 | } | Out-Null
43 |
44 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: '$watchDirectoryJobName' started."
45 | }
46 | else
47 | {
48 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: Skipping start of '$watchDirectoryJobName'. To enable you should mount a directory into 'C:\deploy'."
49 | }
50 |
51 | # Apply any patch folders configured in SITECORE_DEVELOPMENT_PATCHES
52 | Write-Host "$(Get-Date -Format $timeFormat): Applying SITECORE_DEVELOPMENT_PATCHES..."
53 | Push-Location $PSScriptRoot\..\..\
54 | try {
55 | . .\scripts\Get-PatchFolders.ps1
56 | Get-PatchFolders -Path dev-patches | ForEach-Object {
57 | Write-Host "$(Get-Date -Format $timeFormat): Applying development patches from $($_.Name)"
58 | & .\scripts\Invoke-XdtTransform.ps1 -XdtPath $_.FullName -Path $WatchDirectoryParameters.Destination
59 | & .\scripts\Install-ConfigurationFolder.ps1 -PatchPath $_.FullName -Path $WatchDirectoryParameters.Destination
60 | }
61 | } finally {
62 | Pop-Location
63 | }
64 |
65 | # Print ready message
66 | Write-Host "$(Get-Date -Format $timeFormat): Development ENTRYPOINT: ready!"
67 |
68 | & "C:\LogMonitor\LogMonitor.exe" "powershell" "C:\Run-W3SVCService.ps1"
--------------------------------------------------------------------------------
/powershell/src/Public/Create-SqlServerCertificate.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Creates a self-signed certificate for SQL Server.
6 | .DESCRIPTION
7 | Creates a self-signed certificate with the specified CommonName, DnsName, and SignerCertificate.
8 | The certificate is configured for SSL Server Authentication and uses RSA with SHA256.
9 | .PARAMETER CommonName
10 | The common name (CN) to use for the certificate subject.
11 | .PARAMETER DnsName
12 | The DNS name to include in the certificate.
13 | .PARAMETER SignerCertificate
14 | The certificate used to sign the new certificate.
15 | .PARAMETER NotBefore
16 | The start date and time for the certificate validity period. Default is the current date and time.
17 | .PARAMETER NotAfter
18 | The end date and time for the certificate validity period. Default is 3285 days from the current date and time.
19 | .INPUTS
20 | None. You cannot pipe objects to Create-SqlServerCertificate.
21 | .OUTPUTS
22 | System.Security.Cryptography.X509Certificates.X509Certificate2. The created certificate.
23 | .EXAMPLE
24 | PS C:\> $signerCert = Get-Item Cert:\LocalMachine\My\{THUMBPRINT}
25 | PS C:\> Create-SqlServerCertificate -CommonName 'sql.server' -DnsName 'localhost' -SignerCertificate $signerCert
26 | #>
27 | function Create-SqlServerCertificate{
28 | param(
29 | [Parameter(Mandatory=$true)]
30 | [Alias("CN")]
31 | [string]
32 | $CommonName,
33 |
34 | [Parameter(Mandatory=$true)]
35 | [string]
36 | $DnsName,
37 |
38 | [Parameter(Mandatory=$true)]
39 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
40 | $SignerCertificate,
41 |
42 | [Parameter()]
43 | [System.DateTimeOffset]
44 | $NotBefore = [System.DateTimeOffset]::UtcNow,
45 |
46 | [Parameter()]
47 | [System.DateTimeOffset]
48 | $NotAfter = [System.DateTimeOffset]::UtcNow.AddDays(3285)
49 | )
50 |
51 | Write-Information -MessageData "Creating a certificate for SqlServer with '$($SignerCertificate.Thumbprint)' signer." -InformationAction Continue
52 |
53 | $certificateParams = @{
54 | Type = "SSLServerAuthentication"
55 | Subject = "CN=$CommonName"
56 | DnsName = @($DnsName, 'localhost')
57 | KeyAlgorithm = "RSA"
58 | KeyLength = 2048
59 | HashAlgorithm = "SHA256"
60 | TextExtension = "2.5.29.37={text}1.3.6.1.5.5.7.3.1"
61 | NotBefore = $NotBefore.DateTime
62 | NotAfter = $NotAfter.DateTime
63 | KeySpec = "KeyExchange"
64 | Provider = "Microsoft RSA SChannel Cryptographic Provider"
65 | Signer = $SignerCertificate
66 | FriendlyName = "Sitecore Container Development Sql Server Certificate"
67 | };
68 |
69 | $certificate = New-SelfSignedCertificate @certificateParams
70 |
71 | return $certificate
72 | }
73 |
--------------------------------------------------------------------------------
/powershell/test/Public/Create-PfxFile.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Create-PfxFile' {
5 | $certPath = Join-Path -Path $TestDrive -ChildPath "certificate.pfx"
6 | $certPassword = ConvertTo-SecureString -String "password" -AsPlainText -Force
7 |
8 | BeforeAll {
9 | $certificate = New-SelfSignedCertificate -DnsName "mockCertificate" -CertStoreLocation "Cert:\CurrentUser\My"
10 | }
11 |
12 | AfterAll {
13 | if ($certificate -ne $null) {
14 | Remove-Item -Path $certificate.PSPath -Force
15 | }
16 | }
17 |
18 | It 'requires $Certificate and $OutCertPath and $Password' {
19 | $result = Test-ParamIsMandatory -Command Create-PfxFile -Parameter Certificate
20 | $result | Should Be $true
21 | $result = Test-ParamIsMandatory -Command Create-PfxFile -Parameter OutCertPath
22 | $result | Should Be $true
23 | $result = Test-ParamIsMandatory -Command Create-PfxFile -Parameter Password
24 | $result | Should Be $true
25 | }
26 |
27 | It 'throws if $Certificate is $null' {
28 | { Create-PfxFile -Certificate $null } | Should -Throw
29 | }
30 |
31 | It 'throws if $OutCertPath is $null' {
32 | { Create-PfxFile -OutCertPath $null } | Should -Throw
33 | }
34 |
35 | It 'throws if $Password is $null' {
36 | { Create-PfxFile -Password $null } | Should -Throw
37 | }
38 |
39 | Context 'When exporting a certificate to a PFX file' {
40 | It 'Should export the certificate successfully' {
41 | # Act
42 | Create-PfxFile -Certificate $certificate -OutCertPath $certPath -Password $certPassword
43 |
44 | # Assert
45 | Test-Path $certPath | Should -Be $true
46 | $exportedContent = [System.IO.File]::ReadAllBytes($certPath)
47 | $exportedContent.Length | Should -BeGreaterThan 0
48 | }
49 | }
50 |
51 | Context 'When the certificate is invalid' {
52 | It 'Should throw an error' {
53 | # Arrange
54 | $invalidCertificate = $null
55 |
56 | # Act & Assert
57 | { Create-PfxFile -Certificate $invalidCertificate -OutCertPath $certPath -Password $certPassword } | Should -Throw
58 | }
59 | }
60 |
61 | Context 'When the output path is invalid' {
62 | It 'Should throw an error' {
63 | # Arrange
64 | $invalidCertPath = Join-Path -Path $TestDrive -ChildPath "invalid\path\to\certificate.pfx"
65 |
66 | # Act & Assert
67 | { Create-PfxFile -Certificate $certificate -OutCertPath $invalidCertPath -Password $certPassword } | Should -Throw
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/image/test/scripts/Get-PatchFolders.Tests.ps1:
--------------------------------------------------------------------------------
1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path
2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
3 | . "$here\..\..\src\scripts\$sut"
4 | . $PSScriptRoot\..\TestUtils.ps1
5 |
6 | function Set-EnvironmentValue($Value) {
7 | $env:SITECORE_DEVELOPMENT_PATCHES = $Value
8 | }
9 |
10 | Describe 'Get-PatchFolders' {
11 | Mock Write-Host {}
12 |
13 | BeforeAll {
14 | Set-EnvironmentValue -Value $null
15 | }
16 |
17 | It 'requires $Path' {
18 | $result = Test-ParamIsMandatory -Command Get-PatchFolders -Parameter Path
19 | $result | Should -Be $true
20 | }
21 |
22 | It 'throws if $Path is a file' {
23 | $filePath = Join-Path $TestDrive 'file.txt'
24 | Set-Content $filePath 'foo'
25 | {Get-PatchFolders -Path $filePath} | Should -Throw
26 | }
27 |
28 | It 'returns nothing if the environment variable is empty' {
29 | $folders = (Get-PatchFolders -Path $TestDrive)
30 | $folders | Should -BeNullOrEmpty
31 | }
32 |
33 | It 'gets the configured patch folders' {
34 | Set-EnvironmentValue -Value 'foo,bar,baz'
35 | $test1 = New-Item -Path (Join-Path $TestDrive 'foo') -ItemType 'Directory' -Force
36 | $test2 = New-Item -Path (Join-Path $TestDrive 'bar') -ItemType 'Directory' -Force
37 | $test3 = New-Item -Path (Join-Path $TestDrive 'baz') -ItemType 'Directory' -Force
38 | $folders = (Get-PatchFolders -Path $TestDrive)
39 | $folders | Should -HaveCount 3
40 |
41 | $tests = @($test1, $test2, $test3) | ForEach-Object { $_.FullName }
42 | $folderNames = $folders | ForEach-Object { $_.FullName }
43 | $tests | ForEach-Object { $_ | Should -BeIn $folderNames }
44 | }
45 |
46 | It 'writes warning if a provided folder does not exist' {
47 | Set-EnvironmentValue -Value 'foo,idontexist,meeither'
48 | New-Item -Path (Join-Path $TestDrive 'foo') -ItemType 'Directory' -Force
49 | Get-PatchFolders -Path $TestDrive | Should -HaveCount 1
50 | Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match 'idontexist' }
51 | Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match 'meeither' }
52 | }
53 |
54 | It 'writes a warning if a provided path has invalid characters' {
55 | Set-EnvironmentValue -Value 'foo,subpath/leaking,something?strange,baz*bar'
56 | New-Item -Path (Join-Path $TestDrive 'foo') -ItemType 'Directory' -Force
57 | Get-PatchFolders -Path $TestDrive | Should -HaveCount 1
58 | Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match 'subpath/leaking' }
59 | Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match 'something\?strange' }
60 | Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match 'baz\*bar' }
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Create-SqlServerCertificate.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Create-SqlServerCertificate' {
5 | BeforeAll {
6 | $certStoreLocation = "Cert:\CurrentUser\My"
7 | $signerCert = New-SelfSignedCertificate -DnsName "signer.test" -CertStoreLocation $certStoreLocation
8 | }
9 |
10 | AfterAll {
11 | if ($signerCert -ne $null) {
12 | Remove-Item -Path $signerCert.PSPath -Force
13 | }
14 | }
15 |
16 | It 'requires $CommonName and $DnsName and $SignerCertificate' {
17 | $result = Test-ParamIsMandatory -Command Create-SqlServerCertificate -Parameter CommonName
18 | $result | Should Be $true
19 | $result = Test-ParamIsMandatory -Command Create-SqlServerCertificate -Parameter DnsName
20 | $result | Should Be $true
21 | $result = Test-ParamIsMandatory -Command Create-SqlServerCertificate -Parameter SignerCertificate
22 | $result | Should Be $true
23 | }
24 |
25 | It 'Optional parameter not provided' {
26 | { Create-SqlServerCertificate -CommonName "test" -DnsName "test" -SignerCertificate $signerCert } | Should -Not -Throw
27 | }
28 |
29 | It 'throws if $CommonName is $null or empty' {
30 | { Create-SqlServerCertificate -CommonName $null -DnsName "test" -SignerCertificate $signerCert } | Should -Throw
31 | { Create-SqlServerCertificate -CommonName "" -DnsName "test" -SignerCertificate $signerCert } | Should -Throw
32 | }
33 |
34 | It 'throws if $DnsName is $null or empty' {
35 | { Create-SqlServerCertificate -CommonName "test" -DnsName $null -SignerCertificate $signerCert } | Should -Throw
36 | { Create-SqlServerCertificate -CommonName "test" -DnsName "" -SignerCertificate $signerCert } | Should -Throw
37 | }
38 |
39 | It 'throws if $SignerCertificate is $null' {
40 | { Create-SqlServerCertificate -CommonName "test" -DnsName "test" -SignerCertificate $null } | Should -Throw
41 | }
42 |
43 | Context 'When creating a self-signed certificate' {
44 | It 'creates a certificate with the specified parameters' {
45 | # Arrange
46 | $commonName = 'test.sql.server'
47 | $dnsName = 'test.sql.server'
48 |
49 | # Act
50 | $certificate = Create-SqlServerCertificate -CommonName $commonName -DnsName $dnsName -SignerCertificate $signerCert
51 |
52 | write-host $certificate
53 |
54 | # Assert
55 | $certificate | Should -Not -BeNullOrEmpty
56 | $certificate.Subject | Should -Be "CN=$commonName"
57 | $certificate.DnsNameList | Should -Contain $dnsName
58 | $certificate.DnsNameList | Should -Contain 'localhost'
59 | $certificate.Issuer | Should -Be $signerCert.Subject
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/powershell/src/SitecoreDockerTools.psd1:
--------------------------------------------------------------------------------
1 | @{
2 | RootModule = 'SitecoreDockerTools.psm1'
3 | GUID = '36f0f2d9-8a8a-461f-b54f-3fb109f30b70'
4 | Author = 'Sitecore Corporation A/S'
5 | CompanyName = 'Sitecore Corporation A/S'
6 | Copyright = 'Copyright (C) by Sitecore A/S'
7 | Description = 'PowerShell extensions for Docker-based Sitecore development'
8 | ModuleVersion = '0.0.1'
9 | PowerShellVersion = '5.1'
10 |
11 | # Modules that must be imported into the global environment prior to importing this module
12 | # RequiredModules = @()
13 |
14 | # Assemblies that must be loaded prior to importing this module
15 | # RequiredAssemblies = @()
16 |
17 | # Script files (.ps1) that are run in the caller's environment prior to importing this module.
18 | # ScriptsToProcess = @()
19 |
20 | # Type files (.ps1xml) to be loaded when importing this module
21 | # TypesToProcess = @()
22 |
23 | # Format files (.ps1xml) to be loaded when importing this module
24 | # FormatsToProcess = @()
25 |
26 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
27 | # NestedModules = @()
28 |
29 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
30 | FunctionsToExport = @('Get-SitecoreRandomString','Create-RSAKey','Create-SelfSignedCertificate','Create-SelfSignedCertificateWithSignature','Create-CertificateFile','Create-KeyFile','Create-PfxFile','Create-SqlServerCertificate','ConvertTo-CompressedBase64String','Get-SitecoreCertificateAsBase64String','Set-EnvFileVariable','Add-HostsEntry','Remove-HostsEntry','Write-SitecoreDockerWelcome','Get-EnvFileVariable','Import-LoadedCertificate','Import-CertificateForSigning','Load-Certificate')
31 |
32 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
33 | CmdletsToExport = @()
34 |
35 | # Variables to export from this module
36 | # VariablesToExport = '*'
37 |
38 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
39 | AliasesToExport = @('Set-DockerComposeEnvFileVariable')
40 |
41 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
42 | PrivateData = @{
43 | PSData = @{
44 | Tags = @('sitecore','docker','powershell')
45 | LicenseUri = 'https://doc.sitecore.net/~/media/C23E989268EC4FA588108F839675A5B6.pdf'
46 | ProjectUri = 'https://github.com/Sitecore/docker-tools'
47 | IconUri = 'https://mygetwwwsitecoreeu.blob.core.windows.net/feedicons/sc-packages.png'
48 | }
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/powershell/test/Public/Get-SitecoreCertificateAsBase64String.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | function rehydrateCertificate([string] $base64String, [securestring] $password) {
5 | $rawData = [System.Convert]::FromBase64String($base64String)
6 | return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($rawData, $password)
7 | }
8 |
9 | function getDnsName([System.Security.Cryptography.X509Certificates.X509Certificate2] $cert)
10 | {
11 | return $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::DnsName, $false)
12 | }
13 |
14 | Describe 'Get-SitecoreCertificateAsBase64String' {
15 |
16 | It 'requires $Password' {
17 | $result = Test-ParamIsMandatory -Command Get-SitecoreCertificateAsBase64String -Parameter Password
18 | $result | Should -Be $true
19 | }
20 |
21 | It 'throws if $DnsName is null' {
22 | { Get-SitecoreCertificateAsBase64String -DnsName $null } | Should -Throw
23 | }
24 |
25 | It 'throws if $DnsName is empty' {
26 | { Get-SitecoreCertificateAsBase64String -DnsName "" } | Should -Throw
27 | }
28 |
29 | It 'generates certificate Base64 string with $Password' {
30 | $password = ConvertTo-SecureString -String "Test123" -Force -AsPlainText
31 | $result = Get-SitecoreCertificateAsBase64String -Password $password
32 |
33 | $result | Should -Not -BeNullOrEmpty
34 | $result.length | Should -BeGreaterThan 0
35 | { rehydrateCertificate $result $password } | Should -Not -Throw
36 | }
37 |
38 | It 'uses localhost as default $DnsName' {
39 | $password = ConvertTo-SecureString -String "Test123" -Force -AsPlainText
40 | $result = Get-SitecoreCertificateAsBase64String -Password $password
41 | $cert = rehydrateCertificate $result $password
42 | $dnsName = getDnsName $cert
43 | $dnsName | Should -Be "localhost"
44 | }
45 |
46 | It 'uses provided $DnsName' {
47 | $password = ConvertTo-SecureString -String "Test123" -Force -AsPlainText
48 | $result = Get-SitecoreCertificateAsBase64String -DnsName "test.com" -Password $password
49 | $cert = rehydrateCertificate $result $password
50 | $dnsName = getDnsName $cert
51 | $dnsName | Should -Be "test.com"
52 | }
53 |
54 | It 'should not throw with default key length' {
55 | $password = ConvertTo-SecureString -String "Test123" -Force -AsPlainText
56 | { Get-SitecoreCertificateAsBase64String -DnsName "test.com" -Password $password } | Should -Not -Throw
57 | }
58 |
59 | It 'wrong rsa key length fails validation' {
60 | $password = ConvertTo-SecureString -String "Test123" -Force -AsPlainText
61 | { Get-SitecoreCertificateAsBase64String -DnsName "test.com" -Password $password -KeyLength 2000} | Should -Throw
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sitecore Docker Tools
2 |
3 | Sitecore Docker Tools are utilities which improve developer experience when running Sitecore in a Docker environment. This includes:
4 |
5 | * `sitecore-docker-tools-assets`, a Docker image with development scripts and entrypoints which can be used during Sitecore container builds.
6 | [](https://dev.azure.com/sitecore-devex/docker-tools/_build/latest?definitionId=9&branchName=main)
7 | * `SitecoreDockerTools`, a PowerShell module with functions used on the Sitecore container host to initialize the Sitecore Docker environment.
8 | [](https://dev.azure.com/sitecore-devex/docker-tools/_build/latest?definitionId=10&branchName=main)
9 |
10 | ## Usage
11 |
12 | Released versions of these utilities can be found on the Sitecore Container Registry and the Sitecore PowerShell Gallery. Usage details can be found in the [Sitecore container development documentation](https://doc.sitecore.com/developers/100/developer-tools/en/containers-in-sitecore-development.html).
13 |
14 | ### Docker Image
15 | The scripts found in the Docker image are intended to be copied in via your custom `Dockerfile`.
16 |
17 | ```Dockerfile
18 | FROM ${TOOLS_IMAGE} as tools
19 | FROM ${PARENT_IMAGE}
20 | COPY --from=tools C:\tools C:\tools
21 | ```
22 |
23 | You can enable the [development entrypoint](https://doc.sitecore.com/developers/100/developer-tools/en/deploying-files-into-running-containers.html#idp15256) in your `docker-compose` override.
24 |
25 | ```yml
26 | entrypoint: powershell.exe -Command "& C:\tools\entrypoints\iis\Development.ps1"
27 | ```
28 |
29 | The development entrypoint also enables the application of development-specific configuration patches and configuration transforms at runtime via the `SITECORE_DEVELOPMENT_PATCHES` environment variable. You can see [available patches here](image/src/dev-patches).
30 |
31 | ```yml
32 | environment:
33 | SITECORE_DEVELOPMENT_PATCHES: DevEnvOn,CustomErrorsOff,DebugOn,DiagnosticsOff,InitMessagesOff,RobotDetectionOff
34 | ```
35 |
36 | ### PowerShell Module
37 | The PowerShell module can be installed and imported from the Sitecore PowerShell Gallery.
38 |
39 | ```powershell
40 | Register-PSRepository -Name SitecoreGallery -SourceLocation https://nuget.sitecore.com/resources/v2/
41 | Install-Module SitecoreDockerTools
42 | Import-Module SitecoreDockerTools
43 |
44 | # See available commands
45 | Get-Command -Module SitecoreDockerTools
46 | ```
47 |
48 | ## Building/Using from Source
49 |
50 | ### Docker Image
51 | ```powershell
52 | cd image\src
53 | docker-compose build
54 | ```
55 |
56 | ### PowerShell Module
57 | ```powershell
58 | Import-Module .\powershell\src\SitecoreDockerTools.psd1
59 | ```
60 |
61 | ## Running Tests
62 |
63 | Unit tests require use of [Pester v4](https://pester.dev/docs/v4/introduction/installation).
64 |
65 | From the root folder of either project:
66 |
67 | ```powershell
68 | Import-Module Pester -RequiredVersion 4.9.0
69 | Invoke-Pester -Path .\test\*
70 | ```
71 |
--------------------------------------------------------------------------------
/powershell/src/Public/Import-Certificate.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Imports a loaded certificate into a specified store.
6 | .DESCRIPTION
7 | Imports a loaded certificate into the specified certificate store and location.
8 | .PARAMETER Certificate
9 | The certificate to be imported.
10 | .PARAMETER StoreName
11 | The name of the store where the certificate will be imported.
12 | .PARAMETER StoreLocation
13 | The location of the store where the certificate will be imported.
14 | .INPUTS
15 | None. You cannot pipe objects to Import-LoadedCertificate.
16 | .OUTPUTS
17 | None. Import-LoadedCertificate does not generate any output.
18 | .EXAMPLE
19 | PS C:\> Import-LoadedCertificate -Certificate $cert -StoreName "My" -StoreLocation "CurrentUser"
20 | #>
21 | function Import-LoadedCertificate {
22 | param(
23 | [Parameter(Mandatory = $true)]
24 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
25 | $Certificate,
26 |
27 | [Parameter(Mandatory = $true)]
28 | [string]
29 | $StoreName,
30 |
31 | [Parameter(Mandatory = $true)]
32 | $StoreLocation
33 | )
34 |
35 | Write-Information -MessageData "Importing '$($Certificate.Thumbprint)' certificate into '$StoreName' store of '$StoreLocation' location." -InformationAction Continue
36 |
37 | $store = [System.Security.Cryptography.X509Certificates.X509Store]::new($StoreName, $StoreLocation)
38 |
39 | try {
40 | $store.Open("ReadWrite")
41 | $store.Add($Certificate)
42 | }
43 | finally {
44 | $store.Close()
45 | }
46 | }
47 |
48 | <#
49 | .SYNOPSIS
50 | Imports a certificate for signing.
51 | .DESCRIPTION
52 | Imports a certificate into the specified certificate store for signing purposes.
53 | .PARAMETER SignerCertificate
54 | The certificate to be imported as signer.
55 | .PARAMETER SignerCertificatePassword
56 | The password for the certificate to be imported as signer.
57 | .INPUTS
58 | None. You cannot pipe objects to Import-CertificateForSigning.
59 | .OUTPUTS
60 | System.Security.Cryptography.X509Certificates.X509Certificate2. The imported signer certificate.
61 | .EXAMPLE
62 | PS C:\> Import-CertificateForSigning -SignerCertificate $cert -SignerCertificatePassword $password
63 | #>
64 | function Import-CertificateForSigning{
65 | param(
66 | [Parameter(Mandatory = $true)]
67 | [System.Security.Cryptography.X509Certificates.X509Certificate2]
68 | $SignerCertificate,
69 |
70 | [Parameter(Mandatory = $true)]
71 | [SecureString]$SignerCertificatePassword
72 | )
73 |
74 | Write-Information -MessageData "Importing '$($SignerCertificate.Thumbprint)' certificate for signing." -InformationAction Continue
75 |
76 | $pfxContent = $SignerCertificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $SignerCertificatePassword)
77 |
78 | $importCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
79 | $pfxContent,
80 | $SignerCertificatePassword,
81 | [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
82 | )
83 |
84 | Import-LoadedCertificate -Certificate $importCertificate -StoreName "My" -StoreLocation "CurrentUser"
85 |
86 | return $importCertificate
87 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Get-EnvFileVariable.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Get-EnvFileVariable' {
5 |
6 | BeforeEach {
7 | Push-Location $TestDrive
8 | }
9 |
10 | AfterEach {
11 | Pop-Location
12 | }
13 |
14 | $envFile = '.env'
15 | $content = @(
16 | 'VAR1=VAL1',
17 | 'VAR2=VAL2',
18 | 'VAR3=VAL3',
19 | 'VAR4=''VAL4$Literal''',
20 | "VAR5=''VAL5''Escaped''",
21 | "VAR6='VAL6''EscapedLiteral'",
22 | "VAR7='VAL7"
23 | "VAR8='''VAL8'"
24 | )
25 | Set-Content "$TestDrive\$envFile" -Value $content
26 |
27 | It 'requires $Variable' {
28 | $result = Test-ParamIsMandatory -Command Get-EnvFileVariable -Parameter Variable
29 | $result | Should -Be $true
30 | }
31 |
32 | It 'throws if $Path is invalid' {
33 | { Get-EnvFileVariable -Variable "foo" -Value "bar" -Path "$.baz" } | Should -Throw
34 | }
35 |
36 | It 'throws if $Variable is $null or empty' {
37 | { Get-EnvFileVariable -Variable $null -Value "bar" -Path $envFile } | Should -Throw
38 | { Get-EnvFileVariable -Variable "" -Value "bar" -Path $envFile } | Should -Throw
39 | }
40 |
41 | It 'throws if variable not found' {
42 | { Get-EnvFileVariable -Path $envFile -Variable 'VAR' } | Should -Throw
43 | }
44 |
45 | It 'reads variable correctly using default file path' {
46 | $value = 'VAL2'
47 | $result = Get-EnvFileVariable -Variable 'VAR2'
48 | $result | Should -Be $value
49 | }
50 |
51 | It 'reads variable correctly using relative file path' {
52 | $value = 'VAL2'
53 | $result = Get-EnvFileVariable -Path $envFile -Variable 'VAR2'
54 | $result | Should -Be $value
55 | }
56 |
57 | It 'reads variable correctly using absolute file path' {
58 | $value = 'VAL2'
59 | $result = Get-EnvFileVariable -Path "$TestDrive\$envFile" -Variable 'VAR2'
60 | $result | Should -Be $value
61 | }
62 |
63 | It 'reads variable correctly using a literal value' {
64 | $value = 'VAL4$Literal'
65 | $result = Get-EnvFileVariable -Variable 'VAR4'
66 | $result | Should -Be $value
67 | }
68 |
69 | It 'reads variable correctly using quotes in non-literal strings' {
70 | $value = "''VAL5''Escaped''"
71 | $result = Get-EnvFileVariable -Variable 'VAR5'
72 | $result | Should -Be $value
73 | }
74 |
75 | It 'reads variable correctly using escaped quotes in literals' {
76 | $value = "VAL6'EscapedLiteral"
77 | $result = Get-EnvFileVariable -Variable 'VAR6'
78 | $result | Should -Be $value
79 | }
80 |
81 | It 'reads variable correctly using non-literal strings starting with quote' {
82 | $value = "'VAL7"
83 | $result = Get-EnvFileVariable -Variable 'VAR7'
84 | $result | Should -Be $value
85 | }
86 | It 'reads variable correctly using literal strings starting with quote' {
87 | $value = "'VAL8"
88 | $result = Get-EnvFileVariable -Variable 'VAR8'
89 | $result | Should -Be $value
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/image/src/scripts/Invoke-XdtTransform.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Applies XDT transformations to files.
4 | .DESCRIPTION
5 | Applies XDT transformation on either a single file (file transform) or an entire folder (folder transform).
6 | For folder transform, it assumes: (1) XDT transformation files end in ".xdt", and (2) folder structures match.
7 | .PARAMETER Path
8 | Specifies either the target file to transform (file transform) or a target folder to apply transforms (folder transform).
9 | .PARAMETER XdtPath
10 | Specifies either the XDT transform file (file transform) or a folder containing XDT transform files (folder transform).
11 | .PARAMETER XdtDllPath
12 | Specifies the location of the Microsoft.Web.XmlTransform.dll assembly. Default is 'C:\tools\bin\Microsoft.Web.XmlTransform.dll'.
13 | .EXAMPLE
14 | PS C:\> .\Invoke-XdtTransform.ps1 -Path 'C:\inetpub\wwwroot\Web.config' -XdtPath 'C:\transforms\Web.config.xdt'
15 | Example of file transform.
16 | .EXAMPLE
17 | PS C:\> .\Invoke-XdtTransform.ps1 -Path 'C:\inetpub\wwwroot' -XdtPath 'C:\transforms'
18 | Example of folder transform.
19 | .INPUTS
20 | None
21 | .OUTPUTS
22 | None
23 | #>
24 | [CmdletBinding()]
25 | Param (
26 | [Parameter(Mandatory = $true)]
27 | [ValidateScript( { Test-Path $_ })]
28 | [string]$Path,
29 |
30 | [Parameter(Mandatory = $true)]
31 | [ValidateScript( { Test-Path $_ })]
32 | [string]$XdtPath,
33 |
34 | [Parameter(Mandatory = $false)]
35 | [ValidateScript( { Test-Path $_ -PathType 'Leaf' })]
36 | [string]$XdtDllPath = "C:\tools\bin\Microsoft.Web.XmlTransform.dll"
37 | )
38 |
39 | if (((Test-Path $Path -PathType Container) -and (Test-Path $XdtPath -PathType Leaf)) -or
40 | ((Test-Path $Path -PathType Leaf) -and (Test-Path $XdtPath -PathType Container))) {
41 | throw "'Path' and 'XdtPath' parameter types must match (both files or both folders)"
42 | }
43 |
44 | Add-Type -Path $XdtDllPath
45 |
46 | function ApplyTransform($filePath, $xdtFilePath) {
47 | Write-Verbose "Applying XDT transformation '$xdtFilePath' on '$filePath'..."
48 |
49 | $target = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument;
50 | $target.PreserveWhitespace = $true
51 | $target.Load($filePath);
52 |
53 | $transformation = New-Object Microsoft.Web.XmlTransform.XmlTransformation($xdtFilePath);
54 |
55 | if ($transformation.Apply($target) -eq $false)
56 | {
57 | throw "XDT transformation failed."
58 | }
59 |
60 | $target.Save($filePath);
61 | }
62 |
63 | if (Test-Path $Path -PathType Leaf) {
64 |
65 | # File transform
66 | ApplyTransform $Path $XdtPath
67 |
68 | } else {
69 |
70 | # Folder transform
71 | $transformations = @(Get-ChildItem $XdtPath -File -Include "*.xdt" -Recurse)
72 |
73 | if ($transformations.Length -eq 0) {
74 | Write-Verbose "No transformations in '$XdtPath'"
75 | return
76 | }
77 |
78 | $transformations | ForEach-Object {
79 |
80 | # Assume folder structures match
81 | $targetFullPath = (Resolve-Path $Path).Path
82 | $xdtFullPath = (Resolve-Path $XdtPath).Path
83 | $targetFilePath = $_.FullName.Replace($xdtFullPath, $targetFullPath).Replace(".xdt", "")
84 |
85 | if (-not(Test-Path $targetFilePath -PathType Leaf)) {
86 | Write-Verbose "No matching file '$targetFilePath' for transformation '$($_.FullName)'. Skipping..."
87 | return
88 | }
89 |
90 | ApplyTransform $targetFilePath $_.FullName
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/image/test/scripts/Install-ConfigurationFolder.Tests.ps1:
--------------------------------------------------------------------------------
1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path
2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
3 | $script = "$here\..\..\src\scripts\$sut"
4 | . $PSScriptRoot\..\TestUtils.ps1
5 |
6 | Describe 'Install-ConfigurationFolder.ps1' {
7 |
8 | It 'requires $Path' {
9 | $result = Test-ParamIsMandatory -Command $script -Parameter Path
10 | $result | Should -Be $true
11 | }
12 |
13 | It 'requires $PatchPath' {
14 | $result = Test-ParamIsMandatory -Command $script -Parameter PatchPath
15 | $result | Should -Be $true
16 | }
17 |
18 | It 'throws if $Path is not a folder' {
19 | $filePath = Join-Path $TestDrive 'file.txt'
20 | Set-Content $filePath 'foo'
21 | $folderPath = Join-Path $TestDrive 'folder'
22 | New-Item -Path $folderPath -ItemType 'Directory' -Force
23 | {& $script -Path $filePath -PatchPath $folderPath} | Should -Throw
24 | }
25 |
26 | It 'throws if $PatchPath is not a folder' {
27 | $filePath = Join-Path $TestDrive 'file.txt'
28 | Set-Content $filePath 'foo'
29 | $folderPath = Join-Path $TestDrive 'folder'
30 | New-Item -Path $folderPath -ItemType 'Directory' -Force
31 | {& $script -Path $folderPath -PatchPath $filePath} | Should -Throw
32 | }
33 |
34 | It 'copies configuration files' {
35 | $destination = New-Item -Path (Join-Path $TestDrive 'webroot') -ItemType 'Directory' -Force
36 | $patches = New-Item -Path (Join-Path $TestDrive 'patches') -ItemType 'Directory' -Force
37 | $patch = New-Item -Path (Join-Path $patches 'Patch.config') -Force
38 | Set-Content $patch -Value ''
39 |
40 | & $script -Path $destination -PatchPath $patches
41 | (Join-Path $destination '\Patch.config') | Should -FileContentMatchExactly ''
42 | }
43 |
44 | It 'copies configuration files to a matching path' {
45 | $destination = New-Item -Path (Join-Path $TestDrive 'webroot') -ItemType 'Directory' -Force
46 | $patches = New-Item -Path (Join-Path $TestDrive 'patches') -ItemType 'Directory' -Force
47 | New-Item -Path (Join-Path $patches '\App_Config') -ItemType 'Directory' -Force
48 | New-Item -Path (Join-Path $patches '\App_Config\Environment') -ItemType 'Directory' -Force
49 | $patch = New-Item -Path (Join-Path $patches '\App_Config\Environment\Patch.config') -Force
50 | Set-Content $patch -Value ''
51 |
52 | & $script -Path $destination -PatchPath $patches
53 | (Join-Path $destination '\App_Config\Environment\Patch.config') | Should -FileContentMatchExactly ''
54 | }
55 |
56 | It 'does not copy non-config files' {
57 | $destination = New-Item -Path (Join-Path $TestDrive 'webroot') -ItemType 'Directory' -Force
58 | $patches = New-Item -Path (Join-Path $TestDrive 'patches') -ItemType 'Directory' -Force
59 | New-Item -Path (Join-Path $patches '\App_Config') -ItemType 'Directory' -Force
60 | $transform = New-Item -Path (Join-Path $patches '\App_Config\Patch.xdt') -Force
61 | Set-Content $transform -Value ''
62 |
63 | & $script -Path $destination -PatchPath $patches
64 | (Join-Path $destination '\App_Config\Patch.xdt') | Should -Not -Exist
65 | }
66 |
67 | It 'overwrites existing files' {
68 | $destination = New-Item -Path (Join-Path $TestDrive 'webroot') -ItemType 'Directory' -Force
69 | $existingPatch = New-Item -Path (Join-Path $destination 'Patch.config') -Force
70 | Set-Content $existingPatch -Value ''
71 |
72 | $patches = New-Item -Path (Join-Path $TestDrive 'patches') -ItemType 'Directory' -Force
73 | $patch = New-Item -Path (Join-Path $patches 'Patch.config') -Force
74 | Set-Content $patch -Value ''
75 |
76 | & $script -Path $destination -PatchPath $patches
77 | (Join-Path $destination '\Patch.config') | Should -FileContentMatchExactly ''
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/powershell/build/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - main
5 | - dev
6 | paths:
7 | include:
8 | - 'powershell/*'
9 | exclude:
10 | - 'image/*'
11 |
12 | pr:
13 | branches:
14 | include:
15 | - main
16 | - dev
17 | paths:
18 | include:
19 | - 'powershell/*'
20 | exclude:
21 | - 'image/*'
22 |
23 | resources:
24 | - repo: self
25 |
26 | variables:
27 | sitecoreVersion: 10.4
28 | revision: $[counter(format('sitecoreVersion{0}', variables['sitecoreVersion']), 0)]
29 | moduleVer: $(sitecoreVersion).$(revision)
30 | ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
31 | prerelease: ''
32 | ${{ if ne(variables['Build.SourceBranchName'], 'main') }}:
33 | prerelease: '-unstable'
34 | buildVer: $(moduleVer)$(prerelease)
35 |
36 | pool: Default
37 |
38 | stages:
39 | - stage: Build
40 |
41 | jobs:
42 | - job: Build
43 |
44 | steps:
45 |
46 | - task: PowerShell@2
47 | displayName: Update manifest version
48 | inputs:
49 | targetType: 'inline'
50 | script: |
51 | if ("$(prerelease)") {
52 | Update-ModuleManifest -Path .\SitecoreDockerTools.psd1 -ModuleVersion $(moduleVer) -Prerelease $(prerelease)
53 | Write-Host "Updated module manifest. ModuleVersion: $(moduleVer), Prerelease: $(prerelease)"
54 | } else {
55 | Update-ModuleManifest -Path .\SitecoreDockerTools.psd1 -ModuleVersion $(moduleVer)
56 | Write-Host "Updated module manifest. ModuleVersion: $(moduleVer)"
57 | }
58 | failOnStderr: true
59 | workingDirectory: '$(Build.SourcesDirectory)/powershell/src'
60 | - task: NuGetCommand@2
61 | displayName: NuGet pack
62 | inputs:
63 | command: 'pack'
64 | packagesToPack: '$(Build.SourcesDirectory)/powershell/src/SitecoreDockerTools.nuspec'
65 | versioningScheme: byEnvVar
66 | versionEnvVar: buildVer
67 | buildProperties: 'VERSION=$(buildVer)'
68 | - task: PublishBuildArtifacts@1
69 | inputs:
70 | PathtoPublish: '$(Build.ArtifactStagingDirectory)'
71 | ArtifactName: 'NuGetPackage'
72 | publishLocation: 'Container'
73 |
74 | - stage: Test
75 | dependsOn: Build
76 |
77 | jobs:
78 | - job: Pester
79 | displayName: Run Pester tests
80 | steps:
81 |
82 | - task: Pester@9
83 | inputs:
84 | scriptFolder: "$(Build.SourcesDirectory)/powershell/test/*"
85 | resultsFile: "$(Build.SourcesDirectory)/powershell/test/Test-Pester.XML"
86 | usePSCore: False
87 | - task: PublishTestResults@2
88 | inputs:
89 | testResultsFormat: "NUnit"
90 | testResultsFiles: "$(Build.SourcesDirectory)/powershell/test/Test-Pester.XML"
91 | failTaskOnFailedTests: true
92 |
93 | - stage: Deploy
94 | dependsOn: Test
95 | condition: ne(variables['Build.Reason'], 'PullRequest')
96 |
97 | jobs:
98 | - job: Deploy
99 | workspace:
100 | clean: all
101 | steps:
102 |
103 | - checkout: none
104 | - task: DownloadPipelineArtifact@2
105 | inputs:
106 | buildType: 'current'
107 | artifactName: 'NuGetPackage'
108 | itemPattern: '**'
109 | targetPath: '$(Pipeline.Workspace)'
110 | - task: NuGetToolInstaller@1
111 | inputs:
112 | versionSpec: 5.x
113 | - task: PowerShell@2
114 | inputs:
115 | targetType: 'inline'
116 | script: |
117 | New-Item -Path '$(Pipeline.Workspace)\NuGet.config' -ItemType File -Force
118 | Add-Content -Path '$(Pipeline.Workspace)\NuGet.config' -Value ' '
119 | nuget sources add -NonInteractive -Name Temp -Source $(NuGetFeedUrl) -ConfigFile '$(Pipeline.Workspace)\NuGet.config'
120 | nuget push -NonInteractive -Source Temp '$(Pipeline.Workspace)/**/*.nupkg' -ConfigFile '$(Pipeline.Workspace)\NuGet.config'
121 | failOnStderr: true
--------------------------------------------------------------------------------
/powershell/test/Public/Load-Certificate.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Load-Certificate' {
5 | BeforeAll {
6 | # Generate a self-signed certificate with password
7 | $certPath = Join-Path -Path $TestDrive -ChildPath "certificate.pfx"
8 | $certificate = New-SelfSignedCertificate -DnsName "mockCertificate" -CertStoreLocation "Cert:\CurrentUser\My"
9 | $certPassword = ConvertTo-SecureString -String "password" -AsPlainText -Force
10 | Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -FilePath $certPath -Password $certPassword
11 |
12 | # Generate a self-signed certificate without password
13 | $certWithoutPasswordPath = Join-Path -Path $TestDrive -ChildPath "certificateWithoutPassword.pfx"
14 | $certificateWithoutPassword = New-SelfSignedCertificate -DnsName "mockCertificate" -CertStoreLocation "Cert:\CurrentUser\My"
15 | Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($certificateWithoutPassword.Thumbprint)" -FilePath $certWithoutPasswordPath -Password (New-Object System.Security.SecureString)
16 | }
17 |
18 | AfterAll {
19 | if ($certificate -ne $null) {
20 | Remove-Item -Path $certificate.PSPath -Force
21 | }
22 | if ($certificateWithoutPassword -ne $null) {
23 | Remove-Item -Path $certificateWithoutPassword.PSPath -Force
24 | }
25 | }
26 |
27 | It 'requires $CertPath' {
28 | $result = Test-ParamIsMandatory -Command Load-Certificate -Parameter CertPath
29 | $result | Should -Be $true
30 | }
31 |
32 | It 'throws if $CertPath is $null or empty' {
33 | { Load-Certificate -CertPath $null } | Should -Throw
34 | { Load-Certificate -CertPath "" } | Should -Throw
35 | }
36 |
37 | It 'throws if $CertPath is $null or empty' {
38 | { Load-Certificate -CertPath $null } | Should -Throw
39 | { Load-Certificate -CertPath "" } | Should -Throw
40 | }
41 |
42 | Context 'When loading a certificate with a password' {
43 | It 'Should load the certificate successfully' {
44 | # Act
45 | $cert = Load-Certificate -CertPath $certPath -CertPassword $certPassword
46 |
47 | # Assert
48 | $cert | Should -BeOfType "System.Security.Cryptography.X509Certificates.X509Certificate2"
49 | }
50 | }
51 |
52 | Context 'When loading a certificate without a password' {
53 | It 'Should load the certificate successfully' {
54 | # Arrange
55 | $certWithoutPasswordPath = Join-Path -Path $TestDrive -ChildPath "certificateWithoutPassword.pfx"
56 | Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -FilePath $certWithoutPasswordPath -Password (New-Object System.Security.SecureString)
57 |
58 | # Act
59 | $cert = Load-Certificate -CertPath $certWithoutPasswordPath
60 |
61 | # Assert
62 | $cert | Should -BeOfType "System.Security.Cryptography.X509Certificates.X509Certificate2"
63 | }
64 | }
65 |
66 | Context 'When the certificate path is invalid' {
67 | It 'Should throw an error' {
68 | # Arrange
69 | $invalidCertPath = Join-Path -Path $TestDrive -ChildPath "invalid\path\to\certificate.pfx"
70 |
71 | # Act & Assert
72 | { Load-Certificate -CertPath $invalidCertPath } | Should -Throw
73 | }
74 | }
75 |
76 | Context 'When the certificate password is incorrect' {
77 | It 'Should throw an error' {
78 | # Arrange
79 | $wrongPassword = ConvertTo-SecureString -String "wrongpassword" -AsPlainText -Force
80 |
81 | # Act & Assert
82 | { Load-Certificate -CertPath $certPath -CertPassword $wrongPassword } | Should -Throw
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Write-SitecoreDockerWelcome.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Welcomes you to Docker-based Sitecore development.
6 | .DESCRIPTION
7 | :)
8 | .EXAMPLE
9 | PS C:\> Write-SitecoreDockerWelcome
10 | .INPUTS
11 | None.
12 | .OUTPUTS
13 | None.
14 | #>
15 | function Write-SitecoreDockerWelcome {
16 |
17 | $lighthouse = @"
18 | \ + / /``\
19 | \ | /
20 | \ | / /``\
21 | \ / \ /
22 | \ /______\ /
23 | / |___|___| \
24 | / |;| |;| \
25 | / \\. . / \
26 | / ||: . | \ /``\
27 | / ||: | \
28 | ||: |
29 | ||: .|
30 | || |
31 | |: |
32 | ||:__ . | ------------------
33 | ___________ _ ||_| |_.| |________________| |___________________
34 | ......................................... | | ..................
35 | .............................. -------------------------------------- .........
36 | ........ _________ .......... | | | .........
37 | / | | | |
38 | / / ------------------------------------------------------
39 | | | | | | |
40 | | | | | | |
41 | ------ ---------------------------------------------------------------
42 | \ _____ _______ ________ ______ ______ _____ ________ _____ /
43 | \ / ____|__ __|__ __| ___/ / ____/ ___ | __ \ | ___| /
44 | ~~\ | (___ | | | | | |__ | | | | | | |__) || |__ /~~
45 | ~~~\ \____\ | | | | | __| | | | | | | _ / | __| /~~~
46 | ~~~~\ ____| )__| |__ | | | |___| |___ | |___| | | \ \ | |___ /~~~~
47 | ~~~~~\ |_____/|________| |_| |____ / \_____/\____/ |__| \_ \_____| /~~~~~
48 | ~~~~~~\ /~~~~~~
49 | ~~~~~~~\ /====~~~
50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
51 | "@
52 | $index = 1
53 | ($lighthouse -split "\r?\n|\r") | ForEach-Object {
54 | $line = $_
55 |
56 | # lighthouse rays
57 | if ($index -lt 11) {
58 | $col = 1
59 | [char[]]$line | ForEach-Object {
60 | # find light rays by column
61 | if (($col -gt 5 -and $col -lt 15) -or ($col -gt 29 -and $col -lt 39)) {
62 | Write-Host $_ -ForegroundColor Yellow -NoNewline
63 | }
64 | else {
65 | Write-Host $_ -NoNewline
66 | }
67 | $col++
68 | }
69 | Write-Host
70 | }
71 |
72 | # grass
73 | elseif ($index -gt 16 -and $index -lt 20) {
74 | [char[]]$line | ForEach-Object {
75 | # find grass by character
76 | if ($_ -eq '.') {
77 | Write-Host $_ -ForegroundColor Green -NoNewline
78 | }
79 | else {
80 | Write-Host $_ -NoNewline
81 | }
82 | }
83 | Write-Host
84 | }
85 |
86 | # ship / water / logo
87 | elseif ($index -gt 24) {
88 | $col = 1
89 | [char[]]$line | ForEach-Object {
90 | # find letters by column and line
91 | if ($col -gt 8 -and $col -lt 72 -and $index -lt 33) {
92 | Write-Host $_ -ForegroundColor Red -NoNewline
93 | }
94 | # find water by character
95 | elseif ($_ -eq "~" -or $_ -eq "=") {
96 | Write-Host $_ -ForegroundColor Blue -NoNewline
97 | }
98 | else {
99 | Write-Host $_ -NoNewline
100 | }
101 | $col++
102 | }
103 | Write-Host
104 | }
105 |
106 | else {
107 | Write-Host $_
108 | }
109 | $index++
110 | }
111 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Get-SitecoreRandomString.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Get-SitecoreRandomString' {
5 |
6 | It 'requires $Length' {
7 | $result = Test-ParamIsMandatory -Command Get-SitecoreRandomString -Parameter Length
8 | $result | Should -Be $true
9 | }
10 |
11 | It 'throws if $Length is less than 1' {
12 | { Get-SitecoreRandomString -Length 0 } | Should -Throw
13 | }
14 |
15 | It 'generates string' {
16 | $key = Get-SitecoreRandomString 10
17 | $key | Should -Not -BeNullOrEmpty
18 | $key.length | Should -Be 10
19 | }
20 |
21 | It 'returns requested length' {
22 | $random = Get-Random -Minimum 8 -Maximum 128
23 | $key = Get-SitecoreRandomString -Length $random
24 | $key.length | Should -Be $random
25 | }
26 |
27 | It 'does not include whitespace in string' {
28 | $key = Get-SitecoreRandomString 100
29 | $key | Should -Not -Match " "
30 | }
31 |
32 | It 'throws if Length is less than 4 () and EnforceComplexty requested' -TestCases @(
33 | @{ Length = 1},
34 | @{ Length = 3}
35 | ){
36 | param($Length)
37 | { Get-SitecoreRandomString -Length $Length -EnforceComplexity } | Should -Throw
38 | }
39 |
40 | It 'throws if there are no allowed character types' {
41 | $random = Get-Random -Minimum 10 -Maximum 20
42 |
43 | { Get-SitecoreRandomString -Length $random -DisallowCaps -DisallowLower -DisallowNumbers -DisallowSpecial } | Should -Throw
44 | }
45 |
46 | It 'includes all character types in string by default' {
47 | $result = Get-SitecoreRandomString 100
48 | $result | Should -MatchExactly "[A-Z]"
49 | $result | Should -MatchExactly "[a-z]"
50 | $result | Should -MatchExactly "[0-9]"
51 | $result | Should -MatchExactly "[^a-zA-Z0-9]"
52 | }
53 |
54 | It 'excludes requested character type: Caps' {
55 | $random = Get-Random -Minimum 10 -Maximum 20
56 | $result = Get-SitecoreRandomString -Length $random -DisallowCaps
57 | $result | Should -Not -MatchExactly "[A-Z]"
58 | }
59 |
60 | It 'excludes requested character type: Lowercase' {
61 | $random = Get-Random -Minimum 10 -Maximum 20
62 | $result = Get-SitecoreRandomString -Length $random -DisallowLower
63 | $result | Should -Not -MatchExactly "[a-z]"
64 | }
65 |
66 | It 'excludes requested character type: Numbers' {
67 | $random = Get-Random -Minimum 10 -Maximum 20
68 | $result = Get-SitecoreRandomString -Length $random -DisallowNumbers
69 | $result | Should -Not -MatchExactly "[0-9]"
70 | }
71 |
72 | It 'excludes requested character type: Special' {
73 | $random = Get-Random -Minimum 10 -Maximum 20
74 | $result = Get-SitecoreRandomString -Length $random -DisallowSpecial
75 | $result | Should -Not -MatchExactly "[^a-zA-Z0-9]"
76 | }
77 |
78 | It 'meets complexity requirements' {
79 | $random = Get-Random -Minimum 15 -Maximum 25
80 | $result = Get-SitecoreRandomString -Length $random -EnforceComplexity
81 |
82 | $nums = 0
83 | $caps = 0
84 | $lower = 0
85 | $special = 0
86 |
87 | $charArray = $result.ToCharArray()
88 |
89 | foreach ($character in $charArray) {
90 |
91 | if ([byte]$character -ge 33 -and [byte]$character -le 47) {
92 | $special = 1
93 | }
94 | if ([byte]$character -ge 48 -and [byte]$character -le 57) {
95 | $nums = 1
96 | }
97 | if ([byte]$character -ge 58 -and [byte]$character -le 64) {
98 | $special = 1
99 | }
100 | if ([byte]$character -ge 65 -and [byte]$character -le 90) {
101 | $caps = 1
102 | }
103 | if ([byte]$character -ge 91 -and [byte]$character -le 96) {
104 | $special = 1
105 | }
106 | if ([byte]$character -ge 97 -and [byte]$character -le 122) {
107 | $lower = 1
108 | }
109 | if ([byte]$character -ge 123 -and [byte]$character -le 126) {
110 | $special = 1
111 | }
112 | }
113 | $total = $nums + $caps + $lower + $special
114 |
115 | $total | Should -eq 4
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/image/build/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - main
5 | - dev
6 | paths:
7 | include:
8 | - 'image/*'
9 | exclude:
10 | - 'powershell/*'
11 |
12 | pr:
13 | branches:
14 | include:
15 | - main
16 | - dev
17 | paths:
18 | include:
19 | - 'image/*'
20 | exclude:
21 | - 'powershell/*'
22 |
23 | resources:
24 | - repo: self
25 |
26 | variables:
27 | sitecoreVersion: 10.3.0
28 | revision: $[counter(format('sitecoreVersion{0}', variables['sitecoreVersion']), 100)]
29 | osName: ltsc2022
30 | baseImage: mcr.microsoft.com/windows/nanoserver:$(osName)
31 | buildImage: mcr.microsoft.com/windows/servercore:$(osName)
32 | buildNumber: $(Build.BuildID)
33 | azureContainerRegistry: $(ACR_ContainerRegistry)
34 | azureSubscriptionEndpoint: AKSServiceConnections
35 | ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
36 | stability: ''
37 | namespace: 'tools'
38 | ${{ if ne(variables['Build.SourceBranchName'], 'main') }}:
39 | stability: '-unstable'
40 | namespace: 'experimental'
41 |
42 | pool: Default
43 |
44 | stages:
45 |
46 | - stage: Versioning
47 |
48 | jobs:
49 | - job: Tagging
50 | steps:
51 |
52 | - task: PowerShell@2
53 | name: Tags
54 | displayName: Generate tags
55 | inputs:
56 | targetType: 'inline'
57 | script: |
58 | Write-Host "Pulling base image $(baseImage)..."
59 | docker pull $(baseImage)
60 |
61 | [string] $osVersion = (docker image inspect $(baseImage) | ConvertFrom-Json).OsVersion
62 | Write-Host "Image OS version is '$osVersion'"
63 |
64 | [string] $longTag = "$(sitecoreVersion).$(revision).$(buildNumber)-$osVersion-$(osName)$(stability)"
65 | [string] $shortTag = "$(sitecoreVersion)-$(osName)$(stability)"
66 | Write-Host "Setting long tag to '$longTag'"
67 | Write-Host "Setting short tag to '$shortTag'"
68 | Write-Host "##vso[task.setvariable variable=longTag;isOutput=true]$longTag"
69 | Write-Host "##vso[task.setvariable variable=shortTag;isOutput=true]$shortTag"
70 |
71 | - stage: Build
72 | dependsOn: Versioning
73 |
74 | jobs:
75 | - job: Build
76 | displayName: Build image
77 | variables:
78 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
79 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
80 | steps:
81 |
82 | - task: DockerCompose@0
83 | displayName: Building image
84 | inputs:
85 | containerregistrytype: Azure Container Registry
86 | azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
87 | azureContainerRegistry: $(azureContainerRegistry)
88 | dockerComposeFile: '**/docker-compose.yml'
89 | dockerComposeFileArgs: |
90 | REGISTRY=$(azureContainerRegistry)/$(namespace)/
91 | VERSION=$(longTag)
92 | BASE_IMAGE=$(baseImage)
93 | BUILD_IMAGE=$(buildImage)
94 | action: Build services
95 | additionalImageTags: '$(shortTag)'
96 | arguments: '--force-rm'
97 | currentWorkingDirectory: '$(Build.SourcesDirectory)/image/src'
98 |
99 | - stage: Test
100 | dependsOn: Build
101 |
102 | jobs:
103 | - job: Pester
104 | displayName: Run Pester tests
105 | steps:
106 |
107 | - task: Pester@9
108 | inputs:
109 | scriptFolder: "$(Build.SourcesDirectory)/image/test/*"
110 | resultsFile: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
111 | usePSCore: False
112 | - task: PublishTestResults@2
113 | inputs:
114 | testResultsFormat: "NUnit"
115 | testResultsFiles: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
116 | failTaskOnFailedTests: true
117 |
118 | - stage: Push
119 | dependsOn:
120 | - Versioning
121 | - Test
122 | condition: ne(variables['Build.Reason'], 'PullRequest')
123 |
124 | jobs:
125 | - job: Push
126 | displayName: Push image
127 | variables:
128 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
129 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
130 | steps:
131 |
132 | - task: DockerCompose@0
133 | displayName: Pushing image
134 | inputs:
135 | containerregistrytype: Azure Container Registry
136 | azureSubscriptionEndpoint: $(azureSubscriptionEndpoint)
137 | azureContainerRegistry: $(azureContainerRegistry)
138 | dockerComposeFile: '**/docker-compose.yml'
139 | dockerComposeFileArgs: |
140 | REGISTRY=$(azureContainerRegistry)/$(namespace)/
141 | VERSION=$(longTag)
142 | BASE_IMAGE=$(baseImage)
143 | BUILD_IMAGE=$(buildImage)
144 | action: Push services
145 | additionalImageTags: '$(shortTag)'
146 | currentWorkingDirectory: '$(Build.SourcesDirectory)/image/src'
--------------------------------------------------------------------------------
/image/src/scripts/Deploy-TdsWdpPackages.ps1:
--------------------------------------------------------------------------------
1 | [cmdletbinding()]
2 | param(
3 | [Parameter(Mandatory = $false)]
4 | [string]$PackagesPath = ".\packages",
5 | [switch]$ViewLogs
6 | )
7 |
8 | #Extracts WDP packages to web root
9 | function ExtractPackages($packagesPath) {
10 | Write-Output "Beginning package extraction"
11 |
12 | Get-ChildItem -Path $packagesPath -Filter "*.wdp.zip" | ForEach-Object { Expand-Archive -Path $_.FullName -DestinationPath C:\temp\TDS -Force }
13 | Move-Item -Path C:\temp\TDS\Content\Website\Bin\*.WebDeployClient.dll -Destination C:\inetpub\wwwroot\bin -Force
14 | Move-Item -Path C:\temp\TDS\Content\Website\temp\* -Destination C:\inetpub\wwwroot\temp -Force
15 | Remove-Item -Path C:\temp\TDS -Recurse -Force
16 |
17 | # Ensure TDS has permissions to delete items after install
18 | cmd /C icacls C:\inetpub\wwwroot\temp\WebDeployItems /grant 'IIS AppPool\DefaultAppPool:(OI)(CI)M'
19 |
20 | Write-Output "Package extraction complete"
21 | }
22 |
23 | #Invokes the deployment and waits for completion
24 | function InvokeDeployment($viewLogs) {
25 | Write-Output "Beginning package deployment"
26 |
27 | $baseAPIUrl = "http://localhost:80/api/TDS/WebDeploy/"
28 |
29 | #Create the request urls
30 | $statusRequest = "$($baseAPIUrl)Status"
31 | $invokeRequest = "$($baseAPIUrl)Invoke"
32 | $removeRequest = "$($baseAPIUrl)Remove"
33 | $logRequest = "$($baseAPIUrl)Messages?flush=true"
34 |
35 | #Get the current status
36 | $retryCount = 20
37 | $requestComplete = $false;
38 | while($retryCount -ge 0)
39 | {
40 | try
41 | {
42 | $statusResponse = Invoke-RestMethod -Uri $statusRequest -TimeoutSec 60
43 |
44 | $requestComplete = $true
45 | break
46 | }
47 | catch
48 | {
49 | Write-Warning "Retrying connection to $statusRequest"
50 | $retryCount--
51 | }
52 | }
53 |
54 | if (!$requestComplete)
55 | {
56 | Write-Error "Could not contact server at $statusRequest"
57 |
58 | exit
59 | }
60 |
61 | #See if a deployment is taking place
62 | if ($statusResponse.DeploymentStatus -eq "Complete" -or $statusResponse.DeploymentStatus -eq "Idle" -or $statusResponse.DeploymentStatus -eq "Failed") {
63 | #Call the Invoke method to start the deployment. This may not be needed if the server is restarting, but if no Assemblies or configs change
64 | #it will be needed
65 | $invokeResponse = Invoke-RestMethod -Uri $invokeRequest -TimeoutSec 60
66 |
67 | if ($invokeResponse -ne "Ok")
68 | {
69 | throw "Request to start deployment failed"
70 | }
71 |
72 | Write-Output "Starting Deployment"
73 | }
74 |
75 | #Wait a bit to allow a deployment to start
76 | Start-Sleep -Seconds 2
77 |
78 | #Get the current status to see which deploy folder is being deployed
79 | $statusResponse = Invoke-RestMethod -Uri $statusRequest -TimeoutSec 60
80 | $currentDeploymentFolder = $statusResponse.CurrentDeploymentFolder
81 |
82 | while ($true) {
83 | #Get the current status
84 | $statusResponse = Invoke-RestMethod -Uri $statusRequest -TimeoutSec 60
85 | Write-Verbose "Server Deploy State: $($statusResponse.DeploymentStatus)"
86 |
87 | #If the deployment folder has changed, complete the progress
88 | if ($statusResponse.CurrentDeploymentFolder -ne $currentDeploymentFolder)
89 | {
90 | Write-Progress -Completed -Activity "Deploying Web Deploy Package from $currentDeploymentFolder"
91 | }
92 |
93 | #Update the progress bar
94 | Write-Progress -PercentComplete $statusResponse.ProgressPercent -Activity "Deploying Web Deploy Package from $($statusResponse.CurrentDeploymentFolder)"
95 |
96 | #If the user wants deployment logs, write the logs
97 | if ($viewLogs) {
98 | $logResponse = Invoke-RestMethod -Uri $logRequest -TimeoutSec 60
99 |
100 | Write-Output $logResponse
101 | }
102 |
103 | #Stop if all deployment folders have been deployed
104 | if ($statusResponse.DeploymentStatus -eq "Complete" -or $statusResponse.DeploymentStatus -eq "Idle") {
105 | break
106 | }
107 |
108 | if ($statusResponse.DeploymentStatus -eq "Failed") {
109 | Write-Error "Errors detected during deployment. Please see logs for more details"
110 |
111 | exit 1 #set exit code to failure if there is a problem with the deployment
112 | }
113 |
114 | Start-Sleep -Seconds 5
115 | }
116 |
117 | Write-Output "Removing installer service from webserver"
118 |
119 | Invoke-RestMethod -Uri $removeRequest -TimeoutSec 600
120 |
121 | Write-Progress -Completed -Activity "Deploying Web Deploy Package from $($statusResponse.CurrentDeploymentFolder)"
122 | }
123 |
124 | if (-not ([System.IO.Path]::IsPathRooted($PackagesPath))) {
125 | $currentDirectory = Split-Path $MyInvocation.MyCommand.Path
126 | $PackagesPath = (Get-Item -Path ($currentDirectory + "\" + $PackagesPath)).FullName
127 | }
128 |
129 | ExtractPackages $PackagesPath
130 |
131 | #Wait until server has recognized that files may have changed and begins its restart
132 | Start-Sleep -s 5
133 |
134 | InvokeDeployment $ViewLogs
135 |
136 | Write-Output "Package deployment complete"
--------------------------------------------------------------------------------
/image/test/scripts/Watch-Directory.Tests.ps1:
--------------------------------------------------------------------------------
1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path
2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
3 | $script = "$here\..\..\src\scripts\$sut"
4 | . $PSScriptRoot\..\TestUtils.ps1
5 |
6 | Describe 'Watch-Directory.ps1' {
7 |
8 | # Supress info messages from Watch-Directory.ps1. Can set to "Continue" if necessary for debugging.
9 | $InformationPreference = "SilentlyContinue"
10 |
11 | BeforeAll {
12 | $dummyFile = Join-Path $TestDrive 'dummy.txt'
13 | Set-Content $dummyFile -Value 'dummy'
14 | }
15 |
16 | It 'requires $Path' {
17 | $result = Test-ParamIsMandatory -Command $script -Parameter Path
18 | $result | Should -Be $true
19 | }
20 |
21 | It 'requires $Destination' {
22 | $result = Test-ParamIsMandatory -Command $script -Parameter Destination
23 | $result | Should -Be $true
24 | }
25 |
26 | It 'throws if invalid $Path' {
27 | {& $script -Path 'C:\DoesNotExist' -Destination $TestDrive} | Should -Throw
28 | {& $script -Path $dummyFile -Destination $TestDrive} | Should -Throw
29 | {& $script -Path $null -Destination $TestDrive} | Should -Throw
30 | }
31 |
32 | It 'throws if invalid $Destination' {
33 | {& $script -Path $TestDrive -Destination 'C:\DoesNotExist'} | Should -Throw
34 | {& $script -Path $TestDrive -Destination $dummyFile} | Should -Throw
35 | {& $script -Path $TestDrive -Destination $null} | Should -Throw
36 | }
37 |
38 | It 'copies existing files' {
39 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
40 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
41 | New-Item -Path "$($src)\file.txt" -ItemType 'File'
42 |
43 | & $script -Path $src -Destination $dst -Timeout 100
44 |
45 | "$($dst)\file.txt" | Should -Exist
46 | }
47 |
48 | It 'copies new files' {
49 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
50 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
51 |
52 | $copiesNewFilesScript = {
53 | param ($src, $dst, $script)
54 | Start-Sleep -Milliseconds 500;
55 | New-Item -Path "$($src)\file.txt";
56 | & $script -Path $src -Destination $dst -Timeout 2000 -Sleep 100
57 | }
58 |
59 | $job = Start-Job -ScriptBlock $copiesNewFilesScript -ArgumentList $src, $dst, $script
60 | $job | Wait-Job | Remove-Job
61 |
62 | "$($dst)\file.txt" | Should -Exist
63 | }
64 |
65 | # Note: current test isn't working correct, "dst" folder always contains "file.txt".
66 | It 'deletes files' -Skip {
67 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
68 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
69 | New-Item -Path "$($src)\file.txt" -ItemType 'File'
70 | New-Item -Path "$($dst)\file.txt" -ItemType 'File'
71 |
72 | $job = Start-Job -ScriptBlock { Start-Sleep -Milliseconds 500; Remove-Item -Path "$($args[0])\file.txt" -Recurse } -ArgumentList $src
73 | & $script -Path $src -Destination $dst -Timeout 2000 -Sleep 100
74 | $job | Wait-Job | Remove-Job
75 |
76 | "$($dst)\file.txt" | Should -Not -Exist
77 | }
78 |
79 | It 'ignores excluded files on copy' {
80 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
81 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
82 | New-Item -Path "$($src)\file.disabled" -ItemType 'File'
83 | New-Item -Path "$($src)\web.config" -ItemType 'File'
84 |
85 | & $script -Path $src -Destination $dst -Timeout 100 -DefaultExcludedFiles @("*.disabled") -ExcludeFiles @("web.config")
86 |
87 | "$($dst)\file.disabled" | Should -Not -Exist
88 | "$($dst)\web.config" | Should -Not -Exist
89 | }
90 |
91 | It 'ignores excluded directories on copy' {
92 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
93 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
94 | New-Item -Path "$($src)\obj" -ItemType 'Directory'
95 | New-Item -Path "$($src)\obj\file.txt" -ItemType 'File'
96 | New-Item -Path "$($src)\exclude" -ItemType 'Directory'
97 | New-Item -Path "$($src)\exclude\file.txt" -ItemType 'File'
98 |
99 | & $script -Path $src -Destination $dst -Timeout 100 -DefaultExcludedDirectories @("obj") -ExcludeDirectories @("exclude")
100 |
101 | "$($dst)\obj\file.txt" | Should -Not -Exist
102 | "$($dst)\exclude\file.txt" | Should -Not -Exist
103 | }
104 |
105 | It 'ignores excluded files on delete' {
106 | $src = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
107 | $dst = New-Item -Path (Join-Path $TestDrive (Get-Random)) -ItemType 'Directory'
108 | New-Item -Path "$($src)\web.config" -ItemType 'File'
109 | New-Item -Path "$($dst)\web.config" -ItemType 'File'
110 |
111 | $job = Start-Job -ScriptBlock { Start-Sleep -Milliseconds 500; Remove-Item -Path "$($args[0])\web.config" } -ArgumentList $src
112 | & $script -Path $src -Destination $dst -Timeout 1000 -Sleep 100 -ExcludeFiles @("web.config")
113 | $job | Wait-Job | Remove-Job
114 |
115 | "$($src)\web.config" | Should -Not -Exist
116 | "$($dst)\web.config" | Should -Exist
117 | }
118 | }
--------------------------------------------------------------------------------
/powershell/src/Public/Get-SitecoreRandomString.ps1:
--------------------------------------------------------------------------------
1 | Set-StrictMode -Version Latest
2 |
3 | <#
4 | .SYNOPSIS
5 | Generates a random string of the specified length e.g. to use as a key or password.
6 | .DESCRIPTION
7 | Generates a random string of the specified length using one or more of the 4 allowed types (all by default).
8 | The allowed types include: capital letters, lowercase letters, numbers, and the ASCII special printable characters.
9 | The -EnforceComplexity option will ensure that at least one of each of the allowed types are present in the string.
10 | .PARAMETER Length
11 | The desired length of the string.
12 | .PARAMETER EnforceComplexity
13 | Ensures the returned string contains at least one of each of the allowed character types.
14 | .PARAMETER DisallowSpecial
15 | Prevent the special characters ~!@#$%^&*_-+=`|(){}[]:;<>.?/
16 | .PARAMETER DisallowDollar
17 | Prevent just $ symbol
18 | .PARAMETER DisallowCaps
19 | Prevent capital letters from appearing in the generated sting.
20 | .PARAMETER DisallowLower
21 | Prevent lower case letters from appearing in the generated string.
22 | .PARAMETER DisallowNumbers
23 | Prevent numbers from appearing in the generated string.
24 | .INPUTS
25 | None. You cannot pipe objects to Get-SitecoreRandomString.
26 | .OUTPUTS
27 | System.String. The random string.
28 | .EXAMPLE
29 | PS C:\> Get-SitecoreRandomString -Length 10
30 | .EXAMPLE
31 | PS C:\> Get-SitecoreRandomString -Length 10 -EnforceComplexity -DisallowDollar
32 | #>
33 | function Get-SitecoreRandomString
34 | {
35 | Param (
36 | [Parameter(Mandatory = $true, Position = 0)]
37 | [Validatescript({ $_ -ge 1})]
38 | [int]
39 | $Length,
40 |
41 | [Parameter(Position = 1)]
42 | [switch]
43 | $EnforceComplexity,
44 |
45 | [Parameter(ParameterSetName = 'custom', Position = 2)]
46 | [switch]
47 | $DisallowSpecial,
48 |
49 | [Parameter(ParameterSetName = 'custom', Position = 3)]
50 | [switch]
51 | $DisallowCaps,
52 |
53 | [Parameter(ParameterSetName = 'custom', Position = 4)]
54 | [switch]
55 | $DisallowLower,
56 |
57 | [Parameter(ParameterSetName = 'custom', Position = 5)]
58 | [switch]
59 | $DisallowNumbers,
60 |
61 | [Parameter(ParameterSetName = 'custom', Position = 6)]
62 | [switch]
63 | $DisallowDollar
64 | )
65 |
66 | $complexity = 0
67 | $charset = @()
68 |
69 | if ($PSCmdlet.ParameterSetName -ne 'custom') {
70 | $DisallowCaps = $false
71 | $DisallowLower = $false
72 | $DisallowNumbers = $false
73 | $DisallowSpecial = $false
74 | $DisallowDollar = $false
75 | }
76 |
77 | if (!$DisallowCaps) {
78 | $charset = ('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z')
79 | $complexity = $complexity + 1
80 | }
81 |
82 | if (!$DisallowLower) {
83 | $charset += ('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z')
84 | $complexity = $complexity + 1
85 | }
86 |
87 | if (!$DisallowNumbers) {
88 | $charset += ('1','2','3','4','5','6','7','8','9','0')
89 | $complexity = $complexity + 1
90 | }
91 |
92 | if (!$DisallowSpecial) {
93 | if (!$DisallowDollar) {
94 | $charset += ('~','!','@','#','$','%','^','&','*','_','-','+','=','`','|','\','(',')','{','}','[',']',':',';','<','>','.','?','/')
95 | }
96 | else
97 | {
98 | $charset += ('~','!','@','#','%','^','&','*','_','-','+','=','`','|','\','(',')','{','}','[',']',':',';','<','>','.','?','/')
99 | }
100 | $complexity = $complexity + 1
101 | }
102 |
103 | if ($EnforceComplexity -and $Length -lt $complexity) {
104 | throw "Requested charater types require a minimum length of $complexity characters."
105 | }
106 |
107 | if ($complexity -eq 0) {
108 | throw "No allowed character types."
109 | }
110 |
111 | Write-Verbose "Choosing from: $charset"
112 | Write-Verbose "Complexity Level: $complexity"
113 |
114 | do {
115 | Write-Verbose "Generating a string of length $Length"
116 |
117 | $generatedString = ""
118 |
119 | for ($i=1; $i -le $Length; $i++){
120 | $generatedString += Get-Random $charset
121 | }
122 |
123 | Write-Verbose "Generated string is $generatedString"
124 |
125 | $nums = 0
126 | $caps = 0
127 | $lower = 0
128 | $special = 0
129 |
130 | if ($EnforceComplexity) {
131 |
132 | Write-Verbose "Checking for complexity..."
133 |
134 | $charArray = $generatedString.ToCharArray()
135 |
136 | foreach ($character in $charArray){
137 | if ([byte]$character -ge 33 -and [byte]$character -le 47) {
138 | $special = 1
139 | }
140 | if ([byte]$character -ge 48 -and [byte]$character -le 57) {
141 | $nums = 1
142 | }
143 | if ([byte]$character -ge 58 -and [byte]$character -le 64) {
144 | $special = 1
145 | }
146 | if ([byte]$character -ge 65 -and [byte]$character -le 90) {
147 | $caps = 1
148 | }
149 | if ([byte]$character -ge 91 -and [byte]$character -le 66) {
150 | $special = 1
151 | }
152 | if ([byte]$character -ge 97 -and [byte]$character -le 122) {
153 | $lower = 1
154 | }
155 | if ([byte]$character -ge 123 -and [byte]$character -le 126) {
156 | $special = 1
157 | }
158 | }
159 |
160 | Write-Verbose "Complexity found: $($nums + $caps + $lower + $special)"
161 |
162 | } else {
163 | $complexity = $nums + $caps + $lower + $special
164 | }
165 |
166 | } while ($nums + $caps + $lower + $special -ne $complexity)
167 |
168 | return $generatedString
169 | }
--------------------------------------------------------------------------------
/image/build/azure-pipelines-ltsc2019.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - main
5 | - dev
6 | - 'release/*'
7 | paths:
8 | include:
9 | - 'image/*'
10 | exclude:
11 | - 'powershell/*'
12 |
13 | pr:
14 | branches:
15 | include:
16 | - main
17 | - dev
18 | - 'release/*'
19 | paths:
20 | include:
21 | - 'image/*'
22 | exclude:
23 | - 'powershell/*'
24 |
25 | resources:
26 | - repo: self
27 |
28 | variables:
29 | sitecoreVersion: $(SITECORE_VERSION)
30 | revision: $[counter(format('sitecoreVersion{0}', variables['sitecoreVersion']), 100)]
31 | osName: 1809
32 | baseImage: mcr.microsoft.com/windows/nanoserver:10.0.17763.2803
33 | buildImage: mcr.microsoft.com/windows/servercore:$(TARGETOS_LTSC2019)
34 | buildNumber: $(Build.BuildID)
35 | azureContainerRegistry: $(ACR_ContainerRegistry)
36 | dockerRegistryServiceConnection: $(DOCKER_REGISTRY_SERVICE_CONNECTION)
37 | sourceBranch: $(Build.SourceBranch)
38 |
39 | pool: $(POOLNAME_LTSC2019)
40 |
41 | stages:
42 |
43 | - stage: Versioning
44 |
45 | jobs:
46 | - job: Tagging
47 | steps:
48 |
49 | - task: PowerShell@2
50 | name: Tags
51 | displayName: Generate tags
52 | inputs:
53 | targetType: 'inline'
54 | script: |
55 | Write-Host "Pulling base image $(baseImage)..."
56 | docker pull $(baseImage)
57 | [string] $osVersion = (docker image inspect $(baseImage) | ConvertFrom-Json).OsVersion
58 | Write-Host "Image OS version is '$osVersion'"
59 |
60 | Write-Host "Setting sourceBranch to $(sourceBranch)"
61 | if("$(sourceBranch)" -eq "refs/heads/main" -or "$(sourceBranch)" -eq "refs/heads/release/$(sitecoreVersion)"){
62 | [string] $stability = ""
63 | [string] $namespace = "tools"
64 | }else{
65 | [string] $stability = "-unstable"
66 | [string] $namespace = "experimental"
67 | }
68 |
69 | Write-Host "Setting stability to '$stability'"
70 | Write-Host "Setting namespace to '$namespace'"
71 | Write-Host "##vso[task.setvariable variable=namespace;isOutput=true]$namespace"
72 | [string] $longTag = "$(sitecoreVersion).$(revision).$(buildNumber)-$osVersion-$(osName)$stability"
73 | [string] $shortTag = "$(sitecoreVersion)-$(osName)$stability"
74 | Write-Host "Setting long tag to '$longTag'"
75 | Write-Host "Setting short tag to '$shortTag'"
76 | Write-Host "##vso[task.setvariable variable=longTag;isOutput=true]$longTag"
77 | Write-Host "##vso[task.setvariable variable=shortTag;isOutput=true]$shortTag"
78 |
79 | - stage: Build
80 | dependsOn: Versioning
81 |
82 | jobs:
83 | - job: Build
84 | displayName: Build image
85 | variables:
86 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
87 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
88 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
89 | steps:
90 |
91 | - task: Docker@2
92 | displayName: Login to Container Registry
93 | inputs:
94 | containerRegistry: $(dockerRegistryServiceConnection)
95 | command: 'login'
96 |
97 | - task: Docker@2
98 | displayName: Build Docker image (Windows - no BuildKit)
99 | inputs:
100 | containerRegistry: $(dockerRegistryServiceConnection)
101 | repository: $(namespace)/sitecore-docker-tools-assets
102 | command: 'build'
103 | Dockerfile: '**/Dockerfile'
104 | buildContext: '$(Build.SourcesDirectory)/image/src'
105 | tags: |
106 | $(longTag)
107 | $(shortTag)
108 | arguments: --build-arg BASE_IMAGE=$(baseImage) --build-arg BUILD_IMAGE=$(buildImage) --force-rm
109 | env:
110 | DOCKER_BUILDKIT: 0
111 |
112 | - task: Docker@2
113 | displayName: Push Docker image
114 | inputs:
115 | containerRegistry: $(dockerRegistryServiceConnection)
116 | repository: $(namespace)/sitecore-docker-tools-assets
117 | command: 'push'
118 | tags: |
119 | $(longTag)
120 | $(shortTag)
121 |
122 | - stage: Test
123 | dependsOn: Build
124 |
125 | jobs:
126 | - job: Pester
127 | displayName: Run Pester tests
128 | steps:
129 |
130 | - task: Pester@9
131 | inputs:
132 | scriptFolder: "$(Build.SourcesDirectory)/image/test/*"
133 | resultsFile: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
134 | usePSCore: False
135 | - task: PublishTestResults@2
136 | inputs:
137 | testResultsFormat: "NUnit"
138 | testResultsFiles: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
139 | failTaskOnFailedTests: true
140 |
141 | - stage: Push
142 | dependsOn:
143 | - Versioning
144 | - Test
145 |
146 | jobs:
147 | - job: Push
148 | displayName: Verify pushed images
149 | variables:
150 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
151 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
152 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
153 | steps:
154 |
155 | - task: PowerShell@2
156 | displayName: Verify pushed images
157 | inputs:
158 | targetType: 'inline'
159 | script: |
160 | $registry = "$(azureContainerRegistry)"
161 | $imageName = "${registry}/$(namespace)/sitecore-docker-tools-assets"
162 | $longTag = "$(longTag)"
163 | $shortTag = "$(shortTag)"
164 |
165 | Write-Host "Images have been pushed to ACR successfully!"
166 | Write-Host "Images are available at:"
167 | Write-Host "- ${imageName}:${longTag}"
168 | Write-Host "- ${imageName}:${shortTag}"
169 |
170 | Write-Host "You can verify the images in Azure Portal or using Azure CLI:"
171 | Write-Host "az acr repository show-tags --name $registry --repository $(namespace)/sitecore-docker-tools-assets"
--------------------------------------------------------------------------------
/image/build/azure-pipelines-ltsc2022.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - main
5 | - dev
6 | - 'release/*'
7 | paths:
8 | include:
9 | - 'image/*'
10 | exclude:
11 | - 'powershell/*'
12 |
13 | pr:
14 | branches:
15 | include:
16 | - main
17 | - dev
18 | - 'release/*'
19 | paths:
20 | include:
21 | - 'image/*'
22 | exclude:
23 | - 'powershell/*'
24 |
25 | resources:
26 | - repo: self
27 |
28 | variables:
29 | sitecoreVersion: $(SITECORE_VERSION)
30 | revision: $[counter(format('sitecoreVersion{0}', variables['sitecoreVersion']), 100)]
31 | osName: $(TARGETOS_LTSC2022)
32 | baseImage: mcr.microsoft.com/windows/nanoserver:10.0.20348.643
33 | buildImage: mcr.microsoft.com/windows/servercore:$(osName)
34 | buildNumber: $(Build.BuildID)
35 | azureContainerRegistry: $(ACR_ContainerRegistry)
36 | dockerRegistryServiceConnection: $(DOCKER_REGISTRY_SERVICE_CONNECTION)
37 | sourceBranch: $(Build.SourceBranch)
38 |
39 | pool: $(POOLNAME_LTSC2022)
40 |
41 | stages:
42 |
43 | - stage: Versioning
44 |
45 | jobs:
46 | - job: Tagging
47 | steps:
48 |
49 | - task: PowerShell@2
50 | name: Tags
51 | displayName: Generate tags
52 | inputs:
53 | targetType: 'inline'
54 | script: |
55 | Write-Host "Pulling base image $(baseImage)..."
56 | docker pull $(baseImage)
57 | [string] $osVersion = (docker image inspect $(baseImage) | ConvertFrom-Json).OsVersion
58 | Write-Host "Image OS version is '$osVersion'"
59 |
60 | Write-Host "Setting sourceBranch to $(sourceBranch)"
61 | if("$(sourceBranch)" -eq "refs/heads/main" -or "$(sourceBranch)" -eq "refs/heads/release/$(sitecoreVersion)"){
62 | [string] $stability = ""
63 | [string] $namespace = "tools"
64 | }else{
65 | [string] $stability = "-unstable"
66 | [string] $namespace = "experimental"
67 | }
68 |
69 | Write-Host "Setting stability to '$stability'"
70 | Write-Host "Setting namespace to '$namespace'"
71 | Write-Host "##vso[task.setvariable variable=namespace;isOutput=true]$namespace"
72 | [string] $longTag = "$(sitecoreVersion).$(revision).$(buildNumber)-$osVersion-$(osName)$stability"
73 | [string] $shortTag = "$(sitecoreVersion)-$(osName)$stability"
74 | Write-Host "Setting long tag to '$longTag'"
75 | Write-Host "Setting short tag to '$shortTag'"
76 | Write-Host "##vso[task.setvariable variable=longTag;isOutput=true]$longTag"
77 | Write-Host "##vso[task.setvariable variable=shortTag;isOutput=true]$shortTag"
78 |
79 | - stage: Build
80 | dependsOn: Versioning
81 |
82 | jobs:
83 | - job: Build
84 | displayName: Build image
85 | variables:
86 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
87 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
88 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
89 | steps:
90 |
91 | - task: Docker@2
92 | displayName: Login to Container Registry
93 | inputs:
94 | containerRegistry: $(dockerRegistryServiceConnection)
95 | command: 'login'
96 |
97 | - task: Docker@2
98 | displayName: Build Docker image (Windows - no BuildKit)
99 | inputs:
100 | containerRegistry: $(dockerRegistryServiceConnection)
101 | repository: $(namespace)/sitecore-docker-tools-assets
102 | command: 'build'
103 | Dockerfile: '**/Dockerfile'
104 | buildContext: '$(Build.SourcesDirectory)/image/src'
105 | tags: |
106 | $(longTag)
107 | $(shortTag)
108 | arguments: --build-arg BASE_IMAGE=$(baseImage) --build-arg BUILD_IMAGE=$(buildImage) --force-rm
109 | env:
110 | DOCKER_BUILDKIT: 0
111 |
112 | - task: Docker@2
113 | displayName: Push Docker image
114 | inputs:
115 | containerRegistry: $(dockerRegistryServiceConnection)
116 | repository: $(namespace)/sitecore-docker-tools-assets
117 | command: 'push'
118 | tags: |
119 | $(longTag)
120 | $(shortTag)
121 |
122 | - stage: Test
123 | dependsOn: Build
124 |
125 | jobs:
126 | - job: Pester
127 | displayName: Run Pester tests
128 | steps:
129 |
130 | - task: Pester@9
131 | inputs:
132 | scriptFolder: "$(Build.SourcesDirectory)/image/test/*"
133 | resultsFile: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
134 | usePSCore: False
135 | - task: PublishTestResults@2
136 | inputs:
137 | testResultsFormat: "NUnit"
138 | testResultsFiles: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
139 | failTaskOnFailedTests: true
140 |
141 | - stage: Push
142 | dependsOn:
143 | - Versioning
144 | - Test
145 |
146 | jobs:
147 | - job: Push
148 | displayName: Verify pushed images
149 | variables:
150 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
151 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
152 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
153 | steps:
154 |
155 | - task: PowerShell@2
156 | displayName: Verify pushed images
157 | inputs:
158 | targetType: 'inline'
159 | script: |
160 | $registry = "$(azureContainerRegistry)"
161 | $imageName = "${registry}/$(namespace)/sitecore-docker-tools-assets"
162 | $longTag = "$(longTag)"
163 | $shortTag = "$(shortTag)"
164 |
165 | Write-Host "Images have been pushed to ACR successfully!"
166 | Write-Host "Images are available at:"
167 | Write-Host "- ${imageName}:${longTag}"
168 | Write-Host "- ${imageName}:${shortTag}"
169 |
170 | Write-Host "You can verify the images in Azure Portal or using Azure CLI:"
171 | Write-Host "az acr repository show-tags --name $registry --repository $(namespace)/sitecore-docker-tools-assets"
--------------------------------------------------------------------------------
/powershell/test/Public/Add-HostsEntry.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | $windir = $env:windir
5 |
6 | Describe 'Add-HostsEntry' {
7 |
8 | $hostsPath = Join-Path -Path $TestDrive -ChildPath "system32\drivers\etc\hosts"
9 | New-Item ([io.Directory]::GetParent($hostsPath)) -ItemType Directory | Out-Null
10 | New-Item $hostsPath -ItemType File | Out-Null
11 |
12 | BeforeAll {
13 | $env:windir = $TestDrive
14 | }
15 | AfterAll {
16 | $env:windir = $windir
17 | }
18 |
19 | It 'requires $HostName' {
20 | $result = Test-ParamIsMandatory -Command Add-HostsEntry -Parameter HostName
21 | $result | Should Be $true
22 | }
23 |
24 | It 'throws if $HostName is $null or empty' {
25 | { Add-HostsEntry -HostName $null } | Should -Throw
26 | { Add-HostsEntry -HostName "" } | Should -Throw
27 | }
28 |
29 | It 'throws if $IPAddress is $null or empty' {
30 | { Add-HostsEntry -HostName "test" -IPAddress $null } | Should -Throw
31 | { Add-HostsEntry -HostName "test" -IPAddress "" } | Should -Throw
32 | }
33 |
34 | Context 'Default $Path' {
35 | Mock Test-Path { $false }
36 |
37 | It 'uses hosts file in system32' {
38 | $expected = Join-Path -Path $env:windir -ChildPath "system32\drivers\etc\hosts"
39 |
40 | Add-HostsEntry -HostName "somehost"
41 |
42 | Assert-MockCalled Test-Path -Times 1 -Exactly -ParameterFilter {
43 | $Path -eq $expected
44 | } -Scope It
45 | }
46 | }
47 |
48 | Context 'Missing hosts file' {
49 | Mock Test-Path { $false }
50 |
51 | It 'writes warning' {
52 | Mock Write-Warning
53 |
54 | Add-HostsEntry -HostName "somehost"
55 |
56 | Assert-MockCalled Write-Warning -Times 1 -Exactly -ParameterFilter {
57 | $Message -eq 'No hosts file found, hosts have not been updated'
58 | } -Scope It
59 | }
60 | }
61 |
62 | Context 'Encoding' {
63 | Mock WriteLines
64 | Mock Get-Content
65 |
66 | It 'reads as UTF8' {
67 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
68 |
69 | Assert-MockCalled Get-Content -Times 1 -Exactly -ParameterFilter {
70 | $Path -eq $hostsPath -and `
71 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")
72 | } -Scope It
73 | }
74 |
75 | It 'writes as UTF8' {
76 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
77 |
78 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
79 | $File -eq $hostsPath -and `
80 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
81 | } -Scope It
82 | }
83 | }
84 |
85 | Context 'When modifying' {
86 | Mock WriteLines
87 |
88 | It 'creates a backup before modifying' {
89 | Add-HostsEntry -Path $hostsPath -HostName 'somehost'
90 | Test-Path "$hostsPath.backup" | Should Be $true
91 | }
92 |
93 | It 'adds as 127.0.0.1 by default' {
94 | Add-HostsEntry -Path $hostsPath -HostName 'somehost'
95 |
96 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
97 | $File -eq $hostsPath -and `
98 | $Content -eq "127.0.0.1`tsomehost"
99 | } -Scope It
100 | }
101 |
102 | It 'adds with given ipaddress' {
103 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
104 |
105 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
106 | $File -eq $hostsPath -and `
107 | $Content -eq "10.10.10.10`tsomehost"
108 | } -Scope It
109 | }
110 | }
111 |
112 | Context 'When modifying with existing hosts' {
113 | Mock WriteLines
114 |
115 | It 'does not throw when no entry in hosts exists' {
116 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
117 |
118 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
119 | $File -eq $hostsPath -and `
120 | $Content -eq "10.10.10.10`tsomehost"
121 | } -Scope It
122 | }
123 |
124 | It 'does not throw when single entry in hosts exists' {
125 | Set-Content -Path $hostsPath -Value "`n127.0.0.1`tsomehost"
126 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
127 |
128 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
129 | $File -eq $hostsPath -and `
130 | $Content -eq "10.10.10.10`tsomehost"
131 | } -Scope It
132 | }
133 |
134 | It 'adds only comments exist' {
135 | Set-Content -Path $hostsPath -Value "# Copyright 1993-2009 Microsoft Corp."
136 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
137 |
138 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
139 | $File -eq $hostsPath -and `
140 | $Content -eq "10.10.10.10`tsomehost"
141 | } -Scope It
142 | }
143 |
144 | It 'is not added when default IP Address already exists for host' {
145 | Set-Content -Path $hostsPath -Value "`n127.0.0.1`tsomehost`n10.10.10.10`tsomehost"
146 |
147 | Add-HostsEntry -Path $hostsPath -HostName 'somehost' -IPAddress '10.10.10.10'
148 |
149 | Assert-MockCalled WriteLines -Times 0 -Exactly -ParameterFilter {
150 | $File -eq $hostsPath -and `
151 | $Content -eq "127.0.0.1`tsomehost"
152 | } -Scope It
153 | }
154 |
155 | It 'is not added when specific IP Address already exists for host' {
156 | Set-Content -Path $hostsPath -Value "`n127.0.0.1`tsomehost`n10.10.10.10`tsomehost"
157 |
158 | Add-HostsEntry -Path $hostsPath -HostName 'somehost'
159 |
160 | Assert-MockCalled WriteLines -Times 0 -Exactly -ParameterFilter {
161 | $File -eq $hostsPath -and `
162 | $Content -eq "127.0.0.1`tsomehost"
163 | } -Scope It
164 | }
165 |
166 | It 'is added when existing partial host exists' {
167 | Set-Content -Path $hostsPath -Value "`n127.0.0.1`tsomehost`n10.10.10.10`tsomehost"
168 |
169 | Add-HostsEntry -Path $hostsPath -HostName 'some'
170 |
171 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
172 | $File -eq $hostsPath -and `
173 | $Content -eq "127.0.0.1`tsome"
174 | } -Scope It
175 | }
176 | }
177 | }
178 | }
--------------------------------------------------------------------------------
/image/test/scripts/Invoke-XdtTransform.Tests.ps1:
--------------------------------------------------------------------------------
1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path
2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
3 | $script = "$here\..\..\src\scripts\$sut"
4 | . $PSScriptRoot\..\TestUtils.ps1
5 |
6 | Describe 'Invoke-XdtTransform.ps1' {
7 |
8 | BeforeAll {
9 | if (!(Get-Package Microsoft.Web.Xdt -Destination "$PSScriptRoot\..\packages" -ErrorAction SilentlyContinue)) {
10 | Install-Package Microsoft.Web.Xdt -RequiredVersion 3.0.0 -ProviderName NuGet -Destination "$PSScriptRoot\..\packages" -Force -ForceBootstrap
11 | }
12 | $xdtDllPath = "$PSScriptRoot\..\packages\Microsoft.Web.Xdt.3.0.0\lib\netstandard2.0\Microsoft.Web.XmlTransform.dll"
13 |
14 | $validConfig = Join-Path $TestDrive 'Web.config'
15 | Set-Content $validConfig -Value ''
16 | $validTransform = Join-Path $TestDrive 'Web.config.xdt'
17 | Set-Content $validTransform -Value ''
18 | }
19 |
20 | It 'requires $Path' {
21 | $result = Test-ParamIsMandatory -Command $script -Parameter Path
22 | $result | Should -Be $true
23 | }
24 |
25 | It 'requires $XdtPath' {
26 | $result = Test-ParamIsMandatory -Command $script -Parameter XdtPath
27 | $result | Should -Be $true
28 | }
29 |
30 | It 'throws if $Path is a folder and $XdtPath is a file' {
31 | {& $script -Path $TestDrive -XdtPath $validTransform -XdtDllPath $xdtDllPath} | Should -Throw
32 | }
33 |
34 | It 'throws if $Path is a file and $XdtPath is a folder' {
35 | {& $script -Path $validConfig -XdtPath $TestDrive -XdtDllPath $xdtDllPath} | Should -Throw
36 | }
37 |
38 | It 'throws if invalid $XdtDllPath' {
39 | {& $script -Path $validConfig -XdtPath $validTransform -XdtDllPath $null} | Should -Throw
40 | }
41 |
42 | Context 'when passing files' {
43 |
44 | It 'applies file transform' {
45 | $config = Join-Path $TestDrive 'Web.config'
46 | Set-Content $config -Value `
47 | @'
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | '@
58 | $transform = Join-Path $TestDrive 'Web.config.xdt'
59 | Set-Content $transform -Value `
60 | @'
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | '@
71 |
72 | & $script -Path $config -XdtPath $transform -XdtDllPath $xdtDllPath -Verbose
73 |
74 | $config | Should -FileContentMatchExactly ''
75 | $config | Should -FileContentMatchExactly ''
76 | $config | Should -FileContentMatchExactly '^ $'
77 | }
78 |
79 | It 'throws if file transform fails' {
80 | $config = Join-Path $TestDrive 'Web.config'
81 | Set-Content $config -Value `
82 | @'
83 |
84 |
85 |
86 | '@
87 | $transform = Join-Path $TestDrive 'Web.config.xdt'
88 | Set-Content $transform -Value `
89 | @'
90 |
91 |
92 |
93 |
94 | '@
95 |
96 | {& $script -Path $config -XdtPath $transform -XdtDllPath $xdtDllPath} | Should -Throw
97 | }
98 | }
99 |
100 | Context 'when passing folders' {
101 |
102 | It "applies folder transforms from folder" -TestCases @(
103 | @{ target = 'same'; configFolder = '\same'; transformFolder = '\same' },
104 | @{ target = 'different'; configFolder = '\different-one'; transformFolder = '\different-two' }
105 | ) {
106 | param($target, $configFolder, $transformFolder)
107 |
108 | $configs = New-Item -Path (Join-Path $TestDrive $configFolder) -ItemType 'Directory' -Force
109 | $transforms = New-Item -Path (Join-Path $TestDrive $transformFolder) -ItemType 'Directory' -Force
110 |
111 | $webConfig = Join-Path $configs 'Web.config'
112 | Set-Content $webConfig -Value `
113 | @'
114 |
115 |
116 |
117 |
118 |
119 |
120 | '@
121 | $webConfigTransform = Join-Path $transforms 'Web.config.xdt'
122 | Set-Content $webConfigTransform -Value `
123 | @'
124 |
125 |
126 |
127 |
128 |
129 |
130 | '@
131 | New-Item -Path (Join-Path $configs '\App_Config') -ItemType 'Directory' -Force
132 | $layersConfig = Join-Path $configs '\App_Config\Layers.config'
133 | Set-Content $layersConfig -Value `
134 | @'
135 |
136 |
137 |
138 |
139 | '@
140 | New-Item -Path (Join-Path $transforms '\App_Config') -ItemType 'Directory' -Force
141 | $layersConfigTransform = Join-Path $transforms '\App_Config\layers.config.xdt'
142 | Set-Content $layersConfigTransform -Value `
143 | @'
144 |
145 |
146 |
147 |
148 | '@
149 |
150 | & $script -Path (Join-Path $TestDrive $configFolder) -XdtPath (Join-Path $TestDrive $transformFolder) -XdtDllPath $xdtDllPath -Verbose
151 |
152 | $webConfig | Should -FileContentMatchExactly ''
153 | $layersConfig | Should -FileContentMatchExactly ''
154 | }
155 |
156 | It 'skips transforms without matching file' {
157 | $configFolder = '\skips-configs'
158 | $transformFolder = '\skips-transforms'
159 | $configs = New-Item -Path (Join-Path $TestDrive $configFolder) -ItemType 'Directory' -Force
160 | $transforms = New-Item -Path (Join-Path $TestDrive $transformFolder) -ItemType 'Directory' -Force
161 |
162 | $webConfig = Join-Path $configs 'Web.config'
163 | Set-Content $webConfig -Value `
164 | @'
165 |
166 |
167 |
168 |
169 |
170 |
171 | '@
172 | $webConfigTransform = Join-Path $transforms 'Web.config.xdt'
173 | Set-Content $webConfigTransform -Value `
174 | @'
175 |
176 |
177 |
178 |
179 |
180 |
181 | '@
182 | $hangingTransform = Join-Path $transforms 'hanging.config.xdt'
183 | Set-Content $hangingTransform -Value ''
184 |
185 | {& $script -Path (Join-Path $TestDrive $configFolder) -XdtPath (Join-Path $TestDrive $transformFolder) -XdtDllPath $xdtDllPath -Verbose} | Should -Not -Throw
186 |
187 | $webConfig | Should -FileContentMatchExactly ''
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Remove-HostsEntry.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | $windir = $env:windir
5 |
6 | Describe 'Remove-HostsEntry' {
7 |
8 | $hostsPath = Join-Path -Path $TestDrive -ChildPath "system32\drivers\etc\hosts"
9 | New-Item ([io.Directory]::GetParent($hostsPath)) -ItemType Directory | Out-Null
10 | New-Item $hostsPath -ItemType File | Out-Null
11 |
12 | BeforeAll {
13 | $env:windir = $TestDrive
14 | }
15 | AfterAll {
16 | $env:windir = $windir
17 | }
18 |
19 | It 'requires $HostName' {
20 | $result = Test-ParamIsMandatory -Command Remove-HostsEntry -Parameter HostName
21 | $result | Should Be $true
22 | }
23 |
24 | It 'throws if $HostName is $null or empty' {
25 | { Remove-HostsEntry -HostName $null } | Should -Throw
26 | { Remove-HostsEntry -HostName "" } | Should -Throw
27 | }
28 |
29 | Context 'Default $Path' {
30 | Mock Test-Path { $false }
31 |
32 | It 'uses hosts file in system32' {
33 | $expected = Join-Path -Path $env:windir -ChildPath "system32\drivers\etc\hosts"
34 |
35 | Remove-HostsEntry -HostName "somehost"
36 |
37 | Assert-MockCalled Test-Path -Times 1 -Exactly -ParameterFilter {
38 | $Path -eq $expected
39 | } -Scope It
40 | }
41 | }
42 |
43 | Context 'Missing hosts file' {
44 | Mock Test-Path { $false }
45 |
46 | It 'writes warning' {
47 | Mock Write-Warning
48 |
49 | Remove-HostsEntry -HostName "somehost"
50 |
51 | Assert-MockCalled Write-Warning -Times 1 -Exactly -ParameterFilter {
52 | $Message -eq 'No hosts file found, hosts have not been updated'
53 | } -Scope It
54 | }
55 | }
56 |
57 | Context 'Encoding' {
58 | Mock WriteLines
59 | Mock Get-Content {
60 | return @("10.10.10.10`tsomehost")
61 | }
62 |
63 | It 'reads as UTF8' {
64 | Remove-HostsEntry -Path $hostsPath -HostName 'somehost'
65 |
66 | Assert-MockCalled Get-Content -Times 1 -Exactly -ParameterFilter {
67 | $Path -eq $hostsPath -and `
68 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")
69 | } -Scope It
70 | }
71 |
72 | It 'writes as UTF8' {
73 | Remove-HostsEntry -Path $hostsPath -HostName 'somehost'
74 |
75 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
76 | $File -eq $hostsPath -and `
77 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
78 | } -Scope It
79 | }
80 | }
81 |
82 | Context 'When the hosts file is empty' {
83 | Mock Get-Content {
84 | return $null
85 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
86 |
87 | Mock WriteLines
88 |
89 | Remove-HostsEntry -Path $hostsPath -HostName "hostName1"
90 |
91 | It 'does not update the hosts file' {
92 | Assert-MockCalled WriteLines -Times 0 -Exactly -ParameterFilter {
93 | $File -eq $hostsPath
94 | }
95 | }
96 | }
97 |
98 | Context 'When the hosts file contains only comments' {
99 | Mock Get-Content {
100 | return @("# Hosts file comment")
101 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
102 |
103 | Mock WriteLines
104 |
105 | Remove-HostsEntry -Path $hostsPath -HostName "hostName1"
106 |
107 | It 'does not update the hosts file' {
108 | Assert-MockCalled WriteLines -Times 0 -Exactly -ParameterFilter {
109 | $File -eq $hostsPath
110 | }
111 | }
112 | }
113 |
114 | Context 'When there are no matching host headers in the host file' {
115 | Mock Get-Content {
116 | return @("10.10.10.10`thostName1", "20.20.20.20`thostName2", "30.30.30.30`thostName3")
117 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
118 |
119 | Mock WriteLines
120 |
121 | Remove-HostsEntry -Path $hostsPath -HostName "hostName4"
122 |
123 | It 'does not update the hosts file' {
124 | Assert-MockCalled WriteLines -Times 0 -Exactly -ParameterFilter {
125 | $File -eq $hostsPath
126 | }
127 | }
128 | }
129 |
130 | Context 'When there is single host header in the hosts file' {
131 | Mock Get-Content {
132 | return @("10.10.10.10`thostName1")
133 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
134 |
135 | Mock WriteLines
136 |
137 | Remove-HostsEntry -Path $hostsPath -HostName "hostName1"
138 |
139 | It 'removes given host header' {
140 |
141 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
142 | $File -eq $hostsPath -and `
143 | $Content -eq $null -and `
144 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
145 | }
146 | }
147 | }
148 |
149 | Context 'When there are single host header and comment in the hosts file' {
150 | Mock Get-Content {
151 | return @("# Hosts file comment", "10.10.10.10`thostName1")
152 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
153 |
154 | Mock WriteLines
155 |
156 | Remove-HostsEntry -Path $hostsPath -HostName "hostName1"
157 |
158 | It 'removes given host header' {
159 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
160 | $File -eq $hostsPath -and `
161 | @(Compare-Object -ReferenceObject @("# Hosts file comment") -DifferenceObject $Content).Count -eq 0 -and `
162 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
163 | }
164 | }
165 | }
166 |
167 | Context 'When there are multiple host headers in the hosts file' {
168 | Mock Get-Content {
169 | return @("10.10.10.10`thostName1", "20.20.20.20`thostName2", "30.30.30.30`thostName3")
170 | } -ParameterFilter { $Path -eq $hostsPath -and ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")}
171 |
172 | Mock WriteLines
173 |
174 | Remove-HostsEntry -Path $hostsPath -HostName "hostName2"
175 |
176 | It 'removes given host header' {
177 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
178 | $File -eq $hostsPath -and `
179 | @(Compare-Object -ReferenceObject @("10.10.10.10`thostName1", "30.30.30.30`thostName3") -DifferenceObject $Content).Count -eq 0 -and `
180 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
181 | }
182 | }
183 | }
184 | }
185 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Set-EnvFileVariable.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Set-EnvFileVariable' {
5 |
6 | $envFile = Join-Path $TestDrive '.env'
7 | Set-Content $envFile -Value ''
8 |
9 | It 'requires $Variable' {
10 | $result = Test-ParamIsMandatory -Command Set-EnvFileVariable -Parameter Variable
11 | $result | Should -Be $true
12 | }
13 |
14 | It 'requires $Value' {
15 | $result = Test-ParamIsMandatory -Command Set-EnvFileVariable -Parameter Value
16 | $result | Should -Be $true
17 | }
18 |
19 | It 'throws if $Path is invalid' {
20 | { Set-EnvFileVariable -Variable "foo" -Value "bar" -Path "$TestDrive\.baz" } | Should -Throw
21 | }
22 |
23 | It 'throws if $Variable is $null or empty' {
24 | { Set-EnvFileVariable -Variable $null -Value "bar" -Path $envFile } | Should -Throw
25 | { Set-EnvFileVariable -Variable "" -Value "bar" -Path $envFile } | Should -Throw
26 | }
27 |
28 | It 'adds variable to empty file' {
29 | Set-Content $envFile -Value ''
30 | Set-EnvFileVariable -Path $envFile -Variable 'VAR' -Value 'VAL'
31 | $envFile | Should -FileContentMatchExactly '^VAR=VAL$'
32 | }
33 |
34 | It 'adds variable on new line to end of file' {
35 | Set-Content $envFile -Value 'VAR1=VAL1'
36 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value 'VAL2'
37 | $envFile | Should -FileContentMatchMultiline '^VAR1=VAL1\r?\nVAR2=VAL2\r?\n$'
38 | }
39 |
40 | It 'sets existing variable with value to new value' {
41 | Set-Content $envFile -Value @(
42 | 'VAR1=VAL1',
43 | 'VAR2=VAL2',
44 | 'VAR3=VAL3'
45 | )
46 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value 'two'
47 | $envFile | Should -FileContentMatchExactly '^VAR2=two$'
48 | }
49 |
50 | It 'sets existing variable with value to empty' {
51 | Set-Content $envFile -Value @(
52 | 'VAR1=VAL1',
53 | 'VAR2=VAL2',
54 | 'VAR3=VAL3'
55 | )
56 | Set-EnvFileVariable -Path $envFile -Variable 'VAR3' -Value ''
57 | $envFile | Should -FileContentMatchExactly '^VAR3=$'
58 | }
59 |
60 | It 'sets existing variable empty to value' {
61 | Set-Content $envFile -Value @(
62 | 'VAR1=',
63 | 'VAR2=VAL2',
64 | 'VAR3=VAL3'
65 | )
66 | Set-EnvFileVariable -Path $envFile -Variable 'VAR1' -Value 'one'
67 | $envFile | Should -FileContentMatchExactly '^VAR1=one$'
68 | }
69 |
70 | It 'sets existing variable case insensitively' {
71 | Set-Content $envFile -Value @(
72 | 'VAR1=VAL1',
73 | 'VAR2=VAL2',
74 | 'VAR3=VAL3'
75 | )
76 | Set-EnvFileVariable -Path $envFile -Variable 'var1' -Value 'one'
77 | $envFile | Should -FileContentMatchExactly '^var1=one$'
78 | $envFile | Should -Not -FileContentMatchExactly '^VAR1=VAL1$'
79 | $envFile | Should -Not -FileContentMatchExactly '^VAR1=one$'
80 | }
81 |
82 | It 'sets existing variable to value with RegEx substitution characters' {
83 | Set-Content $envFile -Value 'VAR=VAL'
84 | Set-EnvFileVariable -Path $envFile -Variable 'VAR' -Value 'a$&b$$c'
85 | $envFile | Should -FileContentMatch ([regex]::Escape('VAR=a$&b$$c'))
86 | }
87 |
88 | It 'preserves comments' {
89 | Set-Content $envFile -Value @(
90 | '#VAR1=VAL1',
91 | 'VAR2=VAL2',
92 | 'VAR3=VAL3'
93 | )
94 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value 'two'
95 | $envFile | Should -FileContentMatchExactly '^#VAR1=VAL1$'
96 | $envFile | Should -FileContentMatchExactly '^VAR2=two$'
97 | }
98 |
99 | It 'adds variable when existing is commented' {
100 | Set-Content $envFile -Value @(
101 | '#VAR1=VAL1',
102 | 'VAR2=VAL2',
103 | 'VAR3=VAL3'
104 | )
105 | Set-EnvFileVariable -Path $envFile -Variable 'VAR1' -Value 'one'
106 | $envFile | Should -FileContentMatchExactly '^#VAR1=VAL1$'
107 | $envFile | Should -FileContentMatchExactly '^VAR1=one$'
108 | }
109 |
110 | It 'preserves blank lines' {
111 | Set-Content $envFile -Value @(
112 | 'VAR1=VAL1',
113 | '',
114 | 'VAR2=VAL2'
115 | )
116 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value 'two'
117 | $envFile | Should -FileContentMatchExactly '^$'
118 | $envFile | Should -FileContentMatchExactly '^VAR2=two$'
119 | }
120 |
121 | It 'sets variables with literal values when specified' {
122 | Set-Content $envFile -Value @(
123 | 'VAR1=VAL1',
124 | 'VAR2=VAL2',
125 | 'VAR3=VAL3'
126 | )
127 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value 'literal$tring' -AsLiteral
128 | $envFile | Should -FileContentMatchExactly ([regex]::Escape('VAR2=''literal$tring'''))
129 | }
130 |
131 | It 'adds variable with literal values when specified' {
132 | Set-Content $envFile -Value @(
133 | 'VAR1=VAL1',
134 | 'VAR2=VAL2',
135 | 'VAR3=VAL3'
136 | )
137 | Set-EnvFileVariable -Path $envFile -Variable 'VAR4' -Value 'literal$tring' -AsLiteral
138 | $envFile | Should -FileContentMatchExactly ([regex]::Escape('VAR4=''literal$tring'''))
139 | }
140 |
141 | It 'adds variable with literal values when specified' {
142 | Set-Content $envFile -Value @(
143 | 'VAR1=VAL1',
144 | 'VAR2=VAL2',
145 | 'VAR3=VAL3'
146 | )
147 | Set-EnvFileVariable -Path $envFile -Variable 'VAR2' -Value "VAL2'Escaped" -AsLiteral
148 | $envFile | Should -FileContentMatchExactly "VAR2='VAL2''Escaped'"
149 | }
150 |
151 | It 'uses .\.env as default $Path' {
152 | Set-Content $envFile -Value "foo=bar"
153 |
154 | Push-Location $TestDrive
155 | Set-EnvFileVariable -Variable "foo" -Value "baz"
156 | Pop-Location
157 |
158 | $envFile | Should -FileContentMatchExactly '^foo=baz$'
159 | }
160 |
161 | It 'is aliased under old name Set-DockerComposeEnvFileVariable' {
162 | Set-Content $envFile -Value ''
163 | Set-DockerComposeEnvFileVariable -Path $envFile -Variable 'VAR' -Value 'VAL'
164 | $envFile | Should -FileContentMatchExactly '^VAR=VAL$'
165 | }
166 |
167 | Context 'Encoding' {
168 | Mock WriteLines
169 | Mock Get-Content
170 |
171 | It 'reads as UTF8' {
172 | Set-EnvFileVariable -Path $envFile -Variable 'VAR1' -Value 'one'
173 |
174 | Assert-MockCalled Get-Content -Times 1 -Exactly -ParameterFilter {
175 | $Path -eq $envFile -and `
176 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding" -or $Encoding.ToString() -eq "UTF8")
177 | } -Scope It
178 | }
179 |
180 | It 'writes as UTF8' {
181 | Set-EnvFileVariable -Path $envFile -Variable 'VAR1' -Value 'one'
182 |
183 | Assert-MockCalled WriteLines -Times 1 -Exactly -ParameterFilter {
184 | $File -eq $envFile -and `
185 | ($Encoding.ToString() -eq "System.Text.UTF8Encoding+UTF8EncodingSealed" -or $Encoding.ToString() -eq "System.Text.UTF8Encoding")
186 | } -Scope It
187 | }
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/image/build/azure-pipelines-ltsc2025.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - main
5 | - dev
6 | - 'release/*'
7 | paths:
8 | include:
9 | - 'image/*'
10 | exclude:
11 | - 'powershell/*'
12 |
13 | pr:
14 | branches:
15 | include:
16 | - main
17 | - dev
18 | - 'release/*'
19 | paths:
20 | include:
21 | - 'image/*'
22 | exclude:
23 | - 'powershell/*'
24 |
25 | resources:
26 | - repo: self
27 |
28 | variables:
29 | sitecoreVersion: $(SITECORE_VERSION)
30 | revision: $[counter(format('sitecoreVersion{0}', variables['sitecoreVersion']), 100)]
31 | osName: $(TARGETOS_LTSC2025)
32 | baseImage: mcr.microsoft.com/windows/nanoserver:$(osName)
33 | buildImage: mcr.microsoft.com/windows/servercore:$(osName)
34 | buildNumber: $(Build.BuildID)
35 | azureContainerRegistry: $(ACR_ContainerRegistry)
36 | dockerRegistryServiceConnection: $(DOCKER_REGISTRY_SERVICE_CONNECTION)
37 | sourceBranch: $(Build.SourceBranch)
38 |
39 | pool: $(POOLNAME_LTSC2025)
40 |
41 | stages:
42 |
43 | - stage: Versioning
44 |
45 | jobs:
46 | - job: Tagging
47 | steps:
48 |
49 | - task: PowerShell@2
50 | name: Tags
51 | displayName: Generate tags
52 | inputs:
53 | targetType: 'inline'
54 | script: |
55 | Write-Host "Pulling base image $(baseImage)..."
56 | docker pull $(baseImage)
57 | [string] $osVersion = (docker image inspect $(baseImage) | ConvertFrom-Json).OsVersion
58 | Write-Host "Image OS version is '$osVersion'"
59 |
60 | Write-Host "Setting sourceBranch to $(sourceBranch)"
61 | if("$(sourceBranch)" -eq "refs/heads/main" -or "$(sourceBranch)" -eq "refs/heads/release/$(sitecoreVersion)"){
62 | [string] $stability = ""
63 | [string] $namespace = "tools"
64 | }else{
65 | [string] $stability = "-unstable"
66 | [string] $namespace = "experimental"
67 | }
68 |
69 | Write-Host "Setting stability to '$stability'"
70 | Write-Host "Setting namespace to '$namespace'"
71 | Write-Host "##vso[task.setvariable variable=namespace;isOutput=true]$namespace"
72 | [string] $longTag = "$(sitecoreVersion).$(revision).$(buildNumber)-$osVersion-$(osName)$stability"
73 | [string] $shortTag = "$(sitecoreVersion)-$(osName)$stability"
74 | Write-Host "Setting long tag to '$longTag'"
75 | Write-Host "Setting short tag to '$shortTag'"
76 | Write-Host "##vso[task.setvariable variable=longTag;isOutput=true]$longTag"
77 | Write-Host "##vso[task.setvariable variable=shortTag;isOutput=true]$shortTag"
78 |
79 | - stage: Build
80 | dependsOn: Versioning
81 |
82 | jobs:
83 | - job: Build
84 | displayName: Build image
85 | variables:
86 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
87 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
88 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
89 | steps:
90 |
91 | - task: Docker@2
92 | displayName: Login to Container Registry
93 | inputs:
94 | containerRegistry: $(dockerRegistryServiceConnection)
95 | command: 'login'
96 |
97 | - task: Docker@2
98 | displayName: Build Docker image (Windows - no BuildKit)
99 | inputs:
100 | containerRegistry: $(dockerRegistryServiceConnection)
101 | repository: $(namespace)/sitecore-docker-tools-assets
102 | command: 'build'
103 | Dockerfile: '**/Dockerfile'
104 | buildContext: '$(Build.SourcesDirectory)/image/src'
105 | tags: |
106 | $(longTag)
107 | $(shortTag)
108 | arguments: --build-arg BASE_IMAGE=$(baseImage) --build-arg BUILD_IMAGE=$(buildImage) --force-rm
109 | env:
110 | DOCKER_BUILDKIT: 0
111 |
112 | - task: Docker@2
113 | displayName: Push Docker image
114 | inputs:
115 | containerRegistry: $(dockerRegistryServiceConnection)
116 | repository: $(namespace)/sitecore-docker-tools-assets
117 | command: 'push'
118 | tags: |
119 | $(longTag)
120 | $(shortTag)
121 |
122 | - stage: Test
123 | dependsOn: Build
124 |
125 | jobs:
126 | - job: Pester
127 | displayName: Run Pester tests
128 | steps:
129 |
130 | - task: PowerShell@2
131 | displayName: 'Install PowerShell Core'
132 | inputs:
133 | targetType: 'inline'
134 | script: |
135 | Write-Host "Installing PowerShell Core for Windows Server 2025..."
136 |
137 | # Download and install PowerShell Core
138 | $pwshUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi"
139 | $pwshInstaller = "$env:TEMP\PowerShell-7.4.6-win-x64.msi"
140 |
141 | Write-Host "Downloading PowerShell Core..."
142 | Invoke-WebRequest -Uri $pwshUrl -OutFile $pwshInstaller
143 |
144 | Write-Host "Installing PowerShell Core..."
145 | Start-Process -FilePath "msiexec.exe" -ArgumentList "/i", $pwshInstaller, "/quiet", "/norestart" -Wait
146 |
147 | Write-Host "PowerShell Core installation completed"
148 |
149 | # Verify installation by checking the installation path
150 | $pwshPath = "C:\Program Files\PowerShell\7\pwsh.exe"
151 | if (Test-Path $pwshPath) {
152 | Write-Host "✅ PowerShell Core is installed at: $pwshPath"
153 |
154 | # Test PowerShell Core version
155 | $version = & $pwshPath -v
156 | Write-Host "PowerShell Core version: $version"
157 |
158 | # Add to PATH for current session
159 | $env:PATH = "$env:PATH;C:\Program Files\PowerShell\7"
160 | Write-Host "Added PowerShell Core to PATH for current session"
161 |
162 | # Set PATH for subsequent tasks in the pipeline
163 | Write-Host "##vso[task.setvariable variable=PATH]$env:PATH"
164 | Write-Host "Set PATH variable for subsequent pipeline tasks"
165 | } else {
166 | Write-Host "❌ PowerShell Core installation failed - executable not found at $pwshPath"
167 | exit 1
168 | }
169 |
170 | - task: Pester@9
171 | inputs:
172 | scriptFolder: "$(Build.SourcesDirectory)/image/test/*"
173 | resultsFile: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
174 | usePSCore: True
175 | - task: PublishTestResults@2
176 | inputs:
177 | testResultsFormat: "NUnit"
178 | testResultsFiles: "$(Build.SourcesDirectory)/image/test/Test-Pester.XML"
179 | failTaskOnFailedTests: true
180 |
181 | - stage: Push
182 | dependsOn:
183 | - Versioning
184 | - Test
185 |
186 | jobs:
187 | - job: Push
188 | displayName: Verify pushed images
189 | variables:
190 | longTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.longTag']]
191 | shortTag: $[stageDependencies.Versioning.Tagging.outputs['Tags.shortTag']]
192 | namespace: $[stageDependencies.Versioning.Tagging.outputs['Tags.namespace']]
193 | steps:
194 |
195 | - task: PowerShell@2
196 | displayName: Verify pushed images
197 | inputs:
198 | targetType: 'inline'
199 | script: |
200 | $registry = "$(azureContainerRegistry)"
201 | $imageName = "${registry}/$(namespace)/sitecore-docker-tools-assets"
202 | $longTag = "$(longTag)"
203 | $shortTag = "$(shortTag)"
204 |
205 | Write-Host "Images have been pushed to ACR successfully!"
206 | Write-Host "Images are available at:"
207 | Write-Host "- ${imageName}:${longTag}"
208 | Write-Host "- ${imageName}:${shortTag}"
209 |
210 | Write-Host "You can verify the images in Azure Portal or using Azure CLI:"
211 | Write-Host "az acr repository show-tags --name $registry --repository $(namespace)/sitecore-docker-tools-assets"
--------------------------------------------------------------------------------
/image/src/scripts/Watch-Directory.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Sync files from source to destination path.
4 | .DESCRIPTION
5 | Watches source path for file changes and updates the destination path accordingly.
6 | .PARAMETER Path
7 | Path to watch for changes.
8 | .PARAMETER Destination
9 | Destination path to keep updated.
10 | .PARAMETER Sleep
11 | Milliseconds to sleep between sync operations.
12 | .PARAMETER Timeout
13 | Timeout for sync operation in milliseconds. Default is 0 (disabled).
14 | .PARAMETER DefaultExcludedFiles
15 | Default files to skip during sync. Default is "*.user", "*.cs", "*.csproj", "packages.config", "*ncrunch*", ".gitignore", ".gitkeep", ".dockerignore", "*.example", "*.disabled".
16 | .PARAMETER ExcludeFiles
17 | Additional files to skip during sync.
18 | .PARAMETER DefaultExcludedDirectories
19 | Default directories to skip during sync. Default is "obj", "Properties", "node_modules".
20 | .PARAMETER ExcludeDirectories
21 | Additional directories to skip during sync.
22 | .PARAMETER Executable
23 | The executable to start and restart after files are synced
24 | .EXAMPLE
25 | PS C:\> .\Watch-Directory.ps1 -Path 'C:\source' -Destination 'C:\destination' -ExcludeFiles @("web.config")
26 | .INPUTS
27 | None
28 | .OUTPUTS
29 | None
30 | #>
31 | [CmdletBinding(SupportsShouldProcess)]
32 | param(
33 | [Parameter(Mandatory = $true)]
34 | [ValidateScript( { Test-Path $_ -PathType 'Container' })]
35 | [string]$Path,
36 |
37 | [Parameter(Mandatory = $true)]
38 | [ValidateScript( { Test-Path $_ -PathType 'Container' })]
39 | [string]$Destination,
40 |
41 | [Parameter(Mandatory = $false)]
42 | [int]$Sleep = 200,
43 |
44 | [Parameter(Mandatory = $false)]
45 | [int]$Timeout = 0,
46 |
47 | [Parameter(Mandatory = $false)]
48 | [array]$DefaultExcludedFiles = @("*.user", "*.cs", "*.csproj", "packages.config", "*ncrunch*", ".gitignore", ".gitkeep", ".dockerignore", "*.example", "*.disabled"),
49 |
50 | [Parameter(Mandatory = $false)]
51 | [array]$ExcludeFiles = @(),
52 |
53 | [Parameter(Mandatory = $false)]
54 | [array]$DefaultExcludedDirectories = @("obj", "Properties", "node_modules"),
55 |
56 | [Parameter(Mandatory = $false)]
57 | [array]$ExcludeDirectories = @(),
58 |
59 | [Parameter(Mandatory = $false)]
60 | [string]$Executable = ""
61 | )
62 |
63 | # Setup
64 | $ErrorActionPreference = "Stop"
65 | $timeFormat = "HH:mm:ss:fff"
66 |
67 | # Setup exclude rules
68 | $fileRules = ($DefaultExcludedFiles + $ExcludeFiles) | Select-Object -Unique
69 | $directoryRules = ($DefaultExcludedDirectories + $ExcludeDirectories) | Select-Object -Unique
70 |
71 | Write-Information "$(Get-Date -Format $timeFormat): Excluding files: $($fileRules -join ", ")"
72 | Write-Information "$(Get-Date -Format $timeFormat): Excluding directories: $($directoryRules -join ", ")"
73 |
74 | # If -WhatIf was used, stop here
75 | if (!$PSCmdlet.ShouldProcess($Path, "Start file watchers")) {
76 | return
77 | }
78 |
79 | # Cleanup old event if present in current session
80 | Get-EventSubscriber -SourceIdentifier "FileDeleted" -ErrorAction "SilentlyContinue" | Unregister-Event
81 |
82 | # Setup delete watcher
83 | $watcher = New-Object System.IO.FileSystemWatcher
84 | $watcher.Path = $Path
85 | $watcher.IncludeSubdirectories = $true
86 | $watcher.EnableRaisingEvents = $true
87 |
88 | $watcherData = New-Object PSObject -Property @{
89 | Destination = $Destination
90 | ExcludeFiles = $fileRules
91 | TimeFormat = $timeFormat
92 | }
93 |
94 | Register-ObjectEvent $watcher Deleted -SourceIdentifier "FileDeleted" -MessageData $watcherData {
95 | $destinationPath = Join-Path $event.MessageData.Destination $eventArgs.Name
96 |
97 | if ((Test-Path $eventArgs.FullPath) -or # Present on source
98 | !(Test-Path $destinationPath) -or # Not present on destination
99 | (Test-Path -Path $destinationPath -PathType "Container") -or # Folder
100 | (($event.MessageData.ExcludeFiles | % { $eventArgs.Name -like $_ }) -contains $true)) # Excluded
101 | {
102 | return
103 | }
104 |
105 | $retries = 5
106 | while ($retries -gt 0)
107 | {
108 | try
109 | {
110 | Remove-Item -Path $destinationPath -Force -Recurse -ErrorAction Stop
111 | Write-Information "$(Get-Date -Format $event.MessageData.TimeFormat): Deleted '$destinationPath'..."
112 |
113 | $retries = -1
114 | }
115 | catch
116 | {
117 | $retries--
118 | Start-Sleep -Seconds 1
119 | }
120 | }
121 | if ($retries -eq 0)
122 | {
123 | Write-Error "$(Get-Date -Format $event.MessageData.TimeFormat): Could not delete '$destinationPath'..."
124 | }
125 | } | Out-Null
126 |
127 | function StartExecutable()
128 | {
129 | Write-Information "$(Get-Date -Format $timeFormat): Starting executable '$Executable'."
130 | $process = (Start-Process $Executable -PassThru)
131 | Write-Information "$(Get-Date -Format $timeFormat): Succesfully started process with Id $($process.Id)"
132 | return $process
133 | }
134 |
135 | function Sync
136 | {
137 | param(
138 | [Parameter(Mandatory = $true)]
139 | $Path,
140 | [Parameter(Mandatory = $true)]
141 | $Destination,
142 | [Parameter(Mandatory = $false)]
143 | $ExcludeFiles,
144 | [Parameter(Mandatory = $false)]
145 | $ExcludeDirectories,
146 | [Parameter(Mandatory = $false)]
147 | $ListOnly
148 | )
149 |
150 | $command = @("robocopy", "`"$Path`"", "`"$Destination`"", "/E", "/XX", "/MT:1", "/NJH", "/NJS", "/FP", "/NDL", "/NP", "/NS", "/R:5", "/W:1")
151 |
152 | if($ListOnly)
153 | {
154 | $command += "/L "
155 | }
156 |
157 | if ($ExcludeDirectories.Count -gt 0)
158 | {
159 | $command += "/XD "
160 |
161 | $ExcludeDirectories | ForEach-Object {
162 | $command += "`"$_`" "
163 | }
164 |
165 | $command = $command.TrimEnd()
166 | }
167 |
168 | if ($ExcludeFiles.Count -gt 0)
169 | {
170 | $command += "/XF "
171 |
172 | $ExcludeFiles | ForEach-Object {
173 | $command += "`"$_`" "
174 | }
175 |
176 | $command = $command.TrimEnd()
177 | }
178 |
179 | $commandString = $command -join " "
180 |
181 | $dirty = $false
182 | $raw = &([scriptblock]::create($commandString))
183 | $raw | ForEach-Object {
184 | $line = $_.Trim().Replace("`r`n", "").Replace("`t", " ")
185 | $dirty = ![string]::IsNullOrEmpty($line)
186 |
187 | if ($dirty -and $ListOnly -eq $false)
188 | {
189 | Write-Information "$(Get-Date -Format $timeFormat): $line"
190 | }
191 | }
192 |
193 | if ($dirty -and $ListOnly -eq $false)
194 | {
195 | Write-Information "$(Get-Date -Format $timeFormat): Done syncing..."
196 | }
197 |
198 | return $dirty
199 | }
200 |
201 | try
202 | {
203 | $process = $null
204 | if($Executable)
205 | {
206 | $process = StartExecutable
207 | }
208 |
209 | Write-Information "$(Get-Date -Format $timeFormat): Watching '$Path' for changes, will copy to '$Destination'..."
210 |
211 | # Main loop
212 | $timer = [System.Diagnostics.Stopwatch]::StartNew()
213 | while ($Timeout -eq 0 -or $timer.ElapsedMilliseconds -lt $Timeout)
214 | {
215 | $filesChanged = Sync -Path $Path -Destination $Destination -ExcludeFiles $fileRules -ExcludeDirectories $directoryRules -ListOnly $true
216 |
217 | if($filesChanged)
218 | {
219 | Write-Information "$(Get-Date -Format $timeFormat): File changes have been detected"
220 |
221 | if($process)
222 | {
223 | $process.Refresh()
224 | if($process.HasExited -eq $false)
225 | {
226 | Write-Information "$(Get-Date -Format $timeFormat): Waiting for process $($process.Id) to exit."
227 | Stop-Process $process
228 | $process.WaitForExit();
229 | }
230 | }
231 |
232 | Write-Information "$(Get-Date -Format $timeFormat): Going to sync file changes"
233 | $filesChanged = Sync -Path $Path -Destination $Destination -ExcludeFiles $fileRules -ExcludeDirectories $directoryRules -ListOnly $false
234 |
235 | if($process)
236 | {
237 | Write-Information "$(Get-Date -Format $timeFormat): File changes have been synced, restarting executable."
238 | $process = StartExecutable
239 | }
240 | }
241 |
242 | Start-Sleep -Milliseconds $Sleep
243 | }
244 | }
245 | finally
246 | {
247 | # Cleanup
248 | Get-EventSubscriber -SourceIdentifier "FileDeleted" | Unregister-Event
249 |
250 | if ($null -ne $watcher)
251 | {
252 | $watcher.Dispose()
253 | $watcher = $null
254 | }
255 |
256 | Write-Information "$(Get-Date -Format $timeFormat): Stopped."
257 | }
--------------------------------------------------------------------------------
/powershell/test/Public/Get-SitecoreSelfSignedCertificate.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Create-RSA key' {
5 | It 'Valid length provided' {
6 | { Create-RSAKey -KeyLength 4096 } | Should -Not -Throw
7 | }
8 |
9 | It 'Invalid key length' {
10 | { Create-RSAKey -KeyLength 10 } | Should -Throw
11 | }
12 |
13 | It 'No key length provided' {
14 | { Create-RSAKey } | Should -Not -Throw
15 | }
16 | }
17 |
18 | Describe 'New-AuthorityKeyIdentifier' {
19 | It 'Requires $SubjectKeyIdentifier' {
20 | $result = Test-ParamIsMandatory -Command "New-AuthorityKeyIdentifier" -Parameter "SubjectKeyIdentifier"
21 | $result | Should -Be $true
22 | }
23 |
24 | It 'Optional authority parameters not provided' {
25 | $key = Create-RSAKey -KeyLength 4096
26 | $subject = "CN=localhost"
27 | $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
28 | $subject,
29 | $key,
30 | [System.Security.Cryptography.HashAlgorithmName]::SHA256,
31 | [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
32 | $subjectKeyIdentifier = [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new(
33 | $certRequest.PublicKey,
34 | <# critical #> $false)
35 |
36 | { New-AuthorityKeyIdentifier -SubjectKeyIdentifier $subjectKeyIdentifier} | Should -Not -Throw
37 | }
38 | }
39 |
40 | Describe 'Create-SelfSignedCertificate' {
41 | It 'Requires $Key' {
42 | $result = Test-ParamIsMandatory -Command "Create-SelfSignedCertificate" -Parameter "Key"
43 | $result | Should -Be $true
44 | }
45 |
46 | It 'Optional parameters not provided' {
47 | $key = Create-RSAKey -KeyLength 4096
48 |
49 | { Create-SelfSignedCertificate -Key $key } | Should -Not -Throw
50 | }
51 |
52 | Context 'Create self signed with proper params' {
53 | BeforeAll {
54 | $key = Create-RSAKey -KeyLength 4096
55 | $startDate = [System.DateTimeOffset]::Now
56 | $endDate = [System.DateTimeOffset]::Now.AddDays(10)
57 |
58 | $result = Create-SelfSignedCertificate -Key $key -CommonName "Sitecore-test" -Country "UA" -Organization "Sitecore" -NotBefore $startDate -NotAfter $endDate
59 | }
60 |
61 | It 'Generate self signed certificate with private key' {
62 | $result.HasPrivateKey | Should -Be $true
63 | }
64 |
65 | It 'Generate self signed certificate with proper subject' {
66 | $result.Subject | Should -Match "CN=Sitecore-test, C=UA, O=Sitecore"
67 | }
68 |
69 | It 'Generate self signed certificate with correct start date' {
70 | $result.NotBefore.Date | Should -Match $startDate.Date
71 | }
72 |
73 | It 'Generate self signed certificate with correct end date' {
74 | $result.NotAfter.Date | Should -Match $endDate.Date
75 | }
76 | }
77 |
78 | Context "Internal methods called" {
79 | Mock New-AuthorityKeyIdentifier
80 |
81 | $key = Create-RSAKey -KeyLength 4096
82 | Create-SelfSignedCertificate -Key $key
83 |
84 | It 'Authority key identifier called' {
85 | Assert-MockCalled New-AuthorityKeyIdentifier -Times 1 -Exactly
86 | }
87 | }
88 | }
89 |
90 | Describe 'Create-SelfSignedCertificateWithSignature' {
91 | It 'Requires $Key for signed certificate' {
92 | $result = Test-ParamIsMandatory -Command "Create-SelfSignedCertificateWithSignature" -Parameter "Key"
93 | $result | Should -Be $true
94 | }
95 |
96 | It 'Requires $RootCertificate for signed certificate' {
97 | $result = Test-ParamIsMandatory -Command "Create-SelfSignedCertificateWithSignature" -Parameter "RootCertificate"
98 | $result | Should -Be $true
99 | }
100 |
101 | It 'Optional parameters not provided for signed certificate' {
102 | $rootKey = Create-RSAKey -KeyLength 4096
103 | $rootCert = Create-SelfSignedCertificate -Key $rootKey
104 | $signedKey = Create-RSAKey -KeyLength 2048
105 |
106 | { Create-SelfSignedCertificateWithSignature -Key $signedKey -RootCertificate $rootCert } | Should -Not -Throw
107 | }
108 |
109 | Context "Create certificate with singature" {
110 | BeforeAll {
111 | $rootKey = Create-RSAKey -KeyLength 4096
112 | $rootCert = Create-SelfSignedCertificate -Key $rootKey
113 | $signedKey = Create-RSAKey -KeyLength 2048
114 | $startDate = [System.DateTimeOffset]::Now
115 | $endDate = [System.DateTimeOffset]::Now.AddDays(10)
116 |
117 | $result = Create-SelfSignedCertificateWithSignature -Key $signedKey -RootCertificate $rootCert -CommonName "Sitecore-signed" -Country "UA" -Organization "Sitecore" -NotBefore $startDate -NotAfter $endDate
118 | }
119 |
120 | It 'Generate self signed certificate with signature and private key' {
121 | $result.HasPrivateKey | Should -Be $false
122 | }
123 |
124 | It 'Generate self signed certificate with signature and proper subject' {
125 | $result.Subject | Should -Match "CN=Sitecore-signed, C=UA, O=Sitecore"
126 | }
127 |
128 | It 'Generate self signed certificate with signature and correct start date' {
129 | $result.NotBefore.Date | Should -Match $startDate.Date
130 | }
131 |
132 | It 'Generate self signed certificate with signature and correct end date' {
133 | $result.NotAfter.Date | Should -Match $endDate.Date
134 | }
135 | }
136 | }
137 |
138 | Describe 'Create-CertificateFile' {
139 | It 'Requires $Certificate' {
140 | $result = Test-ParamIsMandatory -Command "Create-CertificateFile" -Parameter "Certificate"
141 | $result | Should -Be $true
142 | }
143 |
144 | It 'Certificate file should not be empty' {
145 | $outCertPath = "$TestDrive\localhost.crt"
146 | $key = Create-RSAKey -KeyLength 4096
147 | $certificate = Create-SelfSignedCertificate -Key $key
148 | Create-CertificateFile -Certificate $certificate -OutCertPath $outCertPath
149 |
150 | (Get-Item $outCertPath).Length | Should -Not -BeNullOrEmpty
151 | }
152 |
153 | Context "Certificate file created properly" {
154 | $outCertPath = "$TestDrive\localhost.crt"
155 | $key = Create-RSAKey -KeyLength 4096
156 | $certificate = Create-SelfSignedCertificate -Key $key
157 | Create-CertificateFile -Certificate $certificate -OutCertPath $outCertPath
158 |
159 | It 'Certificate file should not be empty' {
160 | (Get-Item $outCertPath).Length | Should -Not -BeNullOrEmpty
161 | }
162 |
163 | It 'Certificate file should contain entry message' {
164 | Get-Content $outCertPath | Should -Contain "-----BEGIN CERTIFICATE-----"
165 | }
166 |
167 | It 'Certificate file should contain end message' {
168 | Get-Content $outCertPath | Should -Contain "-----END CERTIFICATE-----"
169 | }
170 | }
171 |
172 | Context "Write host called for certificate file" {
173 | Mock Write-Host
174 |
175 | $key = Create-RSAKey -KeyLength 4096
176 | $certificate = Create-SelfSignedCertificate -Key $key
177 | Create-CertificateFile -Certificate $certificate -OutCertPath $TestDrive\localhost.crt
178 |
179 | It 'Write-Host called' {
180 | Assert-MockCalled Write-Host -Times 1 -Exactly
181 | }
182 | }
183 | }
184 |
185 | Describe 'Create-KeyFile' {
186 | It 'Requires $Key' {
187 | $result = Test-ParamIsMandatory -Command "Create-KeyFile" -Parameter "Key"
188 | $result | Should -Be $true
189 | }
190 | Context "Key file created properly" {
191 | $outKeyPath = "$TestDrive\localhost.key"
192 | $key = Create-RSAKey -KeyLength 4096
193 | Create-KeyFile -Key $key -OutKeyPath $outKeyPath
194 |
195 | It 'Key file should not be empty' {
196 | (Get-Item $outKeyPath).Length | Should -Not -BeNullOrEmpty
197 | }
198 |
199 | It 'Key file should contain entry message' {
200 | (Get-Content $outKeyPath) | Should -Contain "-----BEGIN PRIVATE KEY-----"
201 | }
202 |
203 | It 'Key file should contain end message' {
204 | (Get-Content $outKeyPath) | Should -Contain "-----END PRIVATE KEY-----"
205 | }
206 | }
207 |
208 | Context "Write host called for key file" {
209 | Mock Write-Host
210 |
211 | $key = Create-RSAKey -KeyLength 4096
212 | Create-KeyFile -Key $key -OutKeyPath $TestDrive\localhost.key
213 |
214 | It 'Write-Host called' {
215 | Assert-MockCalled Write-Host -Times 1 -Exactly
216 | }
217 | }
218 | }
219 | }
--------------------------------------------------------------------------------
/powershell/src/Private/RSAKeyTool.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNPOSIS
3 | Convert a PrivateKey from the certificate store into a PKCS8 formatted file.
4 | #>
5 |
6 | class RSAKeyUtils
7 | {
8 | static [byte[]] PrivateKeyToPKCS8([System.Security.Cryptography.RSAParameters]$privateKey)
9 | {
10 | [AsnType]$n = [RSAKeyUtils]::CreateIntegerPos($privateKey.Modulus)
11 | [AsnType]$e = [RSAKeyUtils]::CreateIntegerPos($privateKey.Exponent)
12 | [AsnType]$d = [RSAKeyUtils]::CreateIntegerPos($privateKey.D)
13 | [AsnType]$p = [RSAKeyUtils]::CreateIntegerPos($privateKey.P)
14 | [AsnType]$q = [RSAKeyUtils]::CreateIntegerPos($privateKey.Q)
15 | [AsnType]$dp = [RSAKeyUtils]::CreateIntegerPos($privateKey.DP)
16 | [AsnType]$dq = [RSAKeyUtils]::CreateIntegerPos($privateKey.DQ)
17 | [AsnType]$iq = [RSAKeyUtils]::CreateIntegerPos($privateKey.InverseQ)
18 | [AsnType]$version = [RSAKeyUtils]::CreateInteger(@(0))
19 | [AsnType]$key = [RSAKeyUtils]::CreateOctetString([RSAKeyUtils]::CreateSequence(@($version,$n,$e,$d,$p,$q,$dp,$dq,$iq)))
20 | [AsnType]$algorithmID = [RSAKeyUtils]::CreateSequence(@([RSAKeyUtils]::CreateOid("1.2.840.113549.1.1.1"),[RSAKeyUtils]::CreateNull()))
21 | [AsnType]$privateKeyInfo = [RSAKeyUtils]::CreateSequence(@($version,$algorithmID,$key))
22 | return (New-Object -TypeName AsnMessage -ArgumentList $privateKeyInfo.GetBytes(),"PKCS#8").GetBytes()
23 | }
24 |
25 | static [AsnType] CreateOctetString([AsnType]$value)
26 | {
27 | if ([RSAKeyUtils]::IsEmpty($value))
28 | {
29 | return (New-Object -TypeName AsnType -ArgumentList ([byte]0x04,[byte[]]@(0x00)))
30 | }
31 | return (New-Object -TypeName AsnType -ArgumentList ([byte]0x04,[byte[]]@($value.GetBytes())))
32 | }
33 |
34 | static [bool] IsEmpty([byte[]]$octets)
35 | {
36 | if ($null -eq $octets -or 0 -eq $octets.Length)
37 | {
38 | return $true
39 | }
40 | return $false
41 | }
42 |
43 | static [bool] IsEmpty([String]$s)
44 | {
45 | if ($null -eq $s -or 0 -eq $s.Length)
46 | {
47 | return $true
48 | }
49 | return $false
50 | }
51 |
52 | static [bool] IsEmpty([String[]]$strings)
53 | {
54 | if ($null -eq $strings -or 0 -eq $strings.Length)
55 | {
56 | return $true
57 | }
58 | return $false
59 | }
60 |
61 | static [bool] IsEmpty([AsnType]$value)
62 | {
63 | if ($null -eq $value)
64 | {
65 | return $true
66 | }
67 | return $false
68 | }
69 |
70 | static [bool] IsEmpty([AsnType[]]$values)
71 | {
72 | if ($null -eq $values -or 0 -eq $values.Length)
73 | {
74 | return $true
75 | }
76 | return $false
77 | }
78 |
79 | static [bool] IsEmpty([byte[][]]$arrays)
80 | {
81 | if ($null -eq $arrays -or 0 -eq $arrays.Length)
82 | {
83 | return $true
84 | }
85 | return $false
86 | }
87 |
88 | static [AsnType] CreateInteger([byte[]]$value)
89 | {
90 | if ([RSAKeyUtils]::IsEmpty($value))
91 | {
92 | $zero = [byte[]]::CreateInstance([byte], 1)
93 | $zero[0] = 0
94 | return [RSAKeyUtils]::CreateInteger($zero)
95 | }
96 | return (New-Object -TypeName AsnType -ArgumentList ([byte]0x02,[byte[]]$value))
97 | }
98 |
99 | static [AsnType] CreateNull()
100 | {
101 | return (New-Object -TypeName AsnType -ArgumentList ([byte]0x05,[byte[]]@(0x00)))
102 | }
103 |
104 | static [byte[]] Duplicate([byte[]]$b)
105 | {
106 | if ([RSAKeyUtils]::IsEmpty($b))
107 | {
108 | $empty = [byte[]]::CreateInstance([byte], 0)
109 | return $empty
110 | }
111 | [byte[]]$d = [byte[]]::CreateInstance([byte], $b.Length)
112 | [Array]::Copy($b,$d,$b.Length)
113 | return $d
114 | }
115 |
116 | static [AsnType] CreateIntegerPos([byte[]]$value)
117 | {
118 | [byte[]]$i = $null
119 | $d = [RSAKeyUtils]::Duplicate($value)
120 | if ([RSAKeyUtils]::IsEmpty($d))
121 | {
122 | $zero = [byte[]]::CreateInstance([byte], 1)
123 | $zero[0] = 0
124 | $d = $zero
125 | }
126 | if ($d.Length -gt 0 -and $d[0] -gt 0x7F)
127 | {
128 | $i = [byte[]]::CreateInstance([byte], $d.Length + 1)
129 | $i[0] = 0x00
130 | [Array]::Copy($d,0,$i,1,$value.Length)
131 | }
132 | else
133 | {
134 | $i = $d
135 | }
136 | return [RSAKeyUtils]::CreateInteger($i)
137 | }
138 |
139 | static [byte[]] Concatenate([AsnType[]]$values)
140 | {
141 | if ([RSAKeyUtils]::IsEmpty($values))
142 | {
143 | return [byte[]]::CreateInstance([byte], 0)
144 | }
145 | [int]$length = 0
146 | foreach ($t in $values)
147 | {
148 | if ($null -ne $t)
149 | {
150 | $length += $t.GetBytes().Length
151 | }
152 | }
153 | [byte[]]$cated = [byte[]]::CreateInstance([byte], $length)
154 | [int]$current = 0
155 | foreach ($t in $values)
156 | {
157 | if ($null -ne $t)
158 | {
159 | [byte[]]$b = $t.GetBytes()
160 | [Array]::Copy($b,0,$cated,$current,$b.Length)
161 | $current += $b.Length
162 | }
163 | }
164 | return $cated
165 | }
166 |
167 | static [AsnType] CreateSequence([AsnType[]]$values)
168 | {
169 | if ([RSAKeyUtils]::IsEmpty($values))
170 | {
171 | throw (New-Object -TypeName ArgumentException -ArgumentList "A sequence requires at least one value.")
172 | }
173 | [byte[]]$octets = [byte[]][RSAKeyUtils]::Concatenate($values)
174 | return (New-Object -TypeName AsnType -ArgumentList ([byte](0x10 -bor 0x20)), ([byte[]]@($octets)))
175 | }
176 |
177 | static [AsnType] CreateOid([String]$value)
178 | {
179 | if ([RSAKeyUtils]::IsEmpty($value))
180 | {
181 | return $null
182 | }
183 | [String[]]$tokens = $value.Split(@(' ','.'))
184 | if ([RSAKeyUtils]::IsEmpty($tokens))
185 | {
186 | return $null
187 | }
188 | [UInt64]$a = 0
189 | [System.Collections.Generic.List[UInt64]]$arcs = (New-Object -TypeName System.Collections.Generic.List[UInt64])
190 | foreach ($t in $tokens)
191 | {
192 | if ($t.Length -eq 0)
193 | {
194 | break
195 | }
196 | try
197 | {
198 | $a = [Convert]::ToUInt64($t,[CultureInfo]::InvariantCulture)
199 | }
200 | catch [FormatException]
201 | {
202 | break
203 | }
204 | catch [OverflowException]
205 | {
206 | break
207 | }
208 | $arcs.Add($a) > $null
209 | }
210 | if (0 -eq $arcs.Count)
211 | {
212 | return $null
213 | }
214 | [System.Collections.Generic.List[byte]]$octets = (New-Object -TypeName System.Collections.Generic.List[byte])
215 | if ($arcs.Count -ge 1)
216 | {
217 | $a = $arcs[0] * 40
218 | }
219 | if ($arcs.Count -ge 2)
220 | {
221 | $a += $arcs[1]
222 | }
223 | $octets.Add([byte]($a))
224 | for([int]$i = 2; $i -lt $arcs.Count; $i++)
225 | {
226 | [System.Collections.Generic.List[byte]]$temp = (New-Object -TypeName System.Collections.Generic.List[byte])
227 | [UInt64]$arc = $arcs[$i]
228 | do {
229 | $temp.Add(([byte]0x80 -bor ($arc -band 0x7F))) > $null
230 | $arc = $arc -shr 7
231 | } while (0 -ne $arc)
232 | [byte[]]$t = $temp.ToArray()
233 | $t[0] = [byte](0x7F -band $t[0])
234 | [Array]::Reverse($t)
235 | foreach ($b in $t)
236 | {
237 | $octets.Add($b) > $null
238 | }
239 | }
240 | return [RSAKeyUtils]::CreateOid($octets.ToArray())
241 | }
242 |
243 | static [AsnType] CreateOid([byte[]]$value)
244 | {
245 | if ([RSAKeyUtils]::IsEmpty($value))
246 | {
247 | return $null
248 | }
249 | return (New-Object -TypeName AsnType -ArgumentList 0x06,$value)
250 | }
251 | }
252 |
253 | class AsnMessage
254 | {
255 | [byte[]] hidden $Octets
256 | [String] hidden $m_format
257 | [int] $Length
258 |
259 | AsnMessage ([byte[]]$octets,[String]$format)
260 | {
261 | $this.Octets = $octets
262 | $this.m_format = $format
263 | }
264 |
265 | [byte[]] GetBytes()
266 | {
267 | if ($null -eq $this.Octets)
268 | {
269 | return [byte[]]::CreateInstance([byte], 0)
270 | }
271 | return $this.Octets
272 | }
273 |
274 | [String] GetFormat()
275 | {
276 | return $this.m_format
277 | }
278 | }
279 |
280 | class AsnType
281 | {
282 | AsnType ([byte]$tag,[byte[]]$octets)
283 | {
284 | $this.Tag = @($tag)
285 | $this.Octets = $octets
286 | $this.Length = [byte[]]::CreateInstance([byte], 0)
287 | }
288 |
289 | [byte[]] $Tag
290 | [byte[]] $Length
291 | [byte[]] $Octets
292 |
293 | [byte[]] GetBytes()
294 | {
295 | $this.SetLength()
296 | if (0x05 -eq $this.Tag[0])
297 | {
298 | return $this.Concatenate([byte[][]]@($this.Tag,$this.Octets))
299 | }
300 | $val = [byte[][]]@($this.Tag,$this.Length,$this.Octets)
301 | return $this.Concatenate($val)
302 | }
303 |
304 | [void] SetLength()
305 | {
306 | if ($null -eq $this.Octets)
307 | {
308 | $zero = [byte[]]::CreateInstance([byte], 1)
309 | $zero[0] = 0
310 | $this.Length = $zero
311 | return
312 | }
313 | if (0x05 -eq $this.Tag[0])
314 | {
315 | $empty = [byte[]]::CreateInstance([byte], 0)
316 | $this.Length = $empty
317 | return
318 | }
319 | [byte[]]$len = $null
320 | if ($this.Octets.Length -lt 0x80)
321 | {
322 | $len= [byte[]]::CreateInstance([byte], 1)
323 | $len[0] = [byte]$this.Octets.Length
324 | }
325 | elseif ($this.Octets.Length -le 0xFF)
326 | {
327 | $len = [byte[]]::CreateInstance([byte], 2)
328 | $len[0] = 0x81
329 | $len[1] = [byte](($this.Octets.Length -band 0xFF))
330 | }
331 | elseif ($this.Octets.Length -le 0xFFFF)
332 | {
333 | $len = [byte[]]::CreateInstance([byte], 3)
334 | $len[0] = 0x82
335 | $len[1] = [byte](($this.Octets.Length -band 0xFF00) -shr 8)
336 | $len[2] = [byte](($this.Octets.Length -band 0xFF))
337 | }
338 | elseif ($this.Octets.Length -le 0xFFFFFF)
339 | {
340 | $len = [byte[]]::CreateInstance([byte], 4)
341 | $len[0] = 0x83
342 | $len[1] = [byte](($this.Octets.Length -band 0xFF0000) -shr 16)
343 | $len[2] = [byte](($this.Octets.Length -band 0xFF00) -shr 8)
344 | $len[3] = [byte](($this.Octets.Length -band 0xFF))
345 | }
346 | else
347 | {
348 | $len = [byte[]]::CreateInstance([byte], 5)
349 | $len[0] = 0x84
350 | $len[1] = [byte](($this.Octets.Length -band 0xFF000000) -shr 24)
351 | $len[2] = [byte](($this.Octets.Length -band 0xFF0000) -shr 16)
352 | $len[3] = [byte](($this.Octets.Length -band 0xFF00) -shr 8)
353 | $len[4] = [byte](($this.Octets.Length -band 0xFF))
354 | }
355 | $this.Length = $len
356 | }
357 |
358 | [byte[]] Concatenate([byte[][]]$values)
359 | {
360 | if ([RSAKeyUtils]::IsEmpty($values))
361 | {
362 | return [byte[]]
363 | }
364 | [int]$len = 0
365 | foreach ($b in $values)
366 | {
367 | if ($null -ne $b)
368 | {
369 | $len += $b.Length
370 | }
371 | }
372 | [byte[]]$cated = [byte[]]::CreateInstance([byte], $len)
373 | [int]$current = 0
374 | foreach ($b in $values)
375 | {
376 | if ($null -ne $b)
377 | {
378 | [Array]::Copy($b,0,$cated,$current,$b.Length)
379 | $current += $b.Length
380 | }
381 | }
382 | return $cated
383 | }
384 | }
--------------------------------------------------------------------------------
/powershell/test/Private/RSAKeyTool.Tests.ps1:
--------------------------------------------------------------------------------
1 | . $PSScriptRoot\..\TestRunner.ps1 {
2 | . $PSScriptRoot\..\TestUtils.ps1
3 |
4 | Describe 'Private key to PKCS8' {
5 | It 'Throws empty parameters' {
6 | { PrivateKeyToPKCS8($null) } | Should -Throw
7 | }
8 |
9 | It 'Returns non empty byte array' {
10 | $key = Create-RSAKey -KeyLength 4096
11 | $parameters = $key.ExportParameters($true)
12 | $rawKey = [RSAKeyUtils]::PrivateKeyToPKCS8($parameters)
13 |
14 | $rawKey.Length | Should -BeGreaterThan 0
15 | }
16 | }
17 |
18 | Describe 'CreateOctetString' {
19 | It 'Non empty asn value returns sequence' {
20 | $param = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
21 | $result = [RSAKeyUtils]::CreateOctetString($param)
22 |
23 | $result.GetBytes() | Should -Be @(4, 4, 1, 2, 4, 5)
24 | }
25 |
26 | It 'Non empty asn value returns proper length' {
27 | $param = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
28 | $result = [RSAKeyUtils]::CreateOctetString($param)
29 |
30 | $result.GetBytes().Length | Should -Be 6
31 | }
32 | }
33 |
34 | Describe 'IsEmpty byte' {
35 | It 'Returns true if null byte array' {
36 | { [RSAKeyUtils]::IsEmpty([byte[]]($null)) } | Should -BeTrue
37 | }
38 |
39 | It 'Returns true if empty byte array' {
40 | { [RSAKeyUtils]::IsEmpty([byte[]](@())) } | Should -BeTrue
41 | }
42 |
43 | It 'Returns false if non empty byte array' {
44 | [RSAKeyUtils]::IsEmpty([byte[]](@(0x04, 0x05))) | Should -BeFalse
45 | }
46 | }
47 |
48 | Describe 'IsEmpty string array' {
49 | It 'Returns true if null string array' {
50 | { [RSAKeyUtils]::IsEmpty([string[]]($null)) } | Should -BeTrue
51 | }
52 |
53 | It 'Returns true if empty string array' {
54 | { [RSAKeyUtils]::IsEmpty([string[]](@())) } | Should -BeTrue
55 | }
56 |
57 | It 'Returns false if non empty string array' {
58 | [RSAKeyUtils]::IsEmpty([string](@("hello", "test"))) | Should -BeFalse
59 | }
60 | }
61 |
62 | Describe 'IsEmpty asn type' {
63 | It 'Returns true if null asn type' {
64 | { [RSAKeyUtils]::IsEmpty([asnType]($null)) } | Should -BeTrue
65 | }
66 |
67 | It 'Returns false if non empty asn type' {
68 | $param = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
69 | [RSAKeyUtils]::IsEmpty($param) | Should -BeFalse
70 | }
71 | }
72 |
73 | Describe 'IsEmpty asn type array' {
74 | It 'Returns true if null asn type array' {
75 | { [RSAKeyUtils]::IsEmpty([AsnType[]]($null)) } | Should -BeTrue
76 | }
77 |
78 | It 'Returns true if empty asn type array' {
79 | { [RSAKeyUtils]::IsEmpty([AsnType[]](@())) } | Should -BeTrue
80 | }
81 |
82 | It 'Returns false if non empty asn type array' {
83 | $param1 = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
84 | $param2 = [AsnType]::new([byte]0x012, [byte[]](@(0x06, 0x07)))
85 | [RSAKeyUtils]::IsEmpty([AsnType[]](@($param1, $param2))) | Should -BeFalse
86 | }
87 | }
88 |
89 | Describe 'CreateInteger' {
90 | It 'Normal parameters returns non empty value' {
91 | [RSAKeyUtils]::CreateInteger([byte[]](@(0x04, 0x05))) | Should -Not -BeNullOrEmpty
92 | }
93 |
94 | It 'Pass null returns non empty value' {
95 | [RSAKeyUtils]::CreateInteger([byte[]]@()) | Should -Not -BeNullOrEmpty
96 | }
97 |
98 | It 'First value is 2' {
99 | $result = [RSAKeyUtils]::CreateInteger([byte[]]@())
100 |
101 | $result.GetBytes()[0] | Should -Be 2
102 | }
103 | }
104 |
105 | Describe 'CreateNull' {
106 | It 'First byte is 5' {
107 | $result = [RSAKeyUtils]::CreateNull()
108 |
109 | $result.GetBytes()[0] | Should -Be 5
110 | }
111 |
112 | It 'Second byte is 0' {
113 | $result = [RSAKeyUtils]::CreateNull()
114 |
115 | $result.GetBytes()[1] | Should -Be 0
116 | }
117 | }
118 |
119 | Describe 'Duplicate' {
120 | It 'Duplicates array' {
121 | [byte[]]$array = @(0x04, 0x05)
122 | $result = [RSAKeyUtils]::Duplicate($array)
123 |
124 | $result | Should -Be $array
125 | }
126 |
127 | It 'Empty array should not throw' {
128 | { [RSAKeyUtils]::Duplicate(@()) } | Should -Not -Throw
129 | }
130 |
131 | It 'Duplicates empty array' {
132 | $result = [RSAKeyUtils]::Duplicate(@())
133 |
134 | $result | Should -BeNullOrEmpty
135 | }
136 | }
137 |
138 | Describe 'CreateIntegerPos' {
139 | It 'Empty array should not throw' {
140 | { [RSAKeyUtils]::CreateIntegerPos([byte[]]@()) } | Should -Not -Throw
141 | }
142 |
143 | It 'Empty array returns sequence' {
144 | $result = [RSAKeyUtils]::CreateIntegerPos([byte[]]@())
145 |
146 | $result.GetBytes() | Should -Be @(2, 1, 0)
147 | }
148 |
149 | It 'Empty array returns non empty' {
150 | $result = [RSAKeyUtils]::CreateIntegerPos([byte[]]@())
151 |
152 | $result.GetBytes().Length | Should -Be 3
153 | }
154 |
155 | It 'Non empty array returns sequence' {
156 | [byte[]]$array = @(0x04, 0x05)
157 |
158 | $result = [RSAKeyUtils]::CreateIntegerPos($array)
159 |
160 | $result.GetBytes() | Should -Be @(2, 2, 4, 5)
161 | }
162 |
163 | It 'Non empty array returns fixed size' {
164 | [byte[]]$array = @(0x04, 0x05)
165 |
166 | $result = [RSAKeyUtils]::CreateIntegerPos($array)
167 |
168 | $result.GetBytes().Length | Should -Be 4
169 | }
170 | }
171 |
172 | Describe 'Concatenate' {
173 | It 'Empty asn type array should not throw' {
174 | { [RSAKeyUtils]::Concatenate([AsnType[]]@()) } | Should -Not -Throw
175 | }
176 |
177 | It 'Empty asn type array returns null' {
178 | $result = [RSAKeyUtils]::Concatenate([AsnType[]]@())
179 |
180 | $result| Should -Be $null
181 | }
182 |
183 | It 'Empty asn type array returns empty' {
184 | $result = [RSAKeyUtils]::Concatenate([AsnType[]]@())
185 |
186 | $result.Length | Should -Be 0
187 | }
188 |
189 | It 'Non empty asn type array returns concatenated' {
190 | $param1 = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
191 | $param2 = [AsnType]::new([byte]0x012, [byte[]](@(0x06, 0x07)))
192 |
193 | $result = [RSAKeyUtils]::Concatenate([AsnType[]]@($param1, $param2))
194 |
195 | $result.Length | Should -Be 8
196 | }
197 |
198 | It 'Non empty asn type array returns sequence' {
199 | $param1 = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
200 | $param2 = [AsnType]::new([byte]0x03, [byte[]](@(0x06, 0x07)))
201 |
202 | $result = [RSAKeyUtils]::Concatenate([AsnType[]]@($param1, $param2))
203 |
204 | $result | Should -Be @(1, 2, 4, 5, 3, 2, 6, 7)
205 | }
206 | }
207 |
208 | Describe 'CreateSequence' {
209 | It 'Empty asn type array should throw' {
210 | { [RSAKeyUtils]::CreateSequence([AsnType[]]@()) } | Should -Throw
211 | }
212 |
213 | It 'Non empty asn type array returns fixed size' {
214 | $param1 = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
215 | $param2 = [AsnType]::new([byte]0x012, [byte[]](@(0x06, 0x07)))
216 |
217 | $result = [RSAKeyUtils]::CreateSequence([AsnType[]]@($param1, $param2))
218 |
219 | $result.GetBytes().Length | Should -Be 10
220 | }
221 |
222 | It 'Non empty asn type array returns sequence' {
223 | $param1 = [AsnType]::new([byte]0x01, [byte[]](@(0x04, 0x05)))
224 | $param2 = [AsnType]::new([byte]0x03, [byte[]](@(0x06, 0x07)))
225 |
226 | $result = [RSAKeyUtils]::CreateSequence([AsnType[]]@($param1, $param2))
227 |
228 | $result.GetBytes() | Should -Be @(48, 8, 1, 2, 4, 5, 3, 2, 6, 7)
229 | }
230 | }
231 |
232 | Describe 'CreateOid' {
233 | It 'Returns null if empty string' {
234 | $result = [RSAKeyUtils]::CreateOid("")
235 | $result | Should -BeNullOrEmpty
236 | }
237 |
238 | It 'Returns null if only value' {
239 | $result = [RSAKeyUtils]::CreateOid("hello")
240 | $result | Should -BeNullOrEmpty
241 | }
242 |
243 | It 'Returns calculated sequence for specific string' {
244 | $result = [RSAKeyUtils]::CreateOid("1 2 3")
245 | $result.GetBytes() | Should -Be @(6, 2, 42, 3)
246 | }
247 |
248 | It 'Returns proper length for specific string' {
249 | $result = [RSAKeyUtils]::CreateOid("1 2 3")
250 | $result.GetBytes().Length | Should -Be 4
251 | }
252 | }
253 |
254 | Describe 'CreateOid byte array' {
255 | It 'Empty byte array should not throw' {
256 | { [RSAKeyUtils]::CreateOid([byte[]]@()) } | Should -Not -Throw
257 | }
258 |
259 | It 'Returns null if empty byte array' {
260 | $result = [RSAKeyUtils]::CreateOid([byte[]]@())
261 | $result | Should -BeNullOrEmpty
262 | }
263 |
264 | It 'Byte array returns sequence' {
265 | [byte[]]$array = @(0x04, 0x05)
266 | $result = [RSAKeyUtils]::CreateOid($array)
267 | $result.GetBytes() | Should -Be @(6, 2, 4, 5)
268 | }
269 |
270 | It 'Byte array returns proper length' {
271 | [byte[]]$array = @(0x04, 0x05)
272 | $result = [RSAKeyUtils]::CreateOid($array)
273 |
274 | $result.GetBytes().Length | Should -Be 4
275 | }
276 | }
277 |
278 | Describe 'GetBytes' {
279 | It 'Null octets' {
280 | $asn = [AsnMessage]::new($null, "hello")
281 |
282 | $asn.GetBytes() | Should -Be $null
283 | }
284 |
285 | It 'Not empty octets' {
286 | [byte[]]$array = @(0x04, 0x05)
287 | $asn = [AsnMessage]::new($array, "hello")
288 |
289 | $asn.GetBytes() | Should -Be $array
290 | }
291 | }
292 |
293 | Describe 'GetFormat' {
294 | It 'Empty string' {
295 | [byte[]]$array = @(0x04, 0x05)
296 | $asn = [AsnMessage]::new($array, "")
297 |
298 | $asn.GetFormat() | Should -Be ""
299 | }
300 |
301 | It 'Not empty string' {
302 | [byte[]]$array = @(0x04, 0x05)
303 | $asn = [AsnMessage]::new($array, "hello")
304 |
305 | $asn.GetFormat() | Should -Be "hello"
306 | }
307 | }
308 | }
--------------------------------------------------------------------------------