Source/Private/Export_Main.ps1


Set-StrictMode -Version Latest

#region Function: Export-ODUOctopusDeployConfigMain

<#
.SYNOPSIS
Main function controlling a standard export process
.DESCRIPTION
Main function controlling a standard export process:
 - creates folder that will be the root for this export;
 - creates item id lookup object;
 - create job detail object(s) for each API to query;
 - creates background jobs to process detail objects, aggregating item id lookups to process after;
 - creates background jobs to process lookup ids;
 - returns folder path.
.EXAMPLE
Export-ODUOctopusDeployConfigMain
<exports data into newly created folder>
#>

function Export-ODUOctopusDeployConfigMain {
  [CmdletBinding()]
  [OutputType([string])]
  param()
  process {
    # this function has gotten a bit big and should be refactored...

    # get Octopus Server details now, pass into job creation
    $OctopusServer = Get-ODUConfigOctopusServer
    $ServerName = $OctopusServer.Name
    $ServerUrl = $OctopusServer.Url
    $ApiKey = Convert-ODUDecryptText -Text ($OctopusServer.ApiKey)

    # create root folder for this export instance
    $CurrentExportRootFolder = New-ODURootExportFolder -MainExportRoot (Get-ODUConfigExportRootFolder) -ServerName $ServerName -DateTime (Get-Date)

    # get filtered list of api call details to process
    [object[]]$ApiCalls = Get-ODUFilteredExportRestApiCall
    # create folders for each api call
    Write-Verbose "$($MyInvocation.MyCommand) :: Creating folder for api calls"
    New-ODUFolderForEachApiCall -ParentFolder $CurrentExportRootFolder -ApiCalls $ApiCalls

    # for ItemIdOnly calls, create lookup with key of reference property names and value empty array (for capturing values)
    [hashtable]$ItemIdOnlyIdsLookup = @{}
    # only attempt create lookup if items to get
    if (($null -ne $ApiCalls) -and ($ApiCalls.Count -gt 0) -and ($null -ne ($ApiCalls | Where-Object { $_.ApiFetchType -eq $ApiFetchType_ItemIdOnly }))) {
      $ItemIdOnlyIdsLookup = Initialize-ODUFetchTypeItemIdOnlyIdsLookup -ApiCalls ($ApiCalls | Where-Object { $_.ApiFetchType -eq $ApiFetchType_ItemIdOnly })
    }

    # put into simple object for easier Using: reference
    $ItemIdOnlyIdsLookupKeys = $ItemIdOnlyIdsLookup.Keys

    # loop through non-ItemIdOnly calls creating zero, one more more jobs for exporting content from it
    [object[]]$ExportJobDetails = $ApiCalls | Where-Object { $_.ApiFetchType -ne $ApiFetchType_ItemIdOnly } | ForEach-Object {
      $ApiCall = $_
      New-ODUExportJobInfo -ServerBaseUrl $ServerUrl -ApiKey $ApiKey -ApiCall $ApiCall -ParentFolder $CurrentExportRootFolder
    }

    [int]$Throttle = Get-ODUConfigBackgroundJobsMax
    # just in case a sneaky user manually edited the config file to go higher than 9
    if ($Throttle -gt 9) { $Throttle = 9 }

    # process (export/save) the non-ItemIdOnly jobs, capturing ItemIdOnly Ids to process after
    if (($null -ne $ExportJobDetails) -and ($ExportJobDetails.Count -gt 0)) {
      $Jobs = $ExportJobDetails | Start-RSJob -Throttle $Throttle -ModulesToImport $ThisModuleName -ScriptBlock {
        # values are returned, we'll fetch after jobs complete
        Export-ODUJob -ExportJobDetail $_ -ItemIdOnlyReferencePropertyNames $Using:ItemIdOnlyIdsLookupKeys
      }

      $null = Wait-RSJob -Job $Jobs
      # there could be errors; collect all of them first and remove jobs before throwing errors or
      # other jobs will never get removed
      [object[]]$Errors = $null
      $Jobs | ForEach-Object {
        $Job = $_
        if ($Job.HasErrors) {
          $Errors += Select-Object -InputObject $Job -ExpandProperty Error
        } else {
          $ItemIdOnlyDetails = Receive-RSJob -Job $Job
          # transfer values to main hash table
          $ItemIdOnlyDetails.Keys | ForEach-Object { $ItemIdOnlyIdsLookup.$_ += $ItemIdOnlyDetails.$_ }
        }
        $null = Remove-RSJob -Job $Job
      }
      if (($null -ne $Errors) -and ($Errors.Count -gt 0)) {
         $Errors | ForEach-Object { throw $_ }
      }
    }

    # now loop through ItemIdOnly calls, creating jobs using captured ItemIdOnly Ids
    [object[]]$ExportJobDetails = $ApiCalls | Where-Object { $_.ApiFetchType -eq $ApiFetchType_ItemIdOnly } | ForEach-Object {
      $ApiCall = $_
      $ItemIdOnlyPropertyName = $ApiCall.ItemIdOnlyReferencePropertyName
      if (($null -ne $ItemIdOnlyIdsLookup.$ItemIdOnlyPropertyName) -and ($ItemIdOnlyIdsLookup.$ItemIdOnlyPropertyName.Count -gt 0)) {
        New-ODUExportJobInfo -ServerBaseUrl $ServerUrl -ApiKey $ApiKey -ApiCall $ApiCall -ParentFolder $CurrentExportRootFolder -ItemIdOnlyIds $ItemIdOnlyIdsLookup.$ItemIdOnlyPropertyName
      }
    }

    # process (export/save) the ItemIdOnly jobs
    if (($null -ne $ExportJobDetails) -and ($ExportJobDetails.Count -gt 0)) {
      $Jobs = $ExportJobDetails | Start-RSJob -Throttle $Throttle -ModulesToImport $ThisModuleName -ScriptBlock {
        # shouldn't be any values returned; even if there are, we ignore
        $null = Export-ODUJob -ExportJobDetail $_ -ItemIdOnlyReferencePropertyNames $Using:ItemIdOnlyIdsLookupKeys
      }
      $null = Wait-RSJob -Job $Jobs

      # there should be no output that we care about from these jobs but still check for errors
      [object[]]$Errors = $null
      $Jobs | ForEach-Object {
        $Job = $_
        if ($Job.HasErrors) { $Errors += Select-Object -InputObject $Job -ExpandProperty Error }
        $null = Remove-RSJob -Job $Job
      }
      if (($null -ne $Errors) -and ($Errors.Count -gt 0)) {
        $Errors | ForEach-Object { throw $_ }
      }
    }

    # return path to this export
    $CurrentExportRootFolder
  }
}
#endregion


#region Function: Get-ODUItemIdOnlyReferenceValue

<#
.SYNOPSIS
Returns standard export rest api call info filtered based on user black / white list
.DESCRIPTION
Returns standard export rest api call info filtered based on user black / white list
.PARAMETER ExportJobDetail
Details about export job
.PARAMETER ItemIdOnlyReferencePropertyNames
Property names to look for in exported item, find values for these properties and return
.PARAMETER ExportItem
Exported data item to review
.EXAMPLE
Get-ODUItemIdOnlyReferenceValue
<returns subset of rest api call objects>
#>

function Get-ODUItemIdOnlyReferenceValue {
  [CmdletBinding()]
  [OutputType([hashtable])]
  param(
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [object]$ExportJobDetail,
    [Parameter(Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [object]$ExportItem,
    [string[]]$ItemIdOnlyReferencePropertyNames
  )
  process {
    [hashtable]$ItemIdOnlyReferenceValues = @{}
    $ItemIdOnlyReferencePropertyNames | ForEach-Object {
      $ItemIdOnlyReferencePropertyName = $_
      if ($null -ne (Get-Member -InputObject $ExportItem -Name $ItemIdOnlyReferencePropertyName)) {
        Write-Verbose "$($MyInvocation.MyCommand) :: Property $ItemIdOnlyReferencePropertyName FOUND on $($ExportJobDetail.ApiCall.RestName) with id $($ExportItem.Id)"
        # add array entry if first time
        if (! $ItemIdOnlyReferenceValues.Contains($ItemIdOnlyReferencePropertyName)) {
          $ItemIdOnlyReferenceValues.$ItemIdOnlyReferencePropertyName = @()
        }
        Write-Verbose "$($MyInvocation.MyCommand) :: ItemIdOnly reference value is: $($ExportItem.$ItemIdOnlyReferencePropertyName)"
        $ItemIdOnlyReferenceValues.$ItemIdOnlyReferencePropertyName += $ExportItem.$ItemIdOnlyReferencePropertyName
      } else {
        Write-Verbose "$($MyInvocation.MyCommand) :: Property $ItemIdOnlyReferencePropertyName NOT found on $($ExportJobDetail.ApiCall.RestName)"
      }
    }
    $ItemIdOnlyReferenceValues
  }
}
#endregion


#region Function: New-ODUExportJobInfo

<#
.SYNOPSIS
Create PSObject with necessary info to export data from a single api call
.DESCRIPTION
Create PSObject with necessary info to export data from a single api call
.PARAMETER ServerBaseUrl
Base of the url, typically http/s along with domain name but no trailing /
.PARAMETER ApiKey
ApiKey to use with export
.PARAMETER ApiCall
Api call information
.PARAMETER ParentFolder
Root export folder
.PARAMETER ItemIdOnlyIds
List of Ids to use when creating Url
Used with creating jobs for types that can only be exported via Id
.EXAMPLE
New-ODUExportJobInfo
<...>
#>

function New-ODUExportJobInfo {
  [CmdletBinding()]
  param(
    [ValidateNotNullOrEmpty()]
    [string]$ServerBaseUrl = $(throw "$($MyInvocation.MyCommand) : missing parameter ServerBaseUrl"),
    [ValidateNotNullOrEmpty()]
    [string]$ApiKey = $(throw "$($MyInvocation.MyCommand) : missing parameter ApiKey"),
    [ValidateNotNullOrEmpty()]
    [object]$ApiCall = $(throw "$($MyInvocation.MyCommand) : missing parameter ApiCall"),
    [ValidateNotNullOrEmpty()]
    [string]$ParentFolder = $(throw "$($MyInvocation.MyCommand) : missing parameter ParentFolder"),
    [string[]]$ItemIdOnlyIds
  )
  process {
    # this appears to be the Octo desired default; I won't increase this least it beats up the servers
    [int]$DefaultTake = 30
    [object[]]$ExportJobs = @()

    # create basic hash table now
    $ExportFolder = Join-Path -Path $ParentFolder -ChildPath (Get-ODUFolderNameForApiCall -ApiCall $ApiCall)
    $MainUrl = $ServerBaseUrl + $ApiCall.RestMethod
    $ExportJobBaseSettings = @{
      Url          = $MainUrl
      ApiKey       = $ApiKey
      ExportFolder = $ExportFolder
      ApiCall      = $ApiCall
    }

    # if this is a Simple fetch, create a single job and return
    if ($ApiCall.ApiFetchType -eq $ApiFetchType_Simple) {
      Write-Verbose "$($MyInvocation.MyCommand) :: creating Simple fetch export job for $($ApiCall.RestName)"
      # only one value in Simple call, return base settings
      $ExportJobs += [PSCustomObject]$ExportJobBaseSettings

    } elseif ($ApiCall.ApiFetchType -eq $ApiFetchType_MultiFetch) {
      # it order to create the MultiFetch urls we actually need to call the API first
      # with a Take of 1 (retrieve only 1 record, if it exists) then use the TotalResults
      # to construct the urls
      $RestResults = Invoke-ODURestMethod -Url $MainUrl -ApiKey $ApiKey

      # results might be null if user doesn't have access to that api
      if (($null -ne $RestResults) -and ($null -ne (Get-Member -InputObject $RestResults -Name TotalResults))) {
        $TotalLoops = [math]::Floor($RestResults.TotalResults / $DefaultTake)
        # add extra loop if not perfect division
        if (($RestResults.TotalResults % $DefaultTake) -ne 0) { $TotalLoops += 1 }
        for ($LoopCount = 0; $LoopCount -le ($TotalLoops - 1); $LoopCount++) {
          $Skip = $LoopCount * $DefaultTake
          # clone base settings and update url
          $Clone = $ExportJobBaseSettings.Clone()
          $Clone.Url = $MainUrl + '?skip=' + $Skip + '&take=' + $DefaultTake
          $ExportJobs += [PSCustomObject]$Clone
        }
      } else {
        # this is for TenantVariables, which returns multiple values that should be stored in multiple files
        # BUT, for whatever really dumb reason, Octo API does not provide this info in the standard TotalResults / .Items format
        # so we have this dumb workaround here and in the job processing code
        # add with url as-is; processing code will handle it
        $ExportJobs += [PSCustomObject]$ExportJobBaseSettings
      }
    } elseif ($ApiCall.ApiFetchType -eq $ApiFetchType_ItemIdOnly) {
      $ItemIdOnlyIds | ForEach-Object {
        $Id = $_
        # clone base settings and update url
        $Clone = $ExportJobBaseSettings.Clone()
        $Clone.Url = $MainUrl + '/' + $Id
        $ExportJobs += [PSCustomObject]$Clone
      }
    }
    $ExportJobs
  }
}
#endregion