Consolidated ASP Classic MVC framework from best components
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

321 lignes
14KB

  1. <#
  2. .SYNOPSIS
  3. Validates, packages, and deploys this repository to its dedicated IIS site.
  4. .DESCRIPTION
  5. The controller packages the complete repository, computes SHA-256, transfers
  6. the package and host installer over OpenSSH, and invokes the installer.
  7. -LocalPreflightOnly validates source provenance, XML, package layout, and
  8. archive safety without connecting. -PreflightOnly additionally streams the
  9. installer to the host and runs its read-only IIS preflight without writing a
  10. remote installer or package.
  11. #>
  12. [CmdletBinding()]
  13. param(
  14. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  15. [string]$SiteName = 'AspClassicUnifiedFramework',
  16. [ValidatePattern('^[A-Za-z0-9_. -]+$')]
  17. [string]$AppPoolName = 'AspClassicUnifiedFramework',
  18. [string]$DeployRoot = 'D:\Deployments\AspClassicUnifiedFramework',
  19. [string]$BindingIpAddress = '100.97.39.23',
  20. [ValidateRange(1, 65535)]
  21. [int]$BindingPort = 8085,
  22. [AllowEmptyString()]
  23. [string]$HostHeader = '',
  24. [string]$InitialWebConfigPath = '',
  25. [string]$RemoteTarget = 'webserver-1',
  26. [ValidateRange(1, 65535)]
  27. [int]$RemotePort = 22,
  28. [string]$SourcePath = (Split-Path $PSScriptRoot -Parent),
  29. [string]$BaseUrl = '',
  30. [string]$ReleaseId = '',
  31. [string]$ExpectedBranch = 'master',
  32. [ValidateRange(2, 100)]
  33. [int]$KeepReleases = 5,
  34. [string]$SshExe = 'ssh',
  35. [string]$ScpExe = 'scp',
  36. [switch]$AllowAnyBranch,
  37. [switch]$AllowDirty,
  38. [switch]$RunMigrations,
  39. [switch]$SkipSmokeTest,
  40. [Alias('HostPreflightOnly', 'RemotePreflightOnly')]
  41. [switch]$PreflightOnly,
  42. [switch]$LocalPreflightOnly,
  43. [switch]$Rollback,
  44. [string]$RollbackTo = '',
  45. [switch]$DryRun,
  46. [switch]$KeepPackage
  47. )
  48. Set-StrictMode -Version 2.0
  49. $ErrorActionPreference = 'Stop'
  50. function Write-Step { param([string]$Message) Write-Host ('==> ' + $Message) }
  51. function Assert-Command {
  52. param([string]$Name)
  53. if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
  54. throw "$Name was not found on PATH."
  55. }
  56. }
  57. function ConvertTo-SingleQuotedPowerShell {
  58. param([string]$Value)
  59. return "'" + $Value.Replace("'", "''") + "'"
  60. }
  61. function Add-RemoteArgument {
  62. param(
  63. [System.Collections.Generic.List[string]]$Arguments,
  64. [string]$Name,
  65. [string]$Value
  66. )
  67. $Arguments.Add($Name)
  68. $Arguments.Add((ConvertTo-SingleQuotedPowerShell $Value))
  69. }
  70. function Assert-SafeNameValue {
  71. param([string]$Name, [string]$Value)
  72. if ([string]::IsNullOrWhiteSpace($Value) -or $Value -notmatch '^[A-Za-z0-9_. -]+$') {
  73. throw "$Name contains unsupported characters."
  74. }
  75. if ($Value -match '(?i)schedulicious') {
  76. throw "$Name must not identify a Schedulicious resource."
  77. }
  78. }
  79. function Assert-SafeDeploymentValues {
  80. Assert-SafeNameValue -Name 'SiteName' -Value $SiteName
  81. Assert-SafeNameValue -Name 'AppPoolName' -Value $AppPoolName
  82. if ($DeployRoot -match '(?i)schedulicious') {
  83. throw 'DeployRoot must not reference Schedulicious.'
  84. }
  85. if ([string]::IsNullOrWhiteSpace($BindingIpAddress)) {
  86. throw 'BindingIpAddress must not be empty.'
  87. }
  88. $parsedAddress = $null
  89. if (-not [System.Net.IPAddress]::TryParse($BindingIpAddress, [ref]$parsedAddress)) {
  90. throw "BindingIpAddress is not a valid IP address: $BindingIpAddress"
  91. }
  92. if ($HostHeader -match '[:/\\]') {
  93. throw 'HostHeader must be empty or a DNS host name without a scheme, port, slash, or backslash.'
  94. }
  95. if ($HostHeader -match '(?i)schedulicious') {
  96. throw 'HostHeader must not reference Schedulicious.'
  97. }
  98. }
  99. function Copy-ReleaseSource {
  100. param([string]$From, [string]$To)
  101. $excludedNames = @('.git', '.deployment', 'releases')
  102. $packageRoots = @(Get-ChildItem -LiteralPath $From -Force | Where-Object { $excludedNames -notcontains $_.Name })
  103. $unsafeSourceItem = $packageRoots |
  104. ForEach-Object {
  105. if (($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { $_ }
  106. elseif ($_.PSIsContainer) { Get-ChildItem -LiteralPath $_.FullName -Recurse -Force }
  107. } |
  108. Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } |
  109. Select-Object -First 1
  110. if ($null -ne $unsafeSourceItem) {
  111. throw "Source contains a reparse point/symbolic link, which is not package-safe: $($unsafeSourceItem.FullName)"
  112. }
  113. New-Item -ItemType Directory -Force -Path $To | Out-Null
  114. $packageRoots | ForEach-Object {
  115. if ($excludedNames -contains $_.Name) { return }
  116. if (($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
  117. throw "Source contains a reparse point/symbolic link, which is not package-safe: $($_.FullName)"
  118. }
  119. Copy-Item -LiteralPath $_.FullName -Destination $To -Recurse -Force
  120. }
  121. $unsafeEntry = Get-ChildItem -LiteralPath $To -Recurse -Force |
  122. Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } |
  123. Select-Object -First 1
  124. if ($null -ne $unsafeEntry) {
  125. throw "Package staging contains a reparse point/symbolic link: $($unsafeEntry.FullName)"
  126. }
  127. }
  128. function Assert-ReleaseLayout {
  129. param([string]$Root)
  130. $required = @(
  131. 'public\Default.asp',
  132. 'public\web.config',
  133. 'core\autoload_core.asp',
  134. 'app\controllers\autoload_controllers.asp',
  135. 'scripts\install-iis-release.ps1'
  136. )
  137. foreach ($relativePath in $required) {
  138. if (-not (Test-Path -LiteralPath (Join-Path $Root $relativePath) -PathType Leaf)) {
  139. throw "Package/source tree is incomplete; missing $relativePath"
  140. }
  141. }
  142. foreach ($xmlFile in Get-ChildItem -LiteralPath $Root -Recurse -Force -Filter 'web.config') {
  143. try {
  144. [xml](Get-Content -LiteralPath $xmlFile.FullName -Raw) | Out-Null
  145. } catch {
  146. throw "$($xmlFile.FullName) is not valid XML: $($_.Exception.Message)"
  147. }
  148. }
  149. }
  150. Assert-SafeDeploymentValues
  151. $SourcePath = [System.IO.Path]::GetFullPath($SourcePath)
  152. $installerPath = Join-Path $PSScriptRoot 'install-iis-release.ps1'
  153. Assert-ReleaseLayout -Root $SourcePath
  154. $directGitRoot = Test-Path -LiteralPath (Join-Path $SourcePath '.git')
  155. $commit = 'nogit'
  156. if ($directGitRoot) {
  157. Assert-Command 'git'
  158. $branch = (& git -C $SourcePath branch --show-current).Trim()
  159. if ($LASTEXITCODE -ne 0) { throw 'Could not determine the Git branch.' }
  160. if ((-not $AllowAnyBranch) -and $branch -ne $ExpectedBranch) {
  161. throw "Refusing to deploy branch '$branch'; expected '$ExpectedBranch'."
  162. }
  163. $dirty = & git -C $SourcePath status --porcelain
  164. if ($LASTEXITCODE -ne 0) { throw 'Could not inspect the Git worktree.' }
  165. if ((-not $AllowDirty) -and $null -ne $dirty -and @($dirty).Count -gt 0) {
  166. throw 'Refusing to deploy a dirty worktree. Use -AllowDirty only for a reviewed exception.'
  167. }
  168. $commit = (& git -C $SourcePath rev-parse --short=12 HEAD).Trim()
  169. if ($LASTEXITCODE -ne 0) { throw 'Could not determine the Git commit.' }
  170. Write-Host "Source branch: $branch"
  171. Write-Host "Source commit: $commit"
  172. } else {
  173. Write-Warning 'SourcePath is not a standalone Git checkout; branch and dirty-worktree checks cannot be enforced.'
  174. if (-not $AllowAnyBranch) {
  175. throw 'Use a standalone checkout, or pass -AllowAnyBranch for a reviewed non-Git source tree.'
  176. }
  177. }
  178. if ([string]::IsNullOrWhiteSpace($ReleaseId)) {
  179. $ReleaseId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + $commit
  180. }
  181. if ($ReleaseId -notmatch '^[A-Za-z0-9._-]+$') { throw 'ReleaseId contains unsupported characters.' }
  182. if ($Rollback -and [string]::IsNullOrWhiteSpace($RollbackTo)) { throw '-Rollback requires -RollbackTo.' }
  183. if ((-not $Rollback) -and -not [string]::IsNullOrWhiteSpace($RollbackTo)) { throw '-RollbackTo requires -Rollback.' }
  184. if ($RunMigrations -and $Rollback) { throw '-RunMigrations is not valid during rollback.' }
  185. if (($PreflightOnly -or $LocalPreflightOnly) -and $Rollback) {
  186. throw 'Preflight modes cannot be combined with -Rollback.'
  187. }
  188. if ($PreflightOnly -and $LocalPreflightOnly) {
  189. throw '-PreflightOnly and -LocalPreflightOnly are mutually exclusive.'
  190. }
  191. $workRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('asp-iis-deploy-' + [Guid]::NewGuid().ToString('N'))
  192. $packageStage = Join-Path $workRoot 'package'
  193. $packageExtract = Join-Path $workRoot 'verify'
  194. $packagePath = Join-Path $workRoot ($ReleaseId + '.zip')
  195. $remoteDirectory = 'C:\Windows\Temp\asp-iis-deploy-' + $ReleaseId
  196. $remotePackage = $remoteDirectory + '\' + $ReleaseId + '.zip'
  197. $remoteInstaller = $remoteDirectory + '\install-iis-release.ps1'
  198. try {
  199. $sha256 = ''
  200. if (-not $Rollback) {
  201. Write-Step 'Staging the complete repository for packaging'
  202. Copy-ReleaseSource -From $SourcePath -To $packageStage
  203. Assert-ReleaseLayout -Root $packageStage
  204. Add-Type -AssemblyName System.IO.Compression.FileSystem
  205. [System.IO.Compression.ZipFile]::CreateFromDirectory($packageStage, $packagePath, [System.IO.Compression.CompressionLevel]::Optimal, $false)
  206. [System.IO.Compression.ZipFile]::ExtractToDirectory($packagePath, $packageExtract)
  207. Assert-ReleaseLayout -Root $packageExtract
  208. $sha256 = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash
  209. Write-Host "Package: $packagePath"
  210. Write-Host "SHA-256: $sha256"
  211. }
  212. if ($LocalPreflightOnly) {
  213. Write-Step 'Local/controller preflight passed; no network connection or host change was made'
  214. exit 0
  215. }
  216. $remoteArguments = New-Object 'System.Collections.Generic.List[string]'
  217. $remoteArguments.Add('&')
  218. $remoteArguments.Add((ConvertTo-SingleQuotedPowerShell $remoteInstaller))
  219. Add-RemoteArgument $remoteArguments '-SiteName' $SiteName
  220. Add-RemoteArgument $remoteArguments '-AppPoolName' $AppPoolName
  221. Add-RemoteArgument $remoteArguments '-DeployRoot' $DeployRoot
  222. Add-RemoteArgument $remoteArguments '-BindingIpAddress' $BindingIpAddress
  223. Add-RemoteArgument $remoteArguments '-BindingPort' $BindingPort.ToString()
  224. Add-RemoteArgument $remoteArguments '-HostHeader' $HostHeader
  225. Add-RemoteArgument $remoteArguments '-KeepReleases' $KeepReleases.ToString()
  226. if (-not [string]::IsNullOrWhiteSpace($InitialWebConfigPath)) { Add-RemoteArgument $remoteArguments '-InitialWebConfigPath' $InitialWebConfigPath }
  227. if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) { Add-RemoteArgument $remoteArguments '-BaseUrl' $BaseUrl }
  228. if ($Rollback) {
  229. Add-RemoteArgument $remoteArguments '-RollbackTo' $RollbackTo
  230. } elseif ($PreflightOnly) {
  231. $remoteArguments.Add('-PreflightOnly')
  232. } else {
  233. Add-RemoteArgument $remoteArguments '-PackagePath' $remotePackage
  234. Add-RemoteArgument $remoteArguments '-ReleaseId' $ReleaseId
  235. Add-RemoteArgument $remoteArguments '-ExpectedSha256' $sha256
  236. }
  237. if ($RunMigrations) { $remoteArguments.Add('-RunMigrations') }
  238. if ($SkipSmokeTest) { $remoteArguments.Add('-SkipSmokeTest') }
  239. $remoteScript = $remoteArguments -join ' '
  240. $remoteEncodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript))
  241. $remoteCommand = 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -EncodedCommand ' + $remoteEncodedCommand
  242. if ($DryRun) {
  243. Write-Step 'Dry-run complete; no network connection was made'
  244. Write-Host "Would use dedicated site '$SiteName' and app pool '$AppPoolName'."
  245. Write-Host "Would use binding ${BindingIpAddress}:$BindingPort with host header '$HostHeader'."
  246. if ($PreflightOnly) {
  247. Write-Host 'Would stream the installer over SSH for a read-only host preflight; no remote file would be written.'
  248. } else {
  249. Write-Host "Would create remote directory: $remoteDirectory"
  250. Write-Host "Would copy installer: $installerPath"
  251. if (-not $Rollback) { Write-Host "Would copy package: $packagePath" }
  252. Write-Host ('Would execute host script: ' + $remoteScript)
  253. }
  254. exit 0
  255. }
  256. Assert-Command $SshExe
  257. if ($PreflightOnly) {
  258. Write-Step 'Streaming the installer for read-only host preflight'
  259. $installerSource = Get-Content -LiteralPath $installerPath -Raw
  260. $argumentTail = @($remoteArguments | Select-Object -Skip 2) -join ' '
  261. $stdinScript = "& {`r`n" + $installerSource + "`r`n} " + $argumentTail
  262. $stdinScript | & $SshExe -p $RemotePort $RemoteTarget 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -Command -'
  263. if ($LASTEXITCODE -ne 0) { throw "Remote preflight failed with exit code $LASTEXITCODE." }
  264. Write-Step 'Remote host preflight completed successfully without persistent host changes'
  265. exit 0
  266. }
  267. Assert-Command $ScpExe
  268. Write-Step 'Creating remote temporary directory'
  269. $mkdirScript = "New-Item -ItemType Directory -Force -Path $(ConvertTo-SingleQuotedPowerShell $remoteDirectory) | Out-Null"
  270. $mkdirEncoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($mkdirScript))
  271. & $SshExe -p $RemotePort $RemoteTarget ('powershell.exe -NoProfile -NonInteractive -EncodedCommand ' + $mkdirEncoded)
  272. if ($LASTEXITCODE -ne 0) { throw 'Remote directory creation failed.' }
  273. Write-Step 'Copying the host installer'
  274. & $ScpExe -P $RemotePort $installerPath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/install-iis-release.ps1')
  275. if ($LASTEXITCODE -ne 0) { throw 'Installer copy failed.' }
  276. if (-not $Rollback) {
  277. Write-Step 'Copying the release package'
  278. & $ScpExe -P $RemotePort $packagePath ($RemoteTarget + ':' + $remoteDirectory.Replace('\', '/') + '/' + $ReleaseId + '.zip')
  279. if ($LASTEXITCODE -ne 0) { throw 'Package copy failed.' }
  280. }
  281. Write-Step 'Invoking the host-side installer'
  282. & $SshExe -p $RemotePort $RemoteTarget $remoteCommand
  283. if ($LASTEXITCODE -ne 0) { throw "Remote installer failed with exit code $LASTEXITCODE." }
  284. Write-Step 'Remote operation completed successfully'
  285. } finally {
  286. if ($KeepPackage -and (Test-Path -LiteralPath $packagePath)) {
  287. $keptPath = Join-Path (Get-Location) ([System.IO.Path]::GetFileName($packagePath))
  288. Copy-Item -LiteralPath $packagePath -Destination $keptPath -Force
  289. Write-Host "Package retained at $keptPath"
  290. }
  291. if (Test-Path -LiteralPath $workRoot) { Remove-Item -LiteralPath $workRoot -Recurse -Force }
  292. }

Powered by TurnKey Linux.