Public/Install-Puppet.ps1

<#
.SYNOPSIS
    Installs Puppet tooling on a machine
.DESCRIPTION
    Installs the requested version of Puppet agent/server/bolt for your operating system.
    You can either specify the major version that you want installed whereby the latest version for that release will be installed,
    or you can specify a specific version. (e.g. 6.10.2)
.EXAMPLE
    Install-Puppet -MajorVersion 6

    This would install the latest version of Puppet 6 agent for your operating system.
.EXAMPLE
    Install-Puppet -ExactVersion 6.10.2 -Application 'puppetserver'

    This would install Puppet server 6.10.2 for your operating system.
.EXAMPLE
    Install-Puppet -Application 'puppet-bolt'

    This would install the latest version of Puppet bolt for your operating system.
#>

function Install-Puppet
{
    [CmdletBinding()]
    param
    (
        # The major version of Puppet agent to install
        [Parameter(Mandatory = $false)]
        [int]
        $MajorVersion,

        # The specific version of Puppet agent to install
        [Parameter(Mandatory = $false)]
        [version]
        $ExactVersion,

        # Whether to install Puppet server or Puppet agent
        [Parameter(Mandatory = $false)]
        [string]
        [ValidateSet('puppet-agent', 'puppetserver', 'puppet-bolt')]
        $Application = 'puppet-agent',

        # Useful in testing to disable package managers
        [Parameter(DontShow)]
        [switch]
        $UseLegacyMethod
    )
    
    begin
    {
        if ($Application -eq 'puppetserver')
        {
            if (!$IsLinux)
            {
                Throw "Puppet server is only available on Linux"
            }
        }
        # We only care about the versioning of Puppet server/agent
        if (!$MajorVersion -and !$ExactVersion -and ($Application -ne 'puppet-bolt'))
        {
            throw "One of 'MajorVersion' or 'ExactVersion' must be specified"
        }
        if ($MajorVersion -and $ExactVersion)
        {
            Write-Warning "Both 'MajorVersion' and 'ExactVersion' specified, only 'ExactVersion' will be used."
        }
        if ($ExactVersion)
        {
            [int]$MajorVersion = $ExactVersion.Major
        }
    }
    
    process
    {
        # macOS install method
        if ($IsMacOS)
        {
            Write-Verbose "Installing $Application for macOS"
            # This should work with both the legacy and Homebrew methods
            $PuppetCheck = pkgutil --pkgs | Where-Object { $_ -like "*$Application*" }
            if ($PuppetCheck)
            {
                Write-Host "$Application is already installed:`n$($PuppetCheck)"
                break
            }
            $RootCheck = & id -u
            try
            {
                $BrewCheck = Get-Command 'brew'
            }
            catch {}
            if ($BrewCheck -and !$ExactVersion -and !$UseLegacyMethod)
            {
                Write-Verbose "Using homebrew"
                if ($RootCheck -eq 0)
                {
                    throw "Running as root, this will not work with homebrew."
                }
                # In cases where we don't want an exact version _and_ homebrew is available we'll install using that
                # See here for more info https://github.com/puppetlabs/homebrew-puppet
                if ($Application -eq 'puppet-bolt')
                {
                    $Cask = "puppetlabs/puppet/$Application"
                }
                else
                {
                    $Cask = "puppetlabs/puppet/$Application-$MajorVersion"
                }
                Write-Verbose "Installing $Cask"
                & brew install $Cask
                if ($LASTEXITCODE -ne 0)
                {
                    throw "Failed to install $Application using brew"
                }
            }
            <#
                When homebrew is not available or we require a specific version we'll need to query the pupetlabs downloads
                We do this by scrubbing the links at http://downloads.puppet.com/mac/ which isn't foolproof but should be good enough
             #>

            if (!$BrewCheck -or $ExactVersion -or $UseLegacyMethod)
            {
                Write-Warning "Homebrew not installed or exact version specified, falling back to legacy method"
                if ($RootCheck -ne 0)
                {
                    throw "Legacy install method requires root on macOS"
                }
                [version]$OSVersion = & sw_vers -productVersion
                if (!$OSVersion)
                {
                    throw "Failed to determine macOS version"
                }
                Write-Verbose "macOS version is $OSVersion"
                if ($Application -eq 'puppet-agent')
                {
                    $BaseURL = "http://downloads.puppet.com/mac/puppet$($MajorVersion)"
                }
                else
                {
                    $BaseURL = "http://downloads.puppet.com/mac/puppet-tools"
                }
                Write-Verbose "Querying $BaseURL for supported operating systems"
                # Get the contents of that folder and see if we can find a match for our OS version
                try
                {
                    $SupportedOS = Invoke-WebRequest $BaseURL | Select-Object -ExpandProperty Links
                }
                catch
                {
                    throw "Failed to query $BaseURL.`n$($_.Exception.Message)"
                }

                if (!$SupportedOS)
                {
                    throw "No results returned from $BaseURL"
                }

                # See if our OS is compatible...
                foreach ($Link in $SupportedOS.href)
                {
                    # Try getting the most exact version we can find
                    switch ($Link)
                    {
                        "$($OSversion.Major)/"
                        {
                            $MatchedVersion = "$($OSversion.Major)/"
                        }
                        "$($OSversion.Major).$($OSVersion.Minor)/"
                        {
                            $MatchedVersion = "$($OSversion.Major).$($OSVersion.Minor)/"
                        }
                    }
                    # Break out of the loop when we've found a match!
                    if ($MatchedVersion)
                    {
                        Write-Verbose "Matched '$MatchedVersion'"
                        break
                    }
                }
                if (!$MatchedVersion)
                {
                    throw "Unable to find a supported version of Puppet agent for macOS $($OSVersion.toString())"
                }
                # Only support x86_64 at present
                $BaseURL = $BaseURL + "/$MatchedVersion" + "x86_64"

                # Grab the exact version if we've specified one otherwise just get latest
                if ($ExactVersion)
                {
                    $DownloadURL = $BaseURL + "/$Application-$($ExactVersion.ToString())-1.osx$($MatchedVersion -replace '\/','').dmg"
                }
                else
                {
                    $DownloadURL = $BaseURL + "/$Application-latest.dmg"
                }

                # Download it
                $TempFile = Join-Path (Get-PSDrive Temp).Root "$Application.dmg"
                Write-Verbose "Downloading from $DownloadURL to $TempFile"
                try
                {
                    Invoke-WebRequest $DownloadURL -OutFile $TempFile
                }
                catch
                {
                    throw "Failed to download $Application from $DownloadURL.`n$($_.Exception.Message)"
                }
                Write-Verbose "Mounting $TempFile"
                & hdiutil mount $TempFile -quiet
                if ($LASTEXITCODE -ne 0)
                {
                    throw "Failed to mount $TempFile"
                }
                try
                {
                    $PuppetDrive = Get-ChildItem '/Volumes/' | Where-Object { $_.Name -like "$Application*" }
                }
                catch
                {
                    throw "Failed to query Puppet drive.`n$($_.Exception.Message)"
                }
                if ($PuppetDrive.count -gt 1)
                {
                    throw "Too many Puppet drives returned!`nExpected 1 got $($PuppetDrive.count).`n$($PuppetDrive.PSPath)"
                }
                if (!$PuppetDrive)
                {
                    throw "No Puppet drives found"
                }
                # Get the pkg
                $PuppetPKG = Get-ChildItem $PuppetDrive | Where-Object { $_.Name -like '*.pkg' } | Select-Object -ExpandProperty 'PSPath' | Convert-Path
                if (!$PuppetPKG)
                {
                    # Clean-up
                    & hdiutil unmount $PuppetDrive -force -quiet
                    throw "Cannot find $Application pkg installer"
                }
                Write-Verbose "Installing $PuppetPKG"
                & installer -pkg $PuppetPKG -target /
                if ($LASTEXITCODE -ne 0)
                {
                    # Clean-up
                    & hdiutil unmount $PuppetDrive -force -quiet
                    throw "Failed to install $Application"
                }
                # Clean-up
                & hdiutil unmount $PuppetDrive -force -quiet
            }
        }
        # Windows install method
        if ($IsWindows)
        {
            $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
            $Administrator = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
            if (!$Administrator)
            {
                throw "Must be run as administrator"
            }
            switch ($Application)
            {
                'puppet-agent'
                {
                    $Command = 'puppet'
                }
                'puppet-bolt' 
                {
                    $Command = 'bolt'
                }
            }
            $PuppetCheck = Get-Command $Command -ErrorAction SilentlyContinue
            if ($PuppetCheck)
            {
                Write-Host "$Application is already installed:`n$($PuppetCheck.Source)"
                break
            }
            try
            {
                $ChocoCheck = Get-Command 'choco'
            }
            catch {}
            if ($ChocoCheck -and !$UseLegacyMethod)
            {
                if ($ExactVersion)
                {
                    $VersionToInstall = $ExactVersion.ToString()
                }
                else
                {
                    if ($MajorVersion)
                    {
                        # If we've only specified the major version then we need to do some work
                        # Get all versions of the package
                        $AvailableVersions = (& choco list -e $Application -a -r) -replace "$Application\|", ''
                        # Cast to version array
                        $AvailableVersionNumbers = $AvailableVersions | ForEach-Object { [version]$_ }

                        
                        # As it stands the latest version is always first in the array
                        try
                        {
                            $VersionToInstall = ($AvailableVersionNumbers | Where-Object { $_.Major -eq $MajorVersion } | Select-Object -First 1).ToString()
    
                        }
                        catch
                        {
                            throw "Failed to find a version to install."
                        }
                        if (!$VersionToInstall)
                        {
                            throw "Cannot find a version to install."
                        }
                        Write-Verbose "Latest version appears to be $VersionToInstall"
                    }
                }
                Write-Verbose "Attempting to install $Application"
                if ($VersionToInstall)
                {
                    & choco install $Application --version $VersionToInstall -y
                }
                else
                {
                    & choco install $Application -y
                }
                if ($LASTEXITCODE -ne 0)
                {
                    throw "Failed to install $Application"
                }
            }
            else
            {
                Write-Warning "Chocolatey not available, falling back to legacy method"
                if ($Application -eq 'puppet-agent')
                {
                    $BaseURL = "http://downloads.puppetlabs.com/windows/puppet$($MajorVersion)"
                }
                else
                {
                    $BaseURL = "http://downloads.puppetlabs.com/windows/puppet-tools"
                }

                if ($ExactVersion)
                {
                    $DownloadURL = $BaseURL + "/$Application-$($ExactVersion.ToString())-x64.msi"
                }
                else
                {
                    $DownloadURL = $BaseURL + "/$Application-x64-latest.msi"
                }

                # Download it
                $TempFile = Join-Path (Get-PSDrive Temp).Root "$Application.msi"
                Write-Verbose "Downloading from $DownloadURL to $TempFile"
                try
                {
                    Invoke-WebRequest -Uri $DownloadURL -OutFile $TempFile
                }
                catch
                {
                    throw "Failed to download $Application.`n$($_.Exception.Message)"
                }

                # Install it
                Write-Verbose "Installing from $TempFile"
                # Use start process so we can wait for completion
                $Install = Start-Process 'msiexec' -ArgumentList "/qn /norestart /i $TempFile" -Wait -NoNewWindow -PassThru
                if ($Install.ExitCode -ne 0)
                {
                    throw "Failed to install $Application"
                }
            }
        }
        # Linux install method
        if ($IsLinux)
        {
            # We need to check if we're running as root
            $User = & id -u
            if ($User -ne 0)
            {
                throw "Must be run as root"
            }
            Write-Verbose "Checking distribution"
            $Distribution = & awk -F= '/^NAME/{print $2}' /etc/os-release
            if (!$Distribution)
            {
                throw "Unable to determine Linux distribution"
            }
            Write-Verbose "Linux distribution is $Distribution"
            switch -regex ($Distribution)
            {
                '\"CentOS Linux\"'
                {
                    Write-Verbose "Checking to see if $Application is already installed."
                    $PuppetCheck = & yum list installed | Where-Object { $_ -like "$Application*" }
                    if ($PuppetCheck)
                    {
                        Write-Host "$Application already installed:`n$PuppetCheck"
                        break
                    }
                    Write-Verbose "Installing $Application for CentOS"
                    $CentOSVersion = (& awk -F= '/^VERSION_ID/{print $2}' /etc/os-release) -replace '\"', ''
                    if ($Application -eq 'puppet-bolt')
                    {
                        $RepositoryURL = "https://yum.puppet.com/puppet-tools-release-el-$CentOSVersion.noarch.rpm"
                    }
                    else
                    {
                        $RepositoryURL = "https://yum.puppetlabs.com/puppet$MajorVersion-release-el-$CentOSVersion.noarch.rpm"
                    }
                    $TempFile = Join-Path (Get-PSDrive Temp).Root 'puppet.rpm'
                    Write-Verbose "Downloading from $RepositoryURL to $TempFile"
                    try
                    {
                        Invoke-WebRequest -Uri $RepositoryURL -OutFile $TempFile
                    }
                    catch
                    {
                        throw "Failed to download $Application.`n$($_.Exception.Message)"
                    }
                    Write-Verbose "Installing from $TempFile"
                    & rpm -Uvh $TempFile
                    if ($LASTEXITCODE -ne 0)
                    {
                        throw "Failed to add yum repository."
                    }
                    Write-Verbose "Installing $Application"
                    if ($ExactVersion)
                    {
                        & yum install $Application-$ExactVersion -y
                    }
                    else
                    {
                        & yum install $Application -y
                    }
                    if ($LASTEXITCODE -ne 0)
                    {
                        throw "Failed to install $Application"
                    }
                }
                '\"(?:Ubuntu|Debian GNU/Linux)\"'
                {
                    # Do a quick check to see if Puppet is already installed
                    Write-Verbose "Checking to see if $Application is already installed."
                    $PuppetCheck = & dpkg --get-selections | Where-Object { $_ -like "$Application*" }
                    if ($PuppetCheck)
                    {
                        Write-Host "$Application is already installed on your system."
                        break
                    }
                    Write-Verbose "Installing $Application for Debian based OS"
                    $ReleaseName = & lsb_release -c -s
                    if ($Application -eq 'puppet-bolt')
                    {
                        $RepositoryURL = "http://apt.puppet.com/puppet-tools-release-$($ReleaseName).deb"
                    }
                    else
                    {
                        $RepositoryURL = "http://apt.puppet.com/puppet$MajorVersion-release-$($ReleaseName).deb"
                    }
                    $TempFile = Join-Path (Get-PSDrive Temp).Root 'puppet.deb'
                    Write-Verbose "Downloading from $RepositoryURL to $TempFile"
                    try
                    {
                        Invoke-WebRequest -Uri $RepositoryURL -OutFile $TempFile
                    }
                    catch
                    {
                        throw "Failed to download Puppet repository.`n$($_.Exception.Message)"
                    }
                    Write-Verbose "Installing Puppet repository"
                    & dpkg -i $TempFile
                    if ($LASTEXITCODE -ne 0)
                    {
                        throw "Failed to install Puppet repository."
                    }
                    & apt-get update
                    Write-Verbose "Installing $Application"
                    if ($ExactVersion)
                    {
                        # The packages seem to be in the format of "puppet-agent 6.24.0-1focal"
                        $VersionToInstall = "$ExactVersion-1$ReleaseName"
                        Write-Verbose "Installing $VersionToInstall"
                        & apt-get install -y $Application=$VersionToInstall
                    }
                    else
                    {
                        Write-Verbose "Installing latest version"
                        & apt-get install -y $Application
                    }
                    if ($LASTEXITCODE -ne 0)
                    {
                        throw "Failed to install $Application"
                    }
                }
                Default 
                {
                    throw "Unsupported Linux distribution '$Distribution'"
                }
            }
        }
                
    }
    
    end
    {
        
    }
}