EpiCloud.psm1

function AddTlsSecurityProtocolSupport {
    <#
    .SYNOPSIS
    This helper function adds support for TLS protocol 1.1 and/or TLS 1.2
 
    .DESCRIPTION
    This helper function adds support for TLS protocol 1.1 and/or TLS 1.2
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [Bool] $EnableTls11 = $true,
        [Parameter(Mandatory=$false)]
        [Bool] $EnableTls12 = $true
    )

    # Add support for TLS 1.1 and TLS 1.2
    if (-not [Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls11) -AND $EnableTls11) {
        [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls11
    }

    if (-not [Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12) -AND $EnableTls12) {
        [Net.ServicePointManager]::SecurityProtocol += [Net.SecurityProtocolType]::Tls12
    }
}
function GetApiErrorResponse {
    <#
    .SYNOPSIS
    This helper function retrieves the returned error message from the API
 
    .DESCRIPTION
    This helper function retrieves the returned error message from the API
 
    Because of the way Invoke-RestMethod works, the error message would
    otherwise be "hidden" and only the http code would be returned
 
    #>


    [CmdletBinding()]
    [OutputType([String])]
    param(
        $ExceptionResponse
    )

    try {
        $errorResponseStream = $ExceptionResponse.GetResponseStream()
        $errorResponseStreamReader = New-Object System.IO.StreamReader($errorResponseStream)
        $errorResponseStreamReader.BaseStream.Position = 0
        $errorResponseStreamReader.DiscardBufferedData()
        $errorResponse = $errorResponseStreamReader.ReadToEnd()

        $errorResponse
    }
    catch {
        $ExceptionResponse
    }
}
function GetApiObjectEncoded {
    <#
    .SYNOPSIS
    This helper function converts the responses from the API
    so the encoding is correct
 
    .DESCRIPTION
    This helper function converts the responses from the API
    so the encoding is correct
 
    (Related to an encoding bug in Invoke-RestMethod)
 
    #>


    [CmdletBinding(DefaultParameterSetName='Response')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Response')]
        [System.Object[]] $ApiObject,

        [Parameter(Mandatory = $true, ParameterSetName = 'Payload')]
        [System.Object[]] $RequestPayload
    )

    $utf8 = [System.Text.Encoding]::GetEncoding(65001)
    $iso88591 = [System.Text.Encoding]::GetEncoding(28591) #ISO 8859-1 ,Latin-1

    if ($ApiObject) {
        $SerializedObject = ConvertTo-Json -Depth 10 -InputObject $ApiObject -Compress
        $bytesArray = [System.Text.Encoding]::Convert($utf8, $iso88591, $utf8.GetBytes($SerializedObject))
    }
    else {
        $SerializedObject = ConvertTo-Json -Depth 10 -InputObject $RequestPayload -Compress
        $bytesArray = [System.Text.Encoding]::Convert($iso88591, $utf8, $utf8.GetBytes($SerializedObject))
    }

    # Write the first results to the pipline
    $EncodedJsonString = $utf8.GetString($bytesArray)

    $EncodedObject = ConvertFrom-Json -InputObject $EncodedJsonString
    Write-Output $EncodedObject
}
function GetApiRequestSplattingHash {
    <#
    .SYNOPSIS
    This helper function creates a hashtable containing the basic properties needed
    to use the REST-api
 
    .DESCRIPTION
    This helper function creates a hashtable containing the basic properties needed
    to use the REST-api. Other properties will need to be added for most calls but
    that will be handled in the respective functions for those methods.
 
    #>


    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [String] $UriEnding,

        [Parameter(Mandatory = $false)]
        [ValidateSet(
            'Default',
            'Delete',
            'Get',
            'Head',
            'Merge',
            'Options',
            'Patch',
            'Post',
            'Put',
            'Trace'
        )]
        [String] $Method = 'Get'
    )

    if ($Global:EpiCloudApiEndpointUri) {
        $apiEndpoint = $Global:EpiCloudApiEndpointUri
    }
    else {
        $apiEndpoint = 'https://paasportal.episerver.net/api/v1.0/'
    }

    $hashToReturn = @{
        Headers     = @{
            Authorization = ''
        }
        Uri         = $apiEndpoint + $UriEnding
        ContentType = 'application/json'
        Method      = $Method
        ErrorAction = 'Stop'
        TimeoutSec  = 120
        Verbose     = $false
    }

    $hashToReturn
}
function ImportAzureStorageModule {
    $azureModuleLoaded = Get-Module -Name "Azure.Storage"
    $azModuleLoaded = Get-Module -Name "Az.Storage"

    if (-not ($azureModuleLoaded -or $azModuleLoaded)) {
        try {
            $null = Import-Module -Name "Az.Storage" -ErrorAction Stop
            $azModuleLoaded = $true
        }
        catch {
            Write-Verbose "Tried to find 'Az.Storage', module couldn't be imported."
        }
    }

    if (-not ($azureModuleLoaded -or $azModuleLoaded)) {
        try {
            $null = Import-Module -Name "Azure.Storage" -ErrorAction Stop
            $azureModuleLoaded = $true
        }
        catch {
            Write-Verbose "Tried to find 'Azure.Storage', module couldn't be imported."
        }
    }

    if ($azModuleLoaded) {
        "Az"
    }
    elseif ($azureModuleLoaded) {
        $azureModuleLoaded = Get-Module -Name "Azure.Storage"
        if ($azureModuleLoaded.Version.Major -lt 4 -or ($azureModuleLoaded.Version.Major -eq 4 -and $azureModuleLoaded.Version.Minor -lt 4)) {
            # Previous versions of Azure.Storage do not support SAS links with write-only permission.
            throw "'Azure.Storage' version 4.4.0 or greater is required."
        }
        "Azure"
    }
    else {
        throw "'Az.Storage' or 'Azure.Storage' module is required to run this cmdlet."
    }
}
function InvokeApiRequest {
    <#
    .SYNOPSIS
    This helper function creates does the actual API call
 
    .DESCRIPTION
    This helper function does the actual API call and signs the request
 
    #>


    [CmdletBinding(DefaultParameterSetName = 'NoRequestPayload')]
    param(
        [Parameter(Mandatory = $true)]
        [String] $ClientKey,

        [Parameter(Mandatory = $true)]
        [String] $ClientSecret,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable] $RequestSplattingHash,

        [Parameter(Mandatory = $true, ParameterSetName = 'ObjectRequestPayload')]
        [System.Collections.Hashtable] $RequestPayload
    )

    begin {
        AddTlsSecurityProtocolSupport
    }

    process {
        if ($RequestPayload) {
            try {
                $encodedString = GetApiObjectEncoded -RequestPayload $RequestPayload
                $encodedPayload = ConvertTo-Json -Depth 10 -InputObject $encodedString
            }
            catch {
                throw "Failed to encode the request payload. The error was: $($_.Exception.Message)"
            }

            $RequestSplattingHash.Add('Body', $encodedPayload)
        }

        $setApiAuthorizationHeaderParams = @{
            ClientKey    = $ClientKey
            ClientSecret = $ClientSecret
            RequestHash  = $RequestSplattingHash
        }

        SetApiAuthorizationHeader @setApiAuthorizationHeaderParams

        try {
            $response = Invoke-RestMethod @RequestSplattingHash
        }
        catch {
            $errorMessage = GetApiErrorResponse -ExceptionResponse $_.Exception.Response
            throw "API call failed! The error was: $($_.Exception.Message) $errorMessage"
        }

        if (!$response.success) {
            throw "API call failed! The error(s) was: $(($response.errors) -join ', ')"
        }

        if ($response.result) {
            GetApiObjectEncoded -ApiObject $response.result
        }
    }

    end { }
}
function SetApiAuthorizationHeader {
    <#
    .SYNOPSIS
    This helper function contains the logic for signing the request/creating the
    authorization header
 
    .DESCRIPTION
    This helper function contains the logic for signing the request/creating the
    authorization header
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $ClientKey,

        [Parameter(Mandatory=$true)]
        [String] $ClientSecret,

        [Parameter(Mandatory=$true)]
        [System.Collections.Hashtable] $RequestHash
    )

    # Initialize utils required for computing an HMAC and md5 signature/hash
    $hmacAlgorithm = New-Object System.Security.Cryptography.HMACSHA256
    $md5 = [System.Security.Cryptography.MD5]::Create()

    # Set the secret the HMAC algorithm uses for computing the signature
    $hmacAlgorithm.key = [System.Convert]::FromBase64String($ClientSecret)

    # Define the different parts that make up the HMAC signature
    $path = ([System.Uri] $RequestHash.Uri).PathAndQuery
    $method = $RequestHash.Method.ToUpperInvariant()
    $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliSeconds().ToString("0")
    $nonce = (New-Guid).ToString("N")

    # Define the HTTP request payload that will be tacked on to the signature
    if ($RequestHash.Body) {
        $bodyBytes = [Text.Encoding]::UTF8.GetBytes($RequestHash.Body)
    }
    else {
        $bodyBytes = [Text.Encoding]::UTF8.GetBytes('')
    }

    $bodyHashBytes = $md5.ComputeHash($bodyBytes)
    $hashBody = [Convert]::ToBase64String($bodyHashBytes)

    # Combine all the parts into a signature message
    $message = "{0}{1}{2}{3}{4}{5}" -f $ClientKey, $method, $path, $timestamp, $nonce, $hashBody
    $messageBytes = [Text.Encoding]::UTF8.GetBytes($message)

    # Define the HMAC signature from the message
    $signatureHash = $hmacAlgorithm.ComputeHash($messageBytes);
    $signature = [Convert]::ToBase64String($signatureHash)

    # Define the authorization header for the HTTP request
    $authorization = "epi-hmac {0}:{1}:{2}:{3}" -f $ClientKey, $timestamp, $nonce, $signature

    # Set the header
    $RequestHash.Headers.Authorization = $authorization
}
function Add-EpiDeploymentPackage {
    <#
    .SYNOPSIS
        Will upload the specified file to the code package container by using a SAS link.
 
    .DESCRIPTION
        Will upload the specified file to the code package container by using a SAS link.
 
        Requires storage cmdlets from the Azure.Storage module to run.
 
    .PARAMETER Path
        The file name with full path
 
    .PARAMETER BlobName
        The name of Blob in the container.
 
    .PARAMETER SasUrl
        The Sas link contains access to storage account.
 
    .EXAMPLE
        Add-EpiDeploymentPackage -SasUrl "https://thle2307mh134.blob.core.windows.net/deploymentpackages?sv=2017-04-17&sr=c&sig=MGW6ndtX1vNT%2BSRpLUT8vPuurteyb" -Path .\site.cms.app.1.0.0.nupkg
 
        Uploads the file ".\site.cms.app.1.0.0.nupkg" to the code package container on the storage account specified via the SAS link ("SasUrl")
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String] $SasUrl,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [String] $Path,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [String] $BlobName
    )

    begin {
        AddTlsSecurityProtocolSupport

        $DefaultContainer = "deploymentpackages"

        $urlComponent = $SasUrl -split $DefaultContainer
        if ($urlComponent.Length -lt 2 -or -not $SasUrl.StartsWith('https://') -or -not $SasUrl.Contains('.blob.core.windows.net/')) {
            throw "The SasUrl is not correct"
        }

        $sasToken = $urlComponent[1].ToString()
        $storageAccountName = $urlComponent[0].ToString().Replace('https://', '').Replace('.blob.core.windows.net/', '')

        $azureModuleType = ImportAzureStorageModule

        if ($azureModuleType -eq "Azure") {
            $storageAccountContext = New-AzureStorageContext -StorageAccountName $storageAccountName -SASToken $sasToken -ErrorAction Stop
        }
        else {
            $storageAccountContext = New-AzStorageContext -StorageAccountName $storageAccountName -SASToken $sasToken -ErrorAction Stop
        }
    }

    process {
        if (-not $BlobName) {
            $BlobName = Split-Path $Path -leaf
        }

        Write-Verbose "Uploading blob $BlobName to storage account ..."

        $setAzureStorageBlobContentParams = @{
            File        = $Path
            Container   = $DefaultContainer
            Blob        = $BlobName
            Context     = $storageAccountContext
            Force       = $true
            ErrorAction = 'Stop'
        }

        if ($azureModuleType -eq "Azure") {
            $null = Set-AzureStorageBlobContent @setAzureStorageBlobContentParams
        }
        else {
            $null = Set-AzStorageBlobContent @setAzureStorageBlobContentParams
        }

        Write-Verbose "Done!"
    }

    end { }
}
function Complete-EpiDeployment {
    <#
    .SYNOPSIS
        This function will complete the specified code deployment
 
    .DESCRIPTION
        This function will complete the specified code deployment
 
    .EXAMPLE
        $clientKey = '12331bpXbHmuTDkhZmtUAq1scYsEbCIlY4N355SWLmTq1cgi'
        $clientSecret = '123456dkQQZ2ohrVMwtyZAtEkqHd75l2f9ACPIN1nm4pHmZDQ4NMikCNWBlZ2H6D'
        $projectId = '8ad8cc5b-0c49-4b79-8ebd-6451465a92c2'
        $deploymentId = '6c0ed684-8548-48aa-8695-fcfd43477b43'
 
        Complete-EpiDeployment -ClientKey $clientKey -ClientSecret $clientSecret -ProjectId $projectId -Id $deploymentId
 
    .EXAMPLE
        $clientKey = '12331bpXbHmuTDkhZmtUAq1scYsEbCIlY4N355SWLmTq1cgi'
        $clientSecret = '123456dkQQZ2ohrVMwtyZAtEkqHd75l2f9ACPIN1nm4pHmZDQ4NMikCNWBlZ2H6D'
        $projectId = '8ad8cc5b-0c49-4b79-8ebd-6451465a92c2'
        $deploymentId = '6c0ed684-8548-48aa-8695-fcfd43477b43'
 
        Complete-EpiDeployment -ClientKey $clientKey -ClientSecret $clientSecret -ProjectId $projectId -Id $deploymentId -Wait -WaitTimeoutMinutes 10 -PollingIntervalSeconds 10
 
    .PARAMETER ClientKey
        The client key used to access the project.
 
    .PARAMETER ClientSecret
        The client secret used to access the project.
 
    .PARAMETER ProjectId
        The Id (should be a guid) of the project.
 
    .PARAMETER Id
        The Id of the deployment that you want to complete.
 
    .PARAMETER Wait
        Specify this switch to wait for the deployment to finish.
 
    .PARAMETER WaitTimeoutMinutes
        The maximum amount of time, in minutes, to retry to check the deployment status.
        Default to 30 minutes.
 
    .PARAMETER PollingIntervalSeconds
        The time interval, in seconds, to check the deployment status.
        Default to 30 seconds.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String] $ClientKey,

        [Parameter(Mandatory = $true)]
        [String] $ClientSecret,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String] $ProjectId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String] $Id,

        [Parameter(Mandatory = $false)]
        [Switch] $Wait,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60)]
        [Int] $WaitTimeoutMinutes = 30,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60)]
        [Int] $PollingIntervalSeconds = 30
    )

    begin { }

    process {
        $uriEnding = "projects/$ProjectId/deployments/$Id/complete"

        $requestHash = GetApiRequestSplattingHash -UriEnding $uriEnding -Method 'Post'

        $completeEpiDeploymentParams = @{
            ClientKey            = $ClientKey
            ClientSecret         = $ClientSecret
            RequestSplattingHash = $requestHash
        }

        $deploymentDetails = InvokeApiRequest @completeEpiDeploymentParams

        if ($Wait.IsPresent) {
            $timeoutDate = (Get-Date).AddMinutes($WaitTimeoutMinutes)

            do {
                Write-Verbose "Deployment status: $($deploymentDetails.status)"
                Start-Sleep -Second $PollingIntervalSeconds
                $getEpiDeploymentDetailsParams = @{
                    ClientKey    = $ClientKey
                    ClientSecret = $ClientSecret
                    ProjectId    = $ProjectId
                    Id           = $Id
                }
                $deploymentDetails = Get-EpiDeployment @getEpiDeploymentDetailsParams
            }
            while ($deploymentDetails.status -notin @('Failed', 'Succeeded') -and (Get-Date) -le $timeoutDate)
        }

        $deploymentDetails
    }

    end { }
}
function Connect-EpiCloud {
    <#
    .SYNOPSIS
        Adds credentials (ClientKey and Client Secret) for all functions
        in EpiCloud module to be used during the session/context.
 
    .DESCRIPTION
        This function will specify the default credentials (ClientKey and Client Secrets)
        that should be used for all functions in the current session/context.
 
    .EXAMPLE
        Connect-EpiCloud -ClientKey 12331bpXbHmuTDkhZmtUAq1scYsEbCIlY4N355SWLmTq1123 -ClientSecret UowYA4dkQQZ2ohrVMwtyZAtEkqHd75l2f9ACPIN1nm4pHmZDQ4NMikCNWBlZ2H6D
 
        Get-EpiDeploymentPackageLocation -ProjectId 1234567890
 
    .PARAMETER ClientKey
        The client key used to access the project.
 
    .PARAMETER ClientSecret
        The client secret used to access the project.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String] $ClientKey,

        [Parameter(Mandatory = $true)]
        [String] $ClientSecret
    )

    begin {
        $commands = Get-Command -Module 'EpiCloud'
    }

    process {
        foreach ($command in $commands) {
            if ($global:PSDefaultParameterValues."$($command.Name):ClientKey") {
                $global:PSDefaultParameterValues.Remove("$($command.Name):ClientKey")
            }

            if ($global:PSDefaultParameterValues."$($command.Name):ClientSecret") {
                $global:PSDefaultParameterValues.Remove("$($command.Name):ClientSecret")
            }

            $global:PSDefaultParameterValues.Add("$($command.Name):ClientKey", $ClientKey)
            $global:PSDefaultParameterValues.Add("$($command.Name):ClientSecret", $ClientSecret)
        }
    }

    end { }
}
function Get-EpiDeployment {
    <#
        .SYNOPSIS
        Retrieves deployments that have been triggered via the deployment api for the specified project id
 
        .DESCRIPTION
        Retrieves deployments that have been triggered via the deployment api for the specified project id
 
        .PARAMETER ClientKey
        The client key used to access the project
 
        .PARAMETER ClientSecret
        The client secret used to access the project
 
        .PARAMETER ProjectId
        The Id (should be a guid) of the project
 
        .PARAMETER Id
        The Id (should be a guid) of the deployment
 
        .EXAMPLE
        Get-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv
 
        .EXAMPLE
        Get-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv -Id d142a635-c09e-4c56-a4ba-394d0dd7a14a
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $ClientKey,

        [Parameter(Mandatory=$true)]
        [String] $ClientSecret,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName = $true)]
        [String] $ProjectId,

        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName = $true)]
        [String] $Id
    )

    begin { }

    process {
        $uriEnding = "projects/$ProjectId/deployments"

        if ($Id) {
            $uriEnding += "/$Id"
        }

        $requestHash = GetApiRequestSplattingHash -UriEnding $uriEnding

        $invokeApiRequestSplat = @{
            ClientSecret = $ClientSecret
            ClientKey = $ClientKey
            RequestSplattingHash = $requestHash
        }

        InvokeApiRequest @invokeApiRequestSplat
    }

    end { }
}
function Get-EpiDeploymentPackageLocation {
    <#
        .SYNOPSIS
        Retrieves the location where deployment packages can be uploaded
 
        .DESCRIPTION
        Retrieves the location where deployment packages can be uploaded
 
        This will be a SAS-link to an Azure blob storage account
 
        .EXAMPLE
        Get-EpiDeploymentPackageLocation -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv
 
        .PARAMETER ClientKey
        The client key used to access the project
 
        .PARAMETER ClientSecret
        The client secret used to access the project
 
        .PARAMETER ProjectId
        The Id (should be a guid) of the project
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String] $ClientKey,

        [Parameter(Mandatory=$true)]
        [String] $ClientSecret,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName = $true)]
        [String] $ProjectId
    )


    begin { }

    process {

        $uriEnding = "projects/$ProjectId/packages/location"
        $requestHash = GetApiRequestSplattingHash -UriEnding $uriEnding

        $invokeApiRequestSplat = @{
            ClientSecret = $ClientSecret
            ClientKey = $ClientKey
            RequestSplattingHash = $requestHash
        }

        # Since the response only contains one property we expand it
        (InvokeApiRequest @invokeApiRequestSplat).location
    }

    end { }
}
function Reset-EpiDeployment {
    <#
        .SYNOPSIS
        Resets the specified deployment
 
        .DESCRIPTION
        Resets the specified deployment
 
        .PARAMETER ClientKey
        The client key used to access the project
 
        .PARAMETER ClientSecret
        The client secret used to access the project
 
        .PARAMETER ProjectId
        The Id (should be a guid) of the project.
 
        .PARAMETER Id
        The Id (should be a guid) of the deployment
 
        .PARAMETER Wait
        Specify this switch to wait for the deployment to finish.
 
        .PARAMETER WaitTimeoutMinutes
        The maximum amount of time, in minutes, to retry to check the deployment status.
        Default to 30 minutes.
 
        .PARAMETER PollingIntervalSeconds
        The time interval, in seconds, to check the deployment status.
        Default to 30 seconds.
 
        .EXAMPLE
        Reset-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId 423ae883-7202-44cb-a907-3006d0d1cd58 -Id d117c12c-d02e-4b53-aabd-aa8e00a47cdv
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String] $ClientKey,

        [Parameter(Mandatory = $true)]
        [String] $ClientSecret,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String] $ProjectId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String] $Id,

        [Parameter(Mandatory = $false)]
        [Switch] $Wait,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60)]
        [Int] $WaitTimeoutMinutes = 30,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60)]
        [Int] $PollingIntervalSeconds = 30
    )

    begin { }

    process {

        $uriEnding = "projects/$ProjectId/deployments/$Id/reset"
        $requestHash = GetApiRequestSplattingHash -UriEnding $uriEnding -Method 'Post'

        $invokeApiRequestParams = @{
            ClientSecret = $ClientSecret
            ClientKey = $ClientKey
            RequestSplattingHash = $requestHash
        }

        $resetDeploymentDetails = InvokeApiRequest @invokeApiRequestParams

        if ($Wait.IsPresent) {
            $timeoutDate = (Get-Date).AddMinutes($WaitTimeoutMinutes)
            $getEpiDeploymentDetailsParams = @{
                ClientKey    = $ClientKey
                ClientSecret = $ClientSecret
                ProjectId    = $ProjectId
                Id           = $Id
            }
            do {
                Start-Sleep -Second $PollingIntervalSeconds

                $resetDeploymentDetails = Get-EpiDeployment @getEpiDeploymentDetailsParams
                Write-Verbose -Message "Deployment status: $($resetDeploymentDetails.status)"
            }
            while ($resetDeploymentDetails.status -notin @('Failed', 'Succeeded') -and (Get-Date) -le $timeoutDate)
        }

        $resetDeploymentDetails
    }

    end { }
}
function Start-EpiDeployment {
    <#
    .SYNOPSIS
        Starts a code deployment for the specified project
 
    .DESCRIPTION
        Starts a code deployment for the specified project
 
    .PARAMETER ClientKey
        The client key used to access the project.
 
    .PARAMETER ClientSecret
        The client secret used to access the project.
 
    .PARAMETER ProjectId
        The project identifier.
 
    .PARAMETER SourceApp
        The source app(s) for the deployment (if no uploaded code package should be used)
 
    .PARAMETER SourceEnvironment
        The source environment for the deployment (if no uploaded code package should be used)
 
    .PARAMETER TargetEnvironment
        The target environment which the code is deploying to.
 
    .PARAMETER DeploymentPackage
        The code package(s) being deployed
 
    .PARAMETER UseMaintenancePage
        The flag to tell whether maintenance page is used during the deployment.
 
    .PARAMETER IncludeBlob
        Specify this switch to include blobs from the source environment
 
    .PARAMETER IncludeDb
        Specify this switch to include the SQL DB from the source environment
 
    .PARAMETER Wait
        Specify this switch to enable "polling" of the deployment until it's completed.
 
    .PARAMETER PollingIntervalSec
        The timeout, in seconds, to wait between retries when getting the deployment details.
 
    .PARAMETER WaitTimeOutSec
        The maximum amount of time to retry to get the deployment details.
 
    .EXAMPLE
        Start-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv -TargetEnvironment Integration -DeploymentPackage cms.app.1.0.0.nupkg
 
        Deploys a code package to the Integration environment
 
    .EXAMPLE
        Start-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv -TargetEnvironment Integration -DeploymentPackage cms.app.1.0.0.nupkg -Wait -PollingIntervalSec 10 -WaitTimeOutSec 1800
 
        Deploys a code package to the Integration environment and waits for it to finish (for up to 30 minutes)
 
    .EXAMPLE
        Start-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv -SourceEnvironment Integration -SourceApp cms -TargetEnvironment Preproduction -IncludeBlob -IncludeDb
 
        Starts a deployment of the contents of the specified cms app and the blobs and dbs of that environment
 
    .EXAMPLE
        Start-EpiDeployment -ClientKey $myKey -ClientSecret $mySecret -ProjectId d117c12c-d02e-4b53-aabd-aa8e00a47cdv -SourceEnvironment Production -TargetEnvironment Integration -IncludeBlob -IncludeDb
 
        Starts a deployment of the contents of the production environment (blobs and db(s)) to the Integration environment
    #>


    [CmdletBinding(DefaultParameterSetName='DeploymentPackage')]
    param(
        [Parameter(Mandatory = $true)]
        [String] $ClientKey,

        [Parameter(Mandatory = $true)]
        [String] $ClientSecret,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String] $ProjectId,

        [Parameter(Mandatory = $false, ParameterSetName = 'SourceEnvironment')]
        [ValidateSet('cms','commerce')]
        [String[]] $SourceApp,

        [Parameter(Mandatory = $true, ParameterSetName = 'SourceEnvironment')]
        [String] $SourceEnvironment,

        [Parameter(Mandatory = $true)]
        [String] $TargetEnvironment,

        [Parameter(Mandatory = $true, ParameterSetName = 'DeploymentPackage')]
        [String[]] $DeploymentPackage,

        [Parameter(Mandatory = $false)]
        [Switch] $UseMaintenancePage,

        [Parameter(Mandatory = $false, ParameterSetName = 'SourceEnvironment')]
        [Switch] $IncludeBlob,

        [Parameter(Mandatory = $false, ParameterSetName = 'SourceEnvironment')]
        [Switch] $IncludeDb,

        [Parameter(Mandatory = $false)]
        [Switch] $Wait,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 60)]
        [Int] $PollingIntervalSec = 30,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        [Int] $WaitTimeOutSec = 7200
    )

    begin {}
    process {
        if ($Wait.IsPresent) {
            $timeOutDateTime = (Get-Date).AddSeconds($WaitTimeOutSec)
        }
        else {
            $timeOutDateTime = Get-Date
        }

        $uriEnding = "projects/$ProjectId/deployments"
        $requestSplattingHash = GetApiRequestSplattingHash -UriEnding $uriEnding -Method 'Post'

        $startDeploymentParams = @{
            ClientKey            = $ClientKey
            ClientSecret         = $ClientSecret
            RequestSplattingHash = $requestSplattingHash
            RequestPayload       = @{
                TargetEnvironment = $TargetEnvironment
                MaintenancePage   = $UseMaintenancePage.IsPresent
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'DeploymentPackage') {
            $startDeploymentParams.RequestPayload.Packages = $DeploymentPackage
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'SourceEnvironment') {
            $startDeploymentParams.RequestPayload.sourceEnvironment = $SourceEnvironment

            if ($SourceApp) {
                $startDeploymentParams.RequestPayload.sourceApps = $SourceApp
            }

            $startDeploymentParams.RequestPayload.includeBlob = $IncludeBlob.IsPresent
            $startDeploymentParams.RequestPayload.includeDB = $IncludeDb.IsPresent

            if (-not $startDeploymentParams.RequestPayload.sourceApps -and
                -not $IncludeBlob.IsPresent -and
                -not $IncludeDb.IsPresent) {
                    throw "You need to specify at least one of the following parameters: DeploymentPackage, SourceApp, IncludeBlob or IncludeDb."
            }
        }

        Write-Verbose "Starting deployment for the project: $($ProjectId) / targetEnvironment: $($TargetEnvironment)"
        $startDeploymentResponse = InvokeApiRequest @startDeploymentParams

        $deploymentCompletionStates = @('Failed', 'AwaitingVerification', 'Succeeded')
        do {
            if ($Wait.IsPresent) {
                Start-Sleep -Seconds $PollingIntervalSec
            }
            $getDeploymentParams = @{
                ClientKey    = $ClientKey
                ClientSecret = $ClientSecret
                ProjectId    = $ProjectId
                Id           = $startDeploymentResponse.id
            }

            $getDeploymentResponse = Get-EpiDeployment @getDeploymentParams
            Write-Verbose "Deployment status: $($getDeploymentResponse.status)"
        } while ($Wait.IsPresent -and $deploymentCompletionStates -notcontains $getDeploymentResponse.status -and (Get-Date) -le ($timeOutDateTime))

        if ($Wait.IsPresent -and $deploymentCompletionStates -notcontains $getDeploymentResponse.status) {
            throw "Timed out during deployment with status: $($getDeploymentResponse.status)"
        }

        $getDeploymentResponse
    }
    end {}
}