Invoke-Terraform.psm1


Function Copy-TerraformBinary {
    param(
        [parameter(Mandatory)]
        [string]$ZipPath
    )

    $installPath = (Get-TerraformConfiguration).TFPath

    if (-not (Test-Path $installPath -PathType Container)) {
        if (-not (New-Item -Path $installPath -ItemType 'directory')) {
            throw "Failed to create $($installPath) preference directory"
        }
    }

    $binary = 'terraform'
    if ($isWindows) {
        $binary += '.exe'
    }
    $binaryVersion = 'terraform_{0}' -f $TFVersion
    if ($IsWindows) {
        $binaryVersion += '.exe'
    }
    $destPath = (Join-Path $installPath $binaryVersion)

    $tmpPath = [System.IO.Path]::GetTempPath()
    [string] $guid = [System.Guid]::NewGuid()

    $tmpfolder = (Join-Path $tmpPath $guid)

    Expand-Archive -Path $zipPath -DestinationPath $tmpFolder
    Copy-Item -Path $tmpfolder/$binary -Destination $destPath -Force

    # TODO: Is Powershelly way?
    if (-not $IsWindows) {
        & chmod +x $destPath
    }
}
Function Get-TerraformBinary {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion,
        [switch]$SkipChecksum = $False
    )

    $platform = Get-TerraformPlatform
    $archiveName = 'terraform_{0}_{1}_amd64.zip' -f $TFVersion, $platform
    $zipUrl = '{0}/{1}/terraform_{1}_{2}_amd64.zip' -f (Get-TerraformConfiguration).ReleaseUrl, $TFVersion, $platform
    $shaUrl = '{0}/{1}/terraform_{1}_SHA256SUMS' -f (Get-TerraformConfiguration).ReleaseUrl, $TFVersion
    $shaSigUrl = '{0}/{1}/terraform_{1}_SHA256SUMS.sig' -f (Get-TerraformConfiguration).ReleaseUrl, $TFVersion

    $tmpPath = [System.IO.Path]::GetTempPath()
    [string] $guid = [System.Guid]::NewGuid()

    $zipPath = (Join-Path $tmpPath "$($guid).zip")
    $shaPath = (Join-Path $tmpPath "$($guid)_SHA256SUMS")
    $shaSigPath = (Join-Path $tmpPath "$($guid)_SHA256SUMS.sig")

    try {
        Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath
    } catch {
        Write-Error "Unable to request $($zipUrl)"
        throw $_
    }

    try {
        Invoke-WebRequest -Uri $shaUrl -OutFile $shaPath
    } catch {
        throw "Unable to request $($shaUrl)"
    }

    try {
        Invoke-WebRequest -Uri $shaSigUrl -OutFile $shaSigPath
    } catch {
        throw "Unable to request $($shaSigUrl)"
    }

    if ( -not (Test-TerraformArchiveChecksum -SkipChecksum:$SkipChecksum -ArchiveName $archiveName -ZipPath $zipPath -SHAPath $shaPath -SHASigPath $shaSigPath) ) {
        throw 'Terraform Archive failed Checksum test.'
    }
    return $zipPath
}
Function Get-TerraformPath {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion
    )

    if ($isWindows) {
        $fileExt = '.exe'
    }
    return Join-Path (Get-TerraformConfiguration).TFPath "terraform_$($TFVersion)$($fileExt)"
}
function Get-TerraformPlatform {

    if ($IsWindows) {
        return 'windows'
    }

    if ($IsLinux) {
        return 'linux'
    }

    if ($IsMacOS) {
        return 'darwin'
    }
    throw 'Unknown platform.'
}
Function Install-TerraformBinary {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion,
        [switch]$SkipChecksum = $False,
        [switch]$SkipCodeSignature = $False
    )

    $zipPath = Get-TerraformBinary -TFVersion $TFVersion -SkipChecksum:$SkipChecksum

    try {
        Copy-TerraformBinary -ZipPath $zipPath
    } catch {
        Write-Error "Unable to copy binary from $zipPath."
        throw $_
    }

    if (-not (Test-TerraformPath -TFVersion $TFVersion)) {
        throw "Failed to install Terraform $($TFversion) binary."
    }

    if ( -not (Test-TerraformCodeSignature -TFVersion $TFVersion -SkipCodeSignature:$SkipCodeSignature)) {
        Uninstall-Terraform -TFVersion $TFVersion
        throw "Terraform $($TFversion) fail to pass Code Signature test. Uninstalling."
    }
}
function Test-TerraformArchiveChecksum {
    param (
        [parameter(Mandatory)]
        [string]$ArchiveName,
        [parameter(Mandatory)]
        [string]$ZipPath,
        [parameter(Mandatory)]
        [string]$SHAPath,
        [parameter(Mandatory)]
        [string]$SHASigPath,
        [switch]$SkipChecksum = $False

    )

    if ($SkipChecksum -or (Get-TerraformConfiguration).SkipChecksum) {
        Write-Verbose 'Skipping Terraform Archive Checksum test.'
        return $true
    }

    gpg --list-keys (Get-TerraformConfiguration).HashiCorpPGPKeyId # 2>&1 | Out-Null
    if ($LASTEXITCODE -ne 0) {
        gpg --quiet --keyserver (Get-TerraformConfiguration).PGPKeyServer --recv (Get-TerraformConfiguration).HashiCorpPGPKeyId
        if ($LASTEXITCODE -ne 0) {
            throw 'Unable to retrieve HashiCorp key'
        }
    }

    gpg --verify $SHASigPath $SHAPath
    if ($LASTEXITCODE -ne 0) {
        throw "Unable to verify signature on $($SHAPath)"
    }
    if (-not ((Get-TerraformConfiguration).SquelchChecksumWarning) -and ($output | Select-String 'WARNING: This key is not certified' -Quiet)) {
        Write-Warning @'
The HashiCorp key has been installed but not certified. Run either of the following

    - Confirm-TerraformHashiCorpKey
    - Set-TerraformSquelchChecksumWarning $true
'@

    }

    $SHASum = (Get-FileHash $ZipPath).Hash
    $HashiCorpSHASum = (Get-Content $shaPath | Select-String $ArchiveName).ToString().Split()[0]
    if ($SHASum -ne $HashiCorpSHASum ) {
        throw "Unable to verify SHASUM with $($SHAPath)"
    }

    Write-Verbose "Terraform archive $($zipPath) passed checksum test"
    return $true
}
Function Test-TerraformCodeSignature {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion,
        [switch]$SkipCodeSignature
    )
    if ($SkipCodeSignature -or (Get-TerraformConfiguration).SkipCodeSignature) {
        Write-Verbose 'Skipping Code Signature test'
        return $true
    }
    if ($IsWindows) {
        # HashiCorp started signing with version 0.12.24
        # TODO return true and throw a Warning
        $tfThumbprint = (Get-AuthenticodeSignature -FilePath (Get-TerraformPath -TFVersion $TFVersion)).SignerCertificate.Thumbprint
        return $tfthumbprint -eq (Get-TerraformConfiguration).HashiCorpWindowsThumbprint
    }
    if ($IsMacOs) {
        # $tfThumbprint = codesign --verify -d --verbose=2 (Get-TerraformPath -TFVersion $TFVersion) | Select-String TeamIdentifier).ToString().Split('=')[1]
        # return $tfthumbprint -eq (Get-TerraformConfiguration).HashiCorpTeamIdentifier
        codesign --verify -d --verbose=2 (Get-TerraformPath -TFVersion $TFVersion)
        return $LASTEXITCODE -eq 0
    }
    if ($IsLinux) {
        Write-Verbose 'CodeSignature check at runtime is not supported on Linux.'
        return $true
    }
    Write-Error 'Unable to test terraform CodeSignature. Unknown platform.'
    throw $_
}
Function Test-TerraformPath {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion
    )
    Write-Verbose "Testing path for Terraform version $($TFVersion) "
    return Test-Path (Get-TerraformPath -TFVersion $TFVersion) -PathType leaf
}
<#
    .SYNOPSIS
        Helper function to sign the Hashi Corp PHP key.
    .DESCRIPTION
        Helper function to sign the Hashi Corp PHP key.
    .EXAMPLE
        Confirm-TerraformHashicorpKey

        Runs gpg to sign a HasiCorp PGP key.
    .INPUTS
        None. You cannot pipe objects to Confirm-TerraformHashicorpKey.
    .OUTPUTS
        None. Confirm-TerraformHashicorpKey returns nothing.
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

Function Confirm-TerraformHashicorpKey {
    & gpg --sign-key (Get-TerraformConfiguration).HashiCorpPGPKeyId
}
function Get-TerraformConfiguration {
    Import-Configuration
}
<#
    .SYNOPSIS
        Install a version of terraform.
    .DESCRIPTION
        Install a version of terraform.
    .PARAMETER TFVersion
        The version of terraform to install.
    .PARAMETER SkipChecksum
        Skip release archive checksum verification.
    .PARAMETER SkipCodeSignature
        Skip code signature verifcation.
    .EXAMPLE
        Install-Terraform -TFVersion 0.14.7

        Installs terraform version 0.14.7
    .INPUTS
        None. You cannot pipe objects to Install-Terraform.
    .OUTPUTS
        None. Install-Terraform returns nothing.
    .LINK
        Uninstall-Terraform
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

Function Install-Terraform {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion,
        [switch]$SkipChecksum = $False,
        [switch]$SkipCodeSignature = $False
    )

    if (Test-TerraformPath -TFVersion $TFVersion) {
        Write-Verbose "Terraform $($TFversion) already installed."
        return
    }
    Write-Verbose "Installing terraform version $($TFVersion)"
    Install-TerraformBinary -TFVersion $TFVersion -SkipChecksum:$SkipChecksum -SkipCodeSignature:$SkipCodeSignature
}


Function Invoke-Terraform {
    <#
    .SYNOPSIS
        Run terraform version based on user preference.
    .DESCRIPTION
        Run terraform version based on user preference.
        Additional parameters are passed to the terraform binary.
    .PARAMETER TFVersion
        Override preferred version of terraform to run.
    .PARAMETER SkipCodeSignature
        Skip code signature verifcation.
    .EXAMPLE
        Invoke-Terraform -TFVersion 0.14.7

        Runs terraform version 0.14.7
    .EXAMPLE
        Invoke-Terraform

        Runs terraform version based on user preference or default preference.
    .INPUTS
        None. You cannot pipe objects to Invoke-Terraform.
    .OUTPUTS
        None. Invoke-Terraform returns nothing.
    .LINK
        Install-Terraform
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
    #>

    param(
        [string]$TFVersion,
        [switch]$SkipCodeSignature = $False
    )

    # HACK:
    #
    # Due to positional parameters the first unnamed parameter
    # is passed to $TFVersion. This catches non version parameters
    # intended to pass to the terraform run.
    if (-not ($TFVersion -match '^0\.\d\d?\.\d\d?$')) {
        # Build $TFargs and null $TFVersion for default preference
        $TFargs = @($TFVersion) + $args
        $TFVersion = $null
    } else {
        $TFArgs = $args
    }

    if ((Test-Path .terraform-version) -and (-not $TFVersion)) {
        $TFVersion = Get-Content .terraform-version
        # TODO regex validate the version
        Write-Verbose "Found .terraform-version $TFVersion"
    }

    # If Version still isn't set
    if (-not $TFVersion) {
        $TFVersion = (Get-TerraformConfiguration).TFVersion
    }

    if (-not (Test-TerraformPath -TFVersion $TFVersion)) {
        Write-Warning "Terraform version $($TFVersion) not found."

        if ((Get-TerraformConfiguration).AutoDownload) {
            Write-Verbose "Auto downloading terraform version $($TFVersion)"
            Install-Terraform -TFVersion $TFVersion
        } else {
            Write-Error @"
Terraform version $($TFVersion) not installed. Run either

    - Install-Terraform -TFVersion $($TFVersion)
    - Set-TerraformAutoDownload `$true
"@

            throw ''
        }
    }

    if (-not (Test-TerraformCodeSignature -TFVersion $TFVersion -SkipCodeSignature:$SkipCodeSignature)) {
        throw 'Unable to confirm Code Signature of terraform binary'
    }

    & (Get-TerraformPath -TFVersion $TFVersion) $TFargs
}

Set-Alias -Name terraform -Value Invoke-Terraform
<#
    .SYNOPSIS
        Set auto download configuration.
    .DESCRIPTION
        Set auto download configuration.
    .PARAMETER AutoDownload
        Either true or false.
    .EXAMPLE
        Set-TerraformAutoDownload $true

        Sets auto download configuration to true
    .INPUTS
        None. You cannot pipe objects to Set-TerraformAutoDownload.
    .OUTPUTS
        None. Set-TerraformAutoDownload returns nothing.
    .LINK
        Get-TerraformConfiguration
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

function Set-TerraformAutoDownload {
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [parameter(Mandatory)]
        [boolean]$AutoDownload
    )
    begin {
        Write-Debug -Message 'Beginning'
        $configurationPath = Get-ConfigurationPath
    }

    process {
        if ($PSCmdlet.ShouldProcess($configurationPath, "Setting AutoDownload configuration to $($AutoDownload)")) {
            Write-Verbose "Setting AutoDownload configuration to $($AutoDownload)"
            Set-TerraformConfiguration @{ AutoDownload = $AutoDownload } -Confirm:$False
        }
    }
    end {
        Write-Debug -Message 'Ending'
    }
}
function Set-TerraformConfiguration {
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [parameter(Mandatory)]
        [hashtable]$Configuration
    )
    begin {
        Write-Debug -Message 'Beginning'
        $configurationPath = Get-ConfigurationPath
    }
    process {
        # Merge existing configuration with updates
        $existingConfiguration = Import-Configuration
        $existingConfiguration.keys | Where-Object {
            $_ -notin $Configuration.keys
        } | ForEach-Object {
            $Configuration.Add($_, $existingConfiguration.Item($_) )
        }

        # Drop keys not defined by default configuration
        $remove = $Configuration.keys | Where-Object {
            $_ -notin $existingConfiguration.keys
        }
        $remove | ForEach-Object {
            $Configuration.Remove($_)
        }

        if ($PSCmdlet.ShouldProcess($configurationPath, "Setting Configuration configuration to $($Configuration | ConvertTo-Json -Depth 5)")) {
            Write-Verbose "Setting configuration to $($Configuration | ConvertTo-Json -Depth 5)"
            $Configuration | Export-Configuration
        }
    }

    end {
        Write-Debug -Message 'Ending'
    }
}
<#
    .SYNOPSIS
        Set squelch checksum warning configuration.
    .DESCRIPTION
        Set squelch checksum warning configuration.
    .PARAMETER SquelchChecksumWarning
        Either true or false.
    .EXAMPLE
        Set-TerraformSquelchChecksumWarning $true

        Set squelch checksum warning configuration to true
    .INPUTS
        None. You cannot pipe objects to Set-TerraformSquelchChecksumWarning.
    .OUTPUTS
        None. Set-TerraformSquelchChecksumWarningreturns nothing.
    .LINK
        Get-TerraformConfiguration
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

function Set-TerraformSquelchChecksumWarning {
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [parameter(Mandatory)]
        [boolean]$SquelchChecksumWarning
    )
    begin {
        Write-Debug -Message 'Beginning'
        $configurationPath = Get-ConfigurationPath
    }

    process {
        if ($PSCmdlet.ShouldProcess($configurationPath, "Setting SquelchChecksumWarning configuration to $($SquelchChecksumWarning)")) {
            Write-Verbose "Setting SquelchChecksumWarning configuration to $($SquelchChecksumWarning)"
            Set-TerraformConfiguration @{ SquelchChecksumWarning = $SquelchChecksumWarning } -Confirm:$False
        }
    }

    end {
        Write-Debug -Message 'Ending'
    }
}
<#
    .SYNOPSIS
        Set configuration version for terraform.
    .DESCRIPTION
        Set configuration version for terraform.
    .PARAMETER TFVersion
        The preferred version.
    .EXAMPLE
        Set-TerraformVersion -TFVersion 0.14.7

        Sets configuration version for terraform to 0.14.7
    .INPUTS
        None. You cannot pipe objects to Set-TerraformVersion.
    .OUTPUTS
        None. Set-TerraformVersion returns nothing.
    .LINK
        Get-TerraformConfiguration
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

Function Set-TerraformVersion {
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [parameter(Mandatory)]
        [string]$TFVersion
    )
    begin {
        Write-Debug -Message 'Beginning'
        $configurationPath = Get-ConfigurationPath
    }

    process {
        if ($PSCmdlet.ShouldProcess($configurationPath, "Setting TFVesion configuration version to $($TFVersion)")) {
            Write-Verbose "Setting TFVersion configuration version to $($TFVersion)"
            Set-TerraformConfiguration @{ TFVersion = $TfVersion } -Confirm:$False
        }
    }

    end {
        Write-Debug -Message 'Ending'
    }
}

Set-Alias -Name Switch-Terraform -Value Set-TerraformVersion
<#
    .SYNOPSIS
        Uninstall a version of terraform.
    .DESCRIPTION
        Unnstall a version of terraform.
    .PARAMETER TFVersion
        The version of terraform to uninstall.
    .EXAMPLE
        Uninstall-Terraform -TFVersion 0.14.7

        Uninstalls terraform version 0.14.7
    .INPUTS
        None. You cannot pipe objects to Uninstall-Terraform.
    .OUTPUTS
        None. Uninstall-Terraform returns nothing.
    .LINK
        Install-Terraform
    .LINK
        Online version: https://github.com/pearcec/Invoke-Terraform
#>

Function Uninstall-Terraform {
    param(
        [parameter(Mandatory)]
        [string]$TFVersion
    )

    if (Test-TerraformPath -TFVersion $TFVersion) {
        Write-Verbose "Uninstalling terraform version $($TFVersion)"
        Remove-Item (Get-TerraformPath -TFVersion $TFVersion) -Force
    } else {
        Write-Warning "Unable to uninstall terraform. Version ($TFVersion) not found."
    }
}
$PSDefaultParameterValues = @{
    'Invoke-WebRequest:Verbose' = $false
    'Invoke-WebRequest:Debug'   = $false
}

$ProgressPreference = 'SilentlyContinue'

# # Dot source public/private functions
# $public = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Public/*.ps1') -Recurse -ErrorAction Stop)
# $private = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Private/*.ps1') -Recurse -ErrorAction Stop)
# foreach ($import in @($public + $private)) {
# try {
# . $import.FullName
# } catch {
# Write-Error "Unable to dot source [$($import.FullName)]"
# throw $_
# }
# }

# Export-ModuleMember -Function $public.Basename -Alias *