Consolidated ASP Classic MVC framework from best components
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

321 行
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.