435 lines
21 KiB
PowerShell
435 lines
21 KiB
PowerShell
Function Install-AlkamiWebApplication {
|
|
<#
|
|
.SYNOPSIS
|
|
Install a Web Application to the appropriate place
|
|
|
|
.DESCRIPTION
|
|
Install a Web Application to the appropriate place.
|
|
Will ensure appropriate app pool exists
|
|
|
|
.PARAMETER WebAppName
|
|
[string] The name of the web application.
|
|
|
|
.PARAMETER SourcePath
|
|
[string] The folder that contains the files. Typically a chocolatey path.
|
|
|
|
.PARAMETER IsClient
|
|
[switch] Is this package installed to client?
|
|
|
|
.PARAMETER IsAdmin
|
|
[switch] Is this package installed to admin?
|
|
|
|
.PARAMETER IsLegacy
|
|
[switch] Is this package installed to the legacy site? (typically Default Web Site)
|
|
|
|
.PARAMETER NoManagedCode
|
|
[switch] Is this .net core, or Managed code?
|
|
|
|
.PARAMETER AppPoolName
|
|
[switch] App pool name. Required for .net core apps. That is, if -NoManagedCode is included.
|
|
|
|
.INPUTS
|
|
WebAppName and SourcePath are required.
|
|
Requires one of IsClient or IsAdmin or IsLegacy
|
|
|
|
.OUTPUTS
|
|
Various diagnostic information about the install process
|
|
|
|
.EXAMPLE
|
|
Install-AlkamiWebApplication -WebAppName BankService -SourcePath C:\Orb\BankService -IsLegacy
|
|
|
|
Various diagnostic information about the install process.
|
|
|
|
#>
|
|
## We could define a named set here as the default, but I would rather not. Let it fail if we don't pass in the param flag
|
|
[CmdletBinding(DefaultParameterSetName='IsClient')]
|
|
[OutputType([System.Collections.ArrayList])]
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("IsClient", '', Justification="This is a Switch that goes with a parameter set. Function usage is confusing without this switch.", Scope = "Function")]
|
|
Param(
|
|
[Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=0)]
|
|
[Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=0)]
|
|
[Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=0)]
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true, Position=0)]
|
|
[string]$WebAppName,
|
|
[Parameter(ParameterSetName='IsAdmin',Mandatory=$true, Position=1)]
|
|
[Parameter(ParameterSetName='IsClient',Mandatory=$true, Position=1)]
|
|
[Parameter(ParameterSetName='IsLegacy',Mandatory=$true, Position=1)]
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true, Position=1)]
|
|
[string]$SourcePath,
|
|
[Parameter(ParameterSetName='IsClient',Mandatory=$true)]
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)]
|
|
[switch]$IsClient,
|
|
[Parameter(ParameterSetName='IsAdmin',Mandatory=$true)]
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)]
|
|
[switch]$IsAdmin,
|
|
[Parameter(ParameterSetName='IsLegacy',Mandatory=$true)]
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$false)]
|
|
[switch]$IsLegacy,
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true)]
|
|
[switch]$NoManagedCode,
|
|
[Parameter(ParameterSetName='IsDotNetCore',Mandatory=$true)]
|
|
[string]$AppPoolName
|
|
)
|
|
process {
|
|
|
|
$logLead = (Get-LogLeadName)
|
|
|
|
#region Invoke-CommandWithRetry subdefinitions
|
|
$icwrSeconds = 3 # 3 seconds between retries
|
|
$icwrJitterMin = 0 # 0 milliseconds, so we wait a minimum of $icwrSeconds before retrying
|
|
$icwrJitterMax = 5000 # 5000 milliseconds, so 5 seconds
|
|
$icwrRetryCount = 5 # 5 retries, between 3s and 8s each, for as many as 40s to complete.
|
|
|
|
#build an argument splat to save copy-paste readability
|
|
$icwrSplat = @{
|
|
Seconds = $icwrSeconds
|
|
JitterMin = $icwrJitterMin
|
|
JitterMax = $icwrJitterMax
|
|
MaxRetries = $icwrRetryCount
|
|
}
|
|
|
|
$setEnabledProtocolsScriptBlock= {
|
|
param ($sb_WebAppName, $sb_sitePath)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(enabledProtocols)"
|
|
(Set-ItemProperty "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name enabledProtocols -Value "http,net.tcp") | Out-Null
|
|
}
|
|
|
|
$setApplicationPoolScriptBlock = {
|
|
param ($sb_WebAppName, $sb_sitePath, $sb_webAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(applicationPool)"
|
|
(Set-ItemProperty -Path "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name applicationPool -Value $sb_webAppPoolName) | Out-Null
|
|
}
|
|
|
|
$setPreloadEnabledScriptBlock = {
|
|
param ($sb_WebAppName, $sb_sitePath)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(preloadEnabled)"
|
|
(Set-ItemProperty "IIS:\Sites\$sb_sitePath\$sb_WebAppName" -Name preloadEnabled -Value "true") | Out-Null
|
|
}
|
|
|
|
|
|
# Used to modify .net CLR version to support .net core.
|
|
$setNoManagedCodeScriptBlock = {
|
|
param ($sb_webAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-ItemProperty(managedRuntimeVersion)"
|
|
(Set-ItemProperty "IIS:\AppPools\$sb_webAppPoolName" -Name managedRuntimeVersion -Value "") | Out-Null
|
|
}
|
|
|
|
$setAlkamiWebAppPoolConfigurationScriptBlock = {
|
|
param ($sb_webAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Set-AlkamiWebAppPoolConfiguration"
|
|
(Set-AlkamiWebAppPoolConfiguration $sb_webAppPoolName) | Out-Null
|
|
}
|
|
|
|
$newAlkamiWebAppPoolScriptBlock = {
|
|
param ($sb_webAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-AlkamiWebAppPool"
|
|
return New-AlkamiWebAppPool -Name $sb_webAppPoolName
|
|
}
|
|
|
|
$newWebApplicationScriptBlock = {
|
|
param ($sb_WebAppName, $sb_sitePath, $sb_appPhysicalPath, $sb_webAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-WebApplication"
|
|
# Use -Force to ensure it gets created correctly
|
|
return New-WebApplication -Name $sb_WebAppName -Site $sb_sitePath -PhysicalPath $sb_appPhysicalPath -ApplicationPool $sb_webAppPoolName -Force
|
|
}
|
|
|
|
# value is not consumed currently, so not returned here
|
|
$newWebVirtualDirectoryScriptBlock = {
|
|
param ($sb_sitePath, $sb_folder, $sb_emptyPath)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : New-WebVirtualDirectory"
|
|
New-WebVirtualDirectory -Site $sb_sitePath -Name $sb_folder -PhysicalPath $sb_emptyPath
|
|
}
|
|
|
|
# not doing out-null so we can see it print in the logs that it got deleted
|
|
$removeWebApplicationScriptBlock = {
|
|
param ($sb_WebAppName, $sb_sitePath)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Remove-WebApplication"
|
|
Remove-WebApplication $sb_WebAppName -Site $sb_sitePath
|
|
}
|
|
|
|
# not doing out-null so we can see it print in the logs that it got deleted
|
|
$RemoveWebAppPoolScriptBlock = {
|
|
param ($sb_oldAppPoolName)
|
|
Write-Host "[Install-AlkamiWebApplication] : Invoke-CommandWithRetry : Remove-WebAppPool"
|
|
Remove-WebAppPool -Name $sb_oldAppPoolName
|
|
}
|
|
#endregion Invoke-CommandWithRetry subdefinitions
|
|
|
|
if (!(Test-Path $SourcePath)) {
|
|
Write-Warning "$logLead : The path $SourcePath was not found, application $WebAppName will not be created"
|
|
return
|
|
}
|
|
|
|
## validate path points to the choco folder unless it is a legacy application
|
|
if ($IsLegacy -and !($sourcePath.ToLower().Contains((Get-OrbPath).ToLower()))) {
|
|
Write-Warning "$logLead : $WebAppName is marked IsLegacy but $SourcePath doesn't seem to be in the expected ORB folder. This will be an Alkami.IOC issue."
|
|
} elseif (!$IsLegacy) {
|
|
$isPackagePathValidated = (Test-PathIsInApprovedPackageLocation $sourcePath)
|
|
if (!($isPackagePathValidated)) {
|
|
Write-Warning "$logLead : The source location at [$sourcePath] doesn't match the expected criteria of non-IsLegacy installs"
|
|
Write-Warning "$logLead : The expectation is that non-IsLegacy installs will be constrained to the Chocolatey lib folder install path"
|
|
#throw "can not finish install - bad path provided"
|
|
}
|
|
}
|
|
|
|
$siteList = @()
|
|
|
|
$webAppPoolName = $WebAppName.Replace('\','_').Replace('/','_')
|
|
$parentAppName = $webAppPoolName
|
|
$newRelicAppName = $webAppPoolName
|
|
|
|
$forceAppPoolsOnClientToUseWebClient = $false
|
|
|
|
if ($IsLegacy) {
|
|
## Use of this function ensures that the Default Web Site exists
|
|
$defaultWebsite = (Get-DefaultWebsite)
|
|
if ($null -eq $defaultWebsite) {
|
|
$defaultWebsite = Invoke-CommandWithRetry -ScriptBlock { return (New-DefaultWebsite) } @icwrSplat
|
|
}
|
|
## Ensure we add this to the list of sites we are going to be installing to
|
|
$siteList += $defaultWebsite
|
|
|
|
$parentAppName = $defaultWebsite.Name
|
|
} else {
|
|
## Test to make sure this is the valid site name. We can do this by looking for the site, if it doesn't exist, look for the path
|
|
## C:\Orb\$parentAppName and then find any sites for that.
|
|
|
|
## By process of elimination, if you weren't in parameter set IsAdmin or IsLegacy, you must be in IsClient.
|
|
$parentAppName = 'WebClient'
|
|
|
|
if ($IsAdmin) {
|
|
$parentAppName = 'WebClientAdmin'
|
|
}
|
|
|
|
# SRE-16828 - If you want to force apps to be under WebClient or WebClientAdmin this is the line to change
|
|
$forceAppPoolsOnClientToUseWebClient = $true
|
|
|
|
$targetFolderPath = (Join-Path (Get-OrbPath) $parentAppName)
|
|
|
|
$siteList = (Get-IISSitesByPath $targetFolderPath)
|
|
}
|
|
|
|
if (Test-IsCollectionNullOrEmpty $siteList) {
|
|
Write-Warning "$logLead : Can't find any sites to bind to for WebAppName: [$WebAppName] and ParentAppName: [$parentAppName]"
|
|
throw "$logLead : Can't find any sites to bind to for WebAppName: [$WebAppName] and ParentAppName: [$parentAppName]"
|
|
} else {
|
|
Write-Host "$logLead : Found these sites [`"$(($siteList).Name -join '`",`"')`"]"
|
|
}
|
|
|
|
# SRE-16828 - If you want to force apps to be under WebClient or WebClientAdmin this is the line to change
|
|
if ($forceAppPoolsOnClientToUseWebClient) {
|
|
$webAppPoolName = $ParentAppName
|
|
}
|
|
|
|
# NoManagedCode (that is, .net core apps) can't run on the same app pool as any other app, as opposed to ManagedCode that can
|
|
if($NoManagedCode){
|
|
$webAppPoolName = $AppPoolName
|
|
}
|
|
|
|
## Get the web app pool to make sure that's configured correctly before we start looping sites
|
|
$appPoolPath = (Join-Path "IIS:\AppPools" $webAppPoolName)
|
|
$appPool = (Get-Item $appPoolPath -ErrorAction SilentlyContinue)
|
|
|
|
## If we couldn't find the application pool for this app, need to create one to go with this application
|
|
## Else we need to ensure the app pool is properly configured
|
|
if ($null -eq $appPool) {
|
|
$icwrArguments = @{
|
|
ScriptBlock = $newAlkamiWebAppPoolScriptBlock
|
|
Arguments = @($webAppPoolName)
|
|
}
|
|
$appPool = Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
} else {
|
|
if (!$forceAppPoolsOnClientToUseWebClient) {
|
|
$icwrArguments = @{
|
|
ScriptBlock = $setAlkamiWebAppPoolConfigurationScriptBlock
|
|
Arguments = @($webAppPoolName)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
}
|
|
|
|
# Set managedRuntimeVersion to noManagedCode for .net core.
|
|
# This *ALWAYS* has to go after Set-WebPoolConfig
|
|
if($NoManagedCode){
|
|
Write-Host "$logLead : Setting Application Attribute .NET CLR Version to '' (noManagedCode) for AppPools\$WebAppPoolName"
|
|
$icwrArguments = @{
|
|
ScriptBlock = $setNoManagedCodeScriptBlock
|
|
Arguments = @($WebAppPoolName)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
|
|
$oldAppPoolNames = @()
|
|
|
|
foreach ($site in $siteList) {
|
|
$sitePath = $site.Name
|
|
|
|
$virtualFolders = [System.Collections.ArrayList]($WebAppName.Split(@('/','\')))
|
|
|
|
## Foreach virtual path in our segmented list, we need to ensure we have a folder on disk to point to
|
|
## By having an empty folder when one doesn't exist, there can't be much data leakage, as there's no child folders
|
|
## Still a risk of ..\ links in whatever might get misconfigured, so we just need to be diligent
|
|
if ($virtualFolders.Count -gt 1) {
|
|
$depthCount = $virtualFolders.Count - 1
|
|
$WebAppName = $virtualFolders[-1]
|
|
|
|
$emptyPath = (Join-Path (Get-OrbPath) "Empty")
|
|
if (!(Test-Path $emptyPath)) {
|
|
(New-Item -Path $emptyPath -ItemType Directory -Force) | Out-Null
|
|
$emptyPathReadmePath = (Join-Path $emptyPath "readme.txt")
|
|
if(!(Test-Path $emptyPathReadmePath)) {
|
|
Set-Content -Path $emptyPathReadmePath -Value @"
|
|
This folder should remain empty except this file.
|
|
|
|
If this folder has directory browsing turned on, it should be turned off.
|
|
|
|
Please report any instance of this directory browsing turned on to security@alkamitech.com along with the URL where you found it.
|
|
|
|
Thank you for your assistance.
|
|
"@
|
|
}
|
|
}
|
|
for ($i = 0; $i -lt $depthCount; $i++) {
|
|
$folder = $virtualFolders[$i]
|
|
## Recursively create subfolders under the site.
|
|
## See https://stackoverflow.com/a/21187565/109749
|
|
if ($null -eq (Get-WebVirtualDirectory -Site $sitePath -Name $folder)) {
|
|
$icwrArguments = @{
|
|
ScriptBlock = $newWebVirtualDirectoryScriptBlock
|
|
Arguments = @($sitePath, $folder, $emptyPath)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
|
|
## Keep appending the folder we just created to the existing $sitePath
|
|
## This gets injected below in the middle of the string
|
|
$sitePath = $sitePath + "\" + $folder
|
|
}
|
|
}
|
|
|
|
$appPhysicalPath = $SourcePath
|
|
$appPhysicalPathContent = (Join-Path $appPhysicalPath Content)
|
|
## Try to find the folder at c:\programdata\choco\lib\myapp\Content\App which should have views, images, etc in it.
|
|
if (Test-Path $appPhysicalPathContent) {
|
|
$appPhysicalPathContentTest = (Join-Path $appPhysicalPathContent 'App')
|
|
if (Test-Path $appPhysicalPathContentTest) {
|
|
$appPhysicalPath = $appPhysicalPathContentTest
|
|
} else {
|
|
## Try to find the folder at c:\programdata\choco\lib\myapp\Content\* which has files in it, guessing that's what we want instead, even though it breaks the pattern
|
|
if (Test-Path $appPhysicalPathContent) {
|
|
$appPhysicalPathContentTest = (Get-ChildItem $appPhysicalPathContent) | Where-Object { $_.PSIsContainer } | Select-Object -First 1
|
|
if (($null -ne $appPhysicalPathContentTest) -and ($null -ne (Get-ChildItem $appPhysicalPathContentTest))) {
|
|
$appPhysicalPath = $appPhysicalPathContentTest
|
|
} elseif ($null -ne (Get-ChildItem $appPhysicalPathContent)) {
|
|
## Sigh, just use the c:\programdata\choco\lib\myapp\Content cos there's stuff in there, who knows anymore, life is bleak
|
|
$appPhysicalPath = $appPhysicalPathContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$existingApp = Get-WebApplication $WebAppName -Site $sitePath
|
|
|
|
if ($null -ne $existingApp) {
|
|
## Ensure the path matches the expected location in case it has moved, see SRE-12829
|
|
if ($existingApp.PhysicalPath -ne $appPhysicalPath) {
|
|
$icwrArguments = @{
|
|
ScriptBlock = $removeWebApplicationScriptBlock
|
|
Arguments = @($WebAppName, $sitePath)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
$existingApp = $null ## force it to null so the next value kicks in
|
|
}
|
|
}
|
|
|
|
$existingApp = Get-WebApplication $WebAppName -Site $sitePath
|
|
|
|
if ($null -eq $existingApp) {
|
|
Write-Host "Creating [$WebAppName] for [$sitePath]"
|
|
$icwrArguments = @{
|
|
ScriptBlock = $newWebApplicationScriptBlock
|
|
Arguments = @($WebAppName, $sitePath, $appPhysicalPath, $webAppPoolName)
|
|
}
|
|
$existingApp = Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
} else {
|
|
Write-Host "Found existing Web Application for [$WebAppName] on [$sitePath]"
|
|
}
|
|
|
|
## If the name of the application pool matches the one that's on the existing webapplication then groovy
|
|
## Otherwise we have to re-point the web-app to use the app pool with the same name.
|
|
## We already made sure we have such an app pool that matches the name, so we just need to do the attaching.
|
|
if ( $existingApp.applicationPool -ne $webAppPoolName) {
|
|
## We have a valid app pool that uses the same name as the web app, and we are here because they didn't match.
|
|
|
|
## Random bug catch, eep
|
|
if (![string]::IsNullOrWhiteSpace($existingApp.applicationPool)) {
|
|
## Store the old name so we can delete any unused app pools later
|
|
Write-Verbose "Adding existing application pool that doesn't match the expected name to the delete-list $($existingApp.applicationPool)"
|
|
$oldAppPoolNames += $existingApp.applicationPool
|
|
}
|
|
|
|
## Let's assign the new app pool to the web application
|
|
$icwrArguments = @{
|
|
ScriptBlock = $setApplicationPoolScriptBlock
|
|
Arguments = @($WebAppName, $sitePath, $webAppPoolName)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
|
|
if ($IsLegacy) {
|
|
Write-Host "$logLead : Setting Application Attribute EnabledProtocols [http,net.tcp] for $sitePath\$WebAppName"
|
|
$icwrArguments = @{
|
|
ScriptBlock = $setEnabledProtocolsScriptBlock
|
|
Arguments = @($WebAppName, $sitePath)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
|
|
Write-Host "$logLead : Setting Application Attribute preloadEnabled to True for $sitePath\$WebAppName"
|
|
$icwrArguments = @{
|
|
ScriptBlock = $setPreloadEnabledScriptBlock
|
|
Arguments = @($WebAppName, $sitePath)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
}
|
|
|
|
$environmentUserPrefix = (Get-AppSetting "Environment.Name" -SuppressWarnings)
|
|
|
|
if ($null -ne $environmentUserPrefix) {
|
|
## Find the config file path. This is a web app, so we default to web.config as the file name and in the same root folder as the content
|
|
## If we had a non-web-app we might have to look for another file but this is Install-AlkamiWebApplication.
|
|
$ConfigFilePath = (Join-Path $appPhysicalPath "web.config")
|
|
|
|
if (!(Test-Path $ConfigFilePath)) {
|
|
Write-Warning "Can't find a web.config under the folder [$ConfigFilePath]"
|
|
}
|
|
else {
|
|
## ensure NR name set in web.config with Environment.Name + AppName from config
|
|
## This function will get the application label for us so we don't have to manually fetch it
|
|
Set-NewRelicAppNameConfigFileValue $environmentUserPrefix $ConfigFilePath $newRelicAppName
|
|
}
|
|
}
|
|
|
|
foreach($oldAppPoolName in $oldAppPoolNames) {
|
|
## The same "wrong" app pool could have been used on other apps so only remove them if there are no referenced apps for this pool
|
|
$appPoolPath = (Join-Path "IIS:\AppPools" $oldAppPoolName)
|
|
$existingApp = (Get-Item $appPoolPath -ErrorAction SilentlyContinue)
|
|
|
|
if ($null -ne $existingApp) {
|
|
$existingAppApplicationCount = (Get-IISAppPoolChildApplicationsCount $oldAppPoolName)
|
|
|
|
## The count of that outdated application pool is 0, indicating that apppool isn't in use. Delete it.
|
|
if ($existingAppApplicationCount -eq 0) {
|
|
Write-Verbose "$logLead : Removing identified and unused Web AppPool [$oldAppPoolName]"
|
|
$icwrArguments = @{
|
|
ScriptBlock = $RemoveWebAppPoolScriptBlock
|
|
Arguments = @($oldAppPoolName)
|
|
}
|
|
Invoke-CommandWithRetry @icwrArguments @icwrSplat
|
|
} else {
|
|
Write-Verbose "$logLead : Web AppPool [$oldAppPoolName] still has things attached to it. Not deleting."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |