Files
easyai-ai-gateway/scripts/cluster/oss-object.mjs
T
wangbo 971540a2a4 feat(deploy): 增加三节点 K3s 高可用迁移能力
新增 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 实测。
2026-07-28 04:37:31 +08:00

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}`)
}