@bizone-ai/cli 0.1.6 → 0.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/README.md +83 -4
- package/package.json +2 -2
- package/src/cli.js +7 -1
- package/src/commands/images.js +43 -3
- package/src/commands/resources.js +72 -4
- package/src/commands/start.js +63 -41
- package/src/config.js +2 -1
- package/src/custom-services.js +89 -0
- package/src/docker.js +22 -4
- package/src/http.js +11 -0
- package/src/resource-import.js +61 -0
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ bizone <category> <command> [args] [--no-colors]
|
|
|
63
63
|
| `start` | — | Start the platform |
|
|
64
64
|
| `stop` | — | Stop the platform |
|
|
65
65
|
| `configuration` | `config`,`c`| General CLI configuration |
|
|
66
|
-
| `images` | `img` | Override
|
|
66
|
+
| `images` | `img` | Override image URLs / add custom services |
|
|
67
67
|
| `environment` | `env` | Set environment variables per container |
|
|
68
68
|
| `resources` | `res`, `r` | Resources imported into the resource-manager |
|
|
69
69
|
| `database` | `db` | Manage the bundled MySQL database |
|
|
@@ -92,7 +92,8 @@ File shape:
|
|
|
92
92
|
"config": { "key": "value" },
|
|
93
93
|
"images": { "type": "image-url" },
|
|
94
94
|
"env": { "type": { "NAME": "value" } },
|
|
95
|
-
"resources": { "namespace": "path" }
|
|
95
|
+
"resources": { "namespace": "path" },
|
|
96
|
+
"custom": { "id": "path-to-service.json" }
|
|
96
97
|
}
|
|
97
98
|
```
|
|
98
99
|
|
|
@@ -137,9 +138,10 @@ bizone config set cloud_tools_enable true
|
|
|
137
138
|
## `images` — override container images
|
|
138
139
|
|
|
139
140
|
```bash
|
|
140
|
-
bizone images get # list image URLs
|
|
141
|
+
bizone images get # list image URLs + custom services
|
|
141
142
|
bizone images set {type} {url} # override an image
|
|
142
|
-
bizone images remove {type}
|
|
143
|
+
bizone images remove {type|id} # reset an image, or unregister a custom service
|
|
144
|
+
bizone images add {id} {path} # register an additional custom service
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
Stored at `$.images.{type}`. Valid types:
|
|
@@ -152,6 +154,61 @@ Example:
|
|
|
152
154
|
bizone images set state-store bizone-local/state-store:1.0.0
|
|
153
155
|
```
|
|
154
156
|
|
|
157
|
+
### Additional custom services (`images add`)
|
|
158
|
+
|
|
159
|
+
Register extra services that aren't part of the built-in topology. Each is
|
|
160
|
+
identified by an `{id}` and points to a JSON file describing the service, using
|
|
161
|
+
the **same shape as the built-in service objects** (see
|
|
162
|
+
`src/containers/mysql.js`):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
bizone images add {id} {path-to-service.json} # register / update
|
|
166
|
+
bizone images remove {id} # unregister
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Stored at `$.custom.{id}`. The JSON file shape:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"type": "redis",
|
|
174
|
+
"image": "redis:7",
|
|
175
|
+
"containerName": "bizone-redis",
|
|
176
|
+
"containerPort": 6379,
|
|
177
|
+
"portKey": "redis_port",
|
|
178
|
+
"defaultPort": "6379",
|
|
179
|
+
"enableKey": null,
|
|
180
|
+
"readyPath": "/.system/status",
|
|
181
|
+
"readyCheck": "systemStatusOk",
|
|
182
|
+
"volume": { "name": "bizone-redis-data", "mount": "/data" },
|
|
183
|
+
"defaultEnv": { "SOME_VAR": "value" }
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- **`image`** and **`containerName`** are required; everything else is optional.
|
|
188
|
+
- `containerName` should start with `bizone-` (so `db destroy` recognizes it)
|
|
189
|
+
and must not collide with a built-in container.
|
|
190
|
+
- `type` defaults to the `{id}` if omitted; it is the key for image/env
|
|
191
|
+
overrides (`bizone env set {type} ...`).
|
|
192
|
+
- `readyCheck` (optional) must be one of `systemStatusOk`, `uiReadinessUp`,
|
|
193
|
+
`mysqlPing`. With `systemStatusOk`/`uiReadinessUp` the CLI polls
|
|
194
|
+
`http://localhost:{port}{readyPath}`; with `mysqlPing` it uses `mysqladmin
|
|
195
|
+
ping`. Omit both `readyPath` and `readyCheck` to skip the readiness wait.
|
|
196
|
+
- `enableKey` (optional) — a config key gating the service. Omit / `null` to
|
|
197
|
+
always run.
|
|
198
|
+
|
|
199
|
+
Custom services are validated when added (the CLI parses the file immediately).
|
|
200
|
+
They **start last**, after the built-in platform is ready, and are **stopped by
|
|
201
|
+
`bizone stop`** like any other container. Relative `{path}` values resolve
|
|
202
|
+
against the `.bizone` config directory.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
bizone images add redis ./redis.json
|
|
208
|
+
bizone start # built-ins first, then redis
|
|
209
|
+
bizone images get # redis shown under "Custom services"
|
|
210
|
+
```
|
|
211
|
+
|
|
155
212
|
---
|
|
156
213
|
|
|
157
214
|
## `environment` — per-container environment variables
|
|
@@ -198,6 +255,7 @@ namespace.
|
|
|
198
255
|
bizone resources get # list configured sources
|
|
199
256
|
bizone resources set {namespace} {path} # set a file or folder source
|
|
200
257
|
bizone resources remove {namespace} # remove a namespace source
|
|
258
|
+
bizone resources import [global] # import now (see below)
|
|
201
259
|
```
|
|
202
260
|
|
|
203
261
|
Stored at `$.resources.{namespace}`. `{path}` may be:
|
|
@@ -224,6 +282,20 @@ POST http://localhost:{resource_manager_port}/admin/{namespace}/status
|
|
|
224
282
|
{ "user": "bizone-cli", "message": "resource initialization", "items": [ ... ] }
|
|
225
283
|
```
|
|
226
284
|
|
|
285
|
+
### `resources import` — import on demand
|
|
286
|
+
|
|
287
|
+
Run an import against the **already-running** platform (resource-manager must be up):
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bizone resources import # import the user-configured namespaces (above)
|
|
291
|
+
bizone resources import global # run ONLY the bizone-resources job (core resources)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- Without `global`: imports the namespaces configured via `resources set`.
|
|
295
|
+
- With `global`: runs the one-shot `bizone-resources` docker job, which imports
|
|
296
|
+
the **core** resources into the `.global` namespace. This is the same job
|
|
297
|
+
`bizone start` runs (unconditionally here — used to (re)seed core resources).
|
|
298
|
+
|
|
227
299
|
---
|
|
228
300
|
|
|
229
301
|
## `start` — start the platform
|
|
@@ -246,6 +318,11 @@ returns control to the shell. Container ids are saved to
|
|
|
246
318
|
3. Start `resource-manager`; wait until `GET /.system/status` returns `{ "status": "OK" }`.
|
|
247
319
|
4. Run the `bizone-resources` initialization job (blocking).
|
|
248
320
|
5. Import any configured `resources` namespaces (see above).
|
|
321
|
+
|
|
322
|
+
Steps 4–5 are **skipped entirely if the core resources were already
|
|
323
|
+
imported**, detected by querying
|
|
324
|
+
`GET /.global/resources?id=action/flow.start&expand=no`; a `200` with a
|
|
325
|
+
non-empty `items` array means the import already happened.
|
|
249
326
|
6. Start `orchestrator`, `ui`, and any enabled optional services in parallel
|
|
250
327
|
(state-store / cloud-tools run only when `*_enable=true`).
|
|
251
328
|
- The orchestrator receives the AWS env vars when `forward_aws_env=true`.
|
|
@@ -253,6 +330,8 @@ returns control to the shell. Container ids are saved to
|
|
|
253
330
|
- `ui` — `GET /actuator/health/readiness` returns `{"status":{"code":"UP"}}`
|
|
254
331
|
- all others — `GET /.system/status` returns `{ "status": "OK" }`
|
|
255
332
|
- each wait times out after `timeout_sec` seconds.
|
|
333
|
+
8. Start any **custom services** registered with `bizone images add` (last),
|
|
334
|
+
then wait for the readiness each declares (see the `images add` section).
|
|
256
335
|
|
|
257
336
|
Each container is started with:
|
|
258
337
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bizone-ai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Assistant CLI for running the Bizone orchestrator platform locally with Docker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node bin/bizone.js",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "node --test test/*.test.js"
|
|
20
20
|
},
|
|
21
21
|
"license": "UNLICENSED"
|
|
22
22
|
}
|
package/src/cli.js
CHANGED
|
@@ -66,7 +66,10 @@ function printHelp() {
|
|
|
66
66
|
);
|
|
67
67
|
log.plain(` ${c("bizone images set {type} {url}")} Override an image`);
|
|
68
68
|
log.plain(
|
|
69
|
-
` ${c("bizone images remove {type}")}
|
|
69
|
+
` ${c("bizone images remove {type|id}")} Reset image / drop custom service`,
|
|
70
|
+
);
|
|
71
|
+
log.plain(
|
|
72
|
+
` ${c("bizone images add {id} {path}")} Register a custom service (json)`,
|
|
70
73
|
);
|
|
71
74
|
log.plain("");
|
|
72
75
|
log.plain(b("Environment:") + color.dim(" (env)"));
|
|
@@ -84,6 +87,9 @@ function printHelp() {
|
|
|
84
87
|
log.plain(
|
|
85
88
|
` ${c("bizone resources remove {namespace}")} Remove a resource source`,
|
|
86
89
|
);
|
|
90
|
+
log.plain(
|
|
91
|
+
` ${c("bizone resources import [global]")} Import user resources (or core with 'global')`,
|
|
92
|
+
);
|
|
87
93
|
log.plain("");
|
|
88
94
|
log.plain(b("Database:") + color.dim(" (db)"));
|
|
89
95
|
log.plain(
|
package/src/commands/images.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// `bizone images|img get|set|remove`
|
|
1
|
+
// `bizone images|img get|set|remove|add`
|
|
2
2
|
|
|
3
3
|
import { loadConfig, saveConfig } from "../config.js";
|
|
4
4
|
import { CONTAINERS, IMAGE_TYPES } from "../defaults.js";
|
|
5
|
+
import { readCustomService } from "../custom-services.js";
|
|
5
6
|
import { color, log } from "../colors.js";
|
|
6
7
|
|
|
7
8
|
function isValidType(type) {
|
|
@@ -29,6 +30,38 @@ export function images(args) {
|
|
|
29
30
|
: color.dim(" (default)");
|
|
30
31
|
log.plain(` ${color.cyan(t)} -> ${val}${tag}`);
|
|
31
32
|
}
|
|
33
|
+
const custom = Object.entries(cfg.custom || {});
|
|
34
|
+
if (custom.length > 0) {
|
|
35
|
+
log.plain(color.bold("Custom services:"));
|
|
36
|
+
for (const [id, path] of custom) {
|
|
37
|
+
log.plain(` ${color.cyan(id)} -> ${path}${color.dim(" (custom)")}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
case "add": {
|
|
43
|
+
if (!type || rest.length === 0) {
|
|
44
|
+
log.err("Usage: bizone images add {id} {path-to-service.json}");
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const id = type; // first positional is the custom service id
|
|
48
|
+
if (isValidType(id)) {
|
|
49
|
+
log.err(`"${id}" is a built-in service type; choose another id.`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const path = rest.join(" ");
|
|
53
|
+
try {
|
|
54
|
+
const svc = readCustomService(id, path); // parse + validate now (fail fast)
|
|
55
|
+
cfg.custom[id] = path;
|
|
56
|
+
saveConfig(cfg);
|
|
57
|
+
log.ok(
|
|
58
|
+
`Added custom service ${color.cyan(id)} -> ${path} ` +
|
|
59
|
+
color.dim(`(${svc.image})`),
|
|
60
|
+
);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.err(`Invalid custom service ${err.message}`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
32
65
|
return 0;
|
|
33
66
|
}
|
|
34
67
|
case "set": {
|
|
@@ -49,10 +82,17 @@ export function images(args) {
|
|
|
49
82
|
}
|
|
50
83
|
case "remove": {
|
|
51
84
|
if (!type) {
|
|
52
|
-
log.err("Usage: bizone images remove {type}");
|
|
85
|
+
log.err("Usage: bizone images remove {type|custom-id}");
|
|
53
86
|
typesHint();
|
|
54
87
|
return 1;
|
|
55
88
|
}
|
|
89
|
+
// A custom service id takes precedence over a built-in image override.
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(cfg.custom || {}, type)) {
|
|
91
|
+
delete cfg.custom[type];
|
|
92
|
+
saveConfig(cfg);
|
|
93
|
+
log.ok(`Removed custom service ${color.cyan(type)}`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
56
96
|
if (Object.prototype.hasOwnProperty.call(cfg.images, type)) {
|
|
57
97
|
delete cfg.images[type];
|
|
58
98
|
saveConfig(cfg);
|
|
@@ -67,7 +107,7 @@ export function images(args) {
|
|
|
67
107
|
return 0;
|
|
68
108
|
}
|
|
69
109
|
default:
|
|
70
|
-
log.err("Usage: bizone images <get|set|remove> ...");
|
|
110
|
+
log.err("Usage: bizone images <get|set|remove|add> ...");
|
|
71
111
|
typesHint();
|
|
72
112
|
return 1;
|
|
73
113
|
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
// `bizone resources|res|r get|set|remove`
|
|
1
|
+
// `bizone resources|res|r get|set|remove|import`
|
|
2
2
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
|
|
5
|
-
import { loadConfig, saveConfig } from "../config.js";
|
|
5
|
+
import { loadConfig, saveConfig, getConfigValue } from "../config.js";
|
|
6
|
+
import { CONTAINERS } from "../defaults.js";
|
|
6
7
|
import { resolveResourcePath } from "../resources-loader.js";
|
|
8
|
+
import {
|
|
9
|
+
dockerAvailable,
|
|
10
|
+
ensureNetwork,
|
|
11
|
+
isContainerRunning,
|
|
12
|
+
} from "../docker.js";
|
|
13
|
+
import {
|
|
14
|
+
runCoreResourcesJob,
|
|
15
|
+
importUserResources,
|
|
16
|
+
} from "../resource-import.js";
|
|
7
17
|
import { color, log } from "../colors.js";
|
|
8
18
|
|
|
9
|
-
export function resources(args) {
|
|
19
|
+
export async function resources(args) {
|
|
10
20
|
const [sub, namespace, ...rest] = args;
|
|
11
21
|
const cfg = loadConfig();
|
|
12
22
|
|
|
@@ -55,8 +65,66 @@ export function resources(args) {
|
|
|
55
65
|
}
|
|
56
66
|
return 0;
|
|
57
67
|
}
|
|
68
|
+
case "import":
|
|
69
|
+
// `resources import` -> import user-configured namespaces.
|
|
70
|
+
// `resources import global` -> run ONLY the core bizone-resources job.
|
|
71
|
+
return await runImport(cfg, namespace);
|
|
58
72
|
default:
|
|
59
|
-
log.err("Usage: bizone resources <get|set|remove> ...");
|
|
73
|
+
log.err("Usage: bizone resources <get|set|remove|import> ...");
|
|
60
74
|
return 1;
|
|
61
75
|
}
|
|
62
76
|
}
|
|
77
|
+
|
|
78
|
+
async function runImport(cfg, target) {
|
|
79
|
+
if (target && target !== "global") {
|
|
80
|
+
log.err("Usage: bizone resources import [global]");
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const rm = CONTAINERS["resource-manager"];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (target === "global") {
|
|
87
|
+
if (!dockerAvailable()) {
|
|
88
|
+
log.err("Docker does not appear to be running or installed.");
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (!isContainerRunning(rm.containerName)) {
|
|
92
|
+
log.err(
|
|
93
|
+
`${rm.containerName} is not running; start the platform first (bizone start).`,
|
|
94
|
+
);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const network = getConfigValue(cfg, "network_name");
|
|
98
|
+
ensureNetwork(network);
|
|
99
|
+
log.step("Running core resources import job (bizone-resources)...");
|
|
100
|
+
await runCoreResourcesJob(cfg, {
|
|
101
|
+
network,
|
|
102
|
+
imageHooks: {
|
|
103
|
+
onBeforePull: (image) =>
|
|
104
|
+
log.step(`Pulling image ${image} (this may take a while)...`),
|
|
105
|
+
onBeforeLogin: (registry) =>
|
|
106
|
+
log.step(`Authenticating to AWS ECR for ${registry}...`),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
log.ok("Core resources import complete");
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// User-configured resources.
|
|
114
|
+
const count = Object.keys(cfg.resources || {}).length;
|
|
115
|
+
if (count === 0) {
|
|
116
|
+
log.warn(
|
|
117
|
+
"No resource namespaces configured. Add one with `bizone resources set {namespace} {path}`.",
|
|
118
|
+
);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
log.step(`Importing ${count} resource namespace(s)...`);
|
|
122
|
+
await importUserResources(cfg);
|
|
123
|
+
log.plain("");
|
|
124
|
+
log.plain(color.green(color.bold(`✓ Imported ${count} namespace(s)`)));
|
|
125
|
+
return 0;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log.err(err.message);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/commands/start.js
CHANGED
|
@@ -9,33 +9,29 @@ import {
|
|
|
9
9
|
loadContainers,
|
|
10
10
|
saveContainers,
|
|
11
11
|
} from "../config.js";
|
|
12
|
-
import { CONTAINERS, AWS_ENV_VARS
|
|
12
|
+
import { CONTAINERS, AWS_ENV_VARS } from "../defaults.js";
|
|
13
13
|
import {
|
|
14
14
|
dockerAvailable,
|
|
15
15
|
ensureNetwork,
|
|
16
16
|
ensureImage,
|
|
17
17
|
runDetached,
|
|
18
|
-
runBlocking,
|
|
19
18
|
removeIfExists,
|
|
20
19
|
mysqlReady,
|
|
21
20
|
isContainerRunning,
|
|
22
21
|
} from "../docker.js";
|
|
23
|
-
import { waitForReady
|
|
24
|
-
import {
|
|
22
|
+
import { waitForReady } from "../http.js";
|
|
23
|
+
import { loadCustomServices } from "../custom-services.js";
|
|
24
|
+
import {
|
|
25
|
+
coreResourcesImported,
|
|
26
|
+
runCoreResourcesJob,
|
|
27
|
+
importUserResources,
|
|
28
|
+
} from "../resource-import.js";
|
|
25
29
|
import { color, log } from "../colors.js";
|
|
26
30
|
|
|
27
|
-
function imageFor(cfg, type) {
|
|
28
|
-
return cfg.images[type] || CONTAINERS[type].image;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
31
|
function hostPort(cfg, service) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Effective env for a container type: baked-in defaults, overridden by any
|
|
36
|
-
// user-set values (`bizone env set`).
|
|
37
|
-
function envFor(cfg, type) {
|
|
38
|
-
return { ...defaultEnvFor(type), ...(cfg.env[type] || {}) };
|
|
32
|
+
const v = service.portKey ? getConfigValue(cfg, service.portKey) : undefined;
|
|
33
|
+
// Custom services may carry no known config key; fall back to their defaultPort.
|
|
34
|
+
return v != null ? v : service.defaultPort;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
// A service runs if it has no enableKey (always-on) or its enableKey is true.
|
|
@@ -78,16 +74,19 @@ export async function start() {
|
|
|
78
74
|
awsProfile,
|
|
79
75
|
};
|
|
80
76
|
|
|
81
|
-
// Helper to start one service detached and record its id.
|
|
77
|
+
// Helper to start one service detached and record its id. Works for both
|
|
78
|
+
// built-in services and custom ones (which carry their own image/defaultEnv).
|
|
82
79
|
const startService = (service, extraEnv = {}) => {
|
|
83
80
|
removeIfExists(service.containerName); // avoid name conflicts from stale runs
|
|
84
|
-
|
|
81
|
+
const image = cfg.images[service.type] || service.image;
|
|
82
|
+
ensureImage(image, imageHooks); // pull + ECR login if needed
|
|
83
|
+
const baseEnv = { ...(service.defaultEnv ?? {}), ...(cfg.env[service.type] || {}) };
|
|
85
84
|
const id = runDetached({
|
|
86
85
|
name: service.containerName,
|
|
87
86
|
hostPort: hostPort(cfg, service),
|
|
88
87
|
containerPort: service.containerPort,
|
|
89
|
-
image
|
|
90
|
-
env: { ...
|
|
88
|
+
image,
|
|
89
|
+
env: { ...baseEnv, ...extraEnv },
|
|
91
90
|
volume: service.volume || null,
|
|
92
91
|
network,
|
|
93
92
|
});
|
|
@@ -140,28 +139,21 @@ export async function start() {
|
|
|
140
139
|
await waitService(rm);
|
|
141
140
|
log.ok("resource-manager ready");
|
|
142
141
|
|
|
143
|
-
// 3.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (namespaces.length > 0) {
|
|
159
|
-
const rmPort = hostPort(cfg, rm);
|
|
160
|
-
log.step(`Importing ${namespaces.length} resource namespace(s)...`);
|
|
161
|
-
for (const [namespace, path] of namespaces) {
|
|
162
|
-
const items = loadItems(path);
|
|
163
|
-
await importNamespace({ port: rmPort, namespace, items });
|
|
164
|
-
log.ok(`Imported "${namespace}" (${items.length} item(s))`);
|
|
142
|
+
// 3 + 4. Resource import — skipped entirely if the core resources were
|
|
143
|
+
// already imported (the resource-manager keeps them in its persistent DB).
|
|
144
|
+
if (await coreResourcesImported(cfg)) {
|
|
145
|
+
log.ok("Resources already imported (skipping import)");
|
|
146
|
+
} else {
|
|
147
|
+
// 3. Core resources init job (blocking).
|
|
148
|
+
log.step("Running resources initialization job (bizone-resources)...");
|
|
149
|
+
await runCoreResourcesJob(cfg, { network, imageHooks });
|
|
150
|
+
log.ok("Resources initialization job complete");
|
|
151
|
+
|
|
152
|
+
// 4. Import user-configured resources.
|
|
153
|
+
const namespaces = Object.keys(cfg.resources || {});
|
|
154
|
+
if (namespaces.length > 0) {
|
|
155
|
+
log.step(`Importing ${namespaces.length} resource namespace(s)...`);
|
|
156
|
+
await importUserResources(cfg);
|
|
165
157
|
}
|
|
166
158
|
}
|
|
167
159
|
|
|
@@ -197,6 +189,36 @@ export async function start() {
|
|
|
197
189
|
),
|
|
198
190
|
);
|
|
199
191
|
|
|
192
|
+
// 7. Custom services (registered via `bizone images add`) — started last.
|
|
193
|
+
const customServices = loadCustomServices(cfg).filter((s) =>
|
|
194
|
+
isEnabled(cfg, s),
|
|
195
|
+
);
|
|
196
|
+
if (customServices.length > 0) {
|
|
197
|
+
log.step(`Starting ${customServices.length} custom service(s)...`);
|
|
198
|
+
for (const service of customServices) {
|
|
199
|
+
log.step(
|
|
200
|
+
`Starting ${service.containerName} (${service.id})` +
|
|
201
|
+
(hostPort(cfg, service) != null
|
|
202
|
+
? ` (port ${hostPort(cfg, service)})`
|
|
203
|
+
: ""),
|
|
204
|
+
);
|
|
205
|
+
startService(service);
|
|
206
|
+
}
|
|
207
|
+
// Wait for readiness where the custom service declares how to check.
|
|
208
|
+
await Promise.all(
|
|
209
|
+
customServices.map(async (s) => {
|
|
210
|
+
if (s.readyCheck === "mysqlPing") {
|
|
211
|
+
await waitForMysql(s.containerName);
|
|
212
|
+
} else if (s.readyPath && s.readyCheck) {
|
|
213
|
+
await waitService(s);
|
|
214
|
+
} else {
|
|
215
|
+
return; // no readiness declared; assume started
|
|
216
|
+
}
|
|
217
|
+
log.ok(`${s.containerName} ready`);
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
200
222
|
log.plain("");
|
|
201
223
|
log.plain(color.green(color.bold("✓ Platform is running")));
|
|
202
224
|
const ui = CONTAINERS.ui;
|
package/src/config.js
CHANGED
|
@@ -20,7 +20,7 @@ function ensureDir() {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const EMPTY = { config: {}, images: {}, env: {}, resources: {} };
|
|
23
|
+
const EMPTY = { config: {}, images: {}, env: {}, resources: {}, custom: {} };
|
|
24
24
|
|
|
25
25
|
export function loadConfig() {
|
|
26
26
|
if (!existsSync(CONFIG_PATH)) {
|
|
@@ -34,6 +34,7 @@ export function loadConfig() {
|
|
|
34
34
|
images: parsed.images || {},
|
|
35
35
|
env: parsed.env || {},
|
|
36
36
|
resources: parsed.resources || {},
|
|
37
|
+
custom: parsed.custom || {},
|
|
37
38
|
};
|
|
38
39
|
} catch (err) {
|
|
39
40
|
throw new Error(`Failed to read config at ${CONFIG_PATH}: ${err.message}`);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Additional, user-defined services registered via `bizone images add {id} {path}`.
|
|
2
|
+
//
|
|
3
|
+
// Each registration maps an id to a JSON file whose contents mirror the service
|
|
4
|
+
// object exported by src/containers/*.js (see src/containers/mysql.js):
|
|
5
|
+
//
|
|
6
|
+
// {
|
|
7
|
+
// "type": "...", // identity used for image/env overrides (defaults to the id)
|
|
8
|
+
// "image": "repo/img:tag", // required
|
|
9
|
+
// "containerName": "...", // required (should start with "bizone-")
|
|
10
|
+
// "containerPort": 8080, // optional (needed for a -p mapping)
|
|
11
|
+
// "portKey": "...", // optional config key for the host port
|
|
12
|
+
// "defaultPort": "8080", // optional host port fallback
|
|
13
|
+
// "enableKey": null, // optional config key gating the service
|
|
14
|
+
// "readyPath": "/.system/status", // optional HTTP readiness path
|
|
15
|
+
// "readyCheck": "systemStatusOk", // optional: systemStatusOk | uiReadinessUp | mysqlPing
|
|
16
|
+
// "volume": { "name": "...", "mount": "..." }, // optional
|
|
17
|
+
// "defaultEnv": { "NAME": "value" } // optional
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// Custom services start LAST (after the built-in platform is ready) and are
|
|
21
|
+
// stopped by `bizone stop` like any other recorded container.
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
|
|
25
|
+
import { CONTAINERS } from "./defaults.js";
|
|
26
|
+
import { resolveResourcePath } from "./resources-loader.js";
|
|
27
|
+
|
|
28
|
+
// Readiness checks a custom service may reference (must match http.js / docker.js).
|
|
29
|
+
export const KNOWN_READY_CHECKS = [
|
|
30
|
+
"systemStatusOk",
|
|
31
|
+
"uiReadinessUp",
|
|
32
|
+
"mysqlPing",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const BUILTIN_CONTAINER_NAMES = new Set(
|
|
36
|
+
Object.values(CONTAINERS).map((s) => s.containerName),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Parse + validate one custom service definition. Throws on a bad shape.
|
|
40
|
+
export function parseCustomService(id, raw) {
|
|
41
|
+
let obj;
|
|
42
|
+
try {
|
|
43
|
+
obj = JSON.parse(raw);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`"${id}": not valid JSON (${err.message})`);
|
|
46
|
+
}
|
|
47
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
48
|
+
throw new Error(`"${id}": expected a JSON object`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof obj.image !== "string" || !obj.image.trim()) {
|
|
51
|
+
throw new Error(`"${id}": missing required string field "image"`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof obj.containerName !== "string" || !obj.containerName.trim()) {
|
|
54
|
+
throw new Error(`"${id}": missing required string field "containerName"`);
|
|
55
|
+
}
|
|
56
|
+
if (BUILTIN_CONTAINER_NAMES.has(obj.containerName)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`"${id}": containerName "${obj.containerName}" collides with a built-in service`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (obj.readyCheck && !KNOWN_READY_CHECKS.includes(obj.readyCheck)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`"${id}": unknown readyCheck "${obj.readyCheck}" (valid: ${KNOWN_READY_CHECKS.join(", ")})`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `type` identifies the service for image/env overrides; default to the id.
|
|
68
|
+
return { id, ...obj, type: obj.type || id };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Read + parse the registration at `path` (resolved like resource paths).
|
|
72
|
+
export function readCustomService(id, path) {
|
|
73
|
+
const resolved = resolveResourcePath(path);
|
|
74
|
+
const raw = readFileSync(resolved, "utf8");
|
|
75
|
+
return parseCustomService(id, raw);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Load every registered custom service from config (`$.custom`).
|
|
79
|
+
// Returns [{ id, ...service }]. Throws with a clear message on the first bad one.
|
|
80
|
+
export function loadCustomServices(cfg) {
|
|
81
|
+
const entries = Object.entries(cfg.custom || {});
|
|
82
|
+
return entries.map(([id, path]) => {
|
|
83
|
+
try {
|
|
84
|
+
return readCustomService(id, path);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(`Custom service ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/docker.js
CHANGED
|
@@ -20,20 +20,27 @@ const AUTH_ERR_RE =
|
|
|
20
20
|
// Registries we have already logged into during this process.
|
|
21
21
|
const loggedInRegistries = new Set();
|
|
22
22
|
|
|
23
|
-
function parseEcr(image) {
|
|
23
|
+
export function parseEcr(image) {
|
|
24
24
|
const m = String(image).match(ECR_RE);
|
|
25
25
|
if (!m) return null;
|
|
26
26
|
return { registry: m[1], region: m[2] };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Build the `aws ecr get-login-password` argument list, optionally pinned to a
|
|
30
|
+
// named profile. Exported for testing the aws_profile wiring.
|
|
31
|
+
export function ecrPasswordArgs(region, profile) {
|
|
32
|
+
const args = ["ecr", "get-login-password", "--region", region];
|
|
33
|
+
if (profile) args.push("--profile", profile);
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// Authenticate docker to the ECR registry backing `image`, using the AWS CLI.
|
|
30
38
|
// No-op (returns false) for non-ECR images. Throws on failure.
|
|
31
39
|
function ecrLogin(ecr, profile) {
|
|
32
40
|
if (!ecr) return false;
|
|
33
41
|
if (loggedInRegistries.has(ecr.registry)) return true;
|
|
34
42
|
|
|
35
|
-
const awsArgs =
|
|
36
|
-
if (profile) awsArgs.push("--profile", profile);
|
|
43
|
+
const awsArgs = ecrPasswordArgs(ecr.region, profile);
|
|
37
44
|
|
|
38
45
|
const pw = spawnSync("aws", awsArgs, { encoding: "utf8" });
|
|
39
46
|
if (pw.status !== 0) {
|
|
@@ -142,6 +149,8 @@ export function runDetached({
|
|
|
142
149
|
network,
|
|
143
150
|
"--network-alias",
|
|
144
151
|
name,
|
|
152
|
+
"--pull",
|
|
153
|
+
"always"
|
|
145
154
|
];
|
|
146
155
|
if (hostPort != null && containerPort != null) {
|
|
147
156
|
args.push("-p", `${hostPort}:${containerPort}`);
|
|
@@ -165,7 +174,16 @@ export function runDetached({
|
|
|
165
174
|
// Run a container in the foreground (blocking), removing it on exit.
|
|
166
175
|
export function runBlocking({ name, image, env = {}, network }) {
|
|
167
176
|
return new Promise((resolve, reject) => {
|
|
168
|
-
const args = [
|
|
177
|
+
const args = [
|
|
178
|
+
"run",
|
|
179
|
+
"--rm",
|
|
180
|
+
"--name",
|
|
181
|
+
name,
|
|
182
|
+
"--network",
|
|
183
|
+
network,
|
|
184
|
+
"--pull",
|
|
185
|
+
"always"
|
|
186
|
+
];
|
|
169
187
|
for (const [k, v] of Object.entries(env)) {
|
|
170
188
|
if (v === undefined || v === null || v === "") continue;
|
|
171
189
|
args.push("-e", `${k}=${v}`);
|
package/src/http.js
CHANGED
|
@@ -51,6 +51,17 @@ export async function waitForReady({
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Check whether a resource exists in the `.global` namespace. Returns true when
|
|
55
|
+
// the endpoint responds 200 with a JSON `{ items: [...] }` of non-zero length.
|
|
56
|
+
// Used to detect whether the core resources init already ran.
|
|
57
|
+
export async function globalResourceExists({ port, id }) {
|
|
58
|
+
const url = `http://localhost:${port}/.global/resources?id=${encodeURIComponent(
|
|
59
|
+
id,
|
|
60
|
+
)}&expand=no`;
|
|
61
|
+
const body = await probe(url);
|
|
62
|
+
return Boolean(body && Array.isArray(body.items) && body.items.length > 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
// Import a set of items into a resource-manager namespace.
|
|
55
66
|
export async function importNamespace({ port, namespace, items }) {
|
|
56
67
|
const url = `http://localhost:${port}/admin/${namespace}/resources`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Shared resource-import operations, used by both `bizone start` and
|
|
2
|
+
// `bizone resources import`.
|
|
3
|
+
//
|
|
4
|
+
// - core resources : the one-shot `bizone-resources` docker job, which imports
|
|
5
|
+
// into the `.global` namespace.
|
|
6
|
+
// - user resources : the namespaces the user configured via `bizone resources set`.
|
|
7
|
+
|
|
8
|
+
import { CONTAINERS, defaultEnvFor } from "./defaults.js";
|
|
9
|
+
import { getConfigValue } from "./config.js";
|
|
10
|
+
import { ensureImage, removeIfExists, runBlocking } from "./docker.js";
|
|
11
|
+
import { importNamespace, globalResourceExists } from "./http.js";
|
|
12
|
+
import { loadItems } from "./resources-loader.js";
|
|
13
|
+
import { log } from "./colors.js";
|
|
14
|
+
|
|
15
|
+
// A core resource that exists once the init job has run. Its presence in the
|
|
16
|
+
// `.global` namespace is how we detect a prior import.
|
|
17
|
+
export const CORE_INIT_PROBE_ID = "action/flow.start";
|
|
18
|
+
|
|
19
|
+
function imageFor(cfg, type) {
|
|
20
|
+
return cfg.images[type] || CONTAINERS[type].image;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function envFor(cfg, type) {
|
|
24
|
+
return { ...defaultEnvFor(type), ...(cfg.env[type] || {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rmPort(cfg) {
|
|
28
|
+
return getConfigValue(cfg, CONTAINERS["resource-manager"].portKey);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// True when the core resources were already imported into `.global`.
|
|
32
|
+
export async function coreResourcesImported(cfg) {
|
|
33
|
+
return globalResourceExists({ port: rmPort(cfg), id: CORE_INIT_PROBE_ID });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run the one-shot `bizone-resources` job (blocking). Imports core resources
|
|
37
|
+
// into `.global`. Requires the resource-manager to be running on `network`.
|
|
38
|
+
export async function runCoreResourcesJob(cfg, { network, imageHooks } = {}) {
|
|
39
|
+
const job = CONTAINERS.resources;
|
|
40
|
+
removeIfExists(job.containerName);
|
|
41
|
+
ensureImage(imageFor(cfg, job.type), imageHooks);
|
|
42
|
+
await runBlocking({
|
|
43
|
+
name: job.containerName,
|
|
44
|
+
image: imageFor(cfg, job.type),
|
|
45
|
+
env: envFor(cfg, job.type),
|
|
46
|
+
network,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Import every user-configured resource namespace into the resource-manager.
|
|
51
|
+
// Returns the number of namespaces imported.
|
|
52
|
+
export async function importUserResources(cfg) {
|
|
53
|
+
const namespaces = Object.entries(cfg.resources || {});
|
|
54
|
+
const port = rmPort(cfg);
|
|
55
|
+
for (const [namespace, path] of namespaces) {
|
|
56
|
+
const items = loadItems(path);
|
|
57
|
+
await importNamespace({ port, namespace, items });
|
|
58
|
+
log.ok(`Imported "${namespace}" (${items.length} item(s))`);
|
|
59
|
+
}
|
|
60
|
+
return namespaces.length;
|
|
61
|
+
}
|