支持飞牛在线升级场景
This commit is contained in:
parent
97e3e6351c
commit
3b5da1c2c2
@ -12,6 +12,8 @@ param(
|
|||||||
[int]$SshPort = 22,
|
[int]$SshPort = 22,
|
||||||
[string]$Username = "root",
|
[string]$Username = "root",
|
||||||
[SecureString]$Password = $null,
|
[SecureString]$Password = $null,
|
||||||
|
[switch]$RemoteSudo,
|
||||||
|
[SecureString]$RemoteSudoPassword = $null,
|
||||||
[string]$KeyPath = "",
|
[string]$KeyPath = "",
|
||||||
[string]$PlinkHostKey = "",
|
[string]$PlinkHostKey = "",
|
||||||
|
|
||||||
@ -119,6 +121,7 @@ function Show-Help {
|
|||||||
Write-Host "主要参数:" -ForegroundColor White
|
Write-Host "主要参数:" -ForegroundColor White
|
||||||
Write-Host " -Server 远程服务器IP或域名;远程部署时必须显式传入"
|
Write-Host " -Server 远程服务器IP或域名;远程部署时必须显式传入"
|
||||||
Write-Host " -SshPort SSH端口,默认 22;内部测试机或客户自定义端口必须显式传入"
|
Write-Host " -SshPort SSH端口,默认 22;内部测试机或客户自定义端口必须显式传入"
|
||||||
|
Write-Host " -RemoteSudo 远端 Docker 需要 sudo 时启用;常见于飞牛 / NAS 普通管理员账号"
|
||||||
Write-Host " -RegistryHost Registry地址;发布和部署时必须显式传入"
|
Write-Host " -RegistryHost Registry地址;发布和部署时必须显式传入"
|
||||||
Write-Host " -RegistryUsername Registry账号,默认 flnxi"
|
Write-Host " -RegistryUsername Registry账号,默认 flnxi"
|
||||||
Write-Host " -RegistryPassword Registry密码,建议用 Read-Host -AsSecureString 输入"
|
Write-Host " -RegistryPassword Registry密码,建议用 Read-Host -AsSecureString 输入"
|
||||||
@ -160,6 +163,14 @@ function ConvertTo-ShellSingleQuoted {
|
|||||||
return "'" + ($Value -replace "'", "'\''") + "'"
|
return "'" + ($Value -replace "'", "'\''") + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Normalize-RemoteShellScript {
|
||||||
|
param([string]$Script)
|
||||||
|
if ($null -eq $Script) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return (($Script -replace "`r`n", "`n") -replace "`r", "")
|
||||||
|
}
|
||||||
|
|
||||||
function Assert-Tool {
|
function Assert-Tool {
|
||||||
param([string]$Name)
|
param([string]$Name)
|
||||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||||
@ -361,6 +372,20 @@ function Ensure-SshPasswordIfNeeded {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Ensure-RemoteSudoPasswordIfNeeded {
|
||||||
|
if ($SkipRemote -or -not $RemoteSudo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($RemoteSudoPassword) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($Password) {
|
||||||
|
$script:RemoteSudoPassword = $Password
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$script:RemoteSudoPassword = Read-Host "请输入远程 sudo 密码" -AsSecureString
|
||||||
|
}
|
||||||
|
|
||||||
function Test-RegistryEndpoint {
|
function Test-RegistryEndpoint {
|
||||||
Assert-RegistryHostConfigured
|
Assert-RegistryHostConfigured
|
||||||
Write-RegistryHttpNotice
|
Write-RegistryHttpNotice
|
||||||
@ -434,13 +459,26 @@ function Invoke-RemoteCommand {
|
|||||||
Assert-RemoteServerConfigured
|
Assert-RemoteServerConfigured
|
||||||
|
|
||||||
$target = "${Username}@${Server}"
|
$target = "${Username}@${Server}"
|
||||||
$remoteCommand = "bash -lc " + (ConvertTo-ShellSingleQuoted -Value $Command)
|
$normalizedCommand = Normalize-RemoteShellScript -Script $Command
|
||||||
$stdinPassword = ""
|
$remoteCommand = "bash -lc " + (ConvertTo-ShellSingleQuoted -Value $normalizedCommand)
|
||||||
|
$stdinLines = New-Object System.Collections.Generic.List[string]
|
||||||
|
if ($RemoteSudo) {
|
||||||
|
$sudoPasswordPlain = ConvertTo-PlainText -SecureValue $RemoteSudoPassword
|
||||||
|
if ([string]::IsNullOrWhiteSpace($sudoPasswordPlain)) {
|
||||||
|
throw "远程 sudo 密码为空,无法执行需要 sudo 的 Docker 命令"
|
||||||
|
}
|
||||||
|
$stdinLines.Add($sudoPasswordPlain)
|
||||||
|
}
|
||||||
if ((-not $SkipRegistryLogin) -and (-not $NoRegistryPasswordInput)) {
|
if ((-not $SkipRegistryLogin) -and (-not $NoRegistryPasswordInput)) {
|
||||||
$stdinPassword = ConvertTo-PlainText -SecureValue $RegistryPassword
|
$stdinRegistryPassword = ConvertTo-PlainText -SecureValue $RegistryPassword
|
||||||
if ([string]::IsNullOrWhiteSpace($stdinPassword)) {
|
if ([string]::IsNullOrWhiteSpace($stdinRegistryPassword)) {
|
||||||
throw "Registry 密码为空,无法远程登录 Registry"
|
throw "Registry 密码为空,无法远程登录 Registry"
|
||||||
}
|
}
|
||||||
|
$stdinLines.Add($stdinRegistryPassword)
|
||||||
|
}
|
||||||
|
$stdinPayload = ""
|
||||||
|
if ($stdinLines.Count -gt 0) {
|
||||||
|
$stdinPayload = ($stdinLines -join "`n") + "`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($KeyPath -and (Test-Path $KeyPath)) {
|
if ($KeyPath -and (Test-Path $KeyPath)) {
|
||||||
@ -461,10 +499,10 @@ function Invoke-RemoteCommand {
|
|||||||
Write-Host "[DRYRUN] ssh -p $SshPort $target bash -lc '<远程部署脚本,敏感信息已隐藏>'" -ForegroundColor DarkGray
|
Write-Host "[DRYRUN] ssh -p $SshPort $target bash -lc '<远程部署脚本,敏感信息已隐藏>'" -ForegroundColor DarkGray
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ($SkipRegistryLogin -or $NoRegistryPasswordInput) {
|
if ($stdinLines.Count -eq 0) {
|
||||||
& ssh @args
|
& ssh @args
|
||||||
} else {
|
} else {
|
||||||
$stdinPassword | ssh @args
|
$stdinPayload | ssh @args
|
||||||
}
|
}
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "远程部署命令失败,退出码:$LASTEXITCODE"
|
throw "远程部署命令失败,退出码:$LASTEXITCODE"
|
||||||
@ -488,10 +526,10 @@ function Invoke-RemoteCommand {
|
|||||||
Write-Host "[DRYRUN] plink -P $SshPort $target bash -lc '<远程部署脚本,敏感信息已隐藏>'" -ForegroundColor DarkGray
|
Write-Host "[DRYRUN] plink -P $SshPort $target bash -lc '<远程部署脚本,敏感信息已隐藏>'" -ForegroundColor DarkGray
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ($SkipRegistryLogin -or $NoRegistryPasswordInput) {
|
if ($stdinLines.Count -eq 0) {
|
||||||
& plink.exe @args
|
& plink.exe @args
|
||||||
} else {
|
} else {
|
||||||
$stdinPassword | plink.exe @args
|
$stdinPayload | plink.exe @args
|
||||||
}
|
}
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "远程部署命令失败,退出码:$LASTEXITCODE"
|
throw "远程部署命令失败,退出码:$LASTEXITCODE"
|
||||||
@ -522,9 +560,13 @@ function New-RemoteDeployScript {
|
|||||||
$remoteObjectBackupEnabled = if ($NoRemoteObjectStorageBackup) { "0" } else { "1" }
|
$remoteObjectBackupEnabled = if ($NoRemoteObjectStorageBackup) { "0" } else { "1" }
|
||||||
$externalDataBackupConfirmed = if ($ExternalDataBackupConfirmed) { "1" } else { "0" }
|
$externalDataBackupConfirmed = if ($ExternalDataBackupConfirmed) { "1" } else { "0" }
|
||||||
$rollbackEnabled = if ($NoRollback) { "0" } else { "1" }
|
$rollbackEnabled = if ($NoRollback) { "0" } else { "1" }
|
||||||
|
$remoteSudoEnabled = if ($RemoteSudo) { "1" } else { "0" }
|
||||||
|
|
||||||
$lines = New-Object System.Collections.Generic.List[string]
|
$lines = New-Object System.Collections.Generic.List[string]
|
||||||
$lines.Add("set -Eeuo pipefail")
|
$lines.Add("set -Eeuo pipefail")
|
||||||
|
if ($RemoteSudo) {
|
||||||
|
$lines.Add("IFS= read -r SUDO_PASSWORD || { echo '未收到远程 sudo 密码'; exit 23; }")
|
||||||
|
}
|
||||||
if (-not $SkipRegistryLogin) {
|
if (-not $SkipRegistryLogin) {
|
||||||
$lines.Add("IFS= read -r REGISTRY_PASSWORD || { echo '未收到 Registry 密码'; exit 22; }")
|
$lines.Add("IFS= read -r REGISTRY_PASSWORD || { echo '未收到 Registry 密码'; exit 22; }")
|
||||||
}
|
}
|
||||||
@ -541,6 +583,7 @@ function New-RemoteDeployScript {
|
|||||||
$lines.Add("external_data_backup_confirmed=$externalDataBackupConfirmed")
|
$lines.Add("external_data_backup_confirmed=$externalDataBackupConfirmed")
|
||||||
$lines.Add("remote_backup_helper_image=$quotedRemoteBackupHelperImage")
|
$lines.Add("remote_backup_helper_image=$quotedRemoteBackupHelperImage")
|
||||||
$lines.Add("rollback_enabled=$rollbackEnabled")
|
$lines.Add("rollback_enabled=$rollbackEnabled")
|
||||||
|
$lines.Add("remote_sudo_enabled=$remoteSudoEnabled")
|
||||||
$lines.Add("export APP_IMAGE=$quotedRegistryImage")
|
$lines.Add("export APP_IMAGE=$quotedRegistryImage")
|
||||||
$lines.Add("export NEXTAUTH_URL=$quotedNextAuthUrl")
|
$lines.Add("export NEXTAUTH_URL=$quotedNextAuthUrl")
|
||||||
$lines.Add("export APP_PUBLIC_URL=$quotedAppPublicUrl")
|
$lines.Add("export APP_PUBLIC_URL=$quotedAppPublicUrl")
|
||||||
@ -558,6 +601,21 @@ db_backup=""
|
|||||||
object_backup=""
|
object_backup=""
|
||||||
minio_volume_name=""
|
minio_volume_name=""
|
||||||
|
|
||||||
|
sudo_auth() {
|
||||||
|
if [ "${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker() {
|
||||||
|
if [ "${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
sudo_auth
|
||||||
|
sudo -n docker "$@"
|
||||||
|
else
|
||||||
|
command docker "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
upsert_env() {
|
upsert_env() {
|
||||||
key="$1"
|
key="$1"
|
||||||
value="$2"
|
value="$2"
|
||||||
@ -576,6 +634,7 @@ read_env_value() {
|
|||||||
awk -F= -v k="$key" '
|
awk -F= -v k="$key" '
|
||||||
index($0, k "=") == 1 {
|
index($0, k "=") == 1 {
|
||||||
value=substr($0, length(k) + 2)
|
value=substr($0, length(k) + 2)
|
||||||
|
gsub(/\r$/, "", value)
|
||||||
gsub(/^"|"$/, "", value)
|
gsub(/^"|"$/, "", value)
|
||||||
gsub(/^'\''|'\''$/, "", value)
|
gsub(/^'\''|'\''$/, "", value)
|
||||||
print value
|
print value
|
||||||
@ -908,9 +967,26 @@ function New-RemoteRollbackScript {
|
|||||||
|
|
||||||
$quotedRemotePath = ConvertTo-ShellSingleQuoted -Value $RemotePath
|
$quotedRemotePath = ConvertTo-ShellSingleQuoted -Value $RemotePath
|
||||||
$quotedReason = ConvertTo-ShellSingleQuoted -Value $Reason
|
$quotedReason = ConvertTo-ShellSingleQuoted -Value $Reason
|
||||||
|
$remoteSudoEnabled = if ($RemoteSudo) { "1" } else { "0" }
|
||||||
|
$sudoReadLine = if ($RemoteSudo) { "IFS= read -r SUDO_PASSWORD || { echo '未收到远程 sudo 密码'; exit 23; }" } else { "" }
|
||||||
|
|
||||||
return @"
|
return @"
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
$sudoReadLine
|
||||||
|
remote_sudo_enabled=$remoteSudoEnabled
|
||||||
|
sudo_auth() {
|
||||||
|
if [ "`${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
printf '%s\n' "`$SUDO_PASSWORD" | sudo -S -p '' true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
docker() {
|
||||||
|
if [ "`${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
sudo_auth
|
||||||
|
sudo -n docker "`$@"
|
||||||
|
else
|
||||||
|
command docker "`$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
cd $quotedRemotePath
|
cd $quotedRemotePath
|
||||||
reason=$quotedReason
|
reason=$quotedReason
|
||||||
context_file="./backups/LAST_DEPLOY_CONTEXT.env"
|
context_file="./backups/LAST_DEPLOY_CONTEXT.env"
|
||||||
@ -1072,6 +1148,7 @@ function Invoke-DeployApp {
|
|||||||
|
|
||||||
Ensure-RegistryPassword
|
Ensure-RegistryPassword
|
||||||
Ensure-SshPasswordIfNeeded
|
Ensure-SshPasswordIfNeeded
|
||||||
|
Ensure-RemoteSudoPasswordIfNeeded
|
||||||
Invoke-ProjectCheck
|
Invoke-ProjectCheck
|
||||||
Test-RegistryEndpoint
|
Test-RegistryEndpoint
|
||||||
$registryImage = Invoke-PublishAppImage -VersionTag $deployTag -Repository $deployRepository -PushLatest:$false
|
$registryImage = Invoke-PublishAppImage -VersionTag $deployTag -Repository $deployRepository -PushLatest:$false
|
||||||
@ -1304,8 +1381,26 @@ function Invoke-ShowStatus {
|
|||||||
|
|
||||||
if (-not $SkipRemote) {
|
if (-not $SkipRemote) {
|
||||||
Ensure-SshPasswordIfNeeded
|
Ensure-SshPasswordIfNeeded
|
||||||
|
Ensure-RemoteSudoPasswordIfNeeded
|
||||||
|
$remoteSudoEnabled = if ($RemoteSudo) { "1" } else { "0" }
|
||||||
|
$sudoReadLine = if ($RemoteSudo) { "IFS= read -r SUDO_PASSWORD || { echo '未收到远程 sudo 密码'; exit 23; }" } else { "" }
|
||||||
$remoteStatus = @"
|
$remoteStatus = @"
|
||||||
set -e
|
set -e
|
||||||
|
$sudoReadLine
|
||||||
|
remote_sudo_enabled=$remoteSudoEnabled
|
||||||
|
sudo_auth() {
|
||||||
|
if [ "`${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
printf '%s\n' "`$SUDO_PASSWORD" | sudo -S -p '' true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
docker() {
|
||||||
|
if [ "`${remote_sudo_enabled:-0}" = "1" ]; then
|
||||||
|
sudo_auth
|
||||||
|
sudo -n docker "`$@"
|
||||||
|
else
|
||||||
|
command docker "`$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
echo '--- docker ps ---'
|
echo '--- docker ps ---'
|
||||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' | grep -E 'flnxi|postgres|minio|license' || true
|
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' | grep -E 'flnxi|postgres|minio|license' || true
|
||||||
echo '--- compose ---'
|
echo '--- compose ---'
|
||||||
|
|||||||
@ -81,4 +81,27 @@ describe('deploy-registry.ps1', () => {
|
|||||||
expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" ps\')')
|
expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" ps\')')
|
||||||
expect(script).not.toContain('$lines.Add("docker compose -f `"$compose_file`"')
|
expect(script).not.toContain('$lines.Add("docker compose -f `"$compose_file`"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('支持飞牛/NAS 普通管理员通过 sudo 执行远程 Docker 升级', () => {
|
||||||
|
expect(script).toContain('[switch]$RemoteSudo')
|
||||||
|
expect(script).toContain('[SecureString]$RemoteSudoPassword = $null')
|
||||||
|
expect(script).toContain('function Ensure-RemoteSudoPasswordIfNeeded')
|
||||||
|
expect(script).toContain('$script:RemoteSudoPassword = $Password')
|
||||||
|
expect(script).toContain('remote_sudo_enabled=$remoteSudoEnabled')
|
||||||
|
expect(script).toContain('IFS= read -r SUDO_PASSWORD')
|
||||||
|
expect(script).toContain('sudo -n docker "$@"')
|
||||||
|
expect(script).toContain('sudo -n docker "`$@"')
|
||||||
|
expect(script).toContain('Invoke-RemoteCommand -Command $rollbackScript -NoRegistryPasswordInput')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('发送到 Linux 的远程脚本会统一去除 Windows 回车符', () => {
|
||||||
|
expect(script).toContain('function Normalize-RemoteShellScript')
|
||||||
|
expect(script).toContain('($Script -replace "`r`n", "`n") -replace "`r", ""')
|
||||||
|
expect(script).toContain('$normalizedCommand = Normalize-RemoteShellScript -Script $Command')
|
||||||
|
expect(script).toContain('ConvertTo-ShellSingleQuoted -Value $normalizedCommand')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('读取远端 .env 时会去除 Windows 行尾回车', () => {
|
||||||
|
expect(script).toContain('gsub(/\\r$/, "", value)')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user