@lazycatcloud/lzc-cli 1.2.2 → 1.2.4

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.
@@ -1,4 +1,3 @@
1
- import tar from "tar";
2
1
  import path from "node:path";
3
2
  import fs from "node:fs";
4
3
  import logger from "loglevel";
@@ -9,6 +8,7 @@ import {
9
8
  dumpToYaml,
10
9
  envTemplateFile,
11
10
  isValidPackageName,
11
+ tarContentDir,
12
12
  } from "../utils.js";
13
13
  import { spawnSync } from "child_process";
14
14
  import { LpkManifest } from "./lpk_create.js";
@@ -17,29 +17,6 @@ import yaml from "js-yaml";
17
17
 
18
18
  const isMacos = process.platform == "darwin";
19
19
 
20
- async function tarContentDir(from, to, cwd = "./") {
21
- const dest = fs.createWriteStream(to);
22
- tar
23
- .c(
24
- {
25
- cwd: cwd,
26
- filter: (filePath, stat) => {
27
- logger.debug(`tar gz ${filePath}`);
28
- return true;
29
- },
30
- sync: true,
31
- portable: {
32
- uid: 0,
33
- gid: 0,
34
- },
35
- },
36
- [from]
37
- )
38
- .pipe(dest);
39
- logger.debug(`pack: ${dest.path}`);
40
- return dest.path;
41
- }
42
-
43
20
  async function archiveFolderTo(appDir, out, format = "zip") {
44
21
  return new Promise(async (resolve, reject) => {
45
22
  if (!fs.existsSync(appDir)) {
@@ -251,7 +228,11 @@ export class LpkBuild {
251
228
  return _prev;
252
229
  }, contentdir);
253
230
  }
254
- tarContentDir("./", path.join(tempDir, "content.tar"), contentdir);
231
+ await tarContentDir(
232
+ ["./"],
233
+ path.join(tempDir, "content.tar"),
234
+ contentdir
235
+ );
255
236
 
256
237
  // 如果是临时的 contentdir, 目录在打包完成后删除
257
238
  if (!this.options["contentdir"]) {
@@ -108,4 +108,21 @@ export class DebugBridge {
108
108
  });
109
109
  });
110
110
  }
111
+
112
+ async buildImage(label, contextTar) {
113
+ const tag = `dev.${this.boxname}.heiyu.space:5000/debug.bridge/${label}`;
114
+ const stream = fs.createReadStream(contextTar);
115
+ const ssh = spawn(this.sshCmd, [`build --tag ${tag}`], {
116
+ shell: true,
117
+ stdio: ["pipe", "inherit", "inherit"],
118
+ });
119
+ stream.pipe(ssh.stdin);
120
+ return new Promise((resolve, reject) => {
121
+ ssh.on("close", (code) => {
122
+ code == 0 ? resolve(tag) : reject("在盒子中构建 image 失败");
123
+ });
124
+ }).finally(() => {
125
+ fs.rmSync(contextTar);
126
+ });
127
+ }
111
128
  }
@@ -16,6 +16,7 @@ import {
16
16
  loadFromYaml,
17
17
  FileLocker,
18
18
  isUserApp,
19
+ createTemplateFileCommon,
19
20
  } from "../utils.js";
20
21
  import os from "node:os";
21
22
  import commandExists from "command-exists";
@@ -23,6 +24,7 @@ import chokidar from "chokidar";
23
24
  import _ from "lodash";
24
25
  import { DebugBridge } from "./lpk_debug_bridge.js";
25
26
  import shellApi from "../shellapi.js";
27
+ import { collectContextFromDockerFile } from "./lpk_devshell_docker.js";
26
28
 
27
29
  // 判断是否需要重新构建
28
30
  // - 先判断 lzc-build.yml 是否发生改变
@@ -264,7 +266,36 @@ export class AppDevShell {
264
266
  return manifest;
265
267
  }
266
268
 
267
- throw "lzc-cli 不支持在盒子中构建 docker 镜像,请从其他地方构建,然后指定image字段";
269
+ const depsStr = deps.sort().join(" ");
270
+ logger.debug("开始创建 Dockerfile 文件");
271
+
272
+ const tempDir = fs.mkdtempSync(".lzc-cli-build-dependencies");
273
+ try {
274
+ const dockerfilePath = path.join(
275
+ contextDirname(import.meta.url),
276
+ "../../template/_lpk/Dockerfile.in"
277
+ );
278
+ await createTemplateFileCommon(
279
+ dockerfilePath,
280
+ path.join(tempDir, "Dockerfile"),
281
+ { dependencies: depsStr }
282
+ );
283
+
284
+ const label = `${await md5String(depsStr)}:latest`;
285
+ logger.debug(`开始在盒子中构建 ${label} 镜像 from ${tempDir}`);
286
+
287
+ const contextTar = await collectContextFromDockerFile(
288
+ tempDir,
289
+ path.resolve(tempDir, "Dockerfile")
290
+ );
291
+ const bridge = new DebugBridge();
292
+ const tag = await bridge.buildImage(label, contextTar);
293
+ delete manifest["application"]["devshell"];
294
+ manifest["application"]["image"] = tag;
295
+ } finally {
296
+ fs.rmSync(tempDir, { recursive: true });
297
+ }
298
+ return manifest;
268
299
  });
269
300
 
270
301
  // 如果 services 中有 devshell 的字段,需要检测是否需要提前构建
@@ -279,7 +310,19 @@ export class AppDevShell {
279
310
  return manifest;
280
311
  }
281
312
 
282
- throw "lzc-cli 不支持在盒子中构建 docker 镜像,请从其他地方构建,然后指定image字段";
313
+ const label = `${manifest["package"]}-devshell:${manifest["version"]}`;
314
+ logger.debug(`开始在盒子中构建 ${label} 镜像`);
315
+
316
+ const contextTar = await collectContextFromDockerFile(
317
+ process.cwd(),
318
+ path.resolve(process.cwd(), config["build"], "Dockerfile")
319
+ );
320
+
321
+ const bridge = new DebugBridge();
322
+ const tag = await bridge.buildImage(label, contextTar);
323
+ delete manifest["application"]["devshell"];
324
+ manifest["application"]["image"] = tag;
325
+ return manifest;
283
326
  });
284
327
 
285
328
  // 如果 devshell 中指定了 image 字段将使用 image 字段
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { DockerfileParser } from "dockerfile-ast";
4
+ import glob from "fast-glob";
5
+ import ignore from "@balena/dockerignore";
6
+ import { tarContentDir } from "../utils.js";
7
+
8
+ export async function collectContextFromDockerFile(contextDir, dockerfilePath) {
9
+ if (!fs.existsSync(dockerfilePath)) {
10
+ throw "未发现 Dockerfile";
11
+ }
12
+
13
+ let src = [path.relative(contextDir, dockerfilePath)];
14
+
15
+ // 通过 COPY 和 ADD 获取所有context中的文件
16
+ const ast = DockerfileParser.parse(fs.readFileSync(dockerfilePath, "utf8"));
17
+ for (let a of ast.getInstructions()) {
18
+ if (["COPY", "ADD"].includes(a.getInstruction())) {
19
+ const from = a.getArguments()[0].getValue().replace(/^\//, "");
20
+ const fromFullPath = path.resolve(contextDir, from);
21
+
22
+ if (fs.existsSync(fromFullPath)) {
23
+ const stat = fs.statSync(fromFullPath);
24
+ if (stat.isDirectory()) {
25
+ let files = await glob(path.join(from, "**"), {
26
+ cwd: contextDir,
27
+ });
28
+ src = src.concat(files);
29
+ } else if (stat.isFile()) {
30
+ src.push(from);
31
+ }
32
+ } else {
33
+ // try use glob
34
+ let files = await glob(from, {
35
+ cwd: contextDir,
36
+ });
37
+ src = src.concat(files);
38
+ }
39
+ }
40
+ }
41
+ // filter by dockerignore
42
+ const dockerIgnoreFile = path.join(contextDir, ".dockerignore");
43
+ if (fs.existsSync(dockerIgnoreFile)) {
44
+ let ig = ignore();
45
+ let data = fs.readFileSync(dockerIgnoreFile, "utf8");
46
+ ig.add(data.split("\n"));
47
+ src = ig.filter(src);
48
+ }
49
+
50
+ return await tarContentDir(
51
+ src,
52
+ path.join(contextDir, "lzc-build-image-context.tar"),
53
+ process.cwd()
54
+ );
55
+ }
package/lib/box/index.js CHANGED
@@ -1,10 +1,47 @@
1
+ import shellapi from "../shellapi.js";
2
+
1
3
  export function boxCommand(box) {
2
4
  let subCommands = [
3
5
  {
4
- command: "switch <boxName>",
6
+ command: "switch <boxname>",
5
7
  desc: "设置默认的盒子",
6
- handler: async () => {
7
- throw "暂时不支持设置默认盒子,请通过在客户端上点击默认的盒子进行设置。";
8
+ handler: async ({ boxname }) => {
9
+ await shellapi.init();
10
+ await shellapi.setDefaultBox(boxname);
11
+ },
12
+ },
13
+ {
14
+ command: "list",
15
+ desc: "查看盒子列表",
16
+ builder: (args) => {
17
+ args.option("v", {
18
+ alias: "verbose",
19
+ describe: "查看详细输出",
20
+ type: "boolean",
21
+ });
22
+ },
23
+ handler: async ({ verbose }) => {
24
+ await shellapi.init();
25
+ const boxes = await shellapi.boxList();
26
+ if (boxes.length === 0) {
27
+ console.log("没有找到任何盒子,赶紧添加一个吧!");
28
+ return;
29
+ }
30
+ if (verbose) {
31
+ console.log(JSON.stringify(boxes, undefined, "\t"));
32
+ return;
33
+ }
34
+
35
+ const list = boxes.map((b) => {
36
+ return {
37
+ 名称: b.box_name,
38
+ 状态: b.status,
39
+ 登录用户: b.login_user,
40
+ 是否管理员: b.is_admin_login ? "✔" : "✖",
41
+ 是否默认盒子: b.is_default_box ? "✔" : "✖",
42
+ };
43
+ });
44
+ console.table(list);
8
45
  },
9
46
  },
10
47
  ];
package/lib/shellapi.js CHANGED
@@ -71,25 +71,43 @@ class ShellApi {
71
71
  return grpc.loadPackageDefinition(coreDefinition).space.heiyu.hportal.shell;
72
72
  }
73
73
 
74
- async initBoxInfo() {
74
+ async boxList() {
75
75
  return new Promise((resolve, reject) => {
76
76
  this.client.queryBoxList({}, this.metadata, function (err, response) {
77
77
  if (err) {
78
78
  reject(err);
79
79
  return;
80
80
  }
81
- for (let box of response.boxes) {
82
- if (box.is_default_box) {
83
- resolve({ uid: box.login_user, boxname: box.box_name });
84
- return;
85
- }
86
- }
87
- reject(
88
- "没有默认盒子信息, 请先使用 lzc-cli box switch 设置默认的盒子信息"
89
- );
81
+ resolve(response.boxes);
90
82
  });
91
83
  });
92
84
  }
85
+
86
+ async initBoxInfo() {
87
+ const boxes = await this.boxList();
88
+ const box = boxes.find((b) => b.is_default_box);
89
+ if (box) {
90
+ return { uid: box.login_user, boxname: box.box_name };
91
+ }
92
+ throw "没有默认盒子信息, 请先使用 lzc-cli box switch 设置默认的盒子信息";
93
+ }
94
+
95
+ async setDefaultBox(boxname) {
96
+ const boxes = await this.boxList();
97
+ const box = boxes.find((b) => b.box_name === boxname);
98
+ if (!box) {
99
+ throw `${boxname} 盒子不存在`;
100
+ }
101
+ return new Promise((resolve, reject) => {
102
+ this.client.modifyBoxConfig(
103
+ { id: box.id, name: box.box_name, set_as_default_box: true },
104
+ this.metadata,
105
+ function (err) {
106
+ err ? reject(err) : resolve();
107
+ }
108
+ );
109
+ });
110
+ }
93
111
  }
94
112
 
95
113
  export default new ShellApi();
package/lib/utils.js CHANGED
@@ -14,6 +14,7 @@ import zlib from "node:zlib";
14
14
  import process from "node:process";
15
15
  import { spawnSync } from "node:child_process";
16
16
  import logger from "loglevel";
17
+ import tar from "tar";
17
18
 
18
19
  export const envsubstr = async (templateContents, args) => {
19
20
  const parse = await importDefault("envsub/js/envsub-parser.js");
@@ -37,6 +38,18 @@ export function ensureDir(filePath) {
37
38
  }
38
39
  }
39
40
 
41
+ export async function createTemplateFileCommon(templateFile, outputFile, env) {
42
+ const options = {
43
+ envs: toPair(env),
44
+ syntax: "default",
45
+ protect: false,
46
+ };
47
+ const output = await envsubstr(fs.readFileSync(templateFile, "utf-8"), {
48
+ options,
49
+ });
50
+ fs.writeFileSync(outputFile, output);
51
+ }
52
+
40
53
  export function loadFromYaml(file) {
41
54
  return yaml.load(fs.readFileSync(file, "utf8"));
42
55
  }
@@ -298,3 +311,33 @@ export function isValidPackageName(packageName) {
298
311
  export function isUserApp(manifest) {
299
312
  return !!manifest["application"]["user_app"];
300
313
  }
314
+
315
+ export async function tarContentDir(from, to, cwd = "./") {
316
+ return new Promise((resolve, reject) => {
317
+ const dest = fs.createWriteStream(to);
318
+ tar
319
+ .c(
320
+ {
321
+ cwd: cwd,
322
+ filter: (filePath) => {
323
+ logger.debug(`tar gz ${filePath}`);
324
+ return true;
325
+ },
326
+ sync: true,
327
+ portable: {
328
+ uid: 0,
329
+ gid: 0,
330
+ },
331
+ },
332
+ from
333
+ )
334
+ .pipe(dest)
335
+ .on("close", () => {
336
+ logger.debug(`pack: ${dest.path}`);
337
+ resolve(dest.path);
338
+ })
339
+ .on("error", (err) => {
340
+ reject(err);
341
+ });
342
+ });
343
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazycatcloud/lzc-cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "lazycat cloud developer kit",
5
5
  "scripts": {
6
6
  "test": "tap",
@@ -27,12 +27,14 @@
27
27
  "author": "zac zeng",
28
28
  "license": "ISC",
29
29
  "dependencies": {
30
+ "@balena/dockerignore": "^1.0.2",
30
31
  "@grpc/grpc-js": "^1.8.18",
31
32
  "@grpc/proto-loader": "^0.7.8",
32
33
  "archiver": "^5.3.0",
33
34
  "chalk": "^4.1.2",
34
35
  "chokidar": "^3.5.3",
35
36
  "command-exists": "^1.2.9",
37
+ "dockerfile-ast": "^0.5.0",
36
38
  "envsub": "^4.0.7",
37
39
  "fast-glob": "^3.2.7",
38
40
  "form-data": "^4.0.0",
@@ -7,18 +7,25 @@ BUSYBOX=/lzcapp/pkg/content/devshell/busybox
7
7
  mkdir -p /root/.ssh
8
8
  mkdir -p /etc/debug.bridge
9
9
 
10
- ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/dropbearmulti -O /usr/bin/dropbearmulti
11
- chmod +x /usr/bin/dropbearmulti
10
+ if ! [ -f /usr/bin/dropbearmulti ]; then
11
+ ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/dropbearmulti -O /usr/bin/dropbearmulti
12
+ chmod +x /usr/bin/dropbearmulti
13
+ fi
12
14
 
13
- ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/rsync -O /usr/bin/rsync
14
- chmod +x /usr/bin/rsync
15
+ if ! [ -f /usr/bin/rsync ]; then
16
+ ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/rsync -O /usr/bin/rsync
17
+ chmod +x /usr/bin/rsync
18
+ fi
15
19
 
16
20
  ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/resources/authorized_keys -O /root/.ssh/authorized_keys
17
21
  chmod 0644 /root/.ssh/authorized_keys
18
22
 
19
23
  ${BUSYBOX} wget ${DEBUG_BRIDGE_LZCAPP}/bannerfile -O /etc/debug.bridge/bannerfile
20
24
 
21
-
22
- mkdir -p /etc/dropbear
23
-
24
- exec dropbearmulti dropbear -R -F -E -B -p 22222
25
+ if ${BUSYBOX} netstat -tln | grep ':22222' >/dev/null; then
26
+ echo "端口22222正在监听"
27
+ ${BUSYBOX} sleep infinity
28
+ else
29
+ mkdir -p /etc/dropbear
30
+ exec dropbearmulti dropbear -R -F -E -B -p 22222
31
+ fi
@@ -32,9 +32,25 @@ icon: ./lazycat.png
32
32
  # - /=http://127.0.0.1:3000
33
33
  # image: registry.lazycat.cloud/lzc-cli/devshell:0.0.4
34
34
 
35
+ # devshell 指定构建Dockerfile
36
+ # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
37
+ # devshell:
38
+ # routes:
39
+ # - /=http://127.0.0.1:3000
40
+ # image: ${package}-devshell:${version}
41
+ # pull_policy: build
42
+ # build: .
43
+
44
+ # dvshell 指定开发依赖的情况
45
+ # 这种情况下,选用 apline:3.16 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
46
+ # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
35
47
  devshell:
36
48
  routes:
37
49
  - /=http://127.0.0.1:3000
50
+ dependencies:
51
+ - nodejs
52
+ - vim
53
+ - npm
38
54
  # setupscript 每次进入到app container后都会执行的配置脚本
39
55
  # - 可以为脚本的路径地址
40
56
  # - 如果构建命令简单,也可以直接写 sh 的命令
@@ -32,9 +32,24 @@ icon: ./lazycat.png
32
32
  # - /=http://127.0.0.1:3000
33
33
  # image: registry.lazycat.cloud/lzc-cli/devshell:0.0.4
34
34
 
35
+ # devshell 指定构建Dockerfile
36
+ # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
37
+ # devshell:
38
+ # routes:
39
+ # - /=http://127.0.0.1:3000
40
+ # image: ${package}-devshell:${version}
41
+ # pull_policy: build
42
+ # build: .
43
+
44
+ # dvshell 指定开发依赖的情况
45
+ # 这种情况下,选用 apline:3.16 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
46
+ # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
35
47
  devshell:
36
48
  routes:
37
49
  - /=http://127.0.0.1:3000
50
+ dependencies:
51
+ - go
52
+ - vim
38
53
  # setupscript 每次进入到app container后都会执行的配置脚本
39
54
  # - 可以为脚本的路径地址
40
55
  # - 如果构建命令简单,也可以直接写 sh 的命令
@@ -32,9 +32,25 @@ icon: ./lazycat.png
32
32
  # - /=http://127.0.0.1:3000
33
33
  # image: registry.lazycat.cloud/lzc-cli/devshell:0.0.4
34
34
 
35
+ # devshell 指定构建Dockerfile
36
+ # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
37
+ # devshell:
38
+ # routes:
39
+ # - /=http://127.0.0.1:3000
40
+ # image: ${package}-devshell:${version}
41
+ # pull_policy: build
42
+ # build: .
43
+
44
+ # dvshell 指定开发依赖的情况
45
+ # 这种情况下,选用 apline:3.16 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
46
+ # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
35
47
  devshell:
36
48
  routes:
37
49
  - /=http://127.0.0.1:3000
50
+ dependencies:
51
+ - nodejs
52
+ - vim
53
+ - npm
38
54
  # setupscript 每次进入到app container后都会执行的配置脚本
39
55
  # - 可以为脚本的路径地址
40
56
  # - 如果构建命令简单,也可以直接写 sh 的命令
@@ -17,6 +17,7 @@ pkgout: ./
17
17
  # icon 指定 lpk 包 icon 的路径路径,如果不指定将会警告
18
18
  # icon 仅仅允许 png 后缀的文件
19
19
  icon: ./lazycat.png
20
+
20
21
  # devshell 自定义应用的开发容器环境
21
22
  # - routers 指定应用容器的访问路由
22
23
 
@@ -30,3 +31,30 @@ icon: ./lazycat.png
30
31
  # routes:
31
32
  # - /=http://127.0.0.1:3000
32
33
  # image: registry.lazycat.cloud/lzc-cli/devshell:0.0.4
34
+
35
+ # devshell 指定构建Dockerfile
36
+ # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
37
+ # devshell:
38
+ # routes:
39
+ # - /=http://127.0.0.1:3000
40
+ # image: ${package}-devshell:${version}
41
+ # pull_policy: build
42
+ # build: .
43
+
44
+ # dvshell 指定开发依赖的情况
45
+ # 这种情况下,选用 apline:3.16 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
46
+ # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
47
+ # devshell:
48
+ # routes:
49
+ # - /=http://127.0.0.1:3000
50
+ # dependencies:
51
+ # - go
52
+ # - vim
53
+ # # setupscript 每次进入到app container后都会执行的配置脚本
54
+ # # - 可以为脚本的路径地址
55
+ # # - 如果构建命令简单,也可以直接写 sh 的命令
56
+ # # setupscript: export GOPROXY=https://goproxy.cn
57
+ # # setupscript: ./setupscript.sh
58
+ # setupscript: |
59
+ # export GOPROXY=https://goproxy.cn
60
+ # export npm_config_registry=https://registry.npmmirror.com