Namespaces (#46)
* staging * fixed unit tests * inc minor version * remove format-callstack * complete * pr comments and bug fixes * test suite fixes * show namespace in requirement events * commonized requirement serialization * fixed verbose logging * updated docsbugfix-exports
parent
bd59f83a8f
commit
c3a731f018
|
@ -7,7 +7,7 @@ $InformationPreference = "Continue"
|
|||
|
||||
# static version parts
|
||||
$Major = 2
|
||||
$Minor = 2
|
||||
$Minor = 3
|
||||
|
||||
# paths
|
||||
$RepoRoot = "$PSScriptRoot/../.."
|
||||
|
|
358
README.md
358
README.md
|
@ -1,116 +1,295 @@
|
|||
# Requirements
|
||||
|
||||
- [Overview](#overview)
|
||||
- [What is a Requirement](#what-is-a-requirement)
|
||||
- [Why Requirements](#why-requirements)
|
||||
- [Code against the Desired State](#code-against-the-desired-state)
|
||||
- [Generically define Requirements](#generically-define-requirements)
|
||||
- [Comparison to DSC](#comparison-to-dsc)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Defining Requirements](#defining-requirements)
|
||||
- [Note: `Describe` the desired state, not the transition](#note-describe-the-desired-state-not-the-transition)
|
||||
- [Enforcing the Configuration](#enforcing-the-configuration)
|
||||
- [Advanced Configurations](#advanced-configurations)
|
||||
- [Types of Requirements](#types-of-requirements)
|
||||
- [Standard Requirement](#standard-requirement)
|
||||
- [Validation Requirements](#validation-requirements)
|
||||
- [Idempotent `Set` Requirements](#idempotent-set-requirements)
|
||||
- [Patterns](#patterns)
|
||||
- [Avoiding state with selectors](#avoiding-state-with-selectors)
|
||||
- [Reusable requirements](#reusable-requirements)
|
||||
- [Defining Requirements with control flow](#defining-requirements-with-control-flow)
|
||||
- [Managing large configurations with Namespaces](#managing-large-configurations-with-namespaces)
|
||||
- [Isomorphic Enforcement](#isomorphic-enforcement)
|
||||
- [Defining DSC Resources](#defining-dsc-resources)
|
||||
- [Formatting the logs](#formatting-the-logs)
|
||||
- [`Format-Table`](#format-table)
|
||||
- [`Format-Checklist`](#format-checklist)
|
||||
- [`Format-Verbose`](#format-verbose)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
# Overview
|
||||
Requirements is a PowerShell Gallery module for declaratively describing a system as a set of "requirements", then idempotently setting each requirement to its desired state.
|
||||
|
||||
The background motivation and implementation design are discussed in detail in [Declarative Idempotency](https://itnext.io/declarative-idempotency-aaa07c6dd9a0?source=friends_link&sk=f0464e8e29525b23aabe766bfb557dd7).
|
||||
|
||||
Trevor Sullivan provides a good overview and tutorial [video](https://www.youtube.com/watch?v=efRnjlZKCGw) about Requirements.
|
||||
Trevor Sullivan provides a good overview and (slightly outdated) tutorial [video](https://www.youtube.com/watch?v=efRnjlZKCGw) about Requirements.
|
||||
|
||||
## Usage
|
||||
## What is a Requirement
|
||||
A "Requirement" is a single atomic component of a system configuration. For a system to be in its desired state, all Requirements in the system must be in a desired state.
|
||||
|
||||
We use the term `Test` to refer to the condition that describes whether the Requirement is in its desired state. We use the term `Set` to refer to the command that a `Requirement` uses to put itself in its desired state if it is known to not be in its desired state.
|
||||
A Requirement is an object defined by three properties:
|
||||
* `Describe` - A `string` that describes the desired state of the Requirement.
|
||||
* `Test` - A `scriptblock` that returns whether the Requirement is in its desired state.
|
||||
* `Set` - A `scriptblock` that can be run to put the Requirement into its desired state if it is not in a desired state.
|
||||
|
||||
### Declaring requirements
|
||||
## Why Requirements
|
||||
|
||||
### Code against the Desired State
|
||||
In DevOps, you may be managing a fleet of servers, containers, cloud resources, files on disk, or many other kinds of components in a heterogeneous system. Lets say you have *n* components in your system and every component is either in a `GOOD` or `BAD` state. You have two options:
|
||||
* You can try and account for every possible configuration of your system and transition between those states, but then you will have *2**n* possible states to manage.
|
||||
* You can only account for the desired state of each individual component, so you will only have *n* states to account for. Much simpler!
|
||||
|
||||
### Generically define Requirements
|
||||
If you only manage cloud resources, then try to use Terraform, ARM, or CloudFormation. If you only manage kubernetes resources, then try and use Helm. These are domain-specific frameworks for managing the desired state of resources and are best suited for their task.
|
||||
|
||||
However, you will often find you have complex configurations, such as configurations that can only be described in PowerShell. You may even have macroconfigurations that consist of one or more Terraform templates. In this case you will probably want something more generic to glue your configurations together without sacrificing the declarative desired state paradigm. This is where Requirements comes in.
|
||||
|
||||
## Comparison to DSC
|
||||
Desired State Configurations allow you to declaratively describe a configuration, then let the local configuration manager handle with setting the configuration to its desired state. This pattern from the outside may seem similar to Requirements, but there are crucial differences.
|
||||
|
||||
DSC is optimized for handling *many* configurations *asynchronously*. For example, applying a configuration in parallel to multiple nodes. In contrast, Requirements applies a *single* configuration *synchronously*. This enables usage in different scenarios, including:
|
||||
* CI/CD scripts
|
||||
* CLIs
|
||||
* Dockerfiles
|
||||
* Linux
|
||||
|
||||
While Requirements supports DSC resources, it does not have a hard dependency on DSC's configuration manager, so if your Requirements do not include DSC resources they will work on any platform that PowerShell Core supports.
|
||||
|
||||
|
||||
# Quickstart
|
||||
|
||||
## Defining Requirements
|
||||
The easiest way to declare a requirement is to define it as a hashtable and let PowerShell's implicit casting handle the rest.
|
||||
|
||||
```powershell
|
||||
$requirements = @(
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Resource 1 is present in the system"
|
||||
Test = { $mySystem -contains 1 }
|
||||
Set = { $mySystem.Add(1) | Out-Null; Start-Sleep 1 }
|
||||
Set = {
|
||||
$mySystem.Add(1) | Out-Null
|
||||
Start-Sleep 1
|
||||
}
|
||||
},
|
||||
@{
|
||||
Name = "Resource 2"
|
||||
Describe = "Resource 2 is present in the system"
|
||||
Test = { $mySystem -contains 2 }
|
||||
Set = { $mySystem.Add(2) | Out-Null; Start-Sleep 1 }
|
||||
Set = {
|
||||
$mySystem.Add(2) | Out-Null
|
||||
Start-Sleep 1
|
||||
}
|
||||
},
|
||||
@{
|
||||
Name = "Resource 3"
|
||||
Describe = "Resource 3 is present in the system"
|
||||
Test = { $mySystem -contains 3 }
|
||||
Set = { $mySystem.Add(3) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 4"
|
||||
Describe = "Resource 4 is present in the system"
|
||||
Test = { $mySystem -contains 4 }
|
||||
Set = { $mySystem.Add(4) | Out-Null; Start-Sleep 1 }
|
||||
},
|
||||
@{
|
||||
Name = "Resource 5"
|
||||
Describe = "Resource 5 is present in the system"
|
||||
Test = { $mySystem -contains 5 }
|
||||
Set = { $mySystem.Add(5) | Out-Null; Start-Sleep 1 }
|
||||
Set = {
|
||||
$mySystem.Add(3) | Out-Null
|
||||
Start-Sleep 1
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Validation Requirements
|
||||
If you wish to assert that a precondition is met before continuing, you can leave out the `Set` block. This is useful for [Defensive programming](https://itnext.io/defensive-powershell-with-validation-attributes-8e7303e179fd?source=friends_link&sk=14765ca9554709a77f8af7d73612ef5b), or when a Requirement requires manual steps.
|
||||
### Note: `Describe` the desired state, not the transition
|
||||
Our `Describe` describes the *desired state* of the Requirement (ex: "Resource 1 is present in the system") and not the `Set` block's transitioning action (ex: "Adding Resource 1 to the system"). This is because the `Set` block is not called if the Requirement is already in its desired state, so if we used the latter `Describe` and Resource 1 was already present in the system, we would be inaccurately logging that the Requirement is modifying the system when it is actually taking no action.
|
||||
|
||||
```powershell
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Azure CLI is authenticated"
|
||||
Test = { az account }
|
||||
}
|
||||
```
|
||||
The sooner you embrace the Desired State mindset, the less friction you will have writing Requirements and managing your complex system configurations.
|
||||
|
||||
#### Idempotent `Set` blocks
|
||||
Sometimes, your `Set` block is already idempotent and an associated `Test` block cannot be defined. In this case, you can leave out the `Test` block.
|
||||
|
||||
```powershell
|
||||
@{
|
||||
Name = "Resource 1"
|
||||
Describe = "Initial state of system is backed up"
|
||||
Set = { Get-StateOfSystem | Out-File "$BackupContainer/$(Get-Date -Format 'yyyyMMddhhmmss').log" }
|
||||
}
|
||||
```
|
||||
|
||||
### Idempotently Setting requirements
|
||||
Simply pipe an array of `Requirement`s to `Invoke-Requirement`
|
||||
## Enforcing the Configuration
|
||||
Once you have an array of Requirements, you can simply pipe the Requirements into `Invoke-Requirement` to put each Requirement into its desired state.
|
||||
|
||||
```powershell
|
||||
$requirements | Invoke-Requirement
|
||||
```
|
||||
|
||||
### Formatting the logs
|
||||
`Invoke-Requirement` will output logging events for each step in a `Requirement`'s execution lifecycle. You can capture these logs with `Format-Table` or `Format-List`, or
|
||||
The status of each Requirement will be logged to the output stream. By default they are shown with `Format-List`, but you can pipe the results to `Format-Table`, or use one of the packaged formatters for Requirements-specific event formatting and filtering.
|
||||
|
||||
# Advanced Configurations
|
||||
|
||||
## Types of Requirements
|
||||
As you learned [previously](#what-is-a-requirement), Requirements consist of `Describe`, `Test`, and `Set` properties. There are 4 types of Requirements--one for every permutation of including or excluding `Test` and `Set`. Note that `Describe` must always be present.
|
||||
|
||||
### Standard Requirement
|
||||
This is the kind you are already [familiar with](#what-is-a-requirement). It includes both a `Test` and `Set`.
|
||||
|
||||
### Validation Requirements
|
||||
If you wish to assert that a precondition is met before continuing, you can leave out the `Set` block. This is useful for [Defensive programming](https://itnext.io/defensive-powershell-with-validation-attributes-8e7303e179fd?source=friends_link&sk=14765ca9554709a77f8af7d73612ef5b), or when a Requirement requires manual steps.
|
||||
|
||||
```powershell
|
||||
$requirements | Invoke-Requirement | Format-Table
|
||||
@{
|
||||
Describe = "Azure CLI is authenticated"
|
||||
Test = { az account }
|
||||
}
|
||||
```
|
||||
|
||||
#### `Format-Table`
|
||||
These logs were using `-Autosize` parameter, which better formats the columns, but does not support outputting as a stream.
|
||||
```
|
||||
Method Lifecycle Name Date
|
||||
------ --------- ---- ----
|
||||
Test Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Test Stop Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Start Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Test Start Resource 2 6/12/2019 12:00:26 PM
|
||||
Test Stop Resource 2 6/12/2019 12:00:26 PM
|
||||
Set Start Resource 2 6/12/2019 12:00:26 PM
|
||||
...
|
||||
### Idempotent `Set` Requirements
|
||||
Sometimes, your `Set` block is already idempotent and an associated `Test` block cannot be defined. In this case, you can leave out the `Test` block.
|
||||
|
||||
```powershell
|
||||
@{
|
||||
Describe = "Initial state of system is backed up"
|
||||
Set = { Get-StateOfSystem | Out-File "$BackupContainer/$(Get-Date -Format 'yyyyMMddhhmmss').log" }
|
||||
}
|
||||
```
|
||||
|
||||
#### `Format-Checklist`
|
||||
`Format-Checklist` will present a live-updating checklist to the user.
|
||||
## Patterns
|
||||
Some people have trouble managing large configurations with Requirements because they try and explicitly define a single array literal of Requirements; however, this is unnecessary and Requirements can be handled like any other PowerShell object. Here are some examples of patterns for managing Requirements.
|
||||
|
||||
![Format-Checklist output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/checklist.png)
|
||||
### Avoiding state with selectors
|
||||
Requirements should strongly avoid maintaining internal state. Requirements is for enforcing declarative programming, whereas maintaining state is an imperative loophole that breaks the declarative paradigm.
|
||||
|
||||
#### `Format-Callstack`
|
||||
Unlike `Format-Checklist`, `Format-Callstack` prints all log events and includes metadata. For complex use cases, you can define nested `Requirement`s (`Requirement`s that contain more `Requirement`s in their `Set` block). `Format-Callstack` will print the stack of `Requirement` names of each `Requirement` as its processed.
|
||||
Instead, use **selectors** to easily derive up-to-date properties of the system using unit-testable functions.
|
||||
|
||||
![Format-Callstack output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/callstack.png)
|
||||
```powershell
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Gets the random storage account name from the environment in the cloud
|
||||
#>
|
||||
function Select-StorageAccountName([string]$EnvName) { ... }
|
||||
|
||||
### Defining DSC Resources
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns a Requirement that ensures a storage ccount exists in Azure
|
||||
#>
|
||||
function New-StorageAccountRequirement {
|
||||
@{
|
||||
Describe = "Storage Account exists"
|
||||
Test = { Test-StorageAccountExists (Select-StorageAccountName $env:EnvName) }
|
||||
Set = { New-StorageAccount (Select-StorageAccountName $env:EnvName) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reusable requirements
|
||||
You can wrap Requirements in a parameterized function or script to avoid redifining Requirements--
|
||||
|
||||
```powershell
|
||||
function New-ResourceGroupRequirement {
|
||||
Param(
|
||||
[string]$Name,
|
||||
[string]$Location
|
||||
)
|
||||
|
||||
Push-Namespace "rg" {
|
||||
@{
|
||||
Describe = "Logged in to Azure"
|
||||
Test = { Get-AzAccount }
|
||||
Set = { Connect-AzAccount }
|
||||
}
|
||||
@{
|
||||
Describe = "Resource Group '$Name' exists"
|
||||
Test = { Get-AzResourceGroup -Name $Name -ErrorAction SilentlyContinue }
|
||||
Set = { New-AzResourceGroup -Name $Name -Location $Location }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then call your function to generate the Requirement--
|
||||
|
||||
```powershell
|
||||
$Name = "my-rg"
|
||||
$Location = "West US 2"
|
||||
|
||||
& {
|
||||
New-ResourceGroupRequirement -Name $Name -Location $Location
|
||||
@{
|
||||
Describe = "Do something with the resource group"
|
||||
Test = { ... }
|
||||
Set = { ... }
|
||||
}
|
||||
} `
|
||||
| Invoke-Requirement `
|
||||
| Format-Table
|
||||
```
|
||||
|
||||
### Defining Requirements with control flow
|
||||
Using control flow statements, like `if` and `foreach`, can dramatically simplify your Requirement definitions. Let's see if we can simiplify our [quickstart example](#defining-requirements).
|
||||
|
||||
```powershell
|
||||
foreach ($resourceId in 1..3) {
|
||||
@{
|
||||
Describe = "Resource $resourceId is present in the system"
|
||||
Test = { $mySystem -contains $resourceId }.GetNewClosure()
|
||||
Set = { $mySystem.Add($resourceId) | Out-Null; Start-Sleep 1 }.GetNewClosure()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we had to call `.GetNewClosure()` to capture the current value of `$resourceId` in the `scriptblock`--otherwise the value would be `3` or `$null` depending on where we invoked it.
|
||||
|
||||
When you define Requirements with control flow in this manner, the Requirements are written to output. As such, this logic should be wrapped in a script, function, or `scriptblock`.
|
||||
|
||||
### Managing large configurations with Namespaces
|
||||
You can group Requirements into namespaces for clearer logging. To add a namespace to Requirements, use the `Push-Namespace` function. You can nest namespaces as well.
|
||||
|
||||
```powershell
|
||||
Push-Namespace "local" {
|
||||
Push-Namespace "clis" {
|
||||
@{
|
||||
Describe = "az is installed"
|
||||
Test = { ... }
|
||||
Set = { ... }
|
||||
}
|
||||
@{
|
||||
Describe = "kubectl is installed"
|
||||
Test = { ... }
|
||||
Set = { ... }
|
||||
}
|
||||
}
|
||||
Push-Namespace "configs" {
|
||||
@{
|
||||
Describe = "cluster config is built"
|
||||
Test = { ... }
|
||||
Set = { ... }
|
||||
}
|
||||
}
|
||||
}
|
||||
Push-Namespace "cloud" {
|
||||
@{
|
||||
Describe = "Terraform is deployed"
|
||||
Test = { ... }
|
||||
Set = { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The above example would result in the Requirements below.
|
||||
|
||||
```
|
||||
Namespace Describe
|
||||
--------- --------
|
||||
local:clis az is installed
|
||||
local:clis kubectl is installed
|
||||
local:configs cluster config is built
|
||||
cloud Terraform is deployed
|
||||
```
|
||||
|
||||
### Isomorphic Enforcement
|
||||
Isomorphic execution means that our Requirements are enforced the same regardless of what context they are enforced in. You will want your Requirements to run in a CICD pipeline for safe deployment practices and run manually from your local machine for development purposes, but in both contexts the Requirements should run exactly the same.
|
||||
|
||||
We will accomplish this by implementing Separation of Concerns, separating our Requirement definitions from our execution logic:
|
||||
* `myrequirements.ps1`, which will return an array of Requirements.
|
||||
* `Invoke-Verbose.ps1`, which will be called in a CICD pipeline and write verbose status information to the output stream.
|
||||
```powershell
|
||||
./myrequirements.ps1 | Invoke-Requirement | Format-Verbose
|
||||
```
|
||||
* `Invoke-Checklist.ps1`, which will be called in a console and interactively write to the host.
|
||||
```powershell
|
||||
./myrequirements.ps1 | Invoke-Requirement | Format-Checklist
|
||||
```
|
||||
|
||||
## Defining DSC Resources
|
||||
If you're using Windows and PowerShell 5, you can use DSC resources with Requirements.
|
||||
|
||||
```PowerShell
|
||||
|
@ -127,16 +306,39 @@ $requirement = @{
|
|||
New-Requirement @requirement | Invoke-Requirement | Format-Checklist
|
||||
```
|
||||
|
||||
## Comparison to DSC
|
||||
Desired State Configurations allow you to declaratively describe a configuration then let the configuration manager handle with setting the configuration to its desired state. This pattern from the outside may seem similar to Requirements, but there are crucial differences.
|
||||
# Formatting the logs
|
||||
`Invoke-Requirement` will output logging events for each step in a `Requirement`'s execution lifecycle. You can capture these logs with `Format-Table` or `Format-List`, or
|
||||
|
||||
DSC is optimized for handling *many* configurations *asynchronously*. For example, applying a configuration in parallel to multiple nodes. In contrast, Requirements applies a *single* configuration *synchronously*. This enables usage in different scenarios, including:
|
||||
* CI/CD scripts
|
||||
* CLIs
|
||||
* Dockerfiles
|
||||
* Linux
|
||||
```powershell
|
||||
$requirements | Invoke-Requirement | Format-Table
|
||||
```
|
||||
|
||||
While Requirements supports DSC resources, it does not have a hard dependency on DSC's configuration manager, so if your Requirements do not include DSC resources they will work on any platform that PowerShell Core supports.
|
||||
## `Format-Table`
|
||||
These logs were using `-Autosize` parameter, which better formats the columns, but does not support outputting as a stream.
|
||||
```
|
||||
Method Lifecycle Name Date
|
||||
------ --------- ---- ----
|
||||
Test Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Test Stop Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Start Resource 1 6/12/2019 12:00:25 PM
|
||||
Set Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Start Resource 1 6/12/2019 12:00:26 PM
|
||||
Validate Stop Resource 1 6/12/2019 12:00:26 PM
|
||||
Test Start Resource 2 6/12/2019 12:00:26 PM
|
||||
Test Stop Resource 2 6/12/2019 12:00:26 PM
|
||||
Set Start Resource 2 6/12/2019 12:00:26 PM
|
||||
...
|
||||
```
|
||||
|
||||
## `Format-Checklist`
|
||||
`Format-Checklist` will present a live-updating checklist to the user.
|
||||
|
||||
![Format-Checklist output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/checklist.png)
|
||||
|
||||
## `Format-Verbose`
|
||||
Unlike `Format-Checklist`, `Format-Verbose` prints all log events and includes metadata. For complex use cases, you can define nested `Requirement`s (`Requirement`s that contain more `Requirement`s in their `Set` block). `Format-Verbose` will print the stack of `Requirement` names of each `Requirement` as its processed.
|
||||
|
||||
![Format-Verbose output](https://raw.githubusercontent.com/microsoft/requirements/master/imgs/callstack.png)
|
||||
|
||||
# Contributing
|
||||
|
||||
|
|
|
@ -64,8 +64,8 @@
|
|||
|
||||
# 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.
|
||||
FunctionsToExport = @(
|
||||
'Format-CallStack',
|
||||
'Format-Checklist',
|
||||
'Format-Verbose',
|
||||
'Invoke-Requirement',
|
||||
'New-Requirement',
|
||||
'Set-Requirement',
|
||||
|
|
13
src/core.ps1
13
src/core.ps1
|
@ -1,7 +1,12 @@
|
|||
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
|
||||
Param()
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\types.ps1"
|
||||
|
||||
$NamespaceDelimiter = ":"
|
||||
|
||||
# idempotently applies a requirement
|
||||
function applyRequirement([Requirement]$Requirement) {
|
||||
$result = $false
|
||||
|
@ -21,7 +26,7 @@ function applyRequirement([Requirement]$Requirement) {
|
|||
$result = &$Requirement.Test
|
||||
[RequirementEvent]::new($Requirement, "Validate", "Stop", $result)
|
||||
if (-not $result) {
|
||||
Write-Error "Failed to apply Requirement '$($Requirement.Name)'"
|
||||
Write-Error "Failed to apply Requirement '$($Requirement.Describe)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,11 +56,11 @@ function sortRequirements([Requirement[]]$Requirements) {
|
|||
$stages = @()
|
||||
while ($Requirements) {
|
||||
$nextStages = $Requirements `
|
||||
| ? { -not ($_.DependsOn | ? { $_ -notin $stages.Name }) }
|
||||
| ? { -not ($_.DependsOn | ? { $_ -notin $stages.Namespace }) }
|
||||
if (-not $nextStages) {
|
||||
throw "Could not resolve the dependencies for Requirements with names: $($Requirements.Name -join ', ')"
|
||||
throw "Could not resolve the dependencies for Requirements with names: $($Requirements.Namespace -join ', ')"
|
||||
}
|
||||
$Requirements = $Requirements | ? { $_.Name -notin $nextStages.Name }
|
||||
$Requirements = $Requirements | ? { $_.Namespace -notin $nextStages.Namespace }
|
||||
$stages += $nextStages
|
||||
}
|
||||
$stages
|
||||
|
|
|
@ -7,27 +7,28 @@ Param()
|
|||
$ErrorActionPreference = "Stop"
|
||||
."$PSScriptRoot\types.ps1"
|
||||
|
||||
function writePending($timestamp, $description) {
|
||||
function writePending($timestamp, $requirement) {
|
||||
$symbol = " "
|
||||
$color = "Yellow"
|
||||
$message = "$timestamp [ $symbol ] $description"
|
||||
$message = "$symbol $timestamp $requirement"
|
||||
Write-Host $message -ForegroundColor $color -NoNewline
|
||||
}
|
||||
|
||||
function writeSuccess($timestamp, $description, $clearString) {
|
||||
function writeSuccess($timestamp, $requirement, $clearString) {
|
||||
$symbol = [char]8730
|
||||
$color = "Green"
|
||||
$message = "$timestamp [ $symbol ] $description"
|
||||
$message = "$symbol $timestamp $requirement"
|
||||
Write-Host "`r$clearString" -NoNewline
|
||||
Write-Host "`r$message" -ForegroundColor $color
|
||||
}
|
||||
|
||||
function writeFail($timestamp, $description, $clearString) {
|
||||
function writeFail($timestamp, $requirement, $clearString) {
|
||||
$symbol = "X"
|
||||
$color = "Red"
|
||||
$message = "$timestamp [ $symbol ] $description"
|
||||
$message = "$symbol $timestamp $requirement"
|
||||
Write-Host "`r$clearString" -NoNewline
|
||||
Write-Host "`n$message`n" -ForegroundColor $color
|
||||
exit -1
|
||||
}
|
||||
|
||||
$fsm = @{
|
||||
|
@ -118,9 +119,8 @@ function Format-Checklist {
|
|||
|
||||
# build transition arguments
|
||||
$timestamp = Get-Date -Date $_.Date -Format "hh:mm:ss"
|
||||
$description = $requirement.Describe
|
||||
$clearString = ' ' * "??:??:?? [ ? ] $($previousRequirement.Describe)".Length
|
||||
$transitionArgs = @($timestamp, $description, $clearString)
|
||||
$clearString = ' ' * "? ??:??:?? $previousRequirement".Length
|
||||
$transitionArgs = @($timestamp, $requirement, $clearString)
|
||||
|
||||
# transition FSM
|
||||
if (-not $nextFsm[$stateVector]) {
|
||||
|
@ -135,15 +135,10 @@ cmdlet, then this is probably a bug in Format-Checklist.
|
|||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Formats every log event with metadata, including a stack of requirement names when using nested Requirements
|
||||
.NOTES
|
||||
Uses Write-Host
|
||||
#>
|
||||
function Format-CallStack {
|
||||
function Format-Verbose {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
|
||||
[CmdletBinding()]
|
||||
[OutputType([string])]
|
||||
Param(
|
||||
# Logged Requirement lifecycle events
|
||||
[Parameter(Mandatory, ValueFromPipeline)]
|
||||
|
@ -151,70 +146,8 @@ function Format-CallStack {
|
|||
[RequirementEvent[]]$RequirementEvent
|
||||
)
|
||||
|
||||
begin {
|
||||
$context = [Stack[string]]::new()
|
||||
}
|
||||
|
||||
process {
|
||||
$timestamp = Get-Date -Date $_.Date -Format 'hh:mm:ss'
|
||||
$name = $_.Requirement.Name
|
||||
$description = $_.Requirement.Describe
|
||||
$method, $state, $result = $_.Method, $_.State, $_.Result
|
||||
switch ($method) {
|
||||
"Test" {
|
||||
switch ($state) {
|
||||
"Start" {
|
||||
$context.Push($name)
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] BEGIN TEST $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] END TEST => $result"
|
||||
$context.Pop() | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
"Set" {
|
||||
switch ($state) {
|
||||
"Start" {
|
||||
$context.Push($name)
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] BEGIN SET $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] END SET"
|
||||
$context.Pop() | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
"Validate" {
|
||||
switch ($state) {
|
||||
"Start" {
|
||||
$context.Push($name)
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] BEGIN TEST $description"
|
||||
}
|
||||
"Stop" {
|
||||
$callstack = $context.ToArray()
|
||||
[array]::Reverse($callstack)
|
||||
$serialized = $callstack -join ">"
|
||||
Write-Host "$timestamp [$serialized] END TEST => $result"
|
||||
$context.Pop() | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$timestamp = Get-Date -Date $_.Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
"{0} {1,-8} {2,-5} {3}" -f $timestamp, $_.Method, $_.State, $_.Requirement
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ function New-Requirement {
|
|||
# The unique identifier for the Requirement
|
||||
[Parameter(ParameterSetName = "Script")]
|
||||
[Parameter(ParameterSetName = "Dsc")]
|
||||
[string] $Name,
|
||||
[string] $Namespace,
|
||||
# A description of the Requirement
|
||||
[Parameter(Mandatory, ParameterSetName = "Script")]
|
||||
[Parameter(Mandatory, ParameterSetName = "Dsc")]
|
||||
|
@ -52,7 +52,7 @@ function New-Requirement {
|
|||
switch ($PSCmdlet.ParameterSetName) {
|
||||
"Script" {
|
||||
[Requirement]@{
|
||||
Name = $Name
|
||||
Namespace = $Namespace
|
||||
Describe = $Describe
|
||||
Test = $Test
|
||||
Set = $Set
|
||||
|
@ -66,7 +66,7 @@ function New-Requirement {
|
|||
Property = $Property
|
||||
}
|
||||
[Requirement]@{
|
||||
Name = $Name
|
||||
Name = $Namespace
|
||||
Describe = $Describe
|
||||
Test = { Invoke-DscResource -Method "Test" @dscParams }.GetNewClosure()
|
||||
Set = { Invoke-DscResource -Method "Set" @dscParams }.GetNewClosure()
|
||||
|
@ -127,3 +127,35 @@ function Set-Requirement {
|
|||
&$Requirement.Set
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Prepends a namespace to the Requirements' name
|
||||
#>
|
||||
function Push-Namespace {
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
# The namespace identifier
|
||||
[Parameter(Mandatory, Position = 0)]
|
||||
[string]$Namespace,
|
||||
# A scriptblock that writes Requirements to output when invoked
|
||||
[Parameter(Mandatory, Position = 1, ParameterSetName = "scriptblock")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[scriptblock]$ScriptBlock,
|
||||
# The array of Requirements to add under the new namespace
|
||||
[Parameter(Mandatory, Position = 1, ParameterSetName = "requirements")]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Requirement[]]$Requirement
|
||||
)
|
||||
|
||||
if ($PSCmdlet.ParameterSetName -eq "scriptblock") {
|
||||
$Requirement = &$ScriptBlock
|
||||
}
|
||||
|
||||
$Requirement `
|
||||
| % {
|
||||
$r = $_.psobject.Copy()
|
||||
$r.Namespace = $Namespace, $r.Namespace -join $NamespaceDelimiter
|
||||
$r
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
|
||||
class Requirement {
|
||||
[string] $Name
|
||||
[string] $Namespace
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Describe
|
||||
[scriptblock] $Test
|
||||
[scriptblock] $Set
|
||||
[string[]] $DependsOn = @()
|
||||
[string] ToString() {
|
||||
return $this.Name
|
||||
if ($this.Namespace) {
|
||||
return $this.Namespace + ">" + $this.Describe
|
||||
}
|
||||
else {
|
||||
return $this.Describe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,31 +18,31 @@ $OutRoot = "$PSScriptRoot/integration"
|
|||
Set = { New-Item -ItemType Directory -Path $OutRoot }
|
||||
} | Invoke-Requirement | Out-Null
|
||||
|
||||
$context = @{count = 0}
|
||||
$context = @{ count = 0 }
|
||||
|
||||
$Requirements = @{
|
||||
Test = @{
|
||||
Name = "MyName"
|
||||
Describe = "MyDescribe"
|
||||
Test = { $true }
|
||||
Namespace = "ns"
|
||||
Describe = "MyDescribe"
|
||||
Test = { $true }
|
||||
}
|
||||
Set = @{
|
||||
Name = "MyName"
|
||||
Describe = "MyDescribe"
|
||||
Set = { $true }
|
||||
Namespace = "ns"
|
||||
Describe = "MyDescribe"
|
||||
Set = { $true }
|
||||
}
|
||||
TestSet = @{
|
||||
Name = "MyName"
|
||||
Describe = "MyDescribe"
|
||||
Test = { $context.count++ % 2 -eq 1 }
|
||||
Set = { $true }
|
||||
Namespace = "ns"
|
||||
Describe = "MyDescribe"
|
||||
Test = { $context.count++ % 2 -eq 1 }
|
||||
Set = { $true }
|
||||
}
|
||||
}
|
||||
|
||||
$Requirements.Keys `
|
||||
| % {
|
||||
$events = $Requirements[$_] | Invoke-Requirement
|
||||
$events | Format-CallStack *> "$OutRoot/Format-CallStack.$_.txt"
|
||||
$events | Format-Checklist *> "$OutRoot/Format-Checklist.$_.txt"
|
||||
$events | Format-Table *> "$OutRoot/Format-Table.$_.txt"
|
||||
$events | Format-Verbose *> "$OutRoot/Format-Verbose.$_.txt"
|
||||
}
|
||||
|
|
|
@ -85,7 +85,6 @@ Describe "Core" {
|
|||
$script:CallTestOnEachRequirement = 0
|
||||
$requirements = 1..3 | % {
|
||||
@{
|
||||
Name = $_
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:CallTestOnEachRequirement++ % 2 }
|
||||
Set = { $false }
|
||||
|
@ -95,62 +94,4 @@ Describe "Core" {
|
|||
$script:CallTestOnEachRequirement | Should -Be 6
|
||||
}
|
||||
}
|
||||
Context "sortRequirements" {
|
||||
It "Should sort an array of requirements into topological order" {
|
||||
$sorted = sortRequirements @(
|
||||
@{
|
||||
Name = "third"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "second"
|
||||
},
|
||||
@{
|
||||
Name = "first"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
},
|
||||
@{
|
||||
Name = "second"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first"
|
||||
}
|
||||
)
|
||||
[string[]]$names = $sorted | % Name
|
||||
0..($sorted.Count - 1) | % {
|
||||
$i, $requirement = $_, $sorted[$_]
|
||||
$requirement.DependsOn `
|
||||
| % { $names.IndexOf($_) | Should -BeLessThan $i }
|
||||
}
|
||||
}
|
||||
It "Should throw an error if there are unresolvable dependencies" {
|
||||
{
|
||||
sortRequirements @(
|
||||
@{
|
||||
Name = "third"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "second"
|
||||
},
|
||||
@{
|
||||
Name = "first"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
},
|
||||
@{
|
||||
Name = "second"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { }
|
||||
Set = { }
|
||||
DependsOn = "first", "third"
|
||||
}
|
||||
)
|
||||
} | Should -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,12 @@ function invoke($Requirement) {
|
|||
}
|
||||
|
||||
Describe "formatters" {
|
||||
Mock Get-Date { return "00:00:00" }
|
||||
$script:InDesiredState = 0
|
||||
$requirement = @{
|
||||
Name = "simple-requirement"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:InDesiredState++ }
|
||||
Set = { }
|
||||
Namespace = "sr"
|
||||
Describe = "Simple Requirement"
|
||||
Test = { $script:InDesiredState++ }
|
||||
Set = { }
|
||||
}
|
||||
$events = invoke $requirement
|
||||
$tempContainer = $PSScriptRoot
|
||||
|
@ -37,16 +36,16 @@ Describe "formatters" {
|
|||
$output = Get-Content $path -Raw
|
||||
Remove-Item $path
|
||||
It "Should format each line as a checklist" {
|
||||
$output | Should -Match "^\d\d:\d\d:\d\d \[ . \] Simple Requirement"
|
||||
$output | Should -Match "^. \d\d:\d\d:\d\d\[sr|Simple Requirement"
|
||||
}
|
||||
}
|
||||
Context "Format-Callstack" {
|
||||
Context "Format-Verbose" {
|
||||
$path = "$tempContainer\$(New-Guid).txt"
|
||||
($events | Format-CallStack) *> $path
|
||||
($events | Format-Verbose) *> $path
|
||||
$output = Get-Content $path
|
||||
Remove-Item $path
|
||||
It "Should format each line as a callstack" {
|
||||
$output | % { $_ | Should -Match "^\d\d:\d\d:\d\d \[.+\] .+" }
|
||||
It "Should format each line" {
|
||||
$output | % { $_ | Should -Match "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d +\w+ +\w+ .+" }
|
||||
}
|
||||
It "Should print 6 lines" {
|
||||
$output.Count | Should -Be 6
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
05:22:35 [MyName] BEGIN SET MyDescribe
|
||||
05:22:35 [MyName] END SET
|
|
@ -1,2 +0,0 @@
|
|||
05:22:34 [MyName] BEGIN TEST MyDescribe
|
||||
05:22:34 [MyName] END TEST => True
|
|
@ -1,6 +0,0 @@
|
|||
05:22:35 [MyName] BEGIN TEST MyDescribe
|
||||
05:22:35 [MyName] END TEST => False
|
||||
05:22:35 [MyName] BEGIN SET MyDescribe
|
||||
05:22:35 [MyName] END SET
|
||||
05:22:35 [MyName] BEGIN TEST MyDescribe
|
||||
05:22:35 [MyName] END TEST => True
|
|
@ -1,3 +1,3 @@
|
|||
05:22:35 [ ] MyDescribe
|
||||
|
||||
05:22:35 [ √ ] MyDescribe
|
||||
02:59:01 ns>MyDescribe
|
||||
|
||||
√ 02:59:01 ns>MyDescribe
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
05:22:34 [ ] MyDescribe
|
||||
|
||||
05:22:34 [ √ ] MyDescribe
|
||||
02:59:01 ns>MyDescribe
|
||||
|
||||
√ 02:59:01 ns>MyDescribe
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
05:22:35 [ ] MyDescribe
|
||||
|
||||
05:22:35 [ √ ] MyDescribe
|
||||
02:59:01 ns>MyDescribe
|
||||
|
||||
√ 02:59:01 ns>MyDescribe
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
9/23/2019 5:22:35 PM Set Start MyName
|
||||
9/23/2019 5:22:35 PM Set Stop True MyName
|
||||
|
||||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
11/17/19 2:59:01 PM Set Start ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Set Stop True ns>MyDescribe
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
9/23/2019 5:22:34 PM Test Start MyName
|
||||
9/23/2019 5:22:34 PM Test Stop True MyName
|
||||
|
||||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
11/17/19 2:59:01 PM Test Start ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Test Stop True ns>MyDescribe
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
9/23/2019 5:22:35 PM Test Start MyName
|
||||
9/23/2019 5:22:35 PM Test Stop False MyName
|
||||
9/23/2019 5:22:35 PM Set Start MyName
|
||||
9/23/2019 5:22:35 PM Set Stop True MyName
|
||||
9/23/2019 5:22:35 PM Validate Start MyName
|
||||
9/23/2019 5:22:35 PM Validate Stop True MyName
|
||||
|
||||
|
||||
Date Method State Result Requirement
|
||||
---- ------ ----- ------ -----------
|
||||
11/17/19 2:59:01 PM Test Start ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Test Stop False ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Set Start ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Set Stop True ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Validate Start ns>MyDescribe
|
||||
11/17/19 2:59:01 PM Validate Stop True ns>MyDescribe
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
2019-11-17 14:59:01 Set Start ns>MyDescribe
|
||||
2019-11-17 14:59:01 Set Stop ns>MyDescribe
|
|
@ -0,0 +1,2 @@
|
|||
2019-11-17 14:59:01 Test Start ns>MyDescribe
|
||||
2019-11-17 14:59:01 Test Stop ns>MyDescribe
|
|
@ -0,0 +1,6 @@
|
|||
2019-11-17 14:59:01 Test Start ns>MyDescribe
|
||||
2019-11-17 14:59:01 Test Stop ns>MyDescribe
|
||||
2019-11-17 14:59:01 Set Start ns>MyDescribe
|
||||
2019-11-17 14:59:01 Set Stop ns>MyDescribe
|
||||
2019-11-17 14:59:01 Validate Start ns>MyDescribe
|
||||
2019-11-17 14:59:01 Validate Stop ns>MyDescribe
|
|
@ -103,3 +103,15 @@ Describe "Set-Requirement" {
|
|||
{ Invoke-Requirement $requirement } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Push-Namespace" {
|
||||
It "Should prepend the namespace to the requirements" {
|
||||
$namespace = "MyReqs"
|
||||
$requirements = @(
|
||||
@{Namespace = "req1" },
|
||||
@{Namespace = "req2" }
|
||||
)
|
||||
Push-Namespace -Namespace $namespace -Requirement $requirements `
|
||||
| % { $_.Namespace | Should -BeLikeExactly "$namespace`:*" }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue