@bizone-ai/cli 0.1.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.
@@ -0,0 +1,8 @@
1
+ export const RESOURCES_JOB = {
2
+ type: "resources",
3
+ image: "699453144787.dkr.ecr.us-east-1.amazonaws.com/bizone-resources:latest",
4
+ containerName: "bizone-resources",
5
+ defaultEnv: {
6
+ RESOURCE_MANAGER_URL: "http://bizone-res-manager",
7
+ }
8
+ }
@@ -0,0 +1,28 @@
1
+ import {BIZONE_SERVICE_ENV} from "./_commons.js";
2
+
3
+ export const STATE_STORE_SERVICE = {
4
+ type: 'state-store',
5
+ image: `699453144787.dkr.ecr.us-east-1.amazonaws.com/state-store:latest`,
6
+ containerName: 'state-store',
7
+ containerPort: 80,
8
+ portKey: 'state_store_port',
9
+ defaultPort: '8088',
10
+ enableKey: 'state_store_enable',
11
+ readyPath: '/.system/status',
12
+ readyCheck: 'systemStatusOk',
13
+ defaultEnv: {
14
+ "http.port": "80",
15
+ "transport.config.endpoint": "http://bizone-res-manager/",
16
+
17
+ 'storage.type': 'mysql',
18
+ "storage.supported": "memory,mysql",
19
+
20
+ 'mysql.storage': 'jdbc:mysql://bizone-mysql:3306',
21
+ 'mysql.storage.user': 'root',
22
+ 'mysql.storage.password.secret': 'mysql/password',
23
+
24
+ 'secret.mysql.password': 'root',
25
+
26
+ ...BIZONE_SERVICE_ENV
27
+ }
28
+ };
@@ -0,0 +1,15 @@
1
+ export const UI_SERVICE = {
2
+ type: 'ui',
3
+ image: "699453144787.dkr.ecr.us-east-1.amazonaws.com/bizone-ui:latest",
4
+ containerName: 'bizone-ui',
5
+ containerPort: 7006,
6
+ portKey: 'ui_port',
7
+ defaultPort: '7006',
8
+ enableKey: null,
9
+ readyPath: '/actuator/health/readiness',
10
+ readyCheck: 'uiReadinessUp',
11
+ defaultEnv: {
12
+ "resource-manager.url": "http://bizone-res-manager/",
13
+ "auth.type": "none"
14
+ }
15
+ }
@@ -0,0 +1,64 @@
1
+ // Static defaults and platform topology for the Bizone CLI.
2
+
3
+ import {ORCHESTRATOR_SERVICE} from "./containers/orchestrator.js";
4
+ import {RESOURCE_MANAGER_SERVICE} from "./containers/resource-manager.js";
5
+ import {STATE_STORE_SERVICE} from "./containers/state-store.js";
6
+ import {MYSQL_SERVICE} from "./containers/mysql.js";
7
+ import {CLOUD_TOOLS_SERVICE} from "./containers/cloud-tools.js";
8
+ import {UI_SERVICE} from "./containers/ui.js";
9
+ import {RESOURCES_JOB} from "./containers/resources-job.js";
10
+
11
+ export const CONTAINERS = {
12
+ mysql: MYSQL_SERVICE,
13
+ 'resource-manager': RESOURCE_MANAGER_SERVICE,
14
+ orchestrator: ORCHESTRATOR_SERVICE,
15
+ 'state-store': STATE_STORE_SERVICE,
16
+ 'cloud-tools': CLOUD_TOOLS_SERVICE,
17
+ ui: UI_SERVICE,
18
+ resources: RESOURCES_JOB
19
+ };
20
+
21
+ // Valid image / environment "types" (a.k.a. container types).
22
+ export const IMAGE_TYPES = Object.keys(CONTAINERS);
23
+
24
+ export function defaultEnvFor(type) {
25
+ return CONTAINERS[type]?.defaultEnv ?? {};
26
+ }
27
+
28
+ // Shared docker network name (default; overridable via the `network_name` config key).
29
+ export const NETWORK_NAME = 'bizone-local';
30
+
31
+ // Defaults for general `config` keys.
32
+ export const DEFAULT_CONFIG = {
33
+ forward_aws_env: 'true',
34
+ network_name: NETWORK_NAME,
35
+ mysql_port: CONTAINERS.mysql.defaultPort,
36
+ orchestrator_port: CONTAINERS.orchestrator.defaultPort,
37
+ resource_manager_port: CONTAINERS["resource-manager"].defaultPort,
38
+ state_store_port: CONTAINERS["state-store"].defaultPort,
39
+ state_store_enable: 'true',
40
+ cloud_tools_port: CONTAINERS["cloud-tools"].defaultPort,
41
+ cloud_tools_enable: 'false',
42
+ ui_port: CONTAINERS.ui.defaultPort,
43
+ timeout_sec: '300',
44
+ };
45
+
46
+ // Known config keys (used for validation / help).
47
+ export const CONFIG_KEYS = Object.keys(DEFAULT_CONFIG);
48
+
49
+ // AWS env vars forwarded into the orchestrator runtime when forward_aws_env=true.
50
+ export const AWS_ENV_VARS = ['AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'];
51
+
52
+ // Predefined secret env-var names. When showing env (`environment get`),
53
+ // values for these keys are masked. Matching is case-insensitive and also
54
+ // triggers on any key containing one of the SECRET_KEY_SUBSTRINGS below.
55
+ export const SECRET_ENV_KEYS = [
56
+ 'AWS_SECRET_ACCESS_KEY',
57
+ 'AWS_ACCESS_KEY_ID',
58
+ ];
59
+
60
+ export const SECRET_KEY_SUBSTRINGS = ['password', 'secret', 'token', 'apikey', 'api_key'];
61
+
62
+ // Importing procedure constants.
63
+ export const IMPORT_USER = 'bizone-cli';
64
+ export const IMPORT_MESSAGE = 'resource initialization';
package/src/docker.js ADDED
@@ -0,0 +1,190 @@
1
+ // Docker helpers. All commands shell out to the local `docker` binary.
2
+
3
+ import { spawnSync, spawn } from 'node:child_process';
4
+
5
+ function run(args, opts = {}) {
6
+ return spawnSync('docker', args, { encoding: 'utf8', ...opts });
7
+ }
8
+
9
+ // --- AWS ECR auto-login ---------------------------------------------------
10
+
11
+ // Matches an AWS ECR registry host and captures (registry, region):
12
+ // {account}.dkr.ecr.{region}.amazonaws.com/{repo}:{tag}
13
+ const ECR_RE = /^([0-9]+\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com)\//;
14
+
15
+ // Error fragments that indicate a missing/expired registry authorization.
16
+ // ECR returns "403 Forbidden" on the manifest HEAD when not authenticated.
17
+ const AUTH_ERR_RE =
18
+ /(pull access denied|not authorized|no basic auth credentials|authentication required|denied|unauthorized|forbidden|\b401\b|\b403\b)/i;
19
+
20
+ // Registries we have already logged into during this process.
21
+ const loggedInRegistries = new Set();
22
+
23
+ function parseEcr(image) {
24
+ const m = String(image).match(ECR_RE);
25
+ if (!m) return null;
26
+ return { registry: m[1], region: m[2] };
27
+ }
28
+
29
+ // Authenticate docker to the ECR registry backing `image`, using the AWS CLI.
30
+ // No-op (returns false) for non-ECR images. Throws on failure.
31
+ function ecrLogin(ecr) {
32
+ if (!ecr) return false;
33
+ if (loggedInRegistries.has(ecr.registry)) return true;
34
+
35
+ const pw = spawnSync('aws', ['ecr', 'get-login-password', '--region', ecr.region], {
36
+ encoding: 'utf8',
37
+ });
38
+ if (pw.status !== 0) {
39
+ throw new Error(
40
+ `Failed to obtain ECR password (aws ecr get-login-password --region ${ecr.region}): ` +
41
+ `${(pw.stderr || pw.error?.message || '').trim()}. Is the AWS CLI installed and configured?`,
42
+ );
43
+ }
44
+
45
+ const login = spawnSync(
46
+ 'docker',
47
+ ['login', '--username', 'AWS', '--password-stdin', ecr.registry],
48
+ { input: pw.stdout.trim(), encoding: 'utf8' },
49
+ );
50
+ if (login.status !== 0) {
51
+ throw new Error(`docker login to ${ecr.registry} failed: ${(login.stderr || '').trim()}`);
52
+ }
53
+
54
+ loggedInRegistries.add(ecr.registry);
55
+ return true;
56
+ }
57
+
58
+ // Ensure `image` is pullable. Tries `docker pull`; on an authorization error
59
+ // against an ECR image, logs in and retries once. Non-auth pull failures are
60
+ // ignored here (the image may already exist locally) and surfaced later by the
61
+ // actual `docker run`.
62
+ export function ensureImage(image, { onBeforePull, onBeforeLogin } = {}) {
63
+ if (onBeforePull) onBeforePull(image);
64
+ let r = run(['pull', image]);
65
+ if (r.status === 0) return;
66
+
67
+ const out = `${r.stdout || ''}${r.stderr || ''}`;
68
+ if (AUTH_ERR_RE.test(out)) {
69
+ const ecr = parseEcr(image);
70
+ if (ecr) {
71
+ if (onBeforeLogin) onBeforeLogin(ecr.registry);
72
+ ecrLogin(ecr);
73
+ if (onBeforePull) onBeforePull(image);
74
+ r = run(['pull', image]);
75
+ if (r.status !== 0) {
76
+ throw new Error(`docker pull ${image} failed after ECR login: ${(r.stderr || '').trim()}`);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ export function dockerAvailable() {
83
+ const r = run(['version', '--format', '{{.Server.Version}}']);
84
+ return r.status === 0;
85
+ }
86
+
87
+ // Create the shared network if it does not already exist. Never fails if present.
88
+ export function ensureNetwork(network) {
89
+ const ls = run(['network', 'ls', '--filter', `name=^${network}$`, '--format', '{{.Name}}']);
90
+ if (ls.status === 0 && ls.stdout.split('\n').includes(network)) {
91
+ return false; // already existed
92
+ }
93
+ const create = run(['network', 'create', network]);
94
+ if (create.status !== 0 && !/already exists/i.test(create.stderr || '')) {
95
+ throw new Error(`Failed to create network ${network}: ${create.stderr.trim()}`);
96
+ }
97
+ return true; // created
98
+ }
99
+
100
+ // Run a container detached. Returns the full container id.
101
+ export function runDetached({ name, hostPort, containerPort, image, env = {}, volume = null, network }) {
102
+ const args = ['run', '-d', '--name', name, '-h', name, '--network', network, '--network-alias', name];
103
+ if (hostPort != null && containerPort != null) {
104
+ args.push('-p', `${hostPort}:${containerPort}`);
105
+ }
106
+ if (volume && volume.name && volume.mount) {
107
+ args.push('-v', `${volume.name}:${volume.mount}`);
108
+ }
109
+ for (const [k, v] of Object.entries(env)) {
110
+ if (v === undefined || v === null || v === '') continue;
111
+ args.push('-e', `${k}=${v}`);
112
+ }
113
+ args.push(image);
114
+
115
+ const r = run(args);
116
+ if (r.status !== 0) {
117
+ throw new Error(`docker run ${name} failed: ${(r.stderr || '').trim()}`);
118
+ }
119
+ return r.stdout.trim();
120
+ }
121
+
122
+ // Run a container in the foreground (blocking), removing it on exit.
123
+ export function runBlocking({ name, image, env = {}, network }) {
124
+ return new Promise((resolve, reject) => {
125
+ const args = ['run', '--rm', '--name', name, '--network', network];
126
+ for (const [k, v] of Object.entries(env)) {
127
+ if (v === undefined || v === null || v === '') continue;
128
+ args.push('-e', `${k}=${v}`);
129
+ }
130
+ args.push(image);
131
+
132
+ const child = spawn('docker', args, { stdio: 'inherit' });
133
+ child.on('error', reject);
134
+ child.on('close', (code) => {
135
+ if (code === 0) resolve();
136
+ else reject(new Error(`Resources init job (${name}) exited with code ${code}`));
137
+ });
138
+ });
139
+ }
140
+
141
+ // Return the set of running container ids (full ids).
142
+ export function runningContainerIds() {
143
+ const r = run(['ps', '--no-trunc', '--format', '{{.ID}}']);
144
+ if (r.status !== 0) return new Set();
145
+ return new Set(r.stdout.split('\n').map((s) => s.trim()).filter(Boolean));
146
+ }
147
+
148
+ // Return the set of running container names.
149
+ export function runningContainerNames() {
150
+ const r = run(['ps', '--format', '{{.Names}}']);
151
+ if (r.status !== 0) return new Set();
152
+ return new Set(r.stdout.split('\n').map((s) => s.trim()).filter(Boolean));
153
+ }
154
+
155
+ export function isContainerRunning(name) {
156
+ return runningContainerNames().has(name);
157
+ }
158
+
159
+ // Remove a named docker volume. Returns true on success.
160
+ export function removeVolume(name) {
161
+ return run(['volume', 'rm', name]).status === 0;
162
+ }
163
+
164
+ // Stop & remove a container by id or name. Ignores "no such container".
165
+ export function stopContainer(idOrName) {
166
+ const stop = run(['stop', idOrName]);
167
+ run(['rm', '-f', idOrName]); // best-effort cleanup
168
+ return stop.status === 0;
169
+ }
170
+
171
+ // Remove a container if it already exists (cleanup before re-running).
172
+ export function removeIfExists(name) {
173
+ run(['rm', '-f', name]);
174
+ }
175
+
176
+ // Whether the MySQL server inside a container accepts connections.
177
+ export function mysqlReady(containerName, { user = 'root', password = 'root' } = {}) {
178
+ const r = run([
179
+ 'exec',
180
+ containerName,
181
+ 'mysqladmin',
182
+ 'ping',
183
+ '-h',
184
+ '127.0.0.1',
185
+ `-u${user}`,
186
+ `-p${password}`,
187
+ '--silent',
188
+ ]);
189
+ return r.status === 0;
190
+ }
package/src/http.js ADDED
@@ -0,0 +1,60 @@
1
+ // HTTP helpers: generic readiness waiting + resource import.
2
+
3
+ import { IMPORT_USER, IMPORT_MESSAGE } from './defaults.js';
4
+
5
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6
+
7
+ // Readiness predicates keyed by name (see SERVICES[].readyCheck).
8
+ const CHECKS = {
9
+ systemStatusOk: (body) => body && body.status === 'OK',
10
+ uiReadinessUp: (body) => body && body.status && body.status.code === 'UP',
11
+ };
12
+
13
+ async function probe(url) {
14
+ try {
15
+ const res = await fetch(url, { method: 'GET' });
16
+ if (!res.ok) return null;
17
+ const text = await res.text();
18
+ try {
19
+ return JSON.parse(text);
20
+ } catch {
21
+ return null;
22
+ }
23
+ } catch {
24
+ return null; // connection refused / not up yet
25
+ }
26
+ }
27
+
28
+ // Generic wait-for-ready. Polls `url`, applying the named check, until it
29
+ // passes or the timeout elapses. Returns true on success, throws on timeout.
30
+ export async function waitForReady({ url, checkName, timeoutSec, label, onTick }) {
31
+ const check = CHECKS[checkName];
32
+ if (!check) throw new Error(`Unknown readiness check: ${checkName}`);
33
+
34
+ const deadline = Date.now() + timeoutSec * 1000;
35
+ let attempt = 0;
36
+ while (Date.now() < deadline) {
37
+ attempt += 1;
38
+ const body = await probe(url);
39
+ if (body && check(body)) return true;
40
+ if (onTick) onTick(attempt);
41
+ await sleep(1500);
42
+ }
43
+ throw new Error(`Timed out waiting for ${label || url} to become ready (${timeoutSec}s)`);
44
+ }
45
+
46
+ // Import a set of items into a resource-manager namespace.
47
+ export async function importNamespace({ port, namespace, items }) {
48
+ const url = `http://localhost:${port}/admin/${namespace}/status`;
49
+ const payload = { user: IMPORT_USER, message: IMPORT_MESSAGE, items };
50
+ const res = await fetch(url, {
51
+ method: 'POST',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify(payload),
54
+ });
55
+ if (!res.ok) {
56
+ const body = await res.text().catch(() => '');
57
+ throw new Error(`Import of namespace "${namespace}" failed: HTTP ${res.status} ${body}`);
58
+ }
59
+ return true;
60
+ }
@@ -0,0 +1,50 @@
1
+ // Reads resource definitions from a file or a folder into an "items" array,
2
+ // following the importing procedure rules:
3
+ //
4
+ // - folder: recursively collect every *.json file's contents as items
5
+ // - single file with an "items" field: use that array
6
+ // - single file otherwise: the file contents become the sole item
7
+
8
+ import { readFileSync, statSync, readdirSync } from 'node:fs';
9
+ import { join, isAbsolute, resolve } from 'node:path';
10
+
11
+ import { BIZONE_DIR } from './config.js';
12
+
13
+ function readJson(path) {
14
+ const raw = readFileSync(path, 'utf8');
15
+ return JSON.parse(raw);
16
+ }
17
+
18
+ function collectJsonFiles(dir, out = []) {
19
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
20
+ const full = join(dir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ collectJsonFiles(full, out);
23
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
24
+ out.push(full);
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+
30
+ // Resolve a configured path. Relative paths resolve against the .bizone dir
31
+ // (where config.json lives) for stable behavior regardless of cwd.
32
+ export function resolveResourcePath(p) {
33
+ return isAbsolute(p) ? p : resolve(BIZONE_DIR, p);
34
+ }
35
+
36
+ export function loadItems(configuredPath) {
37
+ const path = resolveResourcePath(configuredPath);
38
+ const st = statSync(path);
39
+
40
+ if (st.isDirectory()) {
41
+ const files = collectJsonFiles(path);
42
+ return files.map((f) => readJson(f));
43
+ }
44
+
45
+ const content = readJson(path);
46
+ if (content && typeof content === 'object' && Array.isArray(content.items)) {
47
+ return content.items;
48
+ }
49
+ return [content];
50
+ }