Providers/PingOne/Public/New-PingOneEnvironment.ps1

function New-PingOneEnvironment {
    <#
    .SYNOPSIS
        Seeds a PingOne environment with populations, people, groups, resources and applications

    .DESCRIPTION
        Reached through New-TestEnvironment once a PingOne connection is active. Runs every step
        in the only order that works, and keeps going when a step fails so the summary can say
        what did and did not happen rather than stopping at the first error.

        The order is dictated by what references what:

        1. Attributes - the custom schema attributes, including the ownership marker every
                            seeded user is written with. Nothing can be tagged before it exists.
        2. Populations - the containers teardown asks. A user cannot be created without one.
        3. Groups - including the chain's nesting and the dynamic filters, which name a
                            population by id and so need it to exist.
        4. Users - created in their populations, tagged, and then put into their
                            groups, which is why groups come first.
        5. Resources - the custom APIs and their scopes.
        6. Applications - restricted to groups and granted resource scopes, so both of those
                            have to exist already.

        What is deliberately never done, and has no parameter:

        - No seeded population is made the environment's default. The default decides where
          every user created without a population lands, including users nothing to do with
          this module, so changing it changes how the environment behaves.
        - No platform application or built-in resource is created, edited or deleted. They are
          matched by type rather than name, so renaming one in the console cannot change that.
        - No public client is created without PKCE. Seeding an unsafe one would be seeding a
          live weakness rather than inert data.

        The whole run is idempotent: every step reuses what already exists, so a run that
        stopped halfway can simply be run again.

    .PARAMETER Skip
        Steps to leave out.

    .PARAMETER Tier
        Seed only the Core users (the hand-designed edge cases) or only the Bulk users (the
        generated volume). Both by default. Every other object type is
        hand-designed and seeded whole.

    .PARAMETER ShowProgress
        Report progress per user.

    .PARAMETER PassThru
        Return the results object.

    .OUTPUTS
        PSCustomObject describing every step, when -PassThru is used.

    .EXAMPLE
        PS> New-TestEnvironment -WhatIf

        DESCRIPTION: Shows every object the seed would create, creating none
        OUTPUT: A What if: line per object
        USE CASE: Checking what a seed will do before running it against an environment you care about

    .EXAMPLE
        PS> New-TestEnvironment -Tier Core -PassThru

        DESCRIPTION: Seeds every object type with only the designed people
        OUTPUT: The results object
        USE CASE: The fast loop, when the test is behaviour rather than scale

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/

    .LINK
        Connect-PingOneEnvironment
        Get-PingOneEnvironmentReport
        Remove-PingOneEnvironment
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'The step summary is written for the person watching the seed run; the result object carries the same data for scripts.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '',
        Justification = 'Delegated to the step functions, which each call ShouldProcess per object.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Attributes', 'Populations', 'Groups', 'Users', 'Resources', 'Applications')]
        [string[]]$Skip = @(),

        [Parameter()]
        [ValidateSet('Core', 'Bulk')]
        [string[]]$Tier,

        [Parameter()]
        [switch]$ShowProgress,

        [Parameter()]
        [switch]$PassThru
    )

    $correlationId = [Guid]::NewGuid()
    Write-Verbose "Starting New-PingOneEnvironment - CorrelationId: $correlationId"

    $connection = Get-PingOneConnection
    $startTime = Get-Date

    Write-TestMessage -Message ("PingOne Test Environment Creation ({0}, {1})" -f
        $connection.EnvironmentName, $connection.EnvironmentId) -Type Header

    # The user step is the only one that takes arguments from this command. They are built here,
    # rather than read from inside the step's scriptblock, so the parameters are visibly used.
    $userArguments = @{ PassThru = $true; ShowProgress = $ShowProgress }
    if ($Tier) { $userArguments['Tier'] = $Tier }

    # Each step is a name and the call that performs it. Held as data so the order is written
    # once and the loop below cannot drift from it.
    $plan = @(
        @{ Name = 'Attributes'; Label = 'Creating custom user attributes'; Run = { New-PingOneProfileAttribute -PassThru } }
        @{ Name = 'Populations'; Label = 'Creating populations'; Run = { New-PingOnePopulation -PassThru } }
        @{ Name = 'Groups'; Label = 'Creating groups and nesting them'; Run = { New-PingOneGroup -PassThru } }
        @{ Name = 'Users'; Label = 'Creating users and their memberships'; Run = { New-PingOneUser @userArguments } }
        @{ Name = 'Resources'; Label = 'Creating resources and scopes'; Run = { New-PingOneResource -PassThru } }
        @{ Name = 'Applications'; Label = 'Creating applications and granting scopes'; Run = { New-PingOneApplication -PassThru } }
    )

    $steps = [System.Collections.Generic.List[object]]::new()
    $number = 0

    foreach ($step in $plan) {
        $number++
        if ($Skip -contains $step.Name) {
            Write-Host ("Step {0}: Skipping {1}" -f $number, $step.Name.ToLowerInvariant())
            $steps.Add([PSCustomObject]@{ Name = $step.Name; Attempted = $false; Success = $false; Result = $null; Errors = @() })
            continue
        }

        Write-Host ("Step {0}: {1}" -f $number, $step.Label)

        try {
            $result = & $step.Run
            $stepErrors = @()
            if ($result -and $result.Errors) { $stepErrors = @($result.Errors) }
            $steps.Add([PSCustomObject]@{
                    Name      = $step.Name
                    Attempted = $true
                    Success   = ($stepErrors.Count -eq 0)
                    Result    = $result
                    Errors    = $stepErrors
                })
        }
        catch {
            # A step that throws is recorded rather than allowed to end the run, so the steps
            # after it that do not depend on it still happen and the summary says what stopped.
            Write-Warning "Step $($step.Name) failed: $($_.Exception.Message)"
            $steps.Add([PSCustomObject]@{
                    Name      = $step.Name
                    Attempted = $true
                    Success   = $false
                    Result    = $null
                    Errors    = @("Step $($step.Name) failed: $($_.Exception.Message)")
                })
        }
    }

    $endTime = Get-Date
    $attempted = @($steps | Where-Object Attempted)
    $failed = @($attempted | Where-Object { -not $_.Success })

    Write-TestMessage -Message 'Environment Creation Summary' -Type Header
    Write-Host ("Operations Completed: {0}/{1}" -f ($attempted.Count - $failed.Count), $attempted.Count)
    Write-Host ("Duration: {0:hh\:mm\:ss}" -f ($endTime - $startTime))
    foreach ($step in $failed) {
        foreach ($message in $step.Errors) { Write-Host (" ! {0}" -f $message) }
    }

    if ($PassThru) {
        return [PSCustomObject]@{
            CorrelationId   = $correlationId
            EnvironmentId   = $connection.EnvironmentId
            EnvironmentName = $connection.EnvironmentName
            Prefix          = $connection.Prefix
            StartTime       = $startTime
            EndTime         = $endTime
            Duration        = $endTime - $startTime
            Steps           = $steps.ToArray()
            Summary         = [PSCustomObject]@{
                TotalSteps      = $steps.Count
                AttemptedSteps  = $attempted.Count
                SuccessfulSteps = $attempted.Count - $failed.Count
                FailedSteps     = $failed.Count
            }
        }
    }
}