ps/Modules/Alkami.PowerShell.Services/Public/Install-AlkamiService.ps1
2023-05-30 22:51:22 -07:00

351 lines
15 KiB
PowerShell

function Install-AlkamiService {
<#
.SYNOPSIS
Installs an Alkami Windows Service
.DESCRIPTION
Installs an Alkami Windows Service.
Services installed via this function are not installed to a Service Fabric or other mesh implementation.
This installer does not create k8s or Lambdas or anything that is not a Windows Service.
If the service is found under the specified name and path already registered, this function will exit early and do nothing.
.PARAMETER ServicePath
The path to the service folder or service file. If to a folder, assumes that the file matches the path name.
Ex: C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host as a param would then find the file that matches Alkami.Services.Subscriptions.Host.exe under this path
.PARAMETER AssemblyInfo
The expected name of the service. This overrides the default option of matching the path ID.
.PARAMETER DisplayName
The expected display name of the service.
.PARAMETER IsDatabaseAccessRequired
Does this need to access a database?
.PARAMETER StartOnInstall
Should the program start on install? Defaults to true, must be used to not-start on install
.PARAMETER StartTimeout
The StartTimeout to be passed to Start-AlkamiService. Defaults to 60
.PARAMETER SetNewRelicConfiguration
Should the New Relic configuration be applied to this service?
.PARAMETER StartType
Options are "Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart"
See also the switches: StartDelayed, StartDisabled, StartAutomatically
.EXAMPLE
Install-AlkamiService C:\ProgramData\chocolatey\lib\Alkami.Services.Subscriptions.Host
#>
[CmdletBinding(DefaultParameterSetName = 'StartSpecified')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Scope='Function', Justification='Negated-action switch names are non-intuitive. It should take hoops to disable.')]
Param(
[Parameter(Mandatory = $true, Position = 0)]
[Alias("Path")]
[string]$ServicePath,
[Parameter(Mandatory = $false, Position = 1)]
[Alias("Name")]
[string]$AssemblyInfo,
[string]$DisplayName,
[Alias("UseDBUser")]
[switch]$IsDatabaseAccessRequired,
# Set to true for the dev experience. Expect the pipeline to set to false.
[switch]$StartOnInstall = $true,
[int]$StartTimeout = 60,
[switch]$SetNewRelicConfiguration,
[Parameter(Mandatory = $false, Position = 2)]
[Parameter(ParameterSetName = "StartSpecified")]
[ValidateSet("Manual","Delayed","Automatic","Disabled","AutomaticDelayedStart","")]
[string]$StartType = "",
[Alias("AutoStart")]
[Parameter(ParameterSetName = "StartAutomatically")]
[switch]$StartAutomatically,
[Alias("Disabled")]
[Parameter(ParameterSetName = "StartDisabled")]
[switch]$StartDisabled,
[Alias("Manual")]
[Parameter(ParameterSetName = "StartManual")]
[switch]$StartManual,
[Alias("Delayed")]
[Parameter(ParameterSetName = "StartDelayed")]
[switch]$StartDelayed
)
$delayedParameterConstant = "delayed-auto"
$delayedConstant = "AutomaticDelayedStart"
$disabledParameterConstant = "disabled"
$disabledConstant = "Disabled"
$manualParameterConstant = "demand"
$manualConstant = "Manual"
$automaticParameterConstant = "autostart"
$automaticConstant = "Automatic"
$logLead = Get-LogLeadName
$tier0Services = Get-ServicesByTier -Tier 0
$StartOnInstallConfigSetting = (Get-ConfigSetting "Alkami.Installer.StartOnInstall")
if (![string]::IsNullOrWhiteSpace($StartOnInstallConfigSetting) -and ([bool]::TryParse($StartOnInstallConfigSetting,[ref]$StartOnInstallConfigSetting))) {
if ($StartOnInstall -ne $StartOnInstallConfigSetting) {
Write-Warning "$logLead : StartOnInstall flag conflicts with machine config [$StartOnInstallConfigSetting]"
}
$StartOnInstall = $StartOnInstallConfigSetting
}
# Tier 0 is hard coded to Automatic - Delayed -- SRE-17490
if($tier0Services -Contains $AssemblyInfo ){
$StartType = $delayedConstant
# Skip these checks for tier0
} else {
if ([string]::IsNullOrWhiteSpace($StartType)) {
$serviceStartupModeAppSettingKey = "Alkami.Installer.ServiceStartupMode"
$defaultInstallerMode = Get-AppSetting $serviceStartupModeAppSettingKey
if (![string]::IsNullOrWhiteSpace($defaultInstallerMode)) {
if ([string]::Equals($defaultInstallerMode,"OFF",[StringComparison]::CurrentCultureIgnoreCase)){
$StartType = $disabledConstant
} elseif ([string]::Equals($defaultInstallerMode,"ON",[StringComparison]::CurrentCultureIgnoreCase)){
$StartType = $manualConstant
} elseif ([string]::Equals($defaultInstallerMode,$automaticConstant,[StringComparison]::CurrentCultureIgnoreCase)){
$StartType = $automaticConstant
} else {
Write-Warning "$logLead : Can not parse appSetting [$serviceStartupModeAppSettingKey] with value [$defaultInstallerMode] (acceptable: on, off, $automaticConstant)"
}
}
}
if ([string]::IsNullOrWhiteSpace($StartType)) {
$StartType = $manualConstant
}
# Allow the user to type either or on AutomaticDelayedStart or Disabled
# The value here is used in this lookup sequence and later with Set-Service -StartupType
if ($StartDelayed -or ($StartType -eq "Delayed")) {
$StartType = $delayedConstant
}
if ($StartAutomatically) {
$StartType = $automaticConstant
}
if ($StartDisabled) {
$StartType = $disabledConstant
}
if ($StartManual) {
$StartType = $manualConstant
}
}
# Hash lookup - quicker than a switch
$serviceInstallType = @{
Manual = $manualParameterConstant;
Disabled = $disabledParameterConstant;
Automatic = $automaticParameterConstant;
AutomaticDelayedStart = $delayedParameterConstant;
}.$StartType
<#
# This is gated behind the pipeline, so we have no need to test right now.
if (!(Test-IsDeveloperMachine) -and (Test-IsWebServer)) {
Write-Warning "$loglead : Can not install microservices on the web tier"
return
}
#>
if (!(Test-Path $ServicePath)) {
Write-Warning "$logLead : Path passed in does not represent a valid path: [$ServicePath]. Can not install something which does not exist."
return
}
# Ensure it doesn't end with exe (yet)
if ($AssemblyInfo.EndsWith(".exe")) {
$AssemblyInfo = $AssemblyInfo.Remove($AssemblyInfo.LastIndexOf(".exe"))
}
$ServicePath = (Resolve-Path $ServicePath)
$folderPath = $ServicePath
$serviceAlreadyExists = $false
$stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
$serviceAccount = (Get-MachineConfigServiceAccount -IsDatabaseAccessRequired:$IsDatabaseAccessRequired)
$item = (Get-Item $ServicePath)
if ($item.PSIsContainer) {
# path was a folder
$leafName = (Split-Path -Path $ServicePath -Leaf)
if ($leafName -match 'tools') {
$ServicePath = (Split-Path -Path $ServicePath -Parent)
$leafName = (Split-Path -Path $ServicePath -Leaf)
}
# We still don't have this value, let's pretend it's the folder name then
if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) {
$AssemblyInfo = $leafName
}
$exeName = "$AssemblyInfo.exe"
Write-Host "$logLead : looking for [$exeName] in [$ServicePath]"
$exeItems = @()
# Start with @("app","lib","tools") then do the naive scan
foreach($subpath in @("app","lib","tools")) {
$expectedSubpath = Join-Path -Path $ServicePath -ChildPath $subpath
$expectedSubpathExe = Join-Path -Path $expectedSubpath -ChildPath $exeName
if (Test-Path -Path $expectedSubpathExe)
{
$exeItems = @(Get-Item -Path $expectedSubpathExe)
}
else {
Write-Host "Tried to find an item in a subpath [$expectedSubpath] that doesn't exist. This is fine."
}
if (-not (Test-IsCollectionNullOrEmpty -Collection $exeItems)) {
Write-Host "Found an exe in [$expectedSubpath] at [$expectedSubpathExe]"
break;
}
}
# If we still didn't find anything, fall back to the "naive" pattern
if (Test-IsCollectionNullOrEmpty -Collection $exeItems) {
Write-Host "$logLead : Looking for files via naive-lookup path"
$exeItems = Get-ChildItem -Path (Join-Path $ServicePath $exeName) -Recurse
}
# If we are in naive and find the same .exe multiple times, throw
if ($exeItems.Count -gt 1) {
$additionalDetailMessage = ", can not continue as we don't know which to use"
if ($ErrorActionPreference -ne 'Stop') {
$additionalDetailMessage = ", going to try to install the first one."
}
Write-Error "$logLead : More than one EXE available$additionalDetailMessage"
Write-Host "$logLead : $([string].Join(',',$exeItems.FullName))"
}
$exeItem = $exeItems | Select-Object -First 1 # there should only be one exe with the [package name].exe inside the folders
if ($null -eq $exeItem) {
$stopWatch.Stop()
Write-Warning "$logLead : Path passed in does not contain an exe with the filename: [$exeName]. Can not install a non-existent microservice in [$($stopWatch.Elapsed)]."
return
}
$ServicePath = $exeItem.FullName
} else {
# The $item was not a .PSIsContainer so it was a file
# That means we want the folderPath to be the folder of the file
$folderPath = (Split-Path -Path $ServicePath -Parent)
# We still don't have this value, let's pretend it's the folder name then
if ([string]::IsNullOrWhiteSpace($AssemblyInfo)) {
$AssemblyInfo = (Split-Path -Path $ServicePath -Leaf).Replace(".exe","")
}
}
$exeName = (Split-Path -Path $ServicePath -Leaf)
$serviceAlreadyExists = $null -ne (Get-Service -Name $AssemblyInfo -ErrorAction Ignore)
# If the service already existed we don't have to install it, but we still do the other things
if (!$serviceAlreadyExists) {
# Do the thing here to install the MS.
Write-Host "$logLead : Installing [$exeName] from $ServicePath"
$argumentList = @()
$argumentList += "create"
# SRE-17892 - If given a display-name (which only happens in New-AppTierWindowsServices) use that for the "service name"
# This is useful for Nag and Radium as the service name does not match what is expected
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
$argumentList += "$DisplayName"
# HARD PIVOT
# We are now using DisplayName for AssemblyInfo. Before it was used for service lookup by file only
# Because we are changing how it is registered in _selective_ cases, we are going to change
# the rest of the file on how we use it
Write-Host "$logLead : Changing Install-AlkamiService to use [$DisplayName] for `$AssemblyInfo in-script - This notice is for SRE benefit."
$AssemblyInfo = $DisplayName
} else {
$argumentList += "$AssemblyInfo"
}
$argumentList += "binpath=$ServicePath"
$argumentList += "start=$serviceInstallType"
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
$argumentList += "DisplayName=$DisplayName"
}
Write-Host $argumentList
Invoke-SCExe -Arguments $argumentList
Write-Host "$logLead : Successfully finished [$ServicePath] registration of service"
Write-Host "$logLead : 150ms sleep for [$ServicePath] for flushing"
Start-Sleep -Milliseconds 150
} else {
Stop-AlkamiService $AssemblyInfo
$splat = @{
ServiceName = $AssemblyInfo
StartupType = $StartType
}
if (![string]::IsNullOrWhiteSpace($DisplayName)) {
$splat.DisplayName = $DisplayName
}
# Powershell 5.1 doesn't support using Set-Service to set the start type to Automatic-Delayed. So we do it with SC.exe
if($splat.StartupType -eq $delayedConstant){
$params = @("config", $AssemblyInfo, "start=delayed-auto")
Invoke-SCExe -Arguments $params
$splat.Remove('StartupType')
}
# Call this regardless to set the DisplayName
Set-Service @splat
}
if ($null -ne (Get-Service $AssemblyInfo)) {
Write-Host "$logLead : Attempt to Set-ChocolateyPackageNewRelicState"
$packageName = (Get-ChocoPackageFromPath $ServicePath)
if (![string]::IsNullOrWhiteSpace($packageName)) {
Write-Verbose "$logLead : Set-ChocolateyPackageNewRelicState -Name `"$packageName`" -Enabled $SetNewRelicConfiguration"
Set-ChocolateyPackageNewRelicState -Name $packageName -Enabled $SetNewRelicConfiguration
}
} else {
throw "$loglead : Newly registered service could not be found."
}
# We always call this, whether we just installed it or it already existed
# The reason for this is two-fold.
# 1. In case we are correcting a bad install.
# 2. Resets the gMSA "password already rolled" issue that we use "fixlogins" to resolve
# 2a. This wouldn't resolve "the service got installed 3 months ago" need for "fixlogins", just reinstall
Set-WindowsServiceExecutionAccount -ServiceName $AssemblyInfo -ServiceUser $serviceAccount -IsGMSAAccount
Set-ServiceRecoveryOneRestart -ServiceName $AssemblyInfo
if ($StartOnInstall) {
try {
Start-AlkamiService -ServiceName $AssemblyInfo -Timeout $StartTimeout
} catch {
Write-Warning "$logLead : Service could not be started. See logs above."
if ($serviceAlreadyExists) {
throw $_
} else {
Write-Warning "$logLead : Continuing with install so files will be present post chocolatey install. This is not reporting as an error due to the way chocolatey works during installs, where if it fails it rolls back, but we've already registered the service with Windows. The alternative is to uninstall on failure and let the package be removed."
}
}
} else {
Write-Host "$logLead : Service not started, flag to start was not present or was not set to true"
}
$stopWatch.Stop()
Write-Host "$logLead : [$AssemblyInfo] installed at [$ServicePath] in [$($stopWatch.Elapsed)]"
}