新增 WireGuard 全互联、三 server embedded-etcd K3s、CloudNativePG 双实例、Barman OSS 备份、双 NGINX、Kubernetes Secret/RBAC 与本地旧文件按严格 24 小时清理。 新增维护窗口数据迁移、digest 固定滚动发布、应用回滚、跨节点文件 E2E、节点和数据库故障演练、CNPG 恢复与 etcd 快照验收脚本;洛杉矶仅作为带 NoSchedule 污点的仲裁节点。 所有生产 Secret 只在执行时从 0600 本地环境和旧生产容器导入,仓库不保存凭据;公网入口保持人工 DNS 故障切换边界。 验证:bash -n、ShellCheck、kubectl kustomize、Node 语法检查、Secret 扫描、OSS put/head/delete 实测。
104 lines
3.6 KiB
JavaScript
Executable File
104 lines
3.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { createHmac, createHash } from 'node:crypto'
|
|
import { createReadStream, createWriteStream, statSync } from 'node:fs'
|
|
import { request } from 'node:https'
|
|
import { pipeline } from 'node:stream/promises'
|
|
|
|
function fail(message) {
|
|
console.error(`oss_object=FAIL ${message}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
function requiredEnv(name, pattern) {
|
|
const value = process.env[name] || ''
|
|
if (!value || (pattern && !pattern.test(value))) fail(`invalid or missing ${name}`)
|
|
return value
|
|
}
|
|
|
|
function encodeObjectKey(value) {
|
|
if (!value || value.startsWith('/') || value.includes('..')) fail('invalid object key')
|
|
return value.split('/').map(encodeURIComponent).join('/')
|
|
}
|
|
|
|
const [operation, objectKeyValue, filePath] = process.argv.slice(2)
|
|
if (!['put', 'get', 'head', 'delete'].includes(operation || '')) {
|
|
fail('usage: oss-object.mjs {put|get|head|delete} <object-key> [file]')
|
|
}
|
|
if ((operation === 'put' || operation === 'get') && !filePath) fail(`${operation} requires a file`)
|
|
|
|
const accessKey = requiredEnv('ALI_KEY')
|
|
const secretKey = requiredEnv('ALI_SECRET')
|
|
const region = requiredEnv('ALI_REGION', /^[a-z0-9-]+$/)
|
|
const bucket = requiredEnv('ALI_BUCKET', /^[A-Za-z0-9.-]+$/)
|
|
const objectKey = encodeObjectKey(objectKeyValue)
|
|
const hostname = `${bucket}.${region}.aliyuncs.com`
|
|
const canonicalResource = `/${bucket}/${objectKey}`
|
|
const date = new Date().toUTCString()
|
|
const method = operation.toUpperCase()
|
|
const contentType = operation === 'put' ? 'application/octet-stream' : ''
|
|
const stringToSign = `${method}\n\n${contentType}\n${date}\n${canonicalResource}`
|
|
const signature = createHmac('sha1', secretKey).update(stringToSign).digest('base64')
|
|
const headers = {
|
|
Authorization: `OSS ${accessKey}:${signature}`,
|
|
Date: date,
|
|
}
|
|
|
|
let expectedDigest = ''
|
|
if (operation === 'put') {
|
|
let details
|
|
try {
|
|
details = statSync(filePath)
|
|
} catch (error) {
|
|
fail(`cannot stat upload file: ${error.message}`)
|
|
}
|
|
if (!details.isFile()) fail('upload source must be a regular file')
|
|
headers['Content-Type'] = contentType
|
|
headers['Content-Length'] = String(details.size)
|
|
expectedDigest = createHash('sha256')
|
|
}
|
|
|
|
const response = await new Promise((resolve, reject) => {
|
|
const outgoing = request({
|
|
method,
|
|
hostname,
|
|
path: `/${objectKey}`,
|
|
headers,
|
|
timeout: 120_000,
|
|
}, resolve)
|
|
outgoing.once('timeout', () => outgoing.destroy(new Error('request timed out')))
|
|
outgoing.once('error', reject)
|
|
if (operation === 'put') {
|
|
const source = createReadStream(filePath)
|
|
source.on('data', (chunk) => expectedDigest.update(chunk))
|
|
source.once('error', reject)
|
|
source.pipe(outgoing)
|
|
} else {
|
|
outgoing.end()
|
|
}
|
|
}).catch((error) => fail(error.message))
|
|
|
|
const successStatus = operation === 'get' || operation === 'head' ? 200 : [200, 204].includes(response.statusCode)
|
|
if (!successStatus) {
|
|
let responseBody = ''
|
|
for await (const chunk of response) responseBody += chunk
|
|
fail(`HTTP ${response.statusCode} ${responseBody.slice(0, 240).replaceAll(/\s+/g, ' ')}`)
|
|
}
|
|
|
|
if (operation === 'get') {
|
|
const destination = createWriteStream(filePath, { flags: 'wx', mode: 0o600 })
|
|
await pipeline(response, destination).catch((error) => fail(`download failed: ${error.message}`))
|
|
}
|
|
|
|
if (operation === 'put') {
|
|
for await (const _chunk of response) {
|
|
// Drain the response so the request completes before reporting success.
|
|
}
|
|
console.log(`oss_object=PASS operation=put sha256=${expectedDigest.digest('hex')}`)
|
|
} else {
|
|
for await (const _chunk of response) {
|
|
// GET is consumed by pipeline; other successful response bodies are empty.
|
|
}
|
|
console.log(`oss_object=PASS operation=${operation}`)
|
|
}
|