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 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": "0.0.11",
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
- "dist",
11
+ "lib",
11
12
  "package.json",
12
13
  "README.md"
13
14
  ],
14
- "main": "./dist/yj-deploy.umd.js",
15
- "module": "./dist/yj-deploy.mjs",
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": {
@@ -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
- }
@@ -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)}}});