yj-deploy 0.0.11 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/lib/main.js +702 -0
- package/package.json +5 -10
- package/dist/yj-deploy.mjs +0 -433
- package/dist/yj-deploy.umd.js +0 -9
package/README.md
CHANGED
|
@@ -97,9 +97,9 @@ const deploy = new yjDeploy({
|
|
|
97
97
|
allLog: false, // 是否打印所有log信息,主要是包含推送镜像阶段的日志,如果报错可以打开此开关
|
|
98
98
|
|
|
99
99
|
lazyUpload: false, // 是否开启懒上传,针对小程序图片等仅需要部署新增部分文件的项目有奇效
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
rePushNum: 3, // 重新推送次数,为0代表不重新推送
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
// 自动重启服务的rancher配置
|
|
104
104
|
rancherConfig: {
|
|
105
105
|
token: '', // Rancher API 访问令牌 (必填)
|
|
@@ -308,8 +308,6 @@ node uploader.js --tmpName=test
|
|
|
308
308
|
仅对开发环境生效,并且需要先在rancher创建服务后方能成功
|
|
309
309
|
:::
|
|
310
310
|
|
|
311
|
-
<br />
|
|
312
|
-
|
|
313
311
|
|
|
314
312
|
<br />
|
|
315
313
|
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { stdout } from 'single-line-log'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { Client } from 'ssh2'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import https from 'https'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export default class yjDeploy {
|
|
10
|
+
config = {
|
|
11
|
+
host: '',
|
|
12
|
+
port: '',
|
|
13
|
+
username: '',
|
|
14
|
+
password: '',
|
|
15
|
+
namespace: '', // 项目命名空间,等同于镜像地址目录名称
|
|
16
|
+
imageStore: 'dev-images', // 镜像仓库
|
|
17
|
+
tmpName: 'dev', // 镜像环境
|
|
18
|
+
delay: 0, // 延迟上传时间
|
|
19
|
+
// 本地文件目录(必填)
|
|
20
|
+
fileDir: '',
|
|
21
|
+
// 远程sftp服务根目录
|
|
22
|
+
rootDir: '/home/yjweb',
|
|
23
|
+
|
|
24
|
+
// dockerfile文件信息
|
|
25
|
+
dockerfile: {
|
|
26
|
+
name: 'Dockerfile',
|
|
27
|
+
content: [
|
|
28
|
+
'FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest',
|
|
29
|
+
'RUN rm -rf /usr/share/nginx/html/*',
|
|
30
|
+
'COPY dist /usr/share/nginx/html/'
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// upload.sh文件信息
|
|
35
|
+
upload: {
|
|
36
|
+
name: 'upload.sh',
|
|
37
|
+
content: [
|
|
38
|
+
'#!/bin/sh',
|
|
39
|
+
'tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`',
|
|
40
|
+
'echo "- 镜像仓库: $1"',
|
|
41
|
+
'echo "- 镜像环境: $2"',
|
|
42
|
+
'docker build -t harbor.yunjingtech.cn:30002/$1/$tag .',
|
|
43
|
+
'docker push harbor.yunjingtech.cn:30002/$1/$tag',
|
|
44
|
+
'if [ $? -eq 0 ];then',
|
|
45
|
+
'echo - 推送成功',
|
|
46
|
+
'echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag',
|
|
47
|
+
'else',
|
|
48
|
+
'echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息',
|
|
49
|
+
'fi',
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// 上传dist目录信息
|
|
54
|
+
dist: {
|
|
55
|
+
name: 'dist'
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
parallelDir: 20, // 文件夹并行创建数量,如果报错,可以减少此配置
|
|
59
|
+
|
|
60
|
+
parallelFile: 50, // 文件并行上传数量,如果报错,可以减少此配置
|
|
61
|
+
|
|
62
|
+
allLog: false, // 是否显示全部日志
|
|
63
|
+
|
|
64
|
+
lazyUpload: false, // 是否开启懒上传
|
|
65
|
+
|
|
66
|
+
rePushNum: 3, // 重新推送次数,为0代表不重新推送
|
|
67
|
+
|
|
68
|
+
// 仅能通过命令启用
|
|
69
|
+
rancherConfig: {
|
|
70
|
+
token: '', // Rancher API 访问令牌
|
|
71
|
+
namespace: '', // 服务所在的命名空间
|
|
72
|
+
workload: '', // 工作负载名称
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ssh2Conn = null
|
|
77
|
+
// 上传状态
|
|
78
|
+
uploading = false
|
|
79
|
+
// 定时器
|
|
80
|
+
trim = null
|
|
81
|
+
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.config = Object.assign(this.config, config)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
name: 'yj-deploy',
|
|
87
|
+
isPut: this.isPut.bind(this),
|
|
88
|
+
upload: this.upload.bind(this),
|
|
89
|
+
// webpack钩子
|
|
90
|
+
apply: this.apply.bind(this),
|
|
91
|
+
// vite上传钩子
|
|
92
|
+
closeBundle: this.isPut.bind(this)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// webpack钩子
|
|
97
|
+
apply(compiler) {
|
|
98
|
+
if (compiler && compiler.hooks && compiler.hooks.done) {
|
|
99
|
+
compiler.hooks.done.tap('yj-deploy', () => {
|
|
100
|
+
this.isPut()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
return 'build'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 判断是否需要上传,
|
|
108
|
+
* vite和 webpack环境在每次编译完都会触发当前方法
|
|
109
|
+
* 需要通过是否有命令行参数 --deploy
|
|
110
|
+
*/
|
|
111
|
+
isPut () {
|
|
112
|
+
const deploy = this.getArgv('--deploy')
|
|
113
|
+
if(deploy != undefined) {
|
|
114
|
+
clearTimeout(this.trim)
|
|
115
|
+
this.trim = setTimeout(() => {
|
|
116
|
+
this.upload() // 开始上传逻辑
|
|
117
|
+
}, this.config.delay || 0)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 连接跳板机并开始上传
|
|
123
|
+
*/
|
|
124
|
+
upload() {
|
|
125
|
+
console.log('-------------- deploy-start --------------')
|
|
126
|
+
|
|
127
|
+
if(!this.config.host) {
|
|
128
|
+
console.log('- 请配置跳板机地址 host')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if(!this.config.port) {
|
|
133
|
+
console.log('- 请配置跳板机端口 port')
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if(!this.config.username) {
|
|
137
|
+
console.log('- 请配置跳板机账号 username')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if(!this.config.password) {
|
|
141
|
+
console.log('- 请配置跳板机密码 password')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if(this.uploading) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.ssh2Conn = new Client()
|
|
150
|
+
this.ssh2Conn
|
|
151
|
+
.on('ready', () => {
|
|
152
|
+
console.log('- 跳板机连接成功!')
|
|
153
|
+
|
|
154
|
+
this.ssh2Conn.sftp(async (err, sftp) => {
|
|
155
|
+
if (err) {
|
|
156
|
+
console.log('- sftp连接失败')
|
|
157
|
+
this.breakConnect()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!this.config.namespace) {
|
|
161
|
+
console.log('- 请配置项目命名空间 namespace')
|
|
162
|
+
this.breakConnect()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.uploading = true
|
|
166
|
+
|
|
167
|
+
// 是否重置项目
|
|
168
|
+
if(this.getArgv('--reset') !== undefined) {
|
|
169
|
+
console.log('- 重置项目')
|
|
170
|
+
await this.onShell(`rm -r ${this.config.rootDir}/${this.config.namespace}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 上传项目
|
|
174
|
+
this.upLoadProject(sftp, `${this.config.rootDir}/${this.config.namespace}`)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
.connect({
|
|
178
|
+
host: this.config.host,
|
|
179
|
+
port: this.config.port,
|
|
180
|
+
username: this.config.username,
|
|
181
|
+
password: this.config.password
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
this.ssh2Conn.on('error', (error) => {
|
|
185
|
+
console.log(`- 连接失败: ${error}`)
|
|
186
|
+
this.breakConnect()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
this.ssh2Conn.on('end', () => {
|
|
190
|
+
this.uploading = false
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 断开连接
|
|
196
|
+
*/
|
|
197
|
+
breakConnect() {
|
|
198
|
+
this.uploading = false
|
|
199
|
+
console.log('- 已断开连接')
|
|
200
|
+
console.log('-------------- deploy-end --------------')
|
|
201
|
+
this.ssh2Conn.end()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 运行shell命令
|
|
206
|
+
* @param {*} shell
|
|
207
|
+
* @returns Promise
|
|
208
|
+
*/
|
|
209
|
+
onShell(shell) {
|
|
210
|
+
const envIsAllLog = this.getArgv('--allLog') !== undefined
|
|
211
|
+
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
this.ssh2Conn.shell((err, stream) => {
|
|
214
|
+
if (err) {
|
|
215
|
+
console.log('- 远程命令错误:' + err, shell)
|
|
216
|
+
this.breakConnect()
|
|
217
|
+
reject(err)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
let dataList = []
|
|
221
|
+
stream
|
|
222
|
+
.on('close', () => {
|
|
223
|
+
// 远程命令执行完毕
|
|
224
|
+
resolve(dataList.toString())
|
|
225
|
+
})
|
|
226
|
+
.on('data', (data) => {
|
|
227
|
+
let str = data.toString()
|
|
228
|
+
if(envIsAllLog || this.config.allLog) {
|
|
229
|
+
dataList.push('\n' + str)
|
|
230
|
+
} else if(str.startsWith('- ')) {
|
|
231
|
+
dataList.push('\n' + str)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.stderr.on('data', (data) => {
|
|
235
|
+
console.log('- 远程命令错误:\n' + data)
|
|
236
|
+
this.breakConnect()
|
|
237
|
+
reject(data)
|
|
238
|
+
})
|
|
239
|
+
stream.end(shell + '\nexit\n')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 初始化项目
|
|
246
|
+
* @param {*} sftp
|
|
247
|
+
* @param {*} dir
|
|
248
|
+
*/
|
|
249
|
+
projectInit(sftp, dir) {
|
|
250
|
+
return new Promise(async (resolve, reject) => {
|
|
251
|
+
// 创建项目目录
|
|
252
|
+
sftp.mkdir(dir, async (err) => {
|
|
253
|
+
if (err) {
|
|
254
|
+
reject(err)
|
|
255
|
+
} else {
|
|
256
|
+
// 创建dockerfile
|
|
257
|
+
console.log('- 正在创建dockerfile')
|
|
258
|
+
const dockerfile = `${dir}/${this.config.dockerfile.name}`
|
|
259
|
+
await this.onShell(`touch ${dockerfile}`)
|
|
260
|
+
await this.onShell(`echo '${this.config.dockerfile.content.join('\n')}' > ${dockerfile}`)
|
|
261
|
+
|
|
262
|
+
// 创建upload.sh脚本
|
|
263
|
+
console.log('- 正在创建upload.sh')
|
|
264
|
+
const uploadSh = `${dir}/${this.config.upload.name}`
|
|
265
|
+
await this.onShell(`touch ${uploadSh}`)
|
|
266
|
+
await this.onShell(`echo '${this.config.upload.content.join('\n')}' > ${uploadSh}`)
|
|
267
|
+
|
|
268
|
+
resolve()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 判断文件是否存在
|
|
277
|
+
* @param {*} sftp
|
|
278
|
+
* @param {*} remotePath
|
|
279
|
+
* @returns
|
|
280
|
+
*/
|
|
281
|
+
checkRemoteFile(sftp, remotePath) {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
sftp.stat(remotePath, (err) => {
|
|
284
|
+
if (err) {
|
|
285
|
+
resolve(false) // 文件不存在
|
|
286
|
+
} else {
|
|
287
|
+
resolve(true) // 文件存在
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 上传项目
|
|
295
|
+
* @param {*} sftp
|
|
296
|
+
* @param {*} dir
|
|
297
|
+
*/
|
|
298
|
+
upLoadProject(sftp, dir) {
|
|
299
|
+
let rePushNum = 0 // 如果推送失败,是否开启重新推送,如果大于0 ,代表重新推送次数
|
|
300
|
+
if(typeof this.config.rePushNum === 'number' && this.config.rePushNum > 0) {
|
|
301
|
+
rePushNum = this.config.rePushNum
|
|
302
|
+
}
|
|
303
|
+
sftp.readdir(dir, async (err) => {
|
|
304
|
+
if (err) {
|
|
305
|
+
// 不存在目录
|
|
306
|
+
console.log('- 跳板机不存在项目, 开始创建项目')
|
|
307
|
+
this.projectInit(sftp, dir)
|
|
308
|
+
.then(() => {
|
|
309
|
+
// 目录已创建,再次调用上传
|
|
310
|
+
console.log(`- ${this.config.namespace}项目创建成功`)
|
|
311
|
+
this.upLoadProject(sftp, dir)
|
|
312
|
+
})
|
|
313
|
+
.catch((err) => {
|
|
314
|
+
console.log(`- 创建项目失败: ${err}`)
|
|
315
|
+
this.breakConnect()
|
|
316
|
+
})
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 是否开启懒上传
|
|
321
|
+
const lazyUpload = (this.getArgv('--lazyUpload') !== undefined) || this.config.lazyUpload
|
|
322
|
+
|
|
323
|
+
const distDir = `${dir}/${this.config.dist.name}`
|
|
324
|
+
if(!lazyUpload) {
|
|
325
|
+
// 删除dist目录所有内容
|
|
326
|
+
await this.onShell(`rm -rf ${distDir}`)
|
|
327
|
+
}
|
|
328
|
+
// 再创建一个空的dist
|
|
329
|
+
await this.onShell(`mkdir ${distDir}`)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// 获取本地目录下所有文件
|
|
333
|
+
if(!this.config.fileDir) {
|
|
334
|
+
console.log('- 请配置待上传文件目录 fileDir')
|
|
335
|
+
this.breakConnect()
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { localFileList, dirList } = await this.getFilesInDirectory(this.config.fileDir)
|
|
340
|
+
if(localFileList.length === 0) {
|
|
341
|
+
console.log('- 待上传目录没有获取到文件,请检查')
|
|
342
|
+
this.breakConnect()
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 上传文件或目录
|
|
348
|
+
*/
|
|
349
|
+
let num = 0
|
|
350
|
+
let total = localFileList.length
|
|
351
|
+
// 上传文件
|
|
352
|
+
const uploadFile = (file) => {
|
|
353
|
+
return new Promise(async (resolve, reject) => {
|
|
354
|
+
try {
|
|
355
|
+
|
|
356
|
+
if(lazyUpload) {
|
|
357
|
+
const isExist = await this.checkRemoteFile(sftp, `${distDir}${file.remotePath}`)
|
|
358
|
+
if(isExist) {
|
|
359
|
+
return resolve() // 文件已存在,跳过上传
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const readStream = fs.createReadStream(file.localPath)
|
|
364
|
+
const writeStream = sftp.createWriteStream(`${distDir}${file.remotePath}`)
|
|
365
|
+
writeStream.on('close', () => {
|
|
366
|
+
this.progressBar(++num, total)
|
|
367
|
+
resolve()
|
|
368
|
+
})
|
|
369
|
+
writeStream.on('error', (err) => {
|
|
370
|
+
console.log(`- 文件 ${file.remotePath} 上传失败:${err}`)
|
|
371
|
+
reject(err)
|
|
372
|
+
this.breakConnect()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
readStream.pipe(writeStream)
|
|
376
|
+
} catch (error) {
|
|
377
|
+
reject(error)
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 分割数组
|
|
384
|
+
*/
|
|
385
|
+
const chunkArray = (array, chunkSize) => {
|
|
386
|
+
let result = [];
|
|
387
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
388
|
+
result.push(array.slice(i, i + chunkSize));
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 串行执行上传文件
|
|
395
|
+
* @param {Array} promises
|
|
396
|
+
*/
|
|
397
|
+
const sequentialPromises = (promises) => {
|
|
398
|
+
if (promises.length === 0) {
|
|
399
|
+
return Promise.resolve();
|
|
400
|
+
}
|
|
401
|
+
// 获取数组的第一个promise并执行
|
|
402
|
+
const firstPromise = promises.shift();
|
|
403
|
+
return Promise.all(firstPromise.map(item => uploadFile(item))).then(() => sequentialPromises(promises));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 创建目录, 同时创建多个
|
|
407
|
+
const createDir = (promises) => {
|
|
408
|
+
if (promises.length === 0) {
|
|
409
|
+
return Promise.resolve();
|
|
410
|
+
}
|
|
411
|
+
// 获取数组的第一个promise并执行
|
|
412
|
+
const firstPromise = promises.shift();
|
|
413
|
+
let shell = firstPromise.map(remotePath => `${distDir}${remotePath}`).join(' ');
|
|
414
|
+
return this.onShell(`mkdir -p ${shell}`).then(() => createDir(promises));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 先创建目录,将目录分层级创建,增加效率,防止目录过多时串行太慢
|
|
418
|
+
let dirGroup = [...chunkArray(dirList, this.config.parallelDir || 20)]
|
|
419
|
+
// console.log(`- 开始创建目录`)
|
|
420
|
+
await createDir(dirGroup)
|
|
421
|
+
console.log('- 创建目录完成')
|
|
422
|
+
|
|
423
|
+
// 创建文件夹后延迟一下再上传文件,偶尔会报错
|
|
424
|
+
await new Promise((resolve) => {
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
resolve()
|
|
427
|
+
}, 500)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// 再上传文件
|
|
431
|
+
// 根据配置文件并行上传数量分割,如果并行太多可能会报错
|
|
432
|
+
let fileList = [...chunkArray(localFileList, this.config.parallelFile || 50)]
|
|
433
|
+
|
|
434
|
+
// console.log('- 开始上传文件')
|
|
435
|
+
await sequentialPromises(fileList)
|
|
436
|
+
|
|
437
|
+
if(lazyUpload) {
|
|
438
|
+
this.progressBar(total, total)
|
|
439
|
+
console.log('\x1b[32m%s\x1b[0m', `- 已开启懒上传,本次共上传 ${num} 个文件`)
|
|
440
|
+
}
|
|
441
|
+
console.log('- 文件上传成功')
|
|
442
|
+
|
|
443
|
+
console.log('- 开始推送镜像')
|
|
444
|
+
let shell = `cd ${dir} && sh ${this.config.upload.name}`
|
|
445
|
+
|
|
446
|
+
// 判断是不是自动创建的脚本,手动创建的脚本不支持镜像仓库名称参数
|
|
447
|
+
const isNewShell = await this.isNewShell(sftp, dir)
|
|
448
|
+
if(isNewShell) {
|
|
449
|
+
// 添加镜像仓库名称
|
|
450
|
+
const imageStore = this.getArgv('--imageStore') || this.config.imageStore
|
|
451
|
+
shell += ` ${imageStore}`
|
|
452
|
+
} else {
|
|
453
|
+
console.log('\n- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效')
|
|
454
|
+
console.log('- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 添加镜像环境参数
|
|
458
|
+
const tmpName = this.getArgv('--tmpName') || this.config.tmpName
|
|
459
|
+
shell += ` ${tmpName}`
|
|
460
|
+
|
|
461
|
+
// 判断如果失败,是否需要多次重试
|
|
462
|
+
const pushShellList = Array.from(new Array(rePushNum + 1)).map(() => this.onShell(shell))
|
|
463
|
+
await this.pushMirrorImage(pushShellList, pushShellList.length) // 开始推送镜像
|
|
464
|
+
|
|
465
|
+
this.breakConnect()
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 推送镜像
|
|
470
|
+
async pushMirrorImage(shellList, totalNum) {
|
|
471
|
+
if (shellList.length === 0) {
|
|
472
|
+
console.log('- 镜像推送失败,请添加 --allLog 查看全部日志')
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const result = await shellList[0]
|
|
477
|
+
|
|
478
|
+
if(!result.includes('- 镜像推送失败')) {
|
|
479
|
+
// 推送成功
|
|
480
|
+
console.log(result) // 推送镜像脚本返回输出
|
|
481
|
+
console.log('- 镜像推送结束')
|
|
482
|
+
|
|
483
|
+
if(this.getArgv('--restart') !== undefined) {
|
|
484
|
+
await restartService(result, this.config.rancherConfig)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return result
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
console.log(result) // 失败日志
|
|
491
|
+
if(shellList.length > 1) {
|
|
492
|
+
console.log(`- 正在尝试第 ${(totalNum - shellList.length) + 1} 次推送...`)
|
|
493
|
+
}
|
|
494
|
+
return this.pushMirrorImage(shellList.slice(1), totalNum)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 获取脚本内容是不是自动创建的脚本
|
|
499
|
+
* 兼容用户手动根据运维文档创建的推送脚本,因参数不同可能报错的问题
|
|
500
|
+
* 有 $2代表是自动创建的版本,否则老版本
|
|
501
|
+
*/
|
|
502
|
+
async isNewShell(sftp, dir) {
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
sftp.readFile(`${dir}/${this.config.upload.name}`, (err, data) => {
|
|
505
|
+
if(err) {
|
|
506
|
+
return reject(false)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const fileText = data.toString()
|
|
510
|
+
|
|
511
|
+
// 判断脚本是不是最新的支持异常提示脚本,自动重试的脚本
|
|
512
|
+
if(!fileText.includes('- 镜像推送失败')) {
|
|
513
|
+
console.log('\n- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本')
|
|
514
|
+
}
|
|
515
|
+
resolve(fileText.includes('$2'))
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 进度条
|
|
523
|
+
* @param description 命令行开头的文字信息
|
|
524
|
+
* @param bar_length 进度条的长度(单位:字符),默认设为 25
|
|
525
|
+
*/
|
|
526
|
+
progressBar(completed, total, bar_length = 25) {
|
|
527
|
+
let percent = (completed / total).toFixed(4) // 计算进度(子任务的 完成数 除以 总数)
|
|
528
|
+
let cell_num = Math.floor(percent * bar_length) // 计算需要多少个 █ 符号来拼凑图案 // 拼接黑色条
|
|
529
|
+
let cell = ''
|
|
530
|
+
for (let i = 0; i < cell_num; i++) {
|
|
531
|
+
cell += '█'
|
|
532
|
+
} // 拼接灰色条
|
|
533
|
+
let empty = ''
|
|
534
|
+
for (let i = 0; i < bar_length - cell_num; i++) {
|
|
535
|
+
empty += '░'
|
|
536
|
+
} // 拼接最终文本
|
|
537
|
+
let cmdText = `- 文件上传进度: ${cell}${empty} (${completed}/${total}) ${(
|
|
538
|
+
100 * percent
|
|
539
|
+
).toFixed(2)}%` // 在单行输出文本
|
|
540
|
+
stdout(cmdText)
|
|
541
|
+
if(completed == total) {
|
|
542
|
+
console.log('')
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 获取目录下所有文件并分类
|
|
548
|
+
* @param {*} dir
|
|
549
|
+
*/
|
|
550
|
+
getFilesInDirectory(directory) {
|
|
551
|
+
const fileList = [] // 文件夹
|
|
552
|
+
const dirList = [] // 最底层目录, 只需要获取最底层目录即可
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
const traverseFolder = (folderPath, parentFolder, level) => {
|
|
555
|
+
// 读取文件夹列表
|
|
556
|
+
const files = fs.readdirSync(folderPath)
|
|
557
|
+
let isDir = false // 当前文件夹内是否还有目录
|
|
558
|
+
// 遍历文件夹列表
|
|
559
|
+
files.forEach((fileName) => {
|
|
560
|
+
// 拼接当前文件路径
|
|
561
|
+
const filePath = path.join(folderPath, fileName)
|
|
562
|
+
|
|
563
|
+
// 获取文件类型
|
|
564
|
+
const stats = fs.statSync(filePath)
|
|
565
|
+
const isDirectory = stats.isDirectory()
|
|
566
|
+
|
|
567
|
+
if(!isDirectory) {
|
|
568
|
+
fileList.push({
|
|
569
|
+
localPath: filePath, // 本地路径
|
|
570
|
+
remotePath: `${parentFolder}${fileName}`, // 远程路径
|
|
571
|
+
level // 文件或目录层级
|
|
572
|
+
})
|
|
573
|
+
} else {
|
|
574
|
+
isDir = true
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 判断该路径是文件夹还是文件
|
|
578
|
+
if (isDirectory) {
|
|
579
|
+
// 如果是文件夹,递归遍历
|
|
580
|
+
traverseFolder(filePath, `${parentFolder}${fileName}/`, level + 1)
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
if(!isDir) {
|
|
585
|
+
dirList.push(parentFolder)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
traverseFolder(directory, '/', 0)
|
|
590
|
+
|
|
591
|
+
resolve({
|
|
592
|
+
localFileList: fileList,
|
|
593
|
+
dirList
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 获取命令行参数
|
|
600
|
+
* @param { string } name
|
|
601
|
+
*/
|
|
602
|
+
getArgv(name) {
|
|
603
|
+
const argv = process.argv
|
|
604
|
+
let result = undefined
|
|
605
|
+
argv.forEach(item => {
|
|
606
|
+
if(item.indexOf(name) > -1) {
|
|
607
|
+
result = item.split('=')[1] || ''
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
return result
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const httpsAgent = new https.Agent({
|
|
616
|
+
rejectUnauthorized: false
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
const axiosInstance = axios.create({
|
|
620
|
+
proxy: false
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
async function restartService(imageData, config) {
|
|
624
|
+
try {
|
|
625
|
+
const localConfig = {
|
|
626
|
+
baseUrl: 'https://paas-test.ymygz.com:10443/v3',
|
|
627
|
+
clusterId: 'local:p-r7j28'
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const rancherConfig = {
|
|
631
|
+
...localConfig,
|
|
632
|
+
...config
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!rancherConfig.baseUrl) {
|
|
636
|
+
return console.error('- Rancher API 基础地址未配置,请检查 rancherConfig.baseUrl 是否正确')
|
|
637
|
+
}
|
|
638
|
+
if (!rancherConfig.token) {
|
|
639
|
+
return console.error('- Rancher API 访问令牌未配置,请检查 rancherConfig.token 是否正确')
|
|
640
|
+
}
|
|
641
|
+
if (!rancherConfig.clusterId) {
|
|
642
|
+
return console.error('- Rancher 集群 ID 未配置,请检查 rancherConfig.clusterId 是否正确')
|
|
643
|
+
}
|
|
644
|
+
if (!rancherConfig.namespace) {
|
|
645
|
+
return console.error('- 服务所在的命名空间未配置,请检查 rancherConfig.namespace 是否正确')
|
|
646
|
+
}
|
|
647
|
+
if (!rancherConfig.workload) {
|
|
648
|
+
return console.error('- 工作负载名称未配置,请检查 rancherConfig.workload 是否正确')
|
|
649
|
+
}
|
|
650
|
+
if (!imageData) {
|
|
651
|
+
return console.error('- 内部错误')
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const match = imageData.match(/- 镜像地址:\s*(.+)/)
|
|
655
|
+
let imageAddress = ''
|
|
656
|
+
if (match && match[1]) {
|
|
657
|
+
imageAddress = match[1].trim()
|
|
658
|
+
} else {
|
|
659
|
+
return console.error('- 未提取到镜像地址')
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
console.log('- 开始调用 Rancher API 更新服务镜像地址')
|
|
663
|
+
const workloadUrl = `${rancherConfig.baseUrl}/projects/${rancherConfig.clusterId}/workloads/deployment:${rancherConfig.namespace}:${rancherConfig.workload}`
|
|
664
|
+
console.log('- 请求的工作负载 URL:', workloadUrl)
|
|
665
|
+
|
|
666
|
+
const response = await axiosInstance.get(workloadUrl, {
|
|
667
|
+
headers: { Authorization: `Bearer ${rancherConfig.token}` },
|
|
668
|
+
httpsAgent
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
if (!response.data || !response.data.containers || response.data.containers.length === 0) {
|
|
672
|
+
return console.error('- 未找到工作负载的容器信息,请检查 namespace 和 workload 是否正确')
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const workload = response.data
|
|
676
|
+
workload.containers[0].image = imageAddress
|
|
677
|
+
|
|
678
|
+
await axiosInstance.put(workloadUrl, workload, {
|
|
679
|
+
headers: { Authorization: `Bearer ${rancherConfig.token}` },
|
|
680
|
+
httpsAgent
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
console.log('- 镜像地址更新成功,正在重启服务...')
|
|
684
|
+
await axiosInstance.post(
|
|
685
|
+
`${workloadUrl}?action=redeploy`,
|
|
686
|
+
{},
|
|
687
|
+
{
|
|
688
|
+
headers: { Authorization: `Bearer ${rancherConfig.token}` },
|
|
689
|
+
httpsAgent
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
console.log('- 服务重启成功')
|
|
694
|
+
} catch (error) {
|
|
695
|
+
console.error('- 调用 Rancher API 失败,请检查以下信息:')
|
|
696
|
+
console.error(' - 错误代码:', error.code)
|
|
697
|
+
console.error(' - 错误信息:', error.message)
|
|
698
|
+
console.error(' - 请求 URL:', error.config?.url)
|
|
699
|
+
console.error(' - 响应状态:', error.response?.status)
|
|
700
|
+
console.error(' - 响应数据:', error.response?.data)
|
|
701
|
+
}
|
|
702
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yj-deploy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "ssh sftp",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"scripts": {
|
|
6
7
|
"build": "vite build",
|
|
7
8
|
"deploy": "node test.js --restart"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
|
-
"
|
|
11
|
+
"lib",
|
|
11
12
|
"package.json",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
|
-
"main": "./
|
|
15
|
-
"module": "./
|
|
16
|
-
"exports": {
|
|
17
|
-
".": {
|
|
18
|
-
"import": "./dist/yj-deploy.mjs",
|
|
19
|
-
"require": "./dist/yj-deploy.umd.js"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
15
|
+
"main": "./lib/main.js",
|
|
16
|
+
"module": "./lib/main.js",
|
|
22
17
|
"author": "",
|
|
23
18
|
"license": "ISC",
|
|
24
19
|
"dependencies": {
|
package/dist/yj-deploy.mjs
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
var v = Object.defineProperty;
|
|
2
|
-
var L = (u, e, o) => e in u ? v(u, e, { enumerable: !0, configurable: !0, writable: !0, value: o }) : u[e] = o;
|
|
3
|
-
var w = (u, e, o) => (L(u, typeof e != "symbol" ? e + "" : e, o), o);
|
|
4
|
-
const { stdout: U } = require("single-line-log"), F = require("path"), S = require("fs"), { Client: R } = require("ssh2"), N = require("axios"), q = require("https");
|
|
5
|
-
module.exports = class {
|
|
6
|
-
constructor(e) {
|
|
7
|
-
w(this, "config", {
|
|
8
|
-
host: "",
|
|
9
|
-
port: "",
|
|
10
|
-
username: "",
|
|
11
|
-
password: "",
|
|
12
|
-
namespace: "",
|
|
13
|
-
// 项目命名空间,等同于镜像地址目录名称
|
|
14
|
-
imageStore: "dev-images",
|
|
15
|
-
// 镜像仓库
|
|
16
|
-
tmpName: "dev",
|
|
17
|
-
// 镜像环境
|
|
18
|
-
delay: 0,
|
|
19
|
-
// 延迟上传时间
|
|
20
|
-
// 本地文件目录(必填)
|
|
21
|
-
fileDir: "",
|
|
22
|
-
// 远程sftp服务根目录
|
|
23
|
-
rootDir: "/home/yjweb",
|
|
24
|
-
// dockerfile文件信息
|
|
25
|
-
dockerfile: {
|
|
26
|
-
name: "Dockerfile",
|
|
27
|
-
content: [
|
|
28
|
-
"FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest",
|
|
29
|
-
"RUN rm -rf /usr/share/nginx/html/*",
|
|
30
|
-
"COPY dist /usr/share/nginx/html/"
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
// upload.sh文件信息
|
|
34
|
-
upload: {
|
|
35
|
-
name: "upload.sh",
|
|
36
|
-
content: [
|
|
37
|
-
"#!/bin/sh",
|
|
38
|
-
"tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`",
|
|
39
|
-
'echo "- 镜像仓库: $1"',
|
|
40
|
-
'echo "- 镜像环境: $2"',
|
|
41
|
-
"docker build -t harbor.yunjingtech.cn:30002/$1/$tag .",
|
|
42
|
-
"docker push harbor.yunjingtech.cn:30002/$1/$tag",
|
|
43
|
-
"if [ $? -eq 0 ];then",
|
|
44
|
-
"echo - 推送成功",
|
|
45
|
-
"echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag",
|
|
46
|
-
"else",
|
|
47
|
-
"echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息",
|
|
48
|
-
"fi"
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
// 上传dist目录信息
|
|
52
|
-
dist: {
|
|
53
|
-
name: "dist"
|
|
54
|
-
},
|
|
55
|
-
parallelDir: 20,
|
|
56
|
-
// 文件夹并行创建数量,如果报错,可以减少此配置
|
|
57
|
-
parallelFile: 50,
|
|
58
|
-
// 文件并行上传数量,如果报错,可以减少此配置
|
|
59
|
-
allLog: !1,
|
|
60
|
-
// 是否显示全部日志
|
|
61
|
-
lazyUpload: !1,
|
|
62
|
-
// 是否开启懒上传
|
|
63
|
-
rePushNum: 3,
|
|
64
|
-
// 重新推送次数,为0代表不重新推送
|
|
65
|
-
// 仅能通过命令启用
|
|
66
|
-
rancherConfig: {
|
|
67
|
-
// baseUrl: '', // Rancher API 基础地址
|
|
68
|
-
// clusterId: '', // Rancher 集群 ID
|
|
69
|
-
token: "",
|
|
70
|
-
// Rancher API 访问令牌
|
|
71
|
-
namespace: "",
|
|
72
|
-
// 服务所在的命名空间
|
|
73
|
-
workload: ""
|
|
74
|
-
// 工作负载名称
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
w(this, "ssh2Conn", null);
|
|
78
|
-
// 上传状态
|
|
79
|
-
w(this, "uploading", !1);
|
|
80
|
-
// 定时器
|
|
81
|
-
w(this, "trim", null);
|
|
82
|
-
return this.config = Object.assign(this.config, e), {
|
|
83
|
-
name: "yj-deploy",
|
|
84
|
-
isPut: this.isPut.bind(this),
|
|
85
|
-
upload: this.upload.bind(this),
|
|
86
|
-
// webpack钩子
|
|
87
|
-
apply: this.apply.bind(this),
|
|
88
|
-
// vite上传钩子
|
|
89
|
-
closeBundle: this.isPut.bind(this)
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
// webpack钩子
|
|
93
|
-
apply(e) {
|
|
94
|
-
return e && e.hooks && e.hooks.done && e.hooks.done.tap("yj-deploy", () => {
|
|
95
|
-
this.isPut();
|
|
96
|
-
}), "build";
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* 判断是否需要上传,
|
|
100
|
-
* vite和 webpack环境在每次编译完都会触发当前方法
|
|
101
|
-
* 需要通过是否有命令行参数 --deploy
|
|
102
|
-
*/
|
|
103
|
-
isPut() {
|
|
104
|
-
this.getArgv("--deploy") != null && (clearTimeout(this.trim), this.trim = setTimeout(() => {
|
|
105
|
-
this.upload();
|
|
106
|
-
}, this.config.delay || 0));
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* 连接跳板机并开始上传
|
|
110
|
-
*/
|
|
111
|
-
upload() {
|
|
112
|
-
if (console.log("-------------- deploy-start --------------"), !this.config.host) {
|
|
113
|
-
console.log("- 请配置跳板机地址 host");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (!this.config.port) {
|
|
117
|
-
console.log("- 请配置跳板机端口 port");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (!this.config.username) {
|
|
121
|
-
console.log("- 请配置跳板机账号 username");
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (!this.config.password) {
|
|
125
|
-
console.log("- 请配置跳板机密码 password");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
this.uploading || (this.ssh2Conn = new R(), this.ssh2Conn.on("ready", () => {
|
|
129
|
-
console.log("- 跳板机连接成功!"), this.ssh2Conn.sftp(async (e, o) => {
|
|
130
|
-
e && (console.log("- sftp连接失败"), this.breakConnect()), this.config.namespace || (console.log("- 请配置项目命名空间 namespace"), this.breakConnect()), this.uploading = !0, this.getArgv("--reset") !== void 0 && (console.log("- 重置项目"), await this.onShell(`rm -r ${this.config.rootDir}/${this.config.namespace}`)), this.upLoadProject(o, `${this.config.rootDir}/${this.config.namespace}`);
|
|
131
|
-
});
|
|
132
|
-
}).connect({
|
|
133
|
-
host: this.config.host,
|
|
134
|
-
port: this.config.port,
|
|
135
|
-
username: this.config.username,
|
|
136
|
-
password: this.config.password
|
|
137
|
-
}), this.ssh2Conn.on("error", (e) => {
|
|
138
|
-
console.log(`- 连接失败: ${e}`), this.breakConnect();
|
|
139
|
-
}), this.ssh2Conn.on("end", () => {
|
|
140
|
-
this.uploading = !1;
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* 断开连接
|
|
145
|
-
*/
|
|
146
|
-
breakConnect() {
|
|
147
|
-
this.uploading = !1, console.log("- 已断开连接"), console.log("-------------- deploy-end --------------"), this.ssh2Conn.end();
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* 运行shell命令
|
|
151
|
-
* @param {*} shell
|
|
152
|
-
* @returns Promise
|
|
153
|
-
*/
|
|
154
|
-
onShell(e) {
|
|
155
|
-
const o = this.getArgv("--allLog") !== void 0;
|
|
156
|
-
return new Promise((n, i) => {
|
|
157
|
-
this.ssh2Conn.shell((s, t) => {
|
|
158
|
-
if (s) {
|
|
159
|
-
console.log("- 远程命令错误:" + s, e), this.breakConnect(), i(s);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
let r = [];
|
|
163
|
-
t.on("close", () => {
|
|
164
|
-
n(r.toString());
|
|
165
|
-
}).on("data", (c) => {
|
|
166
|
-
let a = c.toString();
|
|
167
|
-
(o || this.config.allLog || a.startsWith("- ")) && r.push(`
|
|
168
|
-
` + a);
|
|
169
|
-
}).stderr.on("data", (c) => {
|
|
170
|
-
console.log(`- 远程命令错误:
|
|
171
|
-
` + c), this.breakConnect(), i(c);
|
|
172
|
-
}), t.end(e + `
|
|
173
|
-
exit
|
|
174
|
-
`);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* 初始化项目
|
|
180
|
-
* @param {*} sftp
|
|
181
|
-
* @param {*} dir
|
|
182
|
-
*/
|
|
183
|
-
projectInit(e, o) {
|
|
184
|
-
return new Promise(async (n, i) => {
|
|
185
|
-
e.mkdir(o, async (s) => {
|
|
186
|
-
if (s)
|
|
187
|
-
i(s);
|
|
188
|
-
else {
|
|
189
|
-
console.log("- 正在创建dockerfile");
|
|
190
|
-
const t = `${o}/${this.config.dockerfile.name}`;
|
|
191
|
-
await this.onShell(`touch ${t}`), await this.onShell(`echo '${this.config.dockerfile.content.join(`
|
|
192
|
-
`)}' > ${t}`), console.log("- 正在创建upload.sh");
|
|
193
|
-
const r = `${o}/${this.config.upload.name}`;
|
|
194
|
-
await this.onShell(`touch ${r}`), await this.onShell(`echo '${this.config.upload.content.join(`
|
|
195
|
-
`)}' > ${r}`), n();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* 判断文件是否存在
|
|
202
|
-
* @param {*} sftp
|
|
203
|
-
* @param {*} remotePath
|
|
204
|
-
* @returns
|
|
205
|
-
*/
|
|
206
|
-
checkRemoteFile(e, o) {
|
|
207
|
-
return new Promise((n) => {
|
|
208
|
-
e.stat(o, (i) => {
|
|
209
|
-
n(!i);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* 上传项目
|
|
215
|
-
* @param {*} sftp
|
|
216
|
-
* @param {*} dir
|
|
217
|
-
*/
|
|
218
|
-
upLoadProject(e, o) {
|
|
219
|
-
let n = 0;
|
|
220
|
-
typeof this.config.rePushNum == "number" && this.config.rePushNum > 0 && (n = this.config.rePushNum), e.readdir(o, async (i) => {
|
|
221
|
-
if (i) {
|
|
222
|
-
console.log("- 跳板机不存在项目, 开始创建项目"), this.projectInit(e, o).then(() => {
|
|
223
|
-
console.log(`- ${this.config.namespace}项目创建成功`), this.upLoadProject(e, o);
|
|
224
|
-
}).catch((l) => {
|
|
225
|
-
console.log(`- 创建项目失败: ${l}`), this.breakConnect();
|
|
226
|
-
});
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const s = this.getArgv("--lazyUpload") !== void 0 || this.config.lazyUpload, t = `${o}/${this.config.dist.name}`;
|
|
230
|
-
if (s || await this.onShell(`rm -rf ${t}`), await this.onShell(`mkdir ${t}`), !this.config.fileDir) {
|
|
231
|
-
console.log("- 请配置待上传文件目录 fileDir"), this.breakConnect();
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const { localFileList: r, dirList: c } = await this.getFilesInDirectory(this.config.fileDir);
|
|
235
|
-
if (r.length === 0) {
|
|
236
|
-
console.log("- 待上传目录没有获取到文件,请检查"), this.breakConnect();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
let a = 0, h = r.length;
|
|
240
|
-
const p = (l) => new Promise(async (d, f) => {
|
|
241
|
-
try {
|
|
242
|
-
if (s && await this.checkRemoteFile(e, `${t}${l.remotePath}`))
|
|
243
|
-
return d();
|
|
244
|
-
const g = S.createReadStream(l.localPath), b = e.createWriteStream(`${t}${l.remotePath}`);
|
|
245
|
-
b.on("close", () => {
|
|
246
|
-
this.progressBar(++a, h), d();
|
|
247
|
-
}), b.on("error", (C) => {
|
|
248
|
-
console.log(`- 文件 ${l.remotePath} 上传失败:${C}`), f(C), this.breakConnect();
|
|
249
|
-
}), g.pipe(b);
|
|
250
|
-
} catch (g) {
|
|
251
|
-
f(g);
|
|
252
|
-
}
|
|
253
|
-
}), m = (l, d) => {
|
|
254
|
-
let f = [];
|
|
255
|
-
for (let g = 0; g < l.length; g += d)
|
|
256
|
-
f.push(l.slice(g, g + d));
|
|
257
|
-
return f;
|
|
258
|
-
}, $ = (l) => {
|
|
259
|
-
if (l.length === 0)
|
|
260
|
-
return Promise.resolve();
|
|
261
|
-
const d = l.shift();
|
|
262
|
-
return Promise.all(d.map((f) => p(f))).then(() => $(l));
|
|
263
|
-
}, k = (l) => {
|
|
264
|
-
if (l.length === 0)
|
|
265
|
-
return Promise.resolve();
|
|
266
|
-
let f = l.shift().map((g) => `${t}${g}`).join(" ");
|
|
267
|
-
return this.onShell(`mkdir -p ${f}`).then(() => k(l));
|
|
268
|
-
};
|
|
269
|
-
let y = [...m(c, this.config.parallelDir || 20)];
|
|
270
|
-
await k(y), console.log("- 创建目录完成"), await new Promise((l) => {
|
|
271
|
-
setTimeout(() => {
|
|
272
|
-
l();
|
|
273
|
-
}, 500);
|
|
274
|
-
});
|
|
275
|
-
let I = [...m(r, this.config.parallelFile || 50)];
|
|
276
|
-
await $(I), s && (this.progressBar(h, h), console.log("\x1B[32m%s\x1B[0m", `- 已开启懒上传,本次共上传 ${a} 个文件`)), console.log("- 文件上传成功"), console.log("- 开始推送镜像");
|
|
277
|
-
let P = `cd ${o} && sh ${this.config.upload.name}`;
|
|
278
|
-
if (await this.isNewShell(e, o)) {
|
|
279
|
-
const l = this.getArgv("--imageStore") || this.config.imageStore;
|
|
280
|
-
P += ` ${l}`;
|
|
281
|
-
} else
|
|
282
|
-
console.log(`
|
|
283
|
-
- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效`), console.log("- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本");
|
|
284
|
-
const x = this.getArgv("--tmpName") || this.config.tmpName;
|
|
285
|
-
P += ` ${x}`;
|
|
286
|
-
const D = Array.from(new Array(n + 1)).map(() => this.onShell(P));
|
|
287
|
-
await this.pushMirrorImage(D, D.length), this.breakConnect();
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
// 推送镜像
|
|
291
|
-
async pushMirrorImage(e, o) {
|
|
292
|
-
if (e.length === 0)
|
|
293
|
-
return console.log("- 镜像推送失败,请添加 --allLog 查看全部日志"), null;
|
|
294
|
-
const n = await e[0];
|
|
295
|
-
return n.includes("- 镜像推送失败") ? (console.log(n), e.length > 1 && console.log(`- 正在尝试第 ${o - e.length + 1} 次推送...`), this.pushMirrorImage(e.slice(1), o)) : (console.log(n), console.log("- 镜像推送结束"), this.getArgv("--restart") !== void 0 && await z(n, this.config.rancherConfig), n);
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* 获取脚本内容是不是自动创建的脚本
|
|
299
|
-
* 兼容用户手动根据运维文档创建的推送脚本,因参数不同可能报错的问题
|
|
300
|
-
* 有 $2代表是自动创建的版本,否则老版本
|
|
301
|
-
*/
|
|
302
|
-
async isNewShell(e, o) {
|
|
303
|
-
return new Promise((n, i) => {
|
|
304
|
-
e.readFile(`${o}/${this.config.upload.name}`, (s, t) => {
|
|
305
|
-
if (s)
|
|
306
|
-
return i(!1);
|
|
307
|
-
const r = t.toString();
|
|
308
|
-
r.includes("- 镜像推送失败") || console.log(`
|
|
309
|
-
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`), n(r.includes("$2"));
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* 进度条
|
|
315
|
-
* @param description 命令行开头的文字信息
|
|
316
|
-
* @param bar_length 进度条的长度(单位:字符),默认设为 25
|
|
317
|
-
*/
|
|
318
|
-
progressBar(e, o, n = 25) {
|
|
319
|
-
let i = (e / o).toFixed(4), s = Math.floor(i * n), t = "";
|
|
320
|
-
for (let a = 0; a < s; a++)
|
|
321
|
-
t += "█";
|
|
322
|
-
let r = "";
|
|
323
|
-
for (let a = 0; a < n - s; a++)
|
|
324
|
-
r += "░";
|
|
325
|
-
let c = `- 文件上传进度: ${t}${r} (${e}/${o}) ${(100 * i).toFixed(2)}%`;
|
|
326
|
-
U(c), e == o && console.log("");
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* 获取目录下所有文件并分类
|
|
330
|
-
* @param {*} dir
|
|
331
|
-
*/
|
|
332
|
-
getFilesInDirectory(e) {
|
|
333
|
-
const o = [], n = [];
|
|
334
|
-
return new Promise((i, s) => {
|
|
335
|
-
const t = (r, c, a) => {
|
|
336
|
-
const h = S.readdirSync(r);
|
|
337
|
-
let p = !1;
|
|
338
|
-
h.forEach((m) => {
|
|
339
|
-
const $ = F.join(r, m), y = S.statSync($).isDirectory();
|
|
340
|
-
y ? p = !0 : o.push({
|
|
341
|
-
localPath: $,
|
|
342
|
-
// 本地路径
|
|
343
|
-
remotePath: `${c}${m}`,
|
|
344
|
-
// 远程路径
|
|
345
|
-
level: a
|
|
346
|
-
// 文件或目录层级
|
|
347
|
-
}), y && t($, `${c}${m}/`, a + 1);
|
|
348
|
-
}), p || n.push(c);
|
|
349
|
-
};
|
|
350
|
-
t(e, "/", 0), i({
|
|
351
|
-
localFileList: o,
|
|
352
|
-
dirList: n
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* 获取命令行参数
|
|
358
|
-
* @param { string } name
|
|
359
|
-
*/
|
|
360
|
-
getArgv(e) {
|
|
361
|
-
const o = process.argv;
|
|
362
|
-
let n;
|
|
363
|
-
return o.forEach((i) => {
|
|
364
|
-
i.indexOf(e) > -1 && (n = i.split("=")[1] || "");
|
|
365
|
-
}), n;
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
const A = new q.Agent({
|
|
369
|
-
rejectUnauthorized: !1
|
|
370
|
-
}), j = N.create({
|
|
371
|
-
proxy: !1
|
|
372
|
-
// 禁用代理
|
|
373
|
-
});
|
|
374
|
-
async function z(u, e) {
|
|
375
|
-
var o, n, i;
|
|
376
|
-
try {
|
|
377
|
-
const t = {
|
|
378
|
-
// baseUrl: 'https://paas-test.ymygz.com:10443/v3', // Rancher API 基础地址
|
|
379
|
-
// token: 'token-2gqbg:pwpqvzqfbj26h9flwzbptcpz5sqn7jwh5c9ht2lfx46j4xz58875fr', // Rancher API 访问令牌
|
|
380
|
-
// clusterId: 'local:p-r7j28', // Rancher 集群 ID
|
|
381
|
-
// namespace: 'dev-wutaishan-onetravel', // 服务所在的命名空间
|
|
382
|
-
// workload: 'web-ymy-img', // 工作负载名称
|
|
383
|
-
...{
|
|
384
|
-
baseUrl: "https://paas-test.ymygz.com:10443/v3",
|
|
385
|
-
// Rancher API 基础地址
|
|
386
|
-
clusterId: "local:p-r7j28"
|
|
387
|
-
// Rancher 集群 ID
|
|
388
|
-
},
|
|
389
|
-
...e
|
|
390
|
-
};
|
|
391
|
-
if (!t.baseUrl)
|
|
392
|
-
return console.error("- Rancher API 基础地址未配置,请检查 rancherConfig.baseUrl 是否正确");
|
|
393
|
-
if (!t.token)
|
|
394
|
-
return console.error("- Rancher API 访问令牌未配置,请检查 rancherConfig.token 是否正确");
|
|
395
|
-
if (!t.clusterId)
|
|
396
|
-
return console.error("- Rancher 集群 ID 未配置,请检查 rancherConfig.clusterId 是否正确");
|
|
397
|
-
if (!t.namespace)
|
|
398
|
-
return console.error("- 服务所在的命名空间未配置,请检查 rancherConfig.namespace 是否正确");
|
|
399
|
-
if (!t.workload)
|
|
400
|
-
return console.error("- 工作负载名称未配置,请检查 rancherConfig.workload 是否正确");
|
|
401
|
-
if (!u)
|
|
402
|
-
return console.error("- 内部错误");
|
|
403
|
-
const r = u.match(/- 镜像地址:\s*(.+)/);
|
|
404
|
-
let c = "";
|
|
405
|
-
if (r && r[1])
|
|
406
|
-
c = r[1].trim();
|
|
407
|
-
else
|
|
408
|
-
return console.error("- 未提取到镜像地址");
|
|
409
|
-
console.log("- 开始调用 Rancher API 更新服务镜像地址");
|
|
410
|
-
const a = `${t.baseUrl}/projects/${t.clusterId}/workloads/deployment:${t.namespace}:${t.workload}`;
|
|
411
|
-
console.log("- 请求的工作负载 URL:", a);
|
|
412
|
-
const h = await j.get(a, {
|
|
413
|
-
headers: { Authorization: `Bearer ${t.token}` },
|
|
414
|
-
httpsAgent: A
|
|
415
|
-
});
|
|
416
|
-
if (!h.data || !h.data.containers || h.data.containers.length === 0)
|
|
417
|
-
return console.error("- 未找到工作负载的容器信息,请检查 namespace 和 workload 是否正确");
|
|
418
|
-
const p = h.data;
|
|
419
|
-
p.containers[0].image = c, await j.put(a, p, {
|
|
420
|
-
headers: { Authorization: `Bearer ${t.token}` },
|
|
421
|
-
httpsAgent: A
|
|
422
|
-
}), console.log("- 镜像地址更新成功,正在重启服务..."), await j.post(
|
|
423
|
-
`${a}?action=redeploy`,
|
|
424
|
-
{},
|
|
425
|
-
{
|
|
426
|
-
headers: { Authorization: `Bearer ${t.token}` },
|
|
427
|
-
httpsAgent: A
|
|
428
|
-
}
|
|
429
|
-
), console.log("- 服务重启成功");
|
|
430
|
-
} catch (s) {
|
|
431
|
-
console.error("- 调用 Rancher API 失败,请检查以下信息:"), console.error(" - 错误代码:", s.code), console.error(" - 错误信息:", s.message), console.error(" - 请求 URL:", (o = s.config) == null ? void 0 : o.url), console.error(" - 响应状态:", (n = s.response) == null ? void 0 : n.status), console.error(" - 响应数据:", (i = s.response) == null ? void 0 : i.data);
|
|
432
|
-
}
|
|
433
|
-
}
|
package/dist/yj-deploy.umd.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
(function(h){typeof define=="function"&&define.amd?define(h):h()})(function(){"use strict";var q=Object.defineProperty;var z=(h,f,d)=>f in h?q(h,f,{enumerable:!0,configurable:!0,writable:!0,value:d}):h[f]=d;var k=(h,f,d)=>(z(h,typeof f!="symbol"?f+"":f,d),d);const{stdout:h}=require("single-line-log"),f=require("path"),d=require("fs"),{Client:v}=require("ssh2"),L=require("axios"),U=require("https");module.exports=class{constructor(e){k(this,"config",{host:"",port:"",username:"",password:"",namespace:"",imageStore:"dev-images",tmpName:"dev",delay:0,fileDir:"",rootDir:"/home/yjweb",dockerfile:{name:"Dockerfile",content:["FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest","RUN rm -rf /usr/share/nginx/html/*","COPY dist /usr/share/nginx/html/"]},upload:{name:"upload.sh",content:["#!/bin/sh","tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`",'echo "- 镜像仓库: $1"','echo "- 镜像环境: $2"',"docker build -t harbor.yunjingtech.cn:30002/$1/$tag .","docker push harbor.yunjingtech.cn:30002/$1/$tag","if [ $? -eq 0 ];then","echo - 推送成功","echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag","else","echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息","fi"]},dist:{name:"dist"},parallelDir:20,parallelFile:50,allLog:!1,lazyUpload:!1,rePushNum:3,rancherConfig:{token:"",namespace:"",workload:""}});k(this,"ssh2Conn",null);k(this,"uploading",!1);k(this,"trim",null);return this.config=Object.assign(this.config,e),{name:"yj-deploy",isPut:this.isPut.bind(this),upload:this.upload.bind(this),apply:this.apply.bind(this),closeBundle:this.isPut.bind(this)}}apply(e){return e&&e.hooks&&e.hooks.done&&e.hooks.done.tap("yj-deploy",()=>{this.isPut()}),"build"}isPut(){this.getArgv("--deploy")!=null&&(clearTimeout(this.trim),this.trim=setTimeout(()=>{this.upload()},this.config.delay||0))}upload(){if(console.log("-------------- deploy-start --------------"),!this.config.host){console.log("- 请配置跳板机地址 host");return}if(!this.config.port){console.log("- 请配置跳板机端口 port");return}if(!this.config.username){console.log("- 请配置跳板机账号 username");return}if(!this.config.password){console.log("- 请配置跳板机密码 password");return}this.uploading||(this.ssh2Conn=new v,this.ssh2Conn.on("ready",()=>{console.log("- 跳板机连接成功!"),this.ssh2Conn.sftp(async(e,o)=>{e&&(console.log("- sftp连接失败"),this.breakConnect()),this.config.namespace||(console.log("- 请配置项目命名空间 namespace"),this.breakConnect()),this.uploading=!0,this.getArgv("--reset")!==void 0&&(console.log("- 重置项目"),await this.onShell(`rm -r ${this.config.rootDir}/${this.config.namespace}`)),this.upLoadProject(o,`${this.config.rootDir}/${this.config.namespace}`)})}).connect({host:this.config.host,port:this.config.port,username:this.config.username,password:this.config.password}),this.ssh2Conn.on("error",e=>{console.log(`- 连接失败: ${e}`),this.breakConnect()}),this.ssh2Conn.on("end",()=>{this.uploading=!1}))}breakConnect(){this.uploading=!1,console.log("- 已断开连接"),console.log("-------------- deploy-end --------------"),this.ssh2Conn.end()}onShell(e){const o=this.getArgv("--allLog")!==void 0;return new Promise((n,i)=>{this.ssh2Conn.shell((s,t)=>{if(s){console.log("- 远程命令错误:"+s,e),this.breakConnect(),i(s);return}let r=[];t.on("close",()=>{n(r.toString())}).on("data",c=>{let a=c.toString();(o||this.config.allLog||a.startsWith("- "))&&r.push(`
|
|
2
|
-
`+a)}).stderr.on("data",c=>{console.log(`- 远程命令错误:
|
|
3
|
-
`+c),this.breakConnect(),i(c)}),t.end(e+`
|
|
4
|
-
exit
|
|
5
|
-
`)})})}projectInit(e,o){return new Promise(async(n,i)=>{e.mkdir(o,async s=>{if(s)i(s);else{console.log("- 正在创建dockerfile");const t=`${o}/${this.config.dockerfile.name}`;await this.onShell(`touch ${t}`),await this.onShell(`echo '${this.config.dockerfile.content.join(`
|
|
6
|
-
`)}' > ${t}`),console.log("- 正在创建upload.sh");const r=`${o}/${this.config.upload.name}`;await this.onShell(`touch ${r}`),await this.onShell(`echo '${this.config.upload.content.join(`
|
|
7
|
-
`)}' > ${r}`),n()}})})}checkRemoteFile(e,o){return new Promise(n=>{e.stat(o,i=>{n(!i)})})}upLoadProject(e,o){let n=0;typeof this.config.rePushNum=="number"&&this.config.rePushNum>0&&(n=this.config.rePushNum),e.readdir(o,async i=>{if(i){console.log("- 跳板机不存在项目, 开始创建项目"),this.projectInit(e,o).then(()=>{console.log(`- ${this.config.namespace}项目创建成功`),this.upLoadProject(e,o)}).catch(l=>{console.log(`- 创建项目失败: ${l}`),this.breakConnect()});return}const s=this.getArgv("--lazyUpload")!==void 0||this.config.lazyUpload,t=`${o}/${this.config.dist.name}`;if(s||await this.onShell(`rm -rf ${t}`),await this.onShell(`mkdir ${t}`),!this.config.fileDir){console.log("- 请配置待上传文件目录 fileDir"),this.breakConnect();return}const{localFileList:r,dirList:c}=await this.getFilesInDirectory(this.config.fileDir);if(r.length===0){console.log("- 待上传目录没有获取到文件,请检查"),this.breakConnect();return}let a=0,g=r.length;const $=l=>new Promise(async(m,p)=>{try{if(s&&await this.checkRemoteFile(e,`${t}${l.remotePath}`))return m();const u=d.createReadStream(l.localPath),x=e.createWriteStream(`${t}${l.remotePath}`);x.on("close",()=>{this.progressBar(++a,g),m()}),x.on("error",D=>{console.log(`- 文件 ${l.remotePath} 上传失败:${D}`),p(D),this.breakConnect()}),u.pipe(x)}catch(u){p(u)}}),w=(l,m)=>{let p=[];for(let u=0;u<l.length;u+=m)p.push(l.slice(u,u+m));return p},y=l=>{if(l.length===0)return Promise.resolve();const m=l.shift();return Promise.all(m.map(p=>$(p))).then(()=>y(l))},A=l=>{if(l.length===0)return Promise.resolve();let p=l.shift().map(u=>`${t}${u}`).join(" ");return this.onShell(`mkdir -p ${p}`).then(()=>A(l))};let P=[...w(c,this.config.parallelDir||20)];await A(P),console.log("- 创建目录完成"),await new Promise(l=>{setTimeout(()=>{l()},500)});let R=[...w(r,this.config.parallelFile||50)];await y(R),s&&(this.progressBar(g,g),console.log("\x1B[32m%s\x1B[0m",`- 已开启懒上传,本次共上传 ${a} 个文件`)),console.log("- 文件上传成功"),console.log("- 开始推送镜像");let j=`cd ${o} && sh ${this.config.upload.name}`;if(await this.isNewShell(e,o)){const l=this.getArgv("--imageStore")||this.config.imageStore;j+=` ${l}`}else console.log(`
|
|
8
|
-
- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效`),console.log("- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本");const N=this.getArgv("--tmpName")||this.config.tmpName;j+=` ${N}`;const I=Array.from(new Array(n+1)).map(()=>this.onShell(j));await this.pushMirrorImage(I,I.length),this.breakConnect()})}async pushMirrorImage(e,o){if(e.length===0)return console.log("- 镜像推送失败,请添加 --allLog 查看全部日志"),null;const n=await e[0];return n.includes("- 镜像推送失败")?(console.log(n),e.length>1&&console.log(`- 正在尝试第 ${o-e.length+1} 次推送...`),this.pushMirrorImage(e.slice(1),o)):(console.log(n),console.log("- 镜像推送结束"),this.getArgv("--restart")!==void 0&&await F(n,this.config.rancherConfig),n)}async isNewShell(e,o){return new Promise((n,i)=>{e.readFile(`${o}/${this.config.upload.name}`,(s,t)=>{if(s)return i(!1);const r=t.toString();r.includes("- 镜像推送失败")||console.log(`
|
|
9
|
-
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`),n(r.includes("$2"))})})}progressBar(e,o,n=25){let i=(e/o).toFixed(4),s=Math.floor(i*n),t="";for(let a=0;a<s;a++)t+="█";let r="";for(let a=0;a<n-s;a++)r+="░";let c=`- 文件上传进度: ${t}${r} (${e}/${o}) ${(100*i).toFixed(2)}%`;h(c),e==o&&console.log("")}getFilesInDirectory(e){const o=[],n=[];return new Promise((i,s)=>{const t=(r,c,a)=>{const g=d.readdirSync(r);let $=!1;g.forEach(w=>{const y=f.join(r,w),P=d.statSync(y).isDirectory();P?$=!0:o.push({localPath:y,remotePath:`${c}${w}`,level:a}),P&&t(y,`${c}${w}/`,a+1)}),$||n.push(c)};t(e,"/",0),i({localFileList:o,dirList:n})})}getArgv(e){const o=process.argv;let n;return o.forEach(i=>{i.indexOf(e)>-1&&(n=i.split("=")[1]||"")}),n}};const b=new U.Agent({rejectUnauthorized:!1}),C=L.create({proxy:!1});async function F(S,e){var o,n,i;try{const t={...{baseUrl:"https://paas-test.ymygz.com:10443/v3",clusterId:"local:p-r7j28"},...e};if(!t.baseUrl)return console.error("- Rancher API 基础地址未配置,请检查 rancherConfig.baseUrl 是否正确");if(!t.token)return console.error("- Rancher API 访问令牌未配置,请检查 rancherConfig.token 是否正确");if(!t.clusterId)return console.error("- Rancher 集群 ID 未配置,请检查 rancherConfig.clusterId 是否正确");if(!t.namespace)return console.error("- 服务所在的命名空间未配置,请检查 rancherConfig.namespace 是否正确");if(!t.workload)return console.error("- 工作负载名称未配置,请检查 rancherConfig.workload 是否正确");if(!S)return console.error("- 内部错误");const r=S.match(/- 镜像地址:\s*(.+)/);let c="";if(r&&r[1])c=r[1].trim();else return console.error("- 未提取到镜像地址");console.log("- 开始调用 Rancher API 更新服务镜像地址");const a=`${t.baseUrl}/projects/${t.clusterId}/workloads/deployment:${t.namespace}:${t.workload}`;console.log("- 请求的工作负载 URL:",a);const g=await C.get(a,{headers:{Authorization:`Bearer ${t.token}`},httpsAgent:b});if(!g.data||!g.data.containers||g.data.containers.length===0)return console.error("- 未找到工作负载的容器信息,请检查 namespace 和 workload 是否正确");const $=g.data;$.containers[0].image=c,await C.put(a,$,{headers:{Authorization:`Bearer ${t.token}`},httpsAgent:b}),console.log("- 镜像地址更新成功,正在重启服务..."),await C.post(`${a}?action=redeploy`,{},{headers:{Authorization:`Bearer ${t.token}`},httpsAgent:b}),console.log("- 服务重启成功")}catch(s){console.error("- 调用 Rancher API 失败,请检查以下信息:"),console.error(" - 错误代码:",s.code),console.error(" - 错误信息:",s.message),console.error(" - 请求 URL:",(o=s.config)==null?void 0:o.url),console.error(" - 响应状态:",(n=s.response)==null?void 0:n.status),console.error(" - 响应数据:",(i=s.response)==null?void 0:i.data)}}});
|