initial commit
parent
d5423e71f8
commit
564902483f
|
@ -0,0 +1,181 @@
|
|||
|
||||
enum Status {
|
||||
NotRun
|
||||
Pass
|
||||
Fail
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Log {
|
||||
|
||||
# We originally used [Status] values as keys instead of casting [Status] values to strings.
|
||||
# We cast values to strings for now due to a bug in PSScriptAnalyzer v1.16.1
|
||||
static [hashtable] $Symbols = @{
|
||||
Pass = [char]8730
|
||||
Fail = "X"
|
||||
NotRun = " "
|
||||
}
|
||||
static [hashtable] $Colors = @{
|
||||
Pass = "Green"
|
||||
NotRun = "Yellow"
|
||||
Fail = "Red"
|
||||
}
|
||||
static [int] $LastLineLength
|
||||
|
||||
static [void] WriteLine([string] $message, [Status] $status) {
|
||||
$message = [Log]::FormatMessage($message, $status)
|
||||
[Log]::LastLineLength = $message.Length
|
||||
$color = [Log]::Colors[ [string]$status ]
|
||||
Write-Host $message -ForegroundColor $color -NoNewline
|
||||
}
|
||||
|
||||
static [void] OverwriteLine([string] $message, [Status] $status) {
|
||||
Write-Host "`r$(' ' * [Log]::LastLineLength)" -NoNewline
|
||||
$message = [Log]::FormatMessage($message, $status)
|
||||
[Log]::LastLineLength = $message.Length
|
||||
$color = [Log]::Colors[ [string]$status ]
|
||||
Write-Host "`r$message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
static [string] FormatMessage([string] $message, [Status] $status) {
|
||||
$symbol = [Log]::Symbols[ [string]$status ]
|
||||
return "$(Get-Date -Format 'hh:mm:ss') [ $symbol ] $message"
|
||||
}
|
||||
|
||||
static [void] WriteError([string] $message) {
|
||||
Write-Host "`n$message`n" -ForegroundColor Red
|
||||
exit -1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensures a requirement is met.
|
||||
|
||||
.DESCRIPTION
|
||||
This cmdlet allows for declaratively defining requirements and implementing consistent logging and idempotency around the status of the requirements.
|
||||
|
||||
.PARAMETER Describe
|
||||
A description of the requirement that is enforced.
|
||||
|
||||
.PARAMETER Test
|
||||
If present, 'Test' is a scriptblock that returns 'true' if the requirement is already met and the 'Set' scriptblock should not run. If not present, 'Set' will always run.
|
||||
|
||||
.PARAMETER Set
|
||||
A scriptblock that imposes the requirement when run. If a "Test' scriptblock is not provided, 'Set' must be idempotent.
|
||||
|
||||
.PARAMETER Message
|
||||
An error message printed if an idempotent 'Set' scriptblock fails during execution.
|
||||
|
||||
.EXAMPLE
|
||||
# A non-idempotent 'Set' scriptblock
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "'Hello world' is logged" `
|
||||
-Test {Get-Content $MyLogFilePath | ? {$_ -eq "Hello world"}} `
|
||||
-Set {"Hello world" >> $MyLogFilePath}
|
||||
|
||||
# An idempotent 'Set' scriptblock
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe "'Hello world' is logged" `
|
||||
-Set {"Hello world" > $MyLogFilePath} `
|
||||
-Message "Could not log 'Hello World'"
|
||||
#>
|
||||
function Invoke-ChecklistRequirement {
|
||||
Param(
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[Parameter(Mandatory, ParameterSetName = "Information")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Describe,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[scriptblock] $Test,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyIfNeeded")]
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[scriptblock] $Set,
|
||||
[Parameter(Mandatory, ParameterSetName = "ApplyAlways")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Message,
|
||||
[switch] $ListRequirement
|
||||
)
|
||||
|
||||
|
||||
try {
|
||||
|
||||
if ($ListRequirement) {
|
||||
[Log]::WriteLine("$Describe`n", [Status]::NotRun)
|
||||
return
|
||||
}
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
|
||||
"ApplyIfNeeded" {
|
||||
[Log]::WriteLine($Describe, [Status]::NotRun)
|
||||
if (&$Test) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
&$Set | Out-Null
|
||||
if (&$Test) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
[Log]::WriteError("Requirement validation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ApplyAlways" {
|
||||
[Log]::WriteLine($Describe, [Status]::NotRun)
|
||||
if (&$Set) {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Pass)
|
||||
}
|
||||
else {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
[Log]::WriteError($Message)
|
||||
}
|
||||
}
|
||||
|
||||
"Information" {
|
||||
[Log]::WriteLine("$Describe`n", [Status]::NotRun)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch {
|
||||
[Log]::OverwriteLine($Describe, [Status]::Fail)
|
||||
Write-Host ""
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Invoke-ChecklistDscRequirement {
|
||||
Param(
|
||||
[string]$Describe,
|
||||
[string]$ResourceName,
|
||||
[string]$ModuleName,
|
||||
[hashtable]$Property
|
||||
)
|
||||
|
||||
$dscParams = @{
|
||||
Name = $ResourceName
|
||||
ModuleName = $ModuleName
|
||||
Property = $Property
|
||||
}
|
||||
|
||||
Invoke-ChecklistRequirement `
|
||||
-Describe $Describe `
|
||||
-Test {Invoke-DscResource -Method "Test" @dscParams} `
|
||||
-Set {Invoke-DscResource -Method "Set" @dscParams}
|
||||
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
# TODO: (MEDIUM) Implement cmdlets wrapping the class implementation
|
||||
# TODO: (MEDIUM) Break out Configuration\ConfigurationManager into Modules\ConfigurationManager
|
||||
# TODO: (MEDIUM) Ensure stack traces propogate from module functions
|
||||
|
||||
using namespace System.Collections.Generic
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$InformationPreference = "Continue"
|
||||
|
||||
$LogContext = [Stack[string]]::new()
|
||||
|
||||
|
||||
Import-Module "Profile"
|
||||
|
||||
|
||||
|
||||
$DockerLinePrefix = " ----->"
|
||||
|
||||
|
||||
function Write-Log {
|
||||
Param(
|
||||
[Parameter(Mandatory, Position=0, ParameterSetName="PushContext")]
|
||||
[Parameter(Mandatory, Position=0, ParameterSetName="ExistingContext")]
|
||||
[string] $Context,
|
||||
[Parameter(Mandatory, Position=1, ParameterSetName="PushContext")]
|
||||
[scriptblock] $ScriptBlock
|
||||
)
|
||||
|
||||
switch ($PSCmdlet.ParameterSetName) {
|
||||
"PushContext" {
|
||||
$LogContext.Push($Context)
|
||||
Write-Log "<begin>"
|
||||
$result = &$ScriptBlock
|
||||
Write-Log "<end>"
|
||||
$LogContext.Pop() | Out-Null
|
||||
if ($result -is [object]) {
|
||||
return $result
|
||||
} else {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
"ExistingContext" {
|
||||
$stack = $LogContext.ToArray()
|
||||
[array]::Reverse($stack)
|
||||
$prefix = "$DockerLinePrefix $(Get-Date -Format "hh:mm:ss") [$($stack -join " > ")]"
|
||||
Write-Information "$prefix $Context"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Invoke-ContextRequirement {
|
||||
Param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Test,
|
||||
[scriptblock]$Set
|
||||
)
|
||||
|
||||
try {
|
||||
Write-Log $Name {
|
||||
|
||||
$requirementAlreadyMet = Write-Log "Test" {&$Test}
|
||||
if (-not $requirementAlreadyMet) {
|
||||
Write-Log "Set" {&$Set | Out-Null}
|
||||
$requirementValidated = Write-Log "Test" {&$Test}
|
||||
if (-not $requirementValidated) {
|
||||
throw "Requirement validation failed"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Invoke-ContextDscRequirement {
|
||||
Param(
|
||||
[string]$Name,
|
||||
[string]$ResourceName,
|
||||
[string]$ModuleName,
|
||||
[hashtable]$Property
|
||||
)
|
||||
|
||||
$dscParams = @{
|
||||
Name = $ResourceName
|
||||
ModuleName = $ModuleName
|
||||
Property = $Property
|
||||
}
|
||||
|
||||
Invoke-Requirement `
|
||||
-Name $Name `
|
||||
-Test {Invoke-DscResource -Method "Test" @dscParams} `
|
||||
-Set {Invoke-DscResource -Method "Set" @dscParams}
|
||||
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,97 @@
|
|||
|
||||
using namespace System.Collections.Generic
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$InformationPreference = "Continue"
|
||||
#$VerbosePreference = "Continue"
|
||||
|
||||
Import-Module "Profile"
|
||||
|
||||
|
||||
|
||||
class Resource {
|
||||
[bool] Test() {
|
||||
throw "Abstract method"
|
||||
return $false
|
||||
}
|
||||
[void] Set() {
|
||||
throw "Abstract method"
|
||||
}
|
||||
[string] ToString() {
|
||||
throw "Abstract method"
|
||||
return ""
|
||||
}
|
||||
static [void] Run([resource[]]$resources) {
|
||||
foreach ($resource in $resources) {
|
||||
Write-Log $resource.Name {
|
||||
$pass = [bool](Write-Log "Test" {$resource.Test()})
|
||||
if (-not $pass) {
|
||||
Write-Log "Set" {$resource.Set()}
|
||||
$pass = [bool](Write-Log "Test" {$resource.Test()})
|
||||
if (-not $pass) {
|
||||
Write-Log "Failed"
|
||||
throw "$($resource.Name) failed to install"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Extension : Resource {
|
||||
|
||||
static [string] $ConfigurationFileName = "Extensions.json"
|
||||
static [string] $Container = "C:\Extensions"
|
||||
static [PSCustomObject] $Configuration
|
||||
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Name
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Path
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable] $BaseParameters
|
||||
|
||||
static Extension() {
|
||||
$fileName = [Extension]::ConfigurationFileName
|
||||
[Extension]::Configuration = Get-MergedConfig $fileName
|
||||
}
|
||||
|
||||
Extension([string] $name) {
|
||||
$this.Name = $name
|
||||
$this.Path = [Extension]::Container + "\$name"
|
||||
$this.BaseParameters = @{
|
||||
Service = $env:Service
|
||||
FlightingRing = $env:FlightingRing
|
||||
Region = $env:Region
|
||||
}
|
||||
$this.BaseParameters[$this.Name] = [Extension]::Configuration.($this.Name)
|
||||
}
|
||||
|
||||
[bool] Test() {
|
||||
$script = $this.Path + "\test.ps1"
|
||||
$params = $this.GetParameters($script)
|
||||
return &$script @params
|
||||
}
|
||||
|
||||
[void] Set() {
|
||||
$script = $this.Path + "\set.ps1"
|
||||
$params = $this.GetParameters($script)
|
||||
&$script @params
|
||||
}
|
||||
|
||||
[hashtable] GetParameters([string] $script) {
|
||||
$params = @{}
|
||||
$supportedParameters = (Get-Command $script).ScriptBlock.Ast.ParamBlock.Parameters.Name `
|
||||
| % {$_ -replace "\$"}
|
||||
$this.BaseParameters.Keys `
|
||||
| ? {$_ -in $supportedParameters} `
|
||||
| % {$params[$_] = $this.BaseParameters[$_]}
|
||||
return $params
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue