ContainerProvider.psm1

###
### SEARCH VARS
### Techniques are pulled from: https://azure.microsoft.com/en-us/documentation/articles/search-chrome-postman/
### Index name must only contain lowercase letters, digits or dashes, cannot start or end with dashes and is limited to 128 characters.
###
$fwdLink = "http://go.microsoft.com/fwlink/?LinkID=627586&clcid=0x409"
$publicQueryKey = '82E9CC3E0342EA5C9B95ED909FC8E039'
$indexName = 'pshct-pub-srch-index'
$apiVersionQP = 'api-version=2015-02-28'
[System.Version] $minVersion = '0.0.0.0'

#region Functions

###
### SUMMARY: Finds the container image from the Azure Search Service
### PARAMS:
### 1. Name: Optional param: Name of the image
### 2. Version: Optional param : Version of the image
###
function Find-ContainerImage
{
    <#
        .SYNOPSIS
        Finds the container image from an online gallery that match specified criteria.
 
        .SYNTAX
        Find-ContainerImage [[-Name] <String>] [[-Version] <Version>] [-SearchKey [String]]
 
        .DESCRIPTION
        Find-ContainerImage finds the images from the online gallery that match specified criteria.
        For each module found, Find-ContainerImage returns a
 
        If the Version is not specified, all versions of the image are returned
        If the Version parameter is specified, Find-ContainerImage only returns the version of
        the image that exactly matches the specified version
         
        .EXAMPLE
        Find-ContainerImage -Name ImageName
 
        .EXAMPLE
        Find-ContainerImage -Version 1.2.3.5
 
        .EXAMPLE
        Find-ContainerImage -Name ImageName -Version 1.2.3.4
    #>


    [cmdletbinding()]
    
    # Handle the input parameters
    param
    (
        [parameter(Mandatory=$false)]
        [System.String]$Name,

        [parameter(Mandatory=$false)]
        [System.Version]$Version = $minVersion,

        [parameter(Mandatory=$false)]
        [System.String]$SearchKey = $publicQueryKey
    )
    
    $result_Search = Find $Name $Version $SearchKey

    # Handle empty search result
    if(!$result_Search)
    {
        Write-Error "No such module found."
        return
    }

    return $result_Search
}

###
### SUMMARY: Downloads and saves the container image
### PARAMS:
### 1. Name: Mandatory param: Name of the image
### 2. Version: Optional param: Version of the image
### 3. Destination: Mandatory param: Destiation where the file needs to be saved
### 4. SearchKey: Searches using this search key
###
### This function will first find the image based on given params
### If version is provided, save that particular version
### Else save the latest version
###
function Save-ContainerImage
{
    <#
        .SYNOPSIS
        Saves a container image without installing it.
 
        .SYNTAX
        Save-ContainerImage [[-Name] <String>] [[-Destination] <String>]
                            [[-Version] <Version>] [-SearchKey [String]]
 
        .DESCRIPTION
        The Save-ContainerImage cmdlet lets you save a container image locally without installing it.
        This lets you inspect the container image before you install, helping to minimize the risks
        of malicious code or malware on your system
 
        As a best practice, when you have finished evaluating a container image for potential risks,
        and before you install the image for use, dleete the image from the path to which you have saved.
         
        .EXAMPLE
        Save-ContainerImage -Name ImageName -Destination C:\temp\ImageName.wim
 
        .EXAMPLE
        Save-ContainerImage -Name ImageName -Version 1.2.3.5 -Destination C:\temp\ImageName.wim
    #>


    [cmdletbinding()]
    
    # Handle the input parameters
    param
    (
        [parameter(Mandatory=$true)]
        [System.String]$Name,

        [parameter(Mandatory=$false)]
        [System.String]$Version = $minVersion,

        [parameter(Mandatory=$true)]
        [System.String]$Destination,

        [parameter(Mandatory=$false)]
        [System.String]$SearchKey = $publicQueryKey
    )

    if(-not (CheckDestination $Destination))
    {
        return
    }

    $result_Search = Find $Name $Version $SearchKey

    # Handle empty search result
    if(!$result_Search)
    {
        throw [System.IO.FileNotFoundException] "No such module found."
    }

    [System.Version] $maxVersion = '0.0.0.0'
    $maxToken, $maxName

    if($Version -ne $minVersion)
    {
        # If version is provided, download that specific version
        $image = $result_Search[0]
        $maxName = $image.name
        $maxToken = $image.sastoken
        $maxVersion = $image.version
    }
    else
    {
        # Else download the latest version
        ForEach($image in $result_Search)
        {
            if($image.version -gt $maxVersion)
            {
                $maxName = $image.name
                $maxToken = $image.sastoken
                $maxVersion = $image.version
             }
        }
    }

    Write-Verbose "Downloading $maxName. Version: $maxVersion"

    Save-ContainerImageFile $maxToken $Destination
}

###
### SUMMARY: Installs the container image
### PARAMS
###
### 1. Name: Mandatory param: Name of the image
### 2. Version: Optional param: Version of the image
### 3. Destination: Mandatory param: Destiation where the file needs to be saved
### 4. SearchKey: Searches using this search key
###
### This function will first find the image based on given params
### If it finds the image, it will be downloaded
### Then it will be installed
###
function Install-ContainerImage
{
    <#
        .SYNOPSIS
        Downloads the image from the cloud and installs them on the local computer
 
        .SYNTAX
        Install-ContainerImage [[-Name] <String>] [[-Destination] <String>]
                            [[-Version] <Version>] [-SearchKey [String]]
 
        .DESCRIPTION
        The Install-ContainerImage gets the container image that meets the specified cirteria from the cloud.
        It saves the image locally and then installs it
         
        .EXAMPLE
        Install-ContainerImage -Name ImageName
 
        .EXAMPLE
        Install-ContainerImage -Name ImageName -Version 1.2.3.5
    #>


    [cmdletbinding()]
    
    # Handle the input parameters
    param
    (
        [parameter(Mandatory=$true)]
        [System.String]$Name,

        [parameter(Mandatory=$false)]
        [System.String]$Version = $minVersion,

        [parameter(Mandatory=$false)]
        [System.String]$SearchKey = $publicQueryKey
    )

    $Destination = $env:TEMP + "\" + $Name + ".wim"

    Write-Verbose "Saving to $Destination"

    try
    {
        Save-ContainerImage -Name $Name `
                                -Version $Version `
                                -Destination $Destination `
                                -SearchKey $SearchKey
    }
    catch
    {
        Write-Error "Unable to download."
        if((Test-Path $Destination))
        {
            Write-Verbose "Removing the installer: $Destination"
            rm $Destination
        }
        return        
    }

    $startInstallTime = Get-Date

    Install-ContainerOSImage -WimPath $Destination `
                             -Force

    $endInstallTime = Get-Date
    $differenceInstallTime = New-TimeSpan -Start $startInstallTime -End $endInstallTime
    $installTime = "Installed in " + $differenceInstallTime.Hours + " hours, " + $differenceInstallTime.Minutes + " minutes, " + $differenceInstallTime.Seconds + " seconds."
    Write-Verbose $installTime

    # Clean up
    Write-Verbose "Removing the installer: $Destination"
    rm $Destination

    Write-Verbose "All Done"
}

###
### SUMMARY: Installs the container image
### PARAMS
###
### 1. Name: Mandatory param: Name of the image
### 2. Version: Optional param: Version of the image
### 3. Destination: Mandatory param: Destiation where the file needs to be saved
### 4. SearchKey: Searches using this search key
###
### This function will first find the image based on given params
### If it finds the image, it will be downloaded
### Then it will be installed
###
function Install-ContainerImageHelper
{
    <#
        .SYNOPSIS
        Downloads the image from the cloud and installs them on the local computer
 
        .SYNTAX
        Install-ContainerImage [[-Name] <String>] [[-Destination] <String>]
                            [[-Version] <Version>] [-SearchKey [String]]
 
        .DESCRIPTION
        The Install-ContainerImage gets the container image that meets the specified cirteria from the cloud.
        It saves the image locally and then installs it
         
        .EXAMPLE
        Install-ContainerImage -Name ImageName
 
        .EXAMPLE
        Install-ContainerImage -Name ImageName -Version 1.2.3.5
    #>


    # Handle the input parameters
    param
    (
        [parameter(Mandatory=$true)]
        [System.String]$SasToken,

        [parameter(Mandatory=$true)]
        [System.String]$Name
    )

    $Destination = $env:TEMP + "\" + $Name + ".wim"

    Write-Verbose "Saving to $Destination"

    try
    {
        Save-ContainerImageFile -downloadURL $SasToken `
                        -Destination $Destination
    }
    catch
    {
        Write-Error "Unable to download."
        if((Test-Path $Destination))
        {
            Write-Verbose "Removing the installer: $Destination"
            rm $Destination
        }
        return
    }

    $startInstallTime = Get-Date

    Install-ContainerOSImage -WimPath $Destination `
                             -Force

    $endInstallTime = Get-Date

    $differenceInstallTime = New-TimeSpan -Start $startInstallTime -End $endInstallTime

    "Installed in " + $differenceInstallTime.Hours + " hours, " + $differenceInstallTime.Minutes + " minutes, " + $differenceInstallTime.Seconds + " seconds."

    # Clean up
    Write-Verbose "Removing the installer: $Destination"
    rm $Destination
    Write-Verbose "All Done"
}

#endregion Functions

#region Helper Functions

###
### SUMMARY: Class for display
###
Class ContainerImageItem 
{
    [string] $Name;
    [string] $description;
    [string] $sasToken;
    [Version] $version;
}

###
### SUMMARY: Displays the search results
### PARAMS:
### 1. SearchResults
###
function Display-SearchResults
{
    param ($searchResults)

    $formatting = @{Expression={$_.Name};Label="Name";width=20}, `
                    @{Expression={$_.version};Label="Version";width=25}, `
                    @{Expression={$_.description};Label="Description";width=60}
        
    $searchResults | Format-Table $formatting
}

###
### SUMMARY: Finds the container image entries on Azure Search
### PARAMS:
### 1. Name: Name of the image
### 2. Version: Version of the image
###
function Find
{
    param($Name, $Version, $queryKey=$publicQueryKey)
    
    if(-not (IsNanoServer))
    {
        Add-Type -AssemblyName System.Net.Http
    }
    $httpPostClient = New-Object System.Net.Http.HttpClient
    $httpPostRequestMsg = New-Object System.Net.Http.HttpRequestMessage(
                            [System.Net.Http.HttpMethod]::Post,
                            $fullUrl)

    # URL
    $resolvedUrl = Resolve-FwdLink $fwdLink

    if (($resolvedUrl.Scheme -ne 'http') -and ($resolvedUrl.Scheme -ne 'https'))
    {
        throw "Unable to get the resolved URL."
    }

    $relativePath = 'indexes/{0}/docs/search?{1}' -f $indexName,$apiVersionQP
    
    $httpPostClient.BaseAddress = New-Object System.Uri($resolvedUrl, $relativePath)
        
    $acceptHeader = New-Object `
                        System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(
                            "application/json")

    $httpPostClient.DefaultRequestHeaders.Accept.Add($acceptHeader)

    # Headers
    $httpPostRequestMsg.Headers.Add("api-key", $queryKey)
    $httpPostRequestMsg.Headers.Add("charset", "utf-8")

    # Body
    <#
     # Azure search do not support case-insensitive search.
     # Until this is resolved we are doing client side
     # filtering
      
    if($Name)
    {
        $query = "name eq '$Name'"
    }
     
    if($Version -ne $minVersion)
    {
        if($query)
        {
            $query += " and"
        }
 
        $query += " version eq '$Version'"
    }
 
    #>

    $httpPostBody = '{
            "filter" : "'
 + $query + '"
            ,"orderby": "name, version desc"
            }'


    $encoding = [System.Text.Encoding]::ASCII
    $httpPostRequestMsg.Content = New-Object System.Net.Http.StringContent(
                                    $httpPostBody, 
                                    $encoding, 
                                    "application/json")

    try
    {
        $responseTask = $httpPostClient.SendAsync($httpPostRequestMsg)
        $responseContent = $responseTask.Result.Content
        $responseBody = $responseContent.ReadAsStringAsync().Result.ToString()

        if(IsNanoServer)
        {
            $jsonDll = [Microsoft.PowerShell.CoreCLR.AssemblyExtensions]::LoadFrom($PSScriptRoot + "\Json.coreclr.dll")
            $jsonParser = $jsonDll.GetTypes() | ? name -match jsonparser
        
            $response = $jsonParser::FromJson($responseBody)
        }
        else
        {
            $response = $responseBody | ConvertFrom-Json
        }

        $responseValue = $response.value
        # apply filtering for Name and Version.
        # These were not applied when HTTP request is sent
        $NameToUseInQuery = if ($Name) { $Name } else { "*" }
        $VersionToUseInQuery = if ($Version -ne $minVersion) { $Version } else { "*" }
        $responseValue = $responseValue | ? { ($_.name -like "$NameToUseInQuery") -and ($_.version -like "$VersionToUseInQuery") }

        $responseClassArray = @()
        foreach($element in $responseValue)
        {
            $item = [ContainerImageItem]::new()
            $item.Name = $element.Name
            $item.description = $element.Description
            $item.version = $element.version
            $item.sasToken = $element.sastoken

            $responseClassArray += $item
        }

        return $responseClassArray
    }
    catch [System.Net.Http.HttpRequestException]
    { 
        Write-Host "Error:System.Net.HttpRequestException"
    } 
    catch [Exception]
    {
        Write-Host "$_.Message"
    } 
    finally 
    {
    }
}

###
### SUMMARY: Download the file given the URI to the given location
###
function Save-ContainerImageFile
{
    param($downloadURL, $destination)

    $startTime = Get-Date

    Write-Verbose $downloadURL

    # Download the file
    if ((IsNanoServer) -or (get-variable pssenderinfo -ErrorAction SilentlyContinue))
    {
        # Use custom Save-HTTPItem function if on Nano or in a remote session
        # This is beacuse BITS service does not work as expected under these circumstances.
        Import-Module "$PSScriptRoot\Save-HttpItem.psm1"
        Save-HTTPItem -Uri $downloadURL `
                        -Destination $destination
    }
    else
    {   
        Start-BitsTransfer -Source $downloadURL `
                        -Destination $destination
    }
    
    $endTime = Get-Date
    $difference = New-TimeSpan -Start $startTime -End $endTime
    $downloadTime = "Downloaded in " + $difference.Hours + " hours, " + $difference.Minutes + " minutes, " + $difference.Seconds + " seconds."
    Write-Verbose $downloadTime
}

###
### SUMMARY: Resolve the fwdlink to get the actual search URL
###
function Resolve-FwdLink
{
    param
    (
        [parameter(Mandatory=$false)]
        [System.String]$Uri
    )
    
    if(-not (IsNanoServer))
    {
        Add-Type -AssemblyName System.Net.Http
    }
    $httpClient = New-Object System.Net.Http.HttpClient
    $response = $httpclient.GetAsync($Uri)
    $link = $response.Result.RequestMessage.RequestUri

    return $link
}

###
### SUMMARY: Checks if the system is nano server or not
### Look into the win32 operating system class
### Returns True if running on Nano
### False otherwise
###
function IsNanoServer
{
    $operatingSystem = Get-CimInstance -ClassName win32_operatingsystem
    $systemSKU = $operatingSystem.OperatingSystemSKU
    return $systemSKU -eq 109
}

###
### SUMMARY: Checks if the given destination is kosher or not
### 1. Check if the user has provider a folder
### If so, throw an exception, only absolute path with file name is acceptable
### 2. Check if parent path exists
### If not, create it for the user
### 3. Check if the file exists
### If so, ask the user for ability to re-write
###
function CheckDestination
{
    param($Destination)

    # Check if entire path is folder structure
    # If folder throw error, ask for file path
    $dest_item = Get-Item $Destination `
                            -ErrorAction SilentlyContinue `
                            -WarningAction SilentlyContinue

    if($dest_item -is [System.IO.DirectoryInfo])
    {
        throw "Please provide file name with path."
    }

    # Check the parent (one minus the whole path)
    # If the given parent directory doesn't exist
    # create it and return
    $folderPath = Split-Path $Destination
    $isFolderPath = Get-Item $folderPath `
                                -ErrorAction SilentlyContinue `
                                -WarningAction SilentlyContinue 

    if($isFolderPath -isnot [System.IO.DirectoryInfo])
    {
        Write-Verbose "Creating directory structure: $folderPath"
        md $folderPath
        return $true
    }
    
    # If given parent directory exists
    # Check if given file exists
    if((Test-Path $Destination))
    {
        # Check for Read-only file
        $list = dir $Destination | where {$_.attributes -match "ReadOnly"}
        if($list.Count -gt 0)
        {
            Write-Error "Cannot over write read-only file: $Destination"
            return $false
        }

        $title = "Overwrite File"
        $message = "Do you want to overwrite the existing file: $Destination ?"

        $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", `
        "Overwrite the existing file."

        $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", `
        "Do not overwrite the existing file."

        $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)

        $result = $host.ui.PromptForChoice($title, $message, $options, 0) 

        switch ($result)
        {
            0 {
                # User selects Yes.
                return $true
            }

            1 {
                # User selects No.
                Write-Host "Re-run the script with a different Destination"
                Write-Host
                return $false
            }
        }

        return $true
    }

    return $true
}

#endregion Helper Functions

################################################################################

$Providername = "ContainerProvider"
$separator = "|#|"

<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
#>

function Find-Package
{
    [CmdletBinding()]
    Param
    (
        [string[]] $names,
        [string] $requiredVersion,
        [string] $minimumVersion,
        [string] $maximumVersion
    )

    $null = write-debug "In $($ProviderName)- Find-Package"

    if ([string]::IsNullOrWhiteSpace($requiredVersion)) {
        $requiredVersion = [System.Version]::new("0.0.0.0")
        $null = write-debug "version is null"
    }
    else {
        $requiredVersion = [System.Version]::new($requiredVersion)
    }
                            
    foreach($container in (Find -Name $names[0] -Version $requiredVersion))
    {
        if ($request.IsCancelled)
        {
            $null = Write-Verbose "Request has been cancelled."
            return
        }

        $fastPackageReference = $container.Name + $separator +
                                    $container.version + $separator + 
                                    $container.Description + $separator + 
                                    $container.sasToken

        $containerSWID = @{
            name = $container.Name
            version = $container.Version
            versionScheme = "semver"
            summary = $container.Description
            source = "Azure Public"
            fastPackageReference = $fastPackageReference
        }

        New-SoftwareIdentity @containerSWID
    }
}

<#
.Synopsis
   Short description
.DESCRIPTION
   Long description
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
#>

function Download-Package
{
    param(
        [string] $fastPackageReference,
        [string] $destLocation
    )
    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None);

    $sasToken = $resultArray[3]

    Save-ContainerImageFile $sasToken $destLocation    
}

function Install-Package
{
    param(
        [string] $fastPackageReference
    )       

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None);

    $name = $resultArray[0]
    $sasToken = $resultArray[3]
    
    $null = write-debug "Name of the container is $name and sastoken is $sasToken"
    
    Install-ContainerImageHelper -SasToken $sasToken -Name $name
}

function Initialize-Provider
{
    write-debug "In $($Providername) - Initialize-Provider"
}

function Get-PackageProviderName
{
    return $Providername
}

function Get-InstalledPackages
{
    param(
        [string]$name,
        [string]$requiredVersion,
        [string]$minimumVersion,
        [string]$maximumVersion
    )

    $containers = Get-ContainerImage

    if ($containers -eq $null -or $containers.Count -eq 0)
    {
        return
    }

    ForEach($container in Get-ContainerImage)
    {
        if ($request.IsCancelled)
        {
            $null = Write-Verbose "Request has been cancelled."
            return
        }

        $containerSWID = @{
            name = $container.Name
            version = $container.Version
            versionScheme = "semver"
            source = "Azure Public"
            fastPackageReference = $container.Name
        }

        New-SoftwareIdentity @containerSWID
    }
}

##########################################################################