@lazycatcloud/lzc-cli 1.1.5 → 1.1.8
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/cmds/app.js +1 -5
- package/cmds/dev.js +11 -5
- package/lib/api.js +24 -7
- package/lib/app/index.js +93 -0
- package/lib/app/lpk_build.js +241 -0
- package/lib/app/lpk_create.js +201 -0
- package/lib/app/lpk_devshell.js +621 -0
- package/lib/app/lpk_installer.js +67 -0
- package/lib/appstore/login.js +134 -0
- package/lib/archiver.js +0 -42
- package/lib/autologin.js +82 -0
- package/lib/box/check_qemu.js +39 -16
- package/lib/box/hportal.js +7 -1
- package/lib/box/index.js +8 -7
- package/lib/box/qemu_vm_mgr.js +12 -11
- package/lib/dev.js +0 -4
- package/lib/env.js +16 -49
- package/lib/generator.js +2 -2
- package/lib/sdk.js +5 -6
- package/lib/utils.js +66 -144
- package/package.json +8 -3
- package/scripts/cli.js +91 -12
- package/template/_lazycat/debug/shell/Dockerfile +5 -3
- package/template/_lazycat/debug/shell/docker-compose.override.yml.in +2 -12
- package/template/_lazycat/debug/shell/entrypoint.sh +3 -1
- package/template/_lpk/Dockerfile.in +8 -0
- package/template/_lpk/devshell/Dockerfile +18 -0
- package/template/_lpk/devshell/build.sh +5 -0
- package/template/_lpk/devshell/entrypoint.sh +8 -0
- package/template/_lpk/devshell/sshd_config +117 -0
- package/template/_lpk/manifest.yml.in +17 -0
- package/template/_lpk/sync/Dockerfile +16 -0
- package/template/_lpk/sync/build.sh +5 -0
- package/template/_lpk/sync/entrypoint.sh +8 -0
- package/template/_lpk/sync/sshd_config +117 -0
- package/template/_lpk/sync.manifest.yml.in +3 -0
- package/template/golang/build.sh +6 -0
- package/template/golang/lzc-build.yml +52 -0
- package/template/ionic_vue3/lzc-build.yml +53 -0
- package/template/ionic_vue3/vite.config.ts +1 -1
- package/template/release/golang/build.sh +1 -1
- package/template/release/ionic_vue3/Dockerfile +1 -1
- package/template/release/ionic_vue3/build.sh +1 -3
- package/template/release/ionic_vue3/docker-compose.yml.in +0 -5
- package/template/release/vue/Dockerfile +1 -1
- package/template/release/vue/build.sh +2 -3
- package/template/release/vue/docker-compose.yml.in +0 -5
- package/template/vue/lzc-build.yml +53 -0
- package/template/vue/src/main.js +3 -14
- package/template/vue/vue.config.js +2 -1
- package/template/_lazycat/debug/shell/nginx.conf.template +0 -64
- package/template/vue/src/lzc.js +0 -110
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
// lzc-cli app devshell
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import logger from "loglevel";
|
|
5
|
+
import { spawnSync, execSync } from "child_process";
|
|
6
|
+
import { LpkBuild } from "./lpk_build.js";
|
|
7
|
+
import { LpkInstaller } from "./lpk_installer.js";
|
|
8
|
+
import {
|
|
9
|
+
mergeYamlInMemory,
|
|
10
|
+
contextDirname,
|
|
11
|
+
ensureDir,
|
|
12
|
+
isFileExist,
|
|
13
|
+
GitIgnore,
|
|
14
|
+
sleep,
|
|
15
|
+
envTemplateFile,
|
|
16
|
+
md5String,
|
|
17
|
+
createTemplateFileCommon,
|
|
18
|
+
} from "../utils.js";
|
|
19
|
+
import yaml from "js-yaml";
|
|
20
|
+
import Key from "../key.js";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import { sdkEnv } from "../env.js";
|
|
23
|
+
import commandExists from "command-exists";
|
|
24
|
+
import chokidar from "chokidar";
|
|
25
|
+
import execa from "execa";
|
|
26
|
+
|
|
27
|
+
function sdkSSHAddress() {
|
|
28
|
+
const sdkUrl = sdkEnv.sdkUrl;
|
|
29
|
+
let url = new URL(sdkUrl);
|
|
30
|
+
return `box@${url.hostname}:2222`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class AppDevShell {
|
|
34
|
+
constructor(cwd, builder) {
|
|
35
|
+
this.cwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
36
|
+
this.lpkBuild = builder;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async init() {
|
|
40
|
+
if (!this.lpkBuild) {
|
|
41
|
+
this.lpkBuild = new LpkBuild(this.cwd);
|
|
42
|
+
await this.lpkBuild.init();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async shellWithBuild() {
|
|
47
|
+
// 确保 sdk key ,并上传到 sdk
|
|
48
|
+
const k = new Key();
|
|
49
|
+
await k.ensure(sdkEnv.sdkUrl);
|
|
50
|
+
const pairs = await k.getKeyPair();
|
|
51
|
+
|
|
52
|
+
// devshell 不需要执行 buildscript 和 contentdir 也不需要
|
|
53
|
+
this.lpkBuild.onBeforeBuildPackage(async (options) => {
|
|
54
|
+
logger.debug("devshell delete 'buildscript' field");
|
|
55
|
+
logger.debug("devshell delete 'contentdir' field");
|
|
56
|
+
delete options["buildscript"];
|
|
57
|
+
delete options["contentdir"];
|
|
58
|
+
return options;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 用 sdk ssh key 并复制到 contentdir 目录
|
|
62
|
+
// docker 中 sshd 的配置中已改成 /lzcapp/pkg/content/devshell/authorized_keys
|
|
63
|
+
this.lpkBuild.onBeforeTarContent(async (contentdir) => {
|
|
64
|
+
let publicKey = pairs["public"];
|
|
65
|
+
let dest = path.join(contentdir, "devshell", "authorized_keys");
|
|
66
|
+
ensureDir(dest);
|
|
67
|
+
fs.copyFileSync(publicKey, dest);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 在生成 manifest.yml 之前合并 sync 模板字段
|
|
71
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
|
|
72
|
+
const services = manifest["services"];
|
|
73
|
+
if (services && Object.keys(services).some((key) => key === "sync")) {
|
|
74
|
+
throw "自定义的 services 中与 devshell 中的 sync 名称冲突";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const syncManifestPath = path.join(
|
|
78
|
+
contextDirname(import.meta.url),
|
|
79
|
+
"../../template/_lpk/sync.manifest.yml.in"
|
|
80
|
+
);
|
|
81
|
+
const syncManifest = yaml.load(
|
|
82
|
+
await envTemplateFile(syncManifestPath, manifest)
|
|
83
|
+
);
|
|
84
|
+
return mergeYamlInMemory([manifest, syncManifest]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 在生成 manifest.yml 之前合并 devshell manifest 模板字段
|
|
88
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
|
|
89
|
+
logger.debug("merge lzc-build.yml devshell routes field");
|
|
90
|
+
const devshell = options["devshell"];
|
|
91
|
+
if (!devshell) {
|
|
92
|
+
throw "devshell 模式下,devshell 字段必须要指定";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const routes = devshell["routes"];
|
|
96
|
+
if (!routes || routes.length == 0) {
|
|
97
|
+
throw "devshell 模式下,必须要指定 routes 内容";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger.debug("options devshell delete 'routes' field");
|
|
101
|
+
delete options["devshell"]["routes"];
|
|
102
|
+
// 如果 devshell 中的 router 和 manifest 中的 prefix 出现冲突
|
|
103
|
+
// 优先使用 devshell 中的。
|
|
104
|
+
routes.forEach((r) => {
|
|
105
|
+
if (!r) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let prefix = r.split("=")[0];
|
|
110
|
+
let index = manifest["application"]["routes"].findIndex((mr) => {
|
|
111
|
+
if (!mr) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return mr.split("=")[0] == prefix;
|
|
115
|
+
});
|
|
116
|
+
if (index > -1) {
|
|
117
|
+
manifest["application"]["routes"].splice(index, 1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const application = { routes };
|
|
121
|
+
return mergeYamlInMemory([manifest, { application }]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 在生成 manifest.yml 之前合并 lzc-build.yml devshell 字段的值
|
|
125
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
|
|
126
|
+
logger.debug("merge lzc-build.yml devshell services\n", options);
|
|
127
|
+
const devshell = {
|
|
128
|
+
application: {
|
|
129
|
+
devshell: options["devshell"],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return mergeYamlInMemory([manifest, devshell]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 如果 services/devshell 中有 dependencies 字段,优先使用
|
|
136
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
|
|
137
|
+
const config = manifest["application"]["devshell"];
|
|
138
|
+
if (!config || !config["dependencies"]) {
|
|
139
|
+
return manifest;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const deps = config["dependencies"];
|
|
143
|
+
if (deps.length == 0) {
|
|
144
|
+
logger.warn("dependencies 内容为空,跳过 dependencies");
|
|
145
|
+
delete manifest["application"]["devshell"]["dependencies"];
|
|
146
|
+
return manifest;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const depsStr = deps.sort().join(" ");
|
|
150
|
+
logger.debug("开始创建 Dockerfile 文件");
|
|
151
|
+
const tempDir = fs.mkdtempSync(".lzc-cli-build-dependencies");
|
|
152
|
+
const dockerfilePath = path.join(
|
|
153
|
+
contextDirname(import.meta.url),
|
|
154
|
+
"../../template/_lpk/Dockerfile.in"
|
|
155
|
+
);
|
|
156
|
+
await createTemplateFileCommon(
|
|
157
|
+
dockerfilePath,
|
|
158
|
+
path.join(tempDir, "Dockerfile"),
|
|
159
|
+
{ dependencies: depsStr }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const tag = `${await md5String(depsStr)}:latest`;
|
|
163
|
+
logger.debug(`开始在盒子中构建 ${tag} 镜像 from ${tempDir}`);
|
|
164
|
+
const sdk = new sdkDocker();
|
|
165
|
+
await sdk.buildImage(tag, tempDir, tempDir);
|
|
166
|
+
|
|
167
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
delete manifest["application"]["devshell"];
|
|
170
|
+
manifest["application"]["image"] = tag;
|
|
171
|
+
return manifest;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 如果 services 中有 devshell 的字段,需要检测是否需要提前构建
|
|
175
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
|
|
176
|
+
const application = manifest["application"];
|
|
177
|
+
if (!application || !application["devshell"]) {
|
|
178
|
+
return manifest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const config = manifest["application"]["devshell"];
|
|
182
|
+
if (!config || !config["build"]) {
|
|
183
|
+
return manifest;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tag = `${manifest["package"]}-devshell:${manifest["version"]}`;
|
|
187
|
+
|
|
188
|
+
logger.debug(`开始在盒子中构建 ${tag} 镜像`);
|
|
189
|
+
|
|
190
|
+
const sdk = new sdkDocker();
|
|
191
|
+
await sdk.buildImage(tag, config["build"], process.cwd());
|
|
192
|
+
|
|
193
|
+
delete manifest["application"]["devshell"];
|
|
194
|
+
manifest["application"]["image"] = tag;
|
|
195
|
+
return manifest;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 如果没有找到 devshell 中没有指定 image 不存在,将默认使用的 lzc-cli/devshell 容器
|
|
199
|
+
this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
|
|
200
|
+
const config = manifest["application"];
|
|
201
|
+
if (config["image"]) {
|
|
202
|
+
return manifest;
|
|
203
|
+
}
|
|
204
|
+
manifest["application"][
|
|
205
|
+
"image"
|
|
206
|
+
] = `registry.lazycat.cloud/lzc-cli/devshell:latest`;
|
|
207
|
+
return manifest;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 在构建生成 lpk 包后,调用 deploy 进行部署
|
|
211
|
+
let installer = new LpkInstaller();
|
|
212
|
+
await installer.init();
|
|
213
|
+
await installer.deploy(this.lpkBuild);
|
|
214
|
+
|
|
215
|
+
await sleep(2000);
|
|
216
|
+
|
|
217
|
+
// 通过 rsync 同步源码到应用容器中
|
|
218
|
+
// TODO: 改成用 sshfs 的方式
|
|
219
|
+
await this.shell(true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async shell(build = false) {
|
|
223
|
+
const k = new Key();
|
|
224
|
+
const pairs = await k.getKeyPair();
|
|
225
|
+
const manifest = await this.lpkBuild.getManifest();
|
|
226
|
+
const devshell = new DevShell(pairs["private"], manifest["package"]);
|
|
227
|
+
if (build || !devshell.isRsyncWatch()) {
|
|
228
|
+
await devshell.shell();
|
|
229
|
+
} else {
|
|
230
|
+
await devshell.directShell();
|
|
231
|
+
}
|
|
232
|
+
logger.debug("exit shell");
|
|
233
|
+
// TODO: shell 在正常情况下,按 Ctrl-D 就会退出,回到原来的本地的 shell ,但
|
|
234
|
+
// 现在会一直卡在退出状态后,必须要另外手动的指定 pkill node
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class DevShell {
|
|
240
|
+
constructor(keyFile, appId) {
|
|
241
|
+
logger.debug("keyFile", keyFile);
|
|
242
|
+
logger.debug("appid", appId);
|
|
243
|
+
|
|
244
|
+
this.tempDir = path.join(os.tmpdir(), "lzc-cli-devshell", appId);
|
|
245
|
+
if (!fs.existsSync(this.tmpdir)) {
|
|
246
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
247
|
+
}
|
|
248
|
+
this.keyFile = keyFile;
|
|
249
|
+
this.appId = appId;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async syncProject(keyFile, appAddr, appId) {
|
|
253
|
+
let jump = sdkSSHAddress();
|
|
254
|
+
// prettier-ignore
|
|
255
|
+
let rsh = [
|
|
256
|
+
"ssh",
|
|
257
|
+
"-J", jump,
|
|
258
|
+
"-o", `"StrictHostKeyChecking=no"`,
|
|
259
|
+
"-o", `"UserKnownHostsFile=/dev/null"`,
|
|
260
|
+
"-o", `"ConnectionAttempts=3"`,
|
|
261
|
+
"-o", `"ConnectTimeout=30"`,
|
|
262
|
+
"-o", `"LogLevel=ERROR"`,
|
|
263
|
+
"-i", keyFile,
|
|
264
|
+
].join(" ");
|
|
265
|
+
// 检查rsync工具是否存在:提示用户
|
|
266
|
+
const rsyncExisted = commandExists.sync("rsync");
|
|
267
|
+
if (!rsyncExisted) {
|
|
268
|
+
console.log(chalk.red("请检查 rsync 是否安装,路径是否正确!"));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// FIXME: 下方执行命令不确定是否有兼容性问题
|
|
273
|
+
execSync(
|
|
274
|
+
`rsync --rsh='${rsh}' -czrPR . root@${appAddr}:/lzcapp/cache/${appId} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete`,
|
|
275
|
+
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// fallback fs.watch on not darwin and windows platform
|
|
280
|
+
// FILEPATH directory or file path
|
|
281
|
+
// CALLBACK => function(eventType, filename)
|
|
282
|
+
// fs.watch 虽然不支持递归,但可以直接监听整个文件夹的变动
|
|
283
|
+
async fallbackWatch(filepath, gitignore, callback) {
|
|
284
|
+
if (gitignore.contain(filepath)) {
|
|
285
|
+
return Promise.resolve();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (filepath.endsWith(".git") || filepath.endsWith(".lazycat")) {
|
|
289
|
+
return Promise.resolve();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fs.watch(filepath, callback(filepath));
|
|
293
|
+
|
|
294
|
+
// 如果为一个文件夹,则扫描当中是否含有子文件夹
|
|
295
|
+
if (isDirSync(filepath)) {
|
|
296
|
+
return gitignore.readdir(filepath, (err, files) => {
|
|
297
|
+
if (err) {
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (files.length <= 0) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
files.forEach((f) => {
|
|
306
|
+
if (f.isDirectory()) {
|
|
307
|
+
this.fallbackWatch(
|
|
308
|
+
path.join(filepath, f.name),
|
|
309
|
+
gitignore,
|
|
310
|
+
callback
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 监听非.gitignore文件
|
|
319
|
+
// TODO: 目前仅仅监听process.cwd()以下的文件
|
|
320
|
+
async watchFile(keyFile, appAddr, appId) {
|
|
321
|
+
const ignore = new GitIgnore(process.cwd());
|
|
322
|
+
await ignore.collect();
|
|
323
|
+
chokidar
|
|
324
|
+
.watch(".", {
|
|
325
|
+
ignored: (path) => {
|
|
326
|
+
if ([".git", ".lazycat"].some((p) => path.startsWith(p))) return true;
|
|
327
|
+
|
|
328
|
+
return ignore.contain(path);
|
|
329
|
+
},
|
|
330
|
+
ignoreInitial: true,
|
|
331
|
+
})
|
|
332
|
+
.on("all", (event, path) => {
|
|
333
|
+
// console.log(event, path);
|
|
334
|
+
this.syncProject(keyFile, appAddr, appId);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 保存一个dev shell状态的文件,如果重复执行dev shell
|
|
339
|
+
// 会先判断这个临时文件是否存在,如果不存在重新部署,更新流程
|
|
340
|
+
// 否则直接ssh连接
|
|
341
|
+
async storeShellStatus(isWatch) {
|
|
342
|
+
fs.writeFileSync(
|
|
343
|
+
path.join(this.tempDir, ".shellStatus"),
|
|
344
|
+
JSON.stringify({
|
|
345
|
+
isWatch,
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
isRsyncWatch() {
|
|
351
|
+
try {
|
|
352
|
+
let obj = this.readShellStatus();
|
|
353
|
+
return obj.isWatch;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async readShellStatus() {
|
|
360
|
+
return JSON.parse(
|
|
361
|
+
fs.readFileSync(path.join(this.tempDir, ".shellStatus"), "utf-8")
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async directShell() {
|
|
366
|
+
const keyFile = this.keyFile;
|
|
367
|
+
const appAddr = `sync.${this.appId}.lzcapp`;
|
|
368
|
+
|
|
369
|
+
let isWatch;
|
|
370
|
+
try {
|
|
371
|
+
({ isWatch } = await this.readShellStatus());
|
|
372
|
+
|
|
373
|
+
if (!isWatch) {
|
|
374
|
+
await this.syncProject(keyFile, appAddr, this.appId);
|
|
375
|
+
// 注册watch函数
|
|
376
|
+
await this.watchFile(keyFile, appAddr, this.appId);
|
|
377
|
+
|
|
378
|
+
this.storeShellStatus(true);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await this.connectShell(this.appId);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
return Promise.reject(e);
|
|
384
|
+
} finally {
|
|
385
|
+
if (!isWatch) {
|
|
386
|
+
this.storeShellStatus(false);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async shell() {
|
|
392
|
+
let keyFile = this.keyFile;
|
|
393
|
+
const appAddr = `sync.${this.appId}.lzcapp`;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// 当进入shell的时候,都同步一次
|
|
397
|
+
await this.syncProject(keyFile, appAddr, this.appId);
|
|
398
|
+
// 注册watch函数
|
|
399
|
+
await this.watchFile(keyFile, appAddr, this.appId);
|
|
400
|
+
|
|
401
|
+
this.storeShellStatus(true);
|
|
402
|
+
|
|
403
|
+
await this.connectShell(this.appId);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
console.log(e);
|
|
406
|
+
// this.reset();
|
|
407
|
+
return Promise.reject(e);
|
|
408
|
+
} finally {
|
|
409
|
+
// 当shell退出后,更新isWatch状态
|
|
410
|
+
this.storeShellStatus(false);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async connectShell(appId) {
|
|
415
|
+
const sdk = new sdkDocker();
|
|
416
|
+
const replacedAppId = appId.replaceAll(".", "_").replaceAll("-", "__");
|
|
417
|
+
await sdk.interactiveShell(`lzc--${replacedAppId}-app-1`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// build strategies based on lzc-build.yml devshell field
|
|
422
|
+
import { DockerClient, dockerPullLzcAppsImage } from "../sdk.js";
|
|
423
|
+
import { DockerfileParser } from "dockerfile-ast";
|
|
424
|
+
import glob from "fast-glob";
|
|
425
|
+
import ignore from "@balena/dockerignore";
|
|
426
|
+
import { createLogUpdate } from "log-update";
|
|
427
|
+
|
|
428
|
+
class sdkDocker {
|
|
429
|
+
constructor() {}
|
|
430
|
+
|
|
431
|
+
// builder 需要在app 的场景下使用, 不过有两个路径要注意
|
|
432
|
+
// 一个是 docker build 时的context. 这个一般是运行cli时候的路径
|
|
433
|
+
async buildImage(tag, buildDir, contextDir) {
|
|
434
|
+
const dockerfilePath = path.join(buildDir, "Dockerfile");
|
|
435
|
+
try {
|
|
436
|
+
await this.dockerRemoteBuildV2(contextDir, dockerfilePath, tag);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
throw e;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async collectContextFromDockerFile(contextDir, dockerfilePath) {
|
|
443
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
444
|
+
throw "未发现 Dockerfile";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let src = [path.relative(contextDir, dockerfilePath)];
|
|
448
|
+
|
|
449
|
+
// 通过 COPY 和 ADD 获取所有context中的文件
|
|
450
|
+
const ast = DockerfileParser.parse(fs.readFileSync(dockerfilePath, "utf8"));
|
|
451
|
+
for (let a of ast.getInstructions()) {
|
|
452
|
+
if (["COPY", "ADD"].includes(a.getInstruction())) {
|
|
453
|
+
const from = a.getArguments()[0].getValue().replace(/^\//, "");
|
|
454
|
+
|
|
455
|
+
const fromFullPath = path.join(contextDir, from);
|
|
456
|
+
|
|
457
|
+
if (fs.existsSync(fromFullPath)) {
|
|
458
|
+
const stat = fs.statSync(path.join(contextDir, from));
|
|
459
|
+
if (stat.isDirectory()) {
|
|
460
|
+
let files = await glob(path.join(from, "**"), {
|
|
461
|
+
cwd: contextDir,
|
|
462
|
+
});
|
|
463
|
+
src = src.concat(files);
|
|
464
|
+
} else if (stat.isFile()) {
|
|
465
|
+
src.push(from);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
// try use glob
|
|
469
|
+
let files = await glob(from, {
|
|
470
|
+
cwd: contextDir,
|
|
471
|
+
});
|
|
472
|
+
src = src.concat(files);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// filter by dockerignore
|
|
477
|
+
const dockerIgnoreFile = path.join(contextDir, ".dockerignore");
|
|
478
|
+
//
|
|
479
|
+
if (fs.existsSync(dockerIgnoreFile)) {
|
|
480
|
+
let ig = ignore();
|
|
481
|
+
let data = fs.readFileSync(dockerIgnoreFile, "utf8");
|
|
482
|
+
ig.add(data.split("\n"));
|
|
483
|
+
src = ig.filter(src);
|
|
484
|
+
}
|
|
485
|
+
return src;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* v2 版本会收集所有需要的context 文件,将其传入buildImage 方法中
|
|
490
|
+
* @param contextDir
|
|
491
|
+
* @param dockerfile
|
|
492
|
+
* @param tag
|
|
493
|
+
*/
|
|
494
|
+
async dockerRemoteBuildV2(contextDir, dockerfile, tag) {
|
|
495
|
+
const src = await this.collectContextFromDockerFile(contextDir, dockerfile);
|
|
496
|
+
const host = new URL(sdkEnv.sdkUrl).host;
|
|
497
|
+
console.log(host);
|
|
498
|
+
const docker = await new DockerClient(host).init();
|
|
499
|
+
try {
|
|
500
|
+
await dockerPullLzcAppsImage(host);
|
|
501
|
+
|
|
502
|
+
return new Promise(async (resolve, reject) => {
|
|
503
|
+
let refresher = new StatusRefresher();
|
|
504
|
+
|
|
505
|
+
logger.info("开始在设备中构建镜像");
|
|
506
|
+
const stream = await docker.buildImage(
|
|
507
|
+
{
|
|
508
|
+
context: contextDir,
|
|
509
|
+
src: src,
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
dockerfile: src[0],
|
|
513
|
+
t: tag,
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
docker.modem.followProgress(
|
|
517
|
+
stream,
|
|
518
|
+
(err, res) => {
|
|
519
|
+
err ? reject(err) : resolve(res);
|
|
520
|
+
},
|
|
521
|
+
(res) => {
|
|
522
|
+
if (res.status) {
|
|
523
|
+
if (res.id) {
|
|
524
|
+
if (["Downloading", "Extracting"].includes(res.status)) {
|
|
525
|
+
refresher.updateStatus({
|
|
526
|
+
id: res.id,
|
|
527
|
+
content: `${res.status} ${res.progress}`,
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
refresher.updateStatus({
|
|
531
|
+
id: res.id,
|
|
532
|
+
content: res.status,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
process.stdout.write(res.status);
|
|
537
|
+
process.stdout.write("\n");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (res.stream) {
|
|
542
|
+
if (res.stream.startsWith("Step")) {
|
|
543
|
+
refresher.reset();
|
|
544
|
+
}
|
|
545
|
+
process.stdout.write(res.stream);
|
|
546
|
+
}
|
|
547
|
+
if (res.error) {
|
|
548
|
+
reject(res.error);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async interactiveShell(containerName, shell = "sh") {
|
|
559
|
+
const host = new URL(sdkEnv.sdkUrl).hostname;
|
|
560
|
+
const docker = await new DockerClient(host).init();
|
|
561
|
+
const container = docker.getContainer(containerName);
|
|
562
|
+
const exec = await container.exec({
|
|
563
|
+
Cmd: [shell],
|
|
564
|
+
AttachStderr: true,
|
|
565
|
+
AttachStdout: true,
|
|
566
|
+
AttachStdin: true,
|
|
567
|
+
Tty: true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const stream = await exec.start({ stdin: true });
|
|
571
|
+
|
|
572
|
+
process.stdin.setRawMode(true);
|
|
573
|
+
stream.pipe(process.stdout);
|
|
574
|
+
process.stdin.pipe(stream);
|
|
575
|
+
|
|
576
|
+
// prepare command
|
|
577
|
+
stream.write("cd /lzcapp/cache\n");
|
|
578
|
+
stream.write("ls\n");
|
|
579
|
+
|
|
580
|
+
await new Promise((resolve) => {
|
|
581
|
+
stream.on("end", () => {
|
|
582
|
+
logger.info("exit");
|
|
583
|
+
resolve();
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
process.stdin.removeAllListeners();
|
|
588
|
+
process.stdin.setRawMode(false);
|
|
589
|
+
process.stdin.resume();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
class StatusRefresher {
|
|
594
|
+
constructor() {
|
|
595
|
+
this.reset();
|
|
596
|
+
}
|
|
597
|
+
refresh() {
|
|
598
|
+
this.logUpdate(this.pulls.map((p) => `${p.id}: ${p.content}`).join("\n"));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
reset() {
|
|
602
|
+
this.pulls = [];
|
|
603
|
+
this.logUpdate = createLogUpdate(process.stdout);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
updateStatus({ id, content }) {
|
|
607
|
+
let index = this.pulls.findIndex((p) => p.id == id);
|
|
608
|
+
if (index > -1) {
|
|
609
|
+
this.pulls[index] = {
|
|
610
|
+
id: id,
|
|
611
|
+
content: content,
|
|
612
|
+
};
|
|
613
|
+
} else {
|
|
614
|
+
this.pulls.push({
|
|
615
|
+
id: id,
|
|
616
|
+
content: content,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
this.refresh();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import BoxAPI from "../api.js";
|
|
2
|
+
import { sdkEnv } from "../env.js";
|
|
3
|
+
import logger from "loglevel";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { dockerPullLzcAppsImage } from "../sdk.js";
|
|
7
|
+
import Key from "../key.js";
|
|
8
|
+
|
|
9
|
+
export class LpkInstaller {
|
|
10
|
+
constructor() {}
|
|
11
|
+
|
|
12
|
+
async init() {
|
|
13
|
+
// 1. 确保 sdk 已经安装
|
|
14
|
+
await sdkEnv.ensure();
|
|
15
|
+
|
|
16
|
+
// 2. pull 镜像需要ssh key
|
|
17
|
+
const k = new Key();
|
|
18
|
+
await k.ensure(sdkEnv.sdkUrl);
|
|
19
|
+
|
|
20
|
+
const host = new URL(sdkEnv.sdkUrl).hostname;
|
|
21
|
+
await dockerPullLzcAppsImage(host);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async deploy(builder) {
|
|
25
|
+
if (!builder) {
|
|
26
|
+
throw "deploy 必须传递一个 builder";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let manifest = await builder.getManifest();
|
|
30
|
+
let api = new BoxAPI(manifest["package"], sdkEnv.sdkUrl);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
let pkgPath = await builder.exec("");
|
|
34
|
+
logger.info("开始部署应用");
|
|
35
|
+
await api.install(pkgPath);
|
|
36
|
+
await api.checkStatus();
|
|
37
|
+
logger.info(
|
|
38
|
+
`请在浏览器中访问 ${sdkEnv.sdkUrl.replace(
|
|
39
|
+
/sdk/,
|
|
40
|
+
manifest["application"]["subdomain"]
|
|
41
|
+
)}`
|
|
42
|
+
);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async install(pkgPath) {
|
|
49
|
+
if (!pkgPath) {
|
|
50
|
+
throw "install 必须指定一个 pkg 路径";
|
|
51
|
+
}
|
|
52
|
+
let pkgNameParts = path.basename(pkgPath).split("-v");
|
|
53
|
+
if (pkgNameParts.length < 2) {
|
|
54
|
+
throw "lpk 文件名不合法:应该使用 $appid-$version.lpk 的格式(如 xxx-v0.0.1.lpk)"
|
|
55
|
+
}
|
|
56
|
+
let pkgName = pkgNameParts[0]
|
|
57
|
+
let api = new BoxAPI(pkgName, sdkEnv.sdkUrl);
|
|
58
|
+
try {
|
|
59
|
+
logger.info("开始安装应用");
|
|
60
|
+
await api.install(pkgPath);
|
|
61
|
+
await api.checkStatus();
|
|
62
|
+
logger.info(`安装成功!`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|