@anonymousmister/crane-tool 1.0.0

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 ADDED
@@ -0,0 +1,115 @@
1
+
2
+
3
+ # Crane-Jib-Tool 🚀
4
+
5
+ 一个轻量级、声明式的容器镜像构建工具。无需 Docker 守护进程,通过 `crane` 实现类似 Google Jib 的文件分层打包与推送功能。支持 JSON 模板、INI 配置注入以及动态变量替换。
6
+
7
+ ## ✨ 特性
8
+
9
+ * **免 Docker 构建**:直接生成 OCI 兼容镜像并推送到远程仓库。
10
+ * **声明式分层**:通过 `crane.json` 定义本地文件到镜像路径的精确映射。
11
+ * **动态模板引擎**:支持 `${Variable}` 占位符,可从环境、文件或命令行注入。
12
+ * **内置时间戳**:自动生成 `${TimestampTag}` 变量,支持版本回溯。
13
+ * **多格式支持**:变量源支持 `.json`、`.ini` 文件及 `KEY=VALUE` 字符串。
14
+
15
+ ## 📦 安装
16
+
17
+ 首先,确保您的项目中已安装必要的依赖:
18
+
19
+ ```bash
20
+ npm install tar ini
21
+ # 或者
22
+ pnpm add tar ini
23
+
24
+ ```
25
+
26
+ ## 🛠 快速开始
27
+
28
+ ### 1. 准备镜像模板 `crane.json`
29
+
30
+ 在项目根目录创建模板文件,使用占位符定义动态内容:
31
+
32
+ ```json
33
+ {
34
+ "from": "nginx:stable-alpine",
35
+ "image": "swr.cn-east-3.myhuaweicloud.com/your-project/${APP_NAME}",
36
+ "tag": "${TimestampTag}",
37
+ "exposedPorts": [80, 443],
38
+ "envs": {
39
+ "APP_ENV": "${NODE_ENV}",
40
+ "BUILD_VERSION": "${VERSION}"
41
+ },
42
+ "layers": [
43
+ {
44
+ "name": "nginx-conf",
45
+ "files": [
46
+ { "from": "./nginx.conf", "to": "etc/nginx/conf.d/default.conf" }
47
+ ]
48
+ },
49
+ {
50
+ "name": "static-assets",
51
+ "files": [
52
+ { "from": "./dist", "to": "usr/share/nginx/html" }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+
58
+ ```
59
+
60
+ ### 2. 执行构建
61
+
62
+ 使用 `-t` 指定模板,使用 `-f` 注入变量:
63
+
64
+ ```bash
65
+ # 基础用法(使用内置时间戳)
66
+ node bin/build.js -t crane.json -f "APP_NAME=my-admin" -f "NODE_ENV=prod"
67
+
68
+ # 覆盖自动生成的 Tag
69
+ node bin/build.js -t crane.json -f "TimestampTag=v1.0.0" -f "APP_NAME=web"
70
+
71
+ ```
72
+
73
+ ## 📖 详细用法
74
+
75
+ ### 变量注入优先级
76
+
77
+ 工具会按以下顺序合并变量(后者覆盖前者):
78
+
79
+ 1. **系统环境变量** (`process.env`)。
80
+ 2. **内置变量**:`${TimestampTag}` (格式: `YYYYMMDDHHMMSS`)。
81
+ 3. **文件变量**:通过 `-f config.json` 或 `-f config.ini` 加载。
82
+ 4. **命令行变量**:通过 `-f "KEY=VALUE"` 直接指定。
83
+
84
+ ### 使用 INI 文件作为变量源
85
+
86
+ 您可以创建一个 `deploy.ini`:
87
+
88
+ ```ini
89
+ APP_NAME = wlhy-wj-admin
90
+ VERSION = 2.4.5
91
+ DOCKER_USER = your_username
92
+ DOCKER_PASS = your_password
93
+
94
+ ```
95
+
96
+ 然后运行:
97
+
98
+ ```bash
99
+ node bin/build.js -t crane.json -f deploy.ini
100
+
101
+ ```
102
+
103
+ ## 🚀 运行流程说明
104
+
105
+ 1. **变量初始化**:收集环境、内置时间戳及 `-f` 传入的所有参数。
106
+ 2. **模板渲染**:将 `crane.json` 中的占位符替换为实际数值。
107
+ 3. **本地打包**:根据 `layers` 定义,将本地文件临时打包为 `tar` 层。
108
+ 4. **推送镜像**:调用 `crane append` 将所有层合并推送到目标仓库。
109
+ 5. **修改元数据**:调用 `crane mutate` 配置 `ExposedPorts` 和 `Env` 变量。
110
+ 6. **自动清理**:构建完成后自动删除 `.crane_tmp` 临时目录。
111
+
112
+ ## ⚠️ 注意事项
113
+
114
+ * **认证**:如果提供了 `DOCKER_USER` 和 `DOCKER_PASS` 变量,工具会自动执行 `crane auth login`。
115
+ * **安全性**:对于私有仓库(如 `192.168.x.x`),工具会自动添加 `--insecure` 标志。
package/bin/build.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import os from 'os';
7
+ import * as tar from 'tar';
8
+ import * as ini from 'ini';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const platform = os.platform();
12
+ const isWin = platform === 'win32';
13
+ const CRANE_BIN = isWin ? 'crane.exe' : 'crane';
14
+ const CRANE_PATH = path.join(__dirname, CRANE_BIN);
15
+
16
+ /**
17
+ * 核心:解析变量池
18
+ * 优先级:环境变量 < 内置变量 < -f 文件变量 < -f 字符串变量
19
+ */
20
+ function buildVarPool(args) {
21
+ const now = new Date();
22
+ const pad = (n) => String(n).padStart(2, '0');
23
+ const defaultTimestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
24
+
25
+ const pool = {
26
+ ...process.env,
27
+ TimestampTag: defaultTimestamp
28
+ };
29
+
30
+ for (let i = 0; i < args.length; i++) {
31
+ if (args[i] === '-f' && args[i + 1]) {
32
+ const val = args[i + 1];
33
+ const fullPath = path.resolve(val);
34
+
35
+ if (fs.existsSync(fullPath)) {
36
+ const ext = path.extname(val).toLowerCase();
37
+ const content = fs.readFileSync(fullPath, 'utf-8');
38
+ if (ext === '.json') {
39
+ try { Object.assign(pool, JSON.parse(content)); } catch (e) { console.error(`❌ JSON 变量文件解析失败: ${val}`); }
40
+ } else if (ext === '.ini') {
41
+ try { Object.assign(pool, ini.parse(content)); } catch (e) { console.error(`❌ INI 变量文件解析失败: ${val}`); }
42
+ }
43
+ } else if (val.includes('=')) {
44
+ const [k, ...vParts] = val.split('=');
45
+ pool[k.trim()] = vParts.join('=').trim();
46
+ }
47
+ i++;
48
+ }
49
+ }
50
+ return pool;
51
+ }
52
+
53
+ /**
54
+ * 核心:递归替换对象中的占位符
55
+ */
56
+ function injectVars(obj, vars) {
57
+ const str = JSON.stringify(obj);
58
+ return JSON.parse(str.replace(/\${?(\w+)}?/g, (match, key) => {
59
+ return vars[key] !== undefined ? vars[key] : match;
60
+ }));
61
+ }
62
+
63
+ async function run() {
64
+ const args = process.argv.slice(2);
65
+ const tIndex = args.indexOf('-t');
66
+ const templatePath = (tIndex > -1 && args[tIndex + 1]) ? path.resolve(args[tIndex + 1]) : null;
67
+
68
+ if (!templatePath || !fs.existsSync(templatePath)) {
69
+ console.error("❌ 错误: 请使用 -t 指定有效的模板文件。例如: crane-build -t crane.json");
70
+ process.exit(1);
71
+ }
72
+
73
+ // 1. 变量处理
74
+ const varPool = buildVarPool(args);
75
+ const rawTemplate = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
76
+ const cfg = injectVars(rawTemplate, varPool);
77
+
78
+ // 2. 核心参数准备
79
+ const IMAGE_BASE = cfg.image;
80
+ const FROM_IMAGE = cfg.from || "nginx:stable-alpine";
81
+ const FINAL_TAG = cfg.tag || varPool.TimestampTag;
82
+ const FULL_IMAGE = `${IMAGE_BASE}:${FINAL_TAG}`;
83
+ const isInsecure = IMAGE_BASE.includes('192.168.') || IMAGE_BASE.includes('localhost') || IMAGE_BASE.includes('10.');
84
+ const flags = isInsecure ? '--insecure' : '';
85
+
86
+ try {
87
+ // 3. 认证逻辑
88
+ const registryHost = IMAGE_BASE.split('/')[0];
89
+ if (varPool.DOCKER_USER && varPool.DOCKER_PASS) {
90
+ console.log(`🔑 [Crane-Build] 正在认证: ${registryHost}`);
91
+ execSync(`"${CRANE_PATH}" auth login ${registryHost} -u "${varPool.DOCKER_USER}" -p "${varPool.DOCKER_PASS}" ${flags}`, { stdio: 'inherit' });
92
+ }
93
+
94
+ console.log(`\n🚀 [Crane-Build] 变量处理完成`);
95
+ console.log(` 最终镜像名: ${FULL_IMAGE}`);
96
+
97
+ const tmpBase = path.join(process.cwd(), '.crane_tmp');
98
+ if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true });
99
+ fs.mkdirSync(tmpBase, { recursive: true });
100
+
101
+ const layersToAppend = [];
102
+
103
+ // 4. 层打包逻辑
104
+ if (cfg.layers && Array.isArray(cfg.layers)) {
105
+ for (const layer of cfg.layers) {
106
+ console.log(`📦 [Crane-Build] 打包层: ${layer.name}`);
107
+ const layerDir = path.join(tmpBase, `l_${layer.name}`);
108
+ fs.mkdirSync(layerDir, { recursive: true });
109
+
110
+ for (const mapping of layer.files) {
111
+ const src = path.resolve(process.cwd(), mapping.from);
112
+ const dest = path.join(layerDir, mapping.to);
113
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
114
+ if (fs.existsSync(src)) {
115
+ if (fs.lstatSync(src).isDirectory()) fs.cpSync(src, dest, { recursive: true });
116
+ else fs.copyFileSync(src, dest);
117
+ } else {
118
+ console.warn(`⚠️ [Crane-Build] 警告: 文件不存在 ${src}`);
119
+ }
120
+ }
121
+
122
+ const layerTar = path.join(tmpBase, `${layer.name}.tar`);
123
+ await tar.c({ gzip: false, file: layerTar, cwd: layerDir }, ['.']);
124
+ layersToAppend.push(`-f "${layerTar}"`);
125
+ }
126
+ }
127
+
128
+ // 5. 推送层数据
129
+ if (layersToAppend.length > 0) {
130
+ console.log(`\n🚚 [Crane-Build] 正在执行合并并推送内容层...`);
131
+ execSync(`"${CRANE_PATH}" append -b ${FROM_IMAGE} ${layersToAppend.join(' ')} -t ${FULL_IMAGE} ${flags}`, { stdio: 'inherit' });
132
+ }
133
+
134
+ // 6. 修改元数据
135
+ console.log(`\n🔧 [Crane-Build] 正在修改镜像运行元数据 (Expose/Envs)...`);
136
+ let mutateCmd = `"${CRANE_PATH}" mutate ${FULL_IMAGE} -t ${FULL_IMAGE} ${flags}`;
137
+ (cfg.exposedPorts || []).forEach(p => mutateCmd += ` --exposed-ports ${p}`);
138
+ Object.entries(cfg.envs || {}).forEach(([k, v]) => mutateCmd += ` --env ${k}="${v}"`);
139
+ execSync(mutateCmd, { stdio: 'inherit' });
140
+
141
+ // 7. 清理
142
+ console.log(`\n🧹 [Crane-Build] 正在清理临时打包文件...`);
143
+ fs.rmSync(tmpBase, { recursive: true, force: true });
144
+
145
+ console.log(`\n✨ [成功] 镜像发布完成!`);
146
+ console.log(` 👉 镜像地址: ${FULL_IMAGE}\n`);
147
+
148
+ } catch (error) {
149
+ console.error(`\n❌ [Crane-Build] 构建中断`);
150
+ if (error.stderr) console.error(error.stderr.toString());
151
+ else console.error(error.message);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ run();
package/bin/install.js ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { execSync } from 'child_process';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const VERSION = '0.20.2';
10
+
11
+ async function install() {
12
+ const platform = os.platform();
13
+ const arch = os.arch();
14
+
15
+ let platformName = '';
16
+ if (platform === 'win32') platformName = 'Windows_x86_64';
17
+ else if (platform === 'darwin') platformName = arch === 'arm64' ? 'Darwin_arm64' : 'Darwin_x86_64';
18
+ else platformName = arch === 'arm64' ? 'Linux_arm64' : 'Linux_x86_64';
19
+
20
+ const url = `https://github.com/google/go-containerregistry/releases/download/v${VERSION}/go-containerregistry_${platformName}.tar.gz`;
21
+ const exeName = platform === 'win32' ? 'crane.exe' : 'crane';
22
+ const tarFile = path.join(__dirname, 'crane.tar.gz');
23
+
24
+ console.log(`[Crane-Install] 正在下载 Crane v${VERSION} (${platformName})...`);
25
+
26
+ try {
27
+ const response = await fetch(url);
28
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
29
+
30
+ const arrayBuffer = await response.arrayBuffer();
31
+ fs.writeFileSync(tarFile, Buffer.from(arrayBuffer));
32
+
33
+ // 解压到当前 bin 目录
34
+ execSync(`tar -xzf "${tarFile}" -C "${__dirname}" ${exeName}`, { stdio: 'inherit' });
35
+ fs.unlinkSync(tarFile);
36
+
37
+ if (platform !== 'win32') {
38
+ fs.chmodSync(path.join(__dirname, exeName), 0o755);
39
+ }
40
+ console.log(`✅ Crane 二进制文件就绪: ${path.join(__dirname, exeName)}`);
41
+ } catch (err) {
42
+ console.error('❌ 下载失败:', err.message);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ install();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@anonymousmister/crane-tool",
3
+ "version": "1.0.0",
4
+ "description": "基于 Crane 的分层镜像构建工具,模仿 jib 的功能",
5
+ "type": "module",
6
+ "main": "bin/build.js",
7
+ "bin": {
8
+ "crane": "./bin/crane",
9
+ "crane-tool-build": "./bin/build.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/xaboy/form-create-designer.git"
19
+ },
20
+ "keywords": [
21
+ "crane",
22
+ "jib",
23
+ "docker",
24
+ "nginx",
25
+ "vite"
26
+ ],
27
+ "author": "misterzhang",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "ini": "^6.0.0",
31
+ "tar": "^7.5.2"
32
+ },
33
+ "scripts": {
34
+ "postinstall": "node bin/install.js"
35
+ }
36
+ }