The Chocolatey Package Internalizer feature available in Chocolatey For Business (C4B) allows you to consume a package from the Chocolatey community repository, and rewrite the packaging to be completely offline.

This allows for the complete offline installation of that package. This is essential for business users where uptime matters, and reliance on a 3rd party can't be trusted. For more information on why organizations should maintain their own repository of Chocolatey packages, see here.

This process works wonderfully, and is often unnoticed if your endpoints have a direct connection to the internet, or you are not leveraging Chocolatey GUI. This is because the icons are retrieved from the <iconUrl> field inside of the nuspec file. There are many reasons why icon files could potentially not be loaded.

  1. Air gapped networks
  2. Restrictive content filter
  3. Proxy configuration
  4. Etc etc etc

This provides a less than ideal experience for users of Chocolatey GUI when using Tile View or when on the Details page of a particular package inside the application.

You may also wish to maintain the icons in your own infrastructure as it again removes a level of reliance on 3rd party resources for your Chocolatey Packages. The following blog post will walk you through setting up the necessary repository infrastructure, and provide code you can use to accomplish bringing icons for packages internal.

Configuring Your Repository Server

We'll be using Sonatype Nexus in this blog post. They use the term "Raw" for a repository that can hold any file type. Your repository server may use another term like "Generic", but it is the same thing. Our Nexus repository will have a hostname of nexus.fabrikam.com, and connect over http on port 8081.

Steps:

  1. Login to Nexus
  2. Click the Gear Icon in the top nav bar
  3. Select Repository
  4. Select Raw (hosted) for the repository type
  5. Give the repository a name, select the appropriate blob store (default in our example), and apply any other policies you need.
  6. Click Create Repository

Gif Overview

Nexus Repository setup example

The Script

Here's the code you'll need to get this working in your environment. Once saved, you can execute it like this (I'll use splatting to keep things readable):

You'll need the following parameters:

InternalizeDownloadPath
name: InternalizerDownloadPath
type: string
mandatory: true
description: This is the path to the download folder created when you run something like 'choco download vlc --internalize'.
IconRepository
name: IconRepository
type: string
mandatory: true
description: The raw (Generic) repository you wish to push your icon files too.
PackageRepository
name: PackageRepository
type: string
mandatory: true
description: The Nuget repository where you store your chocolatey packages. The script will push the updated packages to this location.
Credential
name: Credential
type: System.Management.Automation.PSCredential
mandatory: true
description: This is a set of credentials with access to push items to both your Icon and Package repositories in your repository server.
ApiKey
name: ApiKey
type: string
mandatory: false
description: The api key used to push nuget packages to your repository. If you have used `choco apikey` to add your api key to your Chocolatey config, this is not necessary. Otherwise, please provide an api key with this parameter.
Complete Script
<#
  .SYNOPSIS
  Internalize package icons for internalized packages

  .PARAMETER InternalizerDownloadPath

  This is the path to the download folder created when you run something like 'choco download vlc --internalize'

  .PARAMETER IconRepository

  The raw (Generic) repository you wish to push your icon files too

  .PARAMETER PackageRepository

  The Nuget repository where you store your chocolatey packages. The script will push the updated packages to this location

  .PARAMETER Credential

  This is a set of credentials with access to push items to both your Icon and Package repositories in your repository server

  .PARAMETER ApiKey
  
  The apikey used to push nuget packages to your repository. If you have used `choco apikey` to add your api key to your Chocolatey config, this is not necessary. Otherwise, please provide an api key with this parameter.

  .EXAMPLE
  $params = @{
      InternalizerDownloadPath = 'C:\internalized\download\'
      IconRepository = 'http://nexus.fabrikam.com:8081/repository/icons/'
      PackageRepository = 'http://nexus.fabrikam.com:8081/repository/choco/'
      }

    .\InternalizePackageIcons.ps1 @params

  .NOTES
    Run this script AFTER you have internalized your packages, and point it at the 'download' folder created during that process
#>
[cmdletBinding()]
param(
    [Parameter(Mandatory)]
    [String[]]
    $InternalizerDownloadPath,

    [Parameter(Mandatory)]
    [String]
    $IconRepository,

    [Parameter(Mandatory)]
    [String]
    $PackageRepository,

    [Parameter(Mandatory)]
    [PSCredential]
    $Credential,

    [Parameter()]
    [String]
    $ApiKey
   
)

process {
    $nuspecs = $(Get-ChildItem $InternalizerDownloadPath -Recurse -Include *.nuspec,chocolateyInstall.ps1)

    Write-Verbose "Downloading icons and replacing values in nuspec files"

    foreach ($nuspec in $nuspecs) {

        [xml]$xml =$nuspec | Where-Object { $_.Extension -eq '.nuspec' } | Get-Content
        $iconurl = $xml.package.metadata.iconUrl
        $icon = ($iconurl -split ('/'))[-1]
        $iconPath = Join-Path 'C:\icons' "$icon"

        if ($iconurl) {

            $null = Invoke-WebRequest -Uri $iconurl -OutFile "$($iconPath)" -ErrorAction SilentlyContinue

            $user = $Credential.UserName
            $password = $Credential.GetNetworkCredential().Password

            $credPair = "{0}:{1}" -f $user, $password
            $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::Utf8.GetBytes($credPair))

            $params = @{
                Headers         = @{
                    Authorization = "Basic $encodedCreds"
                }

                UseBasicParsing = $true
                ContentType     = 'text/plain'
            }
            
            if ($iconPath -eq 'C\icons\') {
                $null
            }
            else {
                $newUrl = "$($IconRepository)$icon"
                Write-Verbose "Uploading: $iconPath"
                $null = Invoke-WebRequest -Uri $newUrl -Method Put -infile $iconPath @params -ErrorAction SilentlyContinue

                #Write new URL
                $xml.package.metadata.iconUrl = $newUrl
                $xml.Save($($nuspec.FullName))

                $Script:RepackageDirectory = Split-Path -Parent -Path $InternalizerDownloadPath

                $chocoPackArgs = @('pack',"$($nuspec.FullName)","--output-directory='$RepackageDirectory'")
                & choco @chocoPackArgs
            }

        }

    } 

    Write-Verbose "Uploading modified packages to repository"

    Get-ChildItem $RepackageDirectory -Recurse -Filter *.nupkg | Foreach-Object {
        
        $chocoPushArgs = @('push',"$($_.FullName)","--source='$PackageRepository'")

        if($ApiKey){
            $chocoPushArgs += "--api-key='$ApiKey'"
        }

        if($($PackageRepository.Split(':')[0]) -match 'http'){
            $chocoPushArgs += '--force'
        }

        & choco @chocoPushArgs
    }

    Remove-Item C:\icons -Recurse -Force

}

Example

This is an example of internalizing icons using the information from this blog post as parameter values. Please update accordingly in your own environments.

$params = @{
InternalizerDownloadPath = 'C:\packages\download'
IconRepository = 'http://nexus.yourcompany.com:8081/repository/icons/'
PackageRepository = 'http://nexus.yourcompany.com:8081/repository/ChocolateyPackages/'
Credential = (Get-Credential)
}

. .\InternalizePackageIcons.ps1 @params

Conclusion

You made it! If you followed along, you should now have a repository where you can store your package icons, and all the code necessary to make your Chocolatey packages completely, totally, 100%, without question offline. Thanks for reading, and come back next time when the Mad Scientist strikes again!


comments powered by Disqus