@aopslabs/aops 0.2.0 → 0.2.1
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
CHANGED
|
@@ -36,8 +36,10 @@ hosts. Explicit `aops server ...` commands operate the API on port `5900`,
|
|
|
36
36
|
while `aops cockpit start|stop|restart|status|health|logs` independently
|
|
37
37
|
operates Cockpit on port `5922`.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
On Windows x64, parameterless `aops` opens the bundled Rust TUI. This release
|
|
40
|
+
does not include native TUI payloads for macOS or Linux; use explicit CLI
|
|
41
|
+
commands such as `aops setup`, `aops server status`, or `aops --help` there.
|
|
42
|
+
Explicit commands retain the same CLI behavior on every supported platform.
|
|
41
43
|
When stdin or stdout is not an interactive terminal, parameterless `aops`
|
|
42
44
|
prints short CLI guidance instead of trying to draw the TUI.
|
|
43
45
|
`aops setup` always opens the installation menu, and `aops setup ai` prints a
|
|
@@ -7,6 +7,7 @@ import { createSetupOfficialCatalogProviderV1 } from '../lib/setup-official-cata
|
|
|
7
7
|
import { createSetupAgentAssetsProvider } from '../lib/setup-agent-assets-bridge.js';
|
|
8
8
|
import { buildAopsInstallAgentPrompt, loadAopsInstallSkill, } from '../lib/setup-install-guide.js';
|
|
9
9
|
import { promptSelect } from '../utils/prompts.js';
|
|
10
|
+
import { applyDockerServerSetup, inspectDockerServerSetupPreview, inspectDockerServerSetupStatus, runDockerServerLifecycle, } from '../lib/setup-docker-server.js';
|
|
10
11
|
const COMMUNITY_SETUP_HOME = `AOPS Setup — choose how to begin
|
|
11
12
|
Install interactively aops setup init
|
|
12
13
|
Set up with an AI agent aops setup ai
|
|
@@ -71,6 +72,67 @@ export async function runCommunitySetupInit(options = {}) {
|
|
|
71
72
|
officialCatalog: createSetupOfficialCatalogProviderV1(),
|
|
72
73
|
});
|
|
73
74
|
}
|
|
75
|
+
export async function runCommunityDockerSetupPreview(options = {}) {
|
|
76
|
+
const selectedActions = [options.apply, options.status, Boolean(options.lifecycle)].filter(Boolean).length;
|
|
77
|
+
if (selectedActions > 1)
|
|
78
|
+
throw new Error('setup_docker_action_conflict:choose_one_action');
|
|
79
|
+
if (options.status) {
|
|
80
|
+
const result = await inspectDockerServerSetupStatus(options);
|
|
81
|
+
if (options.json) {
|
|
82
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
banner('AOPS Docker Settings');
|
|
86
|
+
logInfo(`AOPS: ${result.components.server.state} · Database: ${result.components.database.state}`);
|
|
87
|
+
logInfo(`Address: ${result.origin}`);
|
|
88
|
+
logInfo(`Settings: ${result.environmentPath}`);
|
|
89
|
+
if (result.nextAction)
|
|
90
|
+
logInfo(result.nextAction);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (options.lifecycle) {
|
|
94
|
+
const actions = ['start', 'stop', 'restart', 'open-cockpit', 'repair'];
|
|
95
|
+
if (!actions.includes(options.lifecycle)) {
|
|
96
|
+
throw new Error('setup_docker_lifecycle_invalid:use_start_stop_restart_open-cockpit_or_repair');
|
|
97
|
+
}
|
|
98
|
+
if (!options.yes)
|
|
99
|
+
throw new Error('setup_docker_lifecycle_confirmation_required:use_--yes');
|
|
100
|
+
const result = await runDockerServerLifecycle(options.lifecycle, options);
|
|
101
|
+
if (options.json) {
|
|
102
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
banner('AOPS Docker Settings');
|
|
106
|
+
logInfo(`${result.action} completed · ${result.origin}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (options.apply) {
|
|
110
|
+
if (!options.yes)
|
|
111
|
+
throw new Error('setup_docker_apply_confirmation_required:use_--yes');
|
|
112
|
+
const result = await applyDockerServerSetup(options);
|
|
113
|
+
if (options.json) {
|
|
114
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
banner('AOPS Docker Setup');
|
|
118
|
+
logInfo(`Installed · ${result.origin}`);
|
|
119
|
+
logInfo('PostgreSQL migrations and AOPS Server health completed.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const preview = await inspectDockerServerSetupPreview(options);
|
|
123
|
+
if (options.json) {
|
|
124
|
+
process.stdout.write(`${JSON.stringify(preview, null, 2)}\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
banner('AOPS Docker Setup');
|
|
128
|
+
logInfo(`${preview.method.title}: ${preview.method.description}`);
|
|
129
|
+
logInfo(`Image: ${preview.generated.image}`);
|
|
130
|
+
logInfo(`Address: ${preview.generated.origin}`);
|
|
131
|
+
for (const check of preview.checks) {
|
|
132
|
+
logInfo(`${check.status === 'pass' ? 'OK' : 'ACTION'} · ${check.summary}`);
|
|
133
|
+
}
|
|
134
|
+
logInfo('This preview made no changes. Guided Apply will be enabled in the next setup slice.');
|
|
135
|
+
}
|
|
74
136
|
export async function runCommunitySetupMenu() {
|
|
75
137
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
76
138
|
process.stdout.write(COMMUNITY_SETUP_HOME);
|
|
@@ -178,6 +240,26 @@ It explains PostgreSQL ownership, setup paths, explicit Gateway activation,
|
|
|
178
240
|
server health verification, and Cockpit handoff. It performs no setup itself.
|
|
179
241
|
`)
|
|
180
242
|
.action((options) => runCommunitySetupGuide(options));
|
|
243
|
+
command.command('docker')
|
|
244
|
+
.description('Preview or apply the recommended Docker-hosted AOPS Server setup')
|
|
245
|
+
.option('--instance <name>', 'Local AOPS instance name (default: default)')
|
|
246
|
+
.option('--image <reference>', 'Exact local AOPS Server image reference')
|
|
247
|
+
.option('--pull-image <reference>', 'Image source used only when the exact local image is missing')
|
|
248
|
+
.option('--port <port>', 'Loopback AOPS Server port (default: 5900)', (value) => Number.parseInt(String(value), 10))
|
|
249
|
+
.option('--status', 'Read the installed Docker AOPS and PostgreSQL state without changing it')
|
|
250
|
+
.option('--lifecycle <action>', 'Settings action: start | stop | restart | open-cockpit | repair')
|
|
251
|
+
.option('--apply', 'Create the reviewed Docker resources and wait for migrations and health')
|
|
252
|
+
.option('--yes', 'Required with --apply in non-interactive use')
|
|
253
|
+
.option('--json', 'Output one redacted preview or Apply result')
|
|
254
|
+
.addHelpText('after', `
|
|
255
|
+
This first guided-setup slice only inspects Docker, the exact server-only image,
|
|
256
|
+
loopback port, resource names, and private environment-file location. It does
|
|
257
|
+
not create files, containers, networks, or volumes, and it does not accept an
|
|
258
|
+
apply flag unless both --apply and --yes are provided. Apply creates only the
|
|
259
|
+
displayed namespaced resources and rolls back resources created by a failed
|
|
260
|
+
first attempt.
|
|
261
|
+
`)
|
|
262
|
+
.action((options) => runCommunityDockerSetupPreview(options));
|
|
181
263
|
addSetupInitOptions(command.command('init')
|
|
182
264
|
.description('Interactively install AOPS, or inspect/apply an explicit path for automation'))
|
|
183
265
|
.addHelpText('after', `
|
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { resolveCommunityInstanceLayout } from './community-instance-layout.js';
|
|
8
|
+
import { resolveCommunityNativeDefaultSourceRoot } from './community-native-lifecycle.js';
|
|
9
|
+
import { openExternalUrl } from './external-url.js';
|
|
10
|
+
const DEFAULT_PORT = 5900;
|
|
11
|
+
const SERVER_PACKAGE_NAME = '@aopslabs/aops-server';
|
|
12
|
+
const POSTGRES_IMAGE = 'postgres:17-bookworm@sha256:4f736ae292687621d4dbe0d499ffd024a36bd2ee7d8ca6f2ccd4c800f047b394';
|
|
13
|
+
const MANAGED_LABEL_KEY = 'io.aopslab.aops-platform.managed-by';
|
|
14
|
+
const MANAGED_LABEL_VALUE = 'aops-cli';
|
|
15
|
+
const MANAGED_LABEL = `${MANAGED_LABEL_KEY}=${MANAGED_LABEL_VALUE}`;
|
|
16
|
+
function runDockerDefault(args) {
|
|
17
|
+
return spawnSync('docker', [...args], { encoding: 'utf8', windowsHide: true });
|
|
18
|
+
}
|
|
19
|
+
function runDockerRequired(runDocker, args, step) {
|
|
20
|
+
const result = runDocker(args);
|
|
21
|
+
if (result.status !== 0)
|
|
22
|
+
throw new Error(`setup_docker_${step}_failed`);
|
|
23
|
+
return output(result);
|
|
24
|
+
}
|
|
25
|
+
function atomicPrivateEnvironment(target, content) {
|
|
26
|
+
const parent = path.dirname(target);
|
|
27
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
28
|
+
writeFileSync(target, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
29
|
+
chmodSync(target, 0o600);
|
|
30
|
+
}
|
|
31
|
+
function removePrivateEnvironment(target) {
|
|
32
|
+
rmSync(target, { force: true });
|
|
33
|
+
}
|
|
34
|
+
function delay(milliseconds) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
36
|
+
}
|
|
37
|
+
async function probeHealth(origin) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(`${origin}/api/health`, {
|
|
40
|
+
method: 'GET',
|
|
41
|
+
signal: AbortSignal.timeout(5_000),
|
|
42
|
+
});
|
|
43
|
+
return response.ok;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function waitFor(timeoutMilliseconds, sleep, probe, code) {
|
|
50
|
+
const deadline = Date.now() + timeoutMilliseconds;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
if (await probe())
|
|
53
|
+
return;
|
|
54
|
+
await sleep(1_000);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(code);
|
|
57
|
+
}
|
|
58
|
+
function probePort(port) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const server = net.createServer();
|
|
61
|
+
server.unref();
|
|
62
|
+
server.once('error', () => resolve(false));
|
|
63
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
|
|
64
|
+
server.close(() => resolve(true));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function readServerVersion() {
|
|
69
|
+
const packageManifest = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
70
|
+
try {
|
|
71
|
+
const manifest = JSON.parse(readFileSync(packageManifest, 'utf8'));
|
|
72
|
+
if (typeof manifest.aopsDockerServerVersion === 'string'
|
|
73
|
+
&& /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(manifest.aopsDockerServerVersion))
|
|
74
|
+
return manifest.aopsDockerServerVersion;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
}
|
|
78
|
+
const root = resolveCommunityNativeDefaultSourceRoot();
|
|
79
|
+
const candidates = [
|
|
80
|
+
path.join(root, 'package.json'),
|
|
81
|
+
path.join(root, 'apps', 'aops-server', 'package.json'),
|
|
82
|
+
];
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
try {
|
|
85
|
+
const manifest = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
86
|
+
if (manifest.name === SERVER_PACKAGE_NAME
|
|
87
|
+
&& typeof manifest.version === 'string'
|
|
88
|
+
&& /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(manifest.version))
|
|
89
|
+
return manifest.version;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new Error('setup_docker_server_package_identity_unavailable');
|
|
95
|
+
}
|
|
96
|
+
function normalizeArchitecture(arch) {
|
|
97
|
+
if (arch === 'x64')
|
|
98
|
+
return 'amd64';
|
|
99
|
+
if (arch === 'arm64')
|
|
100
|
+
return 'arm64';
|
|
101
|
+
throw new Error('setup_docker_architecture_unsupported');
|
|
102
|
+
}
|
|
103
|
+
function validatePort(value) {
|
|
104
|
+
const port = value ?? DEFAULT_PORT;
|
|
105
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
|
|
106
|
+
throw new Error('setup_docker_port_invalid');
|
|
107
|
+
}
|
|
108
|
+
return port;
|
|
109
|
+
}
|
|
110
|
+
function output(result) {
|
|
111
|
+
return String(result.stdout ?? '').trim();
|
|
112
|
+
}
|
|
113
|
+
function readEnvironmentPort(environmentPath, readEnvironment) {
|
|
114
|
+
try {
|
|
115
|
+
const content = readEnvironment(environmentPath);
|
|
116
|
+
const line = content.split(/\r?\n/).find((candidate) => candidate.startsWith('AOPS_PUBLIC_PORT='));
|
|
117
|
+
if (!line)
|
|
118
|
+
return undefined;
|
|
119
|
+
const value = Number.parseInt(line.slice('AOPS_PUBLIC_PORT='.length), 10);
|
|
120
|
+
return Number.isSafeInteger(value) && value >= 1 && value <= 65_535 ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function inspectDockerResource(runDocker, kind, name) {
|
|
127
|
+
const result = runDocker([kind, 'inspect', name]);
|
|
128
|
+
if (result.status !== 0) {
|
|
129
|
+
const inspection = detail(result, 'inspection failed');
|
|
130
|
+
if (/no such|not found/i.test(inspection)) {
|
|
131
|
+
return { state: 'absent', owned: false, present: false, inspectable: true };
|
|
132
|
+
}
|
|
133
|
+
return { state: 'unknown', owned: false, present: false, inspectable: false };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const values = JSON.parse(output(result));
|
|
137
|
+
const first = values[0];
|
|
138
|
+
const labels = kind === 'container' ? first?.Config?.Labels : first?.Labels;
|
|
139
|
+
const owned = labels?.[MANAGED_LABEL_KEY] === MANAGED_LABEL_VALUE;
|
|
140
|
+
const state = kind === 'container' && typeof first?.State?.Status === 'string'
|
|
141
|
+
? first.State.Status
|
|
142
|
+
: 'present';
|
|
143
|
+
return { state, owned, present: true, inspectable: true };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { state: 'unknown', owned: false, present: true, inspectable: false };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function dockerSetupIdentity(options, dependencies) {
|
|
150
|
+
const platform = dependencies.platform ?? process.platform;
|
|
151
|
+
if (!['win32', 'linux', 'darwin'].includes(platform))
|
|
152
|
+
throw new Error('setup_docker_platform_unsupported');
|
|
153
|
+
const architecture = normalizeArchitecture(dependencies.arch ?? process.arch);
|
|
154
|
+
const layout = resolveCommunityInstanceLayout({
|
|
155
|
+
instanceId: options.instance,
|
|
156
|
+
platform,
|
|
157
|
+
environment: dependencies.environment,
|
|
158
|
+
homeDirectory: dependencies.homeDirectory,
|
|
159
|
+
});
|
|
160
|
+
const environmentPath = path.join(layout.configDir, 'docker.env');
|
|
161
|
+
const environmentPort = (dependencies.pathExists ?? existsSync)(environmentPath)
|
|
162
|
+
? readEnvironmentPort(environmentPath, dependencies.readPrivateEnvironment ?? ((target) => readFileSync(target, 'utf8')))
|
|
163
|
+
: undefined;
|
|
164
|
+
const port = validatePort(options.port ?? environmentPort);
|
|
165
|
+
const serverVersion = (dependencies.resolveServerVersion ?? readServerVersion)();
|
|
166
|
+
const image = options.image ?? `aops-platform:registry-${serverVersion}-${architecture}`;
|
|
167
|
+
const pullImage = options.pullImage ?? `ghcr.io/eeemzs/aops-platform:registry-${serverVersion}-${architecture}`;
|
|
168
|
+
if ([image, pullImage].some((candidate) => candidate.trim() !== candidate || candidate.length === 0 || /[\r\n\0]/.test(candidate))) {
|
|
169
|
+
throw new Error('setup_docker_image_invalid');
|
|
170
|
+
}
|
|
171
|
+
const prefix = `aops-${layout.instanceId}`;
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
instance: layout.instanceId,
|
|
174
|
+
image,
|
|
175
|
+
pullImage,
|
|
176
|
+
port,
|
|
177
|
+
origin: `http://127.0.0.1:${port}`,
|
|
178
|
+
environmentPath,
|
|
179
|
+
resources: Object.freeze({
|
|
180
|
+
serverContainer: `${prefix}-server`,
|
|
181
|
+
postgresContainer: `${prefix}-postgres`,
|
|
182
|
+
network: `${prefix}-network`,
|
|
183
|
+
serverDataVolume: `${prefix}-server-data`,
|
|
184
|
+
postgresDataVolume: `${prefix}-postgres-data`,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function detail(result, fallback) {
|
|
189
|
+
return String(result.stderr ?? result.stdout ?? fallback).trim() || fallback;
|
|
190
|
+
}
|
|
191
|
+
function resourceCheck(runDocker, kind, name) {
|
|
192
|
+
const result = runDocker([kind, 'inspect', name]);
|
|
193
|
+
if (result.status === 0) {
|
|
194
|
+
return { id: `${kind}:${name}`, status: 'fail', summary: `${name} already exists; choose repair or another instance.` };
|
|
195
|
+
}
|
|
196
|
+
const inspection = detail(result, 'inspection failed');
|
|
197
|
+
return /no such|not found/i.test(inspection)
|
|
198
|
+
? { id: `${kind}:${name}`, status: 'pass', summary: `${name} is available.` }
|
|
199
|
+
: { id: `${kind}:${name}`, status: 'fail', summary: `${name} could not be inspected: ${inspection}` };
|
|
200
|
+
}
|
|
201
|
+
function imageCheck(runDocker, image, serverVersion, architecture, pullImage) {
|
|
202
|
+
const result = runDocker(['image', 'inspect', image]);
|
|
203
|
+
if (result.status !== 0) {
|
|
204
|
+
return {
|
|
205
|
+
id: 'image',
|
|
206
|
+
status: 'info',
|
|
207
|
+
summary: `${image} is not on this computer yet; setup will download ${pullImage}.`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const inspected = JSON.parse(output(result));
|
|
212
|
+
const first = inspected[0];
|
|
213
|
+
const labels = first?.Config?.Labels ?? {};
|
|
214
|
+
const valid = first?.Os === 'linux'
|
|
215
|
+
&& first.Architecture === architecture
|
|
216
|
+
&& labels['io.aopslab.aops-platform.install-mode'] === 'registry'
|
|
217
|
+
&& labels['io.aopslab.aops-platform.server-version'] === serverVersion
|
|
218
|
+
&& labels['org.opencontainers.image.title'] === 'AOPS Server';
|
|
219
|
+
return valid
|
|
220
|
+
? { id: 'image', status: 'pass', summary: `${image} is the expected server-only image.` }
|
|
221
|
+
: { id: 'image', status: 'fail', summary: `${image} does not match the AOPS Server ${serverVersion} contract.` };
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return { id: 'image', status: 'fail', summary: `${image} returned an unreadable inspection result.` };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function inspectDockerServerSetupPreview(options = {}, dependencies = {}) {
|
|
228
|
+
const platform = dependencies.platform ?? process.platform;
|
|
229
|
+
if (!['win32', 'linux', 'darwin'].includes(platform)) {
|
|
230
|
+
throw new Error('setup_docker_platform_unsupported');
|
|
231
|
+
}
|
|
232
|
+
const architecture = normalizeArchitecture(dependencies.arch ?? process.arch);
|
|
233
|
+
const port = validatePort(options.port);
|
|
234
|
+
const layout = resolveCommunityInstanceLayout({
|
|
235
|
+
instanceId: options.instance,
|
|
236
|
+
platform,
|
|
237
|
+
environment: dependencies.environment,
|
|
238
|
+
homeDirectory: dependencies.homeDirectory,
|
|
239
|
+
});
|
|
240
|
+
const serverVersion = (dependencies.resolveServerVersion ?? readServerVersion)();
|
|
241
|
+
const image = options.image ?? `aops-platform:registry-${serverVersion}-${architecture}`;
|
|
242
|
+
const pullImage = options.pullImage
|
|
243
|
+
?? `ghcr.io/eeemzs/aops-platform:registry-${serverVersion}-${architecture}`;
|
|
244
|
+
if ([image, pullImage].some((candidate) => candidate.trim() !== candidate || candidate.length === 0 || /[\r\n\0]/.test(candidate))) {
|
|
245
|
+
throw new Error('setup_docker_image_invalid');
|
|
246
|
+
}
|
|
247
|
+
const prefix = `aops-${layout.instanceId}`;
|
|
248
|
+
const resources = Object.freeze({
|
|
249
|
+
serverContainer: `${prefix}-server`,
|
|
250
|
+
postgresContainer: `${prefix}-postgres`,
|
|
251
|
+
network: `${prefix}-network`,
|
|
252
|
+
serverDataVolume: `${prefix}-server-data`,
|
|
253
|
+
postgresDataVolume: `${prefix}-postgres-data`,
|
|
254
|
+
});
|
|
255
|
+
const environmentPath = path.join(layout.configDir, 'docker.env');
|
|
256
|
+
const runDocker = dependencies.runDocker ?? runDockerDefault;
|
|
257
|
+
const checks = [];
|
|
258
|
+
const daemon = runDocker(['info', '--format', '{{json .ServerVersion}}']);
|
|
259
|
+
checks.push(daemon.status === 0
|
|
260
|
+
? { id: 'docker', status: 'pass', summary: `Docker is ready (${output(daemon) || 'server available'}).` }
|
|
261
|
+
: { id: 'docker', status: 'fail', summary: `Docker is not ready: ${detail(daemon, 'start Docker Desktop')}` });
|
|
262
|
+
if (daemon.status === 0) {
|
|
263
|
+
checks.push(imageCheck(runDocker, image, serverVersion, architecture, pullImage));
|
|
264
|
+
checks.push(resourceCheck(runDocker, 'container', resources.serverContainer));
|
|
265
|
+
checks.push(resourceCheck(runDocker, 'container', resources.postgresContainer));
|
|
266
|
+
checks.push(resourceCheck(runDocker, 'network', resources.network));
|
|
267
|
+
checks.push(resourceCheck(runDocker, 'volume', resources.serverDataVolume));
|
|
268
|
+
checks.push(resourceCheck(runDocker, 'volume', resources.postgresDataVolume));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
checks.push({ id: 'image', status: 'fail', summary: 'Start Docker to inspect the local AOPS image.' });
|
|
272
|
+
}
|
|
273
|
+
const available = await (dependencies.portAvailable ?? probePort)(port);
|
|
274
|
+
checks.push(available
|
|
275
|
+
? { id: 'port', status: 'pass', summary: `127.0.0.1:${port} is available.` }
|
|
276
|
+
: { id: 'port', status: 'fail', summary: `127.0.0.1:${port} is already in use.` });
|
|
277
|
+
checks.push((dependencies.pathExists ?? existsSync)(environmentPath)
|
|
278
|
+
? { id: 'environment', status: 'fail', summary: `${environmentPath} already exists; use settings or repair.` }
|
|
279
|
+
: { id: 'environment', status: 'pass', summary: `${environmentPath} is available.` });
|
|
280
|
+
const failed = checks.filter((check) => check.status === 'fail');
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
command: 'setup.docker.preview',
|
|
284
|
+
mutationFree: true,
|
|
285
|
+
status: failed.length === 0 ? 'ready' : 'action-required',
|
|
286
|
+
method: Object.freeze({
|
|
287
|
+
id: 'docker-managed',
|
|
288
|
+
title: 'Docker (recommended)',
|
|
289
|
+
recommended: true,
|
|
290
|
+
description: 'AOPS Server and PostgreSQL run in isolated Docker resources.',
|
|
291
|
+
}),
|
|
292
|
+
generated: Object.freeze({
|
|
293
|
+
instance: layout.instanceId,
|
|
294
|
+
image,
|
|
295
|
+
pullImage,
|
|
296
|
+
port,
|
|
297
|
+
origin: `http://127.0.0.1:${port}`,
|
|
298
|
+
environmentPath,
|
|
299
|
+
postgresPassword: 'generated-during-apply',
|
|
300
|
+
resources,
|
|
301
|
+
}),
|
|
302
|
+
checks: Object.freeze(checks),
|
|
303
|
+
nextActions: Object.freeze(failed.map((check) => check.summary)),
|
|
304
|
+
applyAvailable: failed.length === 0,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
export async function applyDockerServerSetup(options = {}, dependencies = {}) {
|
|
308
|
+
const preview = await inspectDockerServerSetupPreview(options, dependencies);
|
|
309
|
+
if (!preview.applyAvailable) {
|
|
310
|
+
throw new Error(`setup_docker_not_ready:${preview.nextActions[0] ?? 'review_preview'}`);
|
|
311
|
+
}
|
|
312
|
+
const runDocker = dependencies.runDocker ?? runDockerDefault;
|
|
313
|
+
const sleep = dependencies.sleep ?? delay;
|
|
314
|
+
const writeEnvironment = dependencies.writePrivateEnvironment ?? atomicPrivateEnvironment;
|
|
315
|
+
const removeEnvironment = dependencies.removePrivateEnvironment ?? removePrivateEnvironment;
|
|
316
|
+
const password = (dependencies.createSecret ?? (() => randomBytes(32).toString('base64url')))();
|
|
317
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/.test(password)) {
|
|
318
|
+
throw new Error('setup_docker_generated_secret_invalid');
|
|
319
|
+
}
|
|
320
|
+
const { resources } = preview.generated;
|
|
321
|
+
const created = [];
|
|
322
|
+
const steps = [];
|
|
323
|
+
let environmentWritten = false;
|
|
324
|
+
try {
|
|
325
|
+
const imageState = preview.checks.find((check) => check.id === 'image');
|
|
326
|
+
if (imageState?.status === 'info') {
|
|
327
|
+
runDockerRequired(runDocker, ['pull', preview.generated.pullImage], 'image_pull');
|
|
328
|
+
runDockerRequired(runDocker, ['image', 'tag', preview.generated.pullImage, preview.generated.image], 'image_tag');
|
|
329
|
+
}
|
|
330
|
+
const verifiedImage = imageCheck(runDocker, preview.generated.image, (dependencies.resolveServerVersion ?? readServerVersion)(), normalizeArchitecture(dependencies.arch ?? process.arch), preview.generated.pullImage);
|
|
331
|
+
if (verifiedImage.status !== 'pass')
|
|
332
|
+
throw new Error('setup_docker_image_contract_invalid');
|
|
333
|
+
steps.push({ id: imageState?.status === 'info' ? 'image-pull' : 'image-check', status: 'completed' });
|
|
334
|
+
const encodedPassword = encodeURIComponent(password);
|
|
335
|
+
const environmentContent = [
|
|
336
|
+
'POSTGRES_DB=aops',
|
|
337
|
+
'POSTGRES_USER=aops',
|
|
338
|
+
`POSTGRES_PASSWORD=${password}`,
|
|
339
|
+
`AOPS_PG_URL=postgresql://aops:${encodedPassword}@postgres:5432/aops`,
|
|
340
|
+
'AOPS_POSTGRES_TLS=disable',
|
|
341
|
+
`AOPS_PUBLIC_PORT=${preview.generated.port}`,
|
|
342
|
+
'AOPS_AUTH_PROVIDER=trusted-local',
|
|
343
|
+
'',
|
|
344
|
+
].join('\n');
|
|
345
|
+
writeEnvironment(preview.generated.environmentPath, environmentContent);
|
|
346
|
+
environmentWritten = true;
|
|
347
|
+
steps.push({ id: 'environment', status: 'completed' });
|
|
348
|
+
runDockerRequired(runDocker, ['network', 'create', '--label', MANAGED_LABEL, resources.network], 'network_create');
|
|
349
|
+
created.push({ kind: 'network', name: resources.network });
|
|
350
|
+
for (const name of [resources.serverDataVolume, resources.postgresDataVolume]) {
|
|
351
|
+
runDockerRequired(runDocker, ['volume', 'create', '--label', MANAGED_LABEL, name], 'volume_create');
|
|
352
|
+
created.push({ kind: 'volume', name });
|
|
353
|
+
}
|
|
354
|
+
runDockerRequired(runDocker, [
|
|
355
|
+
'run', '--detach', '--name', resources.postgresContainer,
|
|
356
|
+
'--label', MANAGED_LABEL,
|
|
357
|
+
'--restart', 'unless-stopped',
|
|
358
|
+
'--network', resources.network,
|
|
359
|
+
'--network-alias', 'postgres',
|
|
360
|
+
'--env-file', preview.generated.environmentPath,
|
|
361
|
+
'--volume', `${resources.postgresDataVolume}:/var/lib/postgresql/data`,
|
|
362
|
+
POSTGRES_IMAGE,
|
|
363
|
+
], 'postgres_create');
|
|
364
|
+
created.push({ kind: 'container', name: resources.postgresContainer });
|
|
365
|
+
await waitFor(90_000, sleep, () => {
|
|
366
|
+
const result = runDocker([
|
|
367
|
+
'exec', resources.postgresContainer,
|
|
368
|
+
'pg_isready', '-U', 'aops', '-d', 'aops',
|
|
369
|
+
]);
|
|
370
|
+
return result.status === 0;
|
|
371
|
+
}, 'setup_docker_postgres_timeout');
|
|
372
|
+
steps.push({ id: 'postgres', status: 'completed' });
|
|
373
|
+
runDockerRequired(runDocker, [
|
|
374
|
+
'run', '--detach', '--name', resources.serverContainer,
|
|
375
|
+
'--label', MANAGED_LABEL,
|
|
376
|
+
'--restart', 'unless-stopped',
|
|
377
|
+
'--network', resources.network,
|
|
378
|
+
'--publish', `127.0.0.1:${preview.generated.port}:5900`,
|
|
379
|
+
'--env-file', preview.generated.environmentPath,
|
|
380
|
+
'--volume', `${resources.serverDataVolume}:/var/lib/aops`,
|
|
381
|
+
preview.generated.image,
|
|
382
|
+
], 'server_create');
|
|
383
|
+
created.push({ kind: 'container', name: resources.serverContainer });
|
|
384
|
+
steps.push({ id: 'server', status: 'completed' });
|
|
385
|
+
await waitFor(180_000, sleep, () => (dependencies.healthProbe ?? probeHealth)(preview.generated.origin), 'setup_docker_health_timeout');
|
|
386
|
+
steps.push({ id: 'migrations-health', status: 'completed' });
|
|
387
|
+
return Object.freeze({
|
|
388
|
+
schemaVersion: 1,
|
|
389
|
+
command: 'setup.docker.apply',
|
|
390
|
+
status: 'installed',
|
|
391
|
+
method: 'docker-managed',
|
|
392
|
+
instance: preview.generated.instance,
|
|
393
|
+
image: preview.generated.image,
|
|
394
|
+
origin: preview.generated.origin,
|
|
395
|
+
environmentPath: preview.generated.environmentPath,
|
|
396
|
+
resources: preview.generated.resources,
|
|
397
|
+
steps: Object.freeze(steps),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
for (const resource of [...created].reverse()) {
|
|
402
|
+
if (resource.kind === 'container')
|
|
403
|
+
runDocker(['container', 'rm', '--force', resource.name]);
|
|
404
|
+
else if (resource.kind === 'volume')
|
|
405
|
+
runDocker(['volume', 'rm', '--force', resource.name]);
|
|
406
|
+
else
|
|
407
|
+
runDocker(['network', 'rm', resource.name]);
|
|
408
|
+
}
|
|
409
|
+
if (environmentWritten)
|
|
410
|
+
removeEnvironment(preview.generated.environmentPath);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
export async function inspectDockerServerSetupStatus(options = {}, dependencies = {}) {
|
|
415
|
+
const identity = dockerSetupIdentity(options, dependencies);
|
|
416
|
+
const runDocker = dependencies.runDocker ?? runDockerDefault;
|
|
417
|
+
const environmentPresent = (dependencies.pathExists ?? existsSync)(identity.environmentPath);
|
|
418
|
+
const daemon = runDocker(['info', '--format', '{{json .ServerVersion}}']);
|
|
419
|
+
if (daemon.status !== 0) {
|
|
420
|
+
const status = environmentPresent ? 'blocked' : 'absent';
|
|
421
|
+
return Object.freeze({
|
|
422
|
+
schemaVersion: 1,
|
|
423
|
+
command: 'setup.docker.status',
|
|
424
|
+
mutationFree: true,
|
|
425
|
+
status,
|
|
426
|
+
method: 'docker-managed',
|
|
427
|
+
instance: identity.instance,
|
|
428
|
+
origin: identity.origin,
|
|
429
|
+
environmentPath: identity.environmentPath,
|
|
430
|
+
resources: identity.resources,
|
|
431
|
+
components: Object.freeze({
|
|
432
|
+
server: Object.freeze({ state: 'unknown', owned: false }),
|
|
433
|
+
database: Object.freeze({ state: 'unknown', owned: false }),
|
|
434
|
+
network: Object.freeze({ state: 'unknown', owned: false }),
|
|
435
|
+
serverData: Object.freeze({ state: 'unknown', owned: false }),
|
|
436
|
+
databaseData: Object.freeze({ state: 'unknown', owned: false }),
|
|
437
|
+
environment: Object.freeze({ state: environmentPresent ? 'present' : 'absent' }),
|
|
438
|
+
}),
|
|
439
|
+
health: 'not-checked',
|
|
440
|
+
actions: Object.freeze({ start: false, stop: false, restart: false, openCockpit: false, repair: false }),
|
|
441
|
+
nextAction: environmentPresent
|
|
442
|
+
? 'Start Docker Desktop, then refresh.'
|
|
443
|
+
: 'Choose Docker setup to install AOPS.',
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const observed = Object.freeze({
|
|
447
|
+
server: inspectDockerResource(runDocker, 'container', identity.resources.serverContainer),
|
|
448
|
+
database: inspectDockerResource(runDocker, 'container', identity.resources.postgresContainer),
|
|
449
|
+
network: inspectDockerResource(runDocker, 'network', identity.resources.network),
|
|
450
|
+
serverData: inspectDockerResource(runDocker, 'volume', identity.resources.serverDataVolume),
|
|
451
|
+
databaseData: inspectDockerResource(runDocker, 'volume', identity.resources.postgresDataVolume),
|
|
452
|
+
});
|
|
453
|
+
const resources = Object.values(observed);
|
|
454
|
+
const anyPresent = resources.some((resource) => resource.present);
|
|
455
|
+
const allPresent = resources.every((resource) => resource.present);
|
|
456
|
+
const allInspectable = resources.every((resource) => resource.inspectable);
|
|
457
|
+
const allOwned = resources.filter((resource) => resource.present).every((resource) => resource.owned);
|
|
458
|
+
const prefix = [
|
|
459
|
+
observed.network.present,
|
|
460
|
+
observed.serverData.present,
|
|
461
|
+
observed.databaseData.present,
|
|
462
|
+
observed.database.present,
|
|
463
|
+
observed.server.present,
|
|
464
|
+
];
|
|
465
|
+
const validInterruptedPrefix = prefix.every((present, index) => present || prefix.slice(index + 1).every((later) => !later));
|
|
466
|
+
const complete = allPresent && environmentPresent && allInspectable && allOwned;
|
|
467
|
+
const containersRunning = observed.server.state === 'running' && observed.database.state === 'running';
|
|
468
|
+
const containersStopped = ['created', 'exited'].includes(observed.server.state)
|
|
469
|
+
&& ['created', 'exited'].includes(observed.database.state);
|
|
470
|
+
const health = containersRunning
|
|
471
|
+
? (await (dependencies.healthProbe ?? probeHealth)(identity.origin) ? 'healthy' : 'unreachable')
|
|
472
|
+
: 'not-checked';
|
|
473
|
+
let status;
|
|
474
|
+
let nextAction;
|
|
475
|
+
if (!anyPresent && !environmentPresent) {
|
|
476
|
+
status = 'absent';
|
|
477
|
+
nextAction = 'Choose Docker setup to install AOPS.';
|
|
478
|
+
}
|
|
479
|
+
else if (!allInspectable || !allOwned) {
|
|
480
|
+
status = 'blocked';
|
|
481
|
+
nextAction = 'These Docker resources are not safely owned by this AOPS installation.';
|
|
482
|
+
}
|
|
483
|
+
else if (!complete) {
|
|
484
|
+
status = environmentPresent && validInterruptedPrefix ? 'repair-required' : 'blocked';
|
|
485
|
+
nextAction = status === 'repair-required'
|
|
486
|
+
? 'Setup was interrupted. Choose Repair setup to continue without deleting database data.'
|
|
487
|
+
: 'The installation layout is incomplete and cannot be repaired automatically.';
|
|
488
|
+
}
|
|
489
|
+
else if (containersRunning && health === 'healthy') {
|
|
490
|
+
status = 'ready';
|
|
491
|
+
nextAction = null;
|
|
492
|
+
}
|
|
493
|
+
else if (containersStopped) {
|
|
494
|
+
status = 'stopped';
|
|
495
|
+
nextAction = 'Choose Start AOPS.';
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
status = 'attention';
|
|
499
|
+
nextAction = 'Choose Restart AOPS, then refresh.';
|
|
500
|
+
}
|
|
501
|
+
return Object.freeze({
|
|
502
|
+
schemaVersion: 1,
|
|
503
|
+
command: 'setup.docker.status',
|
|
504
|
+
mutationFree: true,
|
|
505
|
+
status,
|
|
506
|
+
method: 'docker-managed',
|
|
507
|
+
instance: identity.instance,
|
|
508
|
+
origin: identity.origin,
|
|
509
|
+
environmentPath: identity.environmentPath,
|
|
510
|
+
resources: identity.resources,
|
|
511
|
+
components: Object.freeze({
|
|
512
|
+
server: Object.freeze({ state: observed.server.state, owned: observed.server.owned }),
|
|
513
|
+
database: Object.freeze({ state: observed.database.state, owned: observed.database.owned }),
|
|
514
|
+
network: Object.freeze({ state: observed.network.state, owned: observed.network.owned }),
|
|
515
|
+
serverData: Object.freeze({ state: observed.serverData.state, owned: observed.serverData.owned }),
|
|
516
|
+
databaseData: Object.freeze({ state: observed.databaseData.state, owned: observed.databaseData.owned }),
|
|
517
|
+
environment: Object.freeze({ state: environmentPresent ? 'present' : 'absent' }),
|
|
518
|
+
}),
|
|
519
|
+
health,
|
|
520
|
+
actions: Object.freeze({
|
|
521
|
+
start: complete && status === 'stopped',
|
|
522
|
+
stop: complete && (observed.server.state === 'running' || observed.database.state === 'running'),
|
|
523
|
+
restart: complete,
|
|
524
|
+
openCockpit: status === 'ready',
|
|
525
|
+
repair: status === 'repair-required',
|
|
526
|
+
}),
|
|
527
|
+
nextAction,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function ensureRepairImage(identity, dependencies, runDocker) {
|
|
531
|
+
let checked = imageCheck(runDocker, identity.image, (dependencies.resolveServerVersion ?? readServerVersion)(), normalizeArchitecture(dependencies.arch ?? process.arch), identity.pullImage);
|
|
532
|
+
if (checked.status === 'info') {
|
|
533
|
+
runDockerRequired(runDocker, ['pull', identity.pullImage], 'image_pull');
|
|
534
|
+
runDockerRequired(runDocker, ['image', 'tag', identity.pullImage, identity.image], 'image_tag');
|
|
535
|
+
checked = imageCheck(runDocker, identity.image, (dependencies.resolveServerVersion ?? readServerVersion)(), normalizeArchitecture(dependencies.arch ?? process.arch), identity.pullImage);
|
|
536
|
+
}
|
|
537
|
+
if (checked.status !== 'pass')
|
|
538
|
+
throw new Error('setup_docker_image_contract_invalid');
|
|
539
|
+
}
|
|
540
|
+
export async function runDockerServerLifecycle(action, options = {}, dependencies = {}) {
|
|
541
|
+
const before = await inspectDockerServerSetupStatus(options, dependencies);
|
|
542
|
+
const allowed = action === 'start' ? before.actions.start
|
|
543
|
+
: action === 'stop' ? before.actions.stop
|
|
544
|
+
: action === 'restart' ? before.actions.restart
|
|
545
|
+
: action === 'open-cockpit' ? before.actions.openCockpit
|
|
546
|
+
: before.actions.repair;
|
|
547
|
+
if (!allowed)
|
|
548
|
+
throw new Error(`setup_docker_${action}_not_available:${before.status}`);
|
|
549
|
+
if (action === 'open-cockpit') {
|
|
550
|
+
await (dependencies.openUrl ?? openExternalUrl)(before.origin);
|
|
551
|
+
return Object.freeze({
|
|
552
|
+
schemaVersion: 1,
|
|
553
|
+
command: 'setup.docker.lifecycle',
|
|
554
|
+
status: 'completed',
|
|
555
|
+
action,
|
|
556
|
+
changed: false,
|
|
557
|
+
instance: before.instance,
|
|
558
|
+
origin: before.origin,
|
|
559
|
+
after: before,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const identity = dockerSetupIdentity(options, dependencies);
|
|
563
|
+
const runDocker = dependencies.runDocker ?? runDockerDefault;
|
|
564
|
+
const sleep = dependencies.sleep ?? delay;
|
|
565
|
+
let changed = false;
|
|
566
|
+
const waitForDatabase = () => waitFor(90_000, sleep, () => {
|
|
567
|
+
const result = runDocker([
|
|
568
|
+
'exec', identity.resources.postgresContainer,
|
|
569
|
+
'pg_isready', '-U', 'aops', '-d', 'aops',
|
|
570
|
+
]);
|
|
571
|
+
return result.status === 0;
|
|
572
|
+
}, 'setup_docker_postgres_timeout');
|
|
573
|
+
const waitForHealth = () => waitFor(180_000, sleep, () => (dependencies.healthProbe ?? probeHealth)(identity.origin), 'setup_docker_health_timeout');
|
|
574
|
+
if (action === 'repair') {
|
|
575
|
+
ensureRepairImage(identity, dependencies, runDocker);
|
|
576
|
+
if (before.components.network.state === 'absent') {
|
|
577
|
+
runDockerRequired(runDocker, ['network', 'create', '--label', MANAGED_LABEL, identity.resources.network], 'network_create');
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
for (const [state, name] of [
|
|
581
|
+
[before.components.serverData.state, identity.resources.serverDataVolume],
|
|
582
|
+
[before.components.databaseData.state, identity.resources.postgresDataVolume],
|
|
583
|
+
]) {
|
|
584
|
+
if (state === 'absent') {
|
|
585
|
+
runDockerRequired(runDocker, ['volume', 'create', '--label', MANAGED_LABEL, name], 'volume_create');
|
|
586
|
+
changed = true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (before.components.database.state === 'absent') {
|
|
590
|
+
runDockerRequired(runDocker, [
|
|
591
|
+
'run', '--detach', '--name', identity.resources.postgresContainer,
|
|
592
|
+
'--label', MANAGED_LABEL,
|
|
593
|
+
'--restart', 'unless-stopped',
|
|
594
|
+
'--network', identity.resources.network,
|
|
595
|
+
'--network-alias', 'postgres',
|
|
596
|
+
'--env-file', identity.environmentPath,
|
|
597
|
+
'--volume', `${identity.resources.postgresDataVolume}:/var/lib/postgresql/data`,
|
|
598
|
+
POSTGRES_IMAGE,
|
|
599
|
+
], 'postgres_create');
|
|
600
|
+
changed = true;
|
|
601
|
+
}
|
|
602
|
+
else if (before.components.database.state !== 'running') {
|
|
603
|
+
runDockerRequired(runDocker, ['start', identity.resources.postgresContainer], 'postgres_start');
|
|
604
|
+
changed = true;
|
|
605
|
+
}
|
|
606
|
+
await waitForDatabase();
|
|
607
|
+
if (before.components.server.state === 'absent') {
|
|
608
|
+
runDockerRequired(runDocker, [
|
|
609
|
+
'run', '--detach', '--name', identity.resources.serverContainer,
|
|
610
|
+
'--label', MANAGED_LABEL,
|
|
611
|
+
'--restart', 'unless-stopped',
|
|
612
|
+
'--network', identity.resources.network,
|
|
613
|
+
'--publish', `127.0.0.1:${identity.port}:5900`,
|
|
614
|
+
'--env-file', identity.environmentPath,
|
|
615
|
+
'--volume', `${identity.resources.serverDataVolume}:/var/lib/aops`,
|
|
616
|
+
identity.image,
|
|
617
|
+
], 'server_create');
|
|
618
|
+
changed = true;
|
|
619
|
+
}
|
|
620
|
+
else if (before.components.server.state !== 'running') {
|
|
621
|
+
runDockerRequired(runDocker, ['start', identity.resources.serverContainer], 'server_start');
|
|
622
|
+
changed = true;
|
|
623
|
+
}
|
|
624
|
+
await waitForHealth();
|
|
625
|
+
}
|
|
626
|
+
else if (action === 'stop') {
|
|
627
|
+
if (before.components.server.state === 'running') {
|
|
628
|
+
runDockerRequired(runDocker, ['stop', identity.resources.serverContainer], 'server_stop');
|
|
629
|
+
changed = true;
|
|
630
|
+
}
|
|
631
|
+
if (before.components.database.state === 'running') {
|
|
632
|
+
runDockerRequired(runDocker, ['stop', identity.resources.postgresContainer], 'postgres_stop');
|
|
633
|
+
changed = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (action === 'start') {
|
|
637
|
+
if (before.components.database.state !== 'running') {
|
|
638
|
+
runDockerRequired(runDocker, ['start', identity.resources.postgresContainer], 'postgres_start');
|
|
639
|
+
changed = true;
|
|
640
|
+
}
|
|
641
|
+
await waitForDatabase();
|
|
642
|
+
if (before.components.server.state !== 'running') {
|
|
643
|
+
runDockerRequired(runDocker, ['start', identity.resources.serverContainer], 'server_start');
|
|
644
|
+
changed = true;
|
|
645
|
+
}
|
|
646
|
+
await waitForHealth();
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
runDockerRequired(runDocker, ['restart', identity.resources.postgresContainer], 'postgres_restart');
|
|
650
|
+
await waitForDatabase();
|
|
651
|
+
runDockerRequired(runDocker, ['restart', identity.resources.serverContainer], 'server_restart');
|
|
652
|
+
await waitForHealth();
|
|
653
|
+
changed = true;
|
|
654
|
+
}
|
|
655
|
+
const after = await inspectDockerServerSetupStatus(options, dependencies);
|
|
656
|
+
return Object.freeze({
|
|
657
|
+
schemaVersion: 1,
|
|
658
|
+
command: 'setup.docker.lifecycle',
|
|
659
|
+
status: 'completed',
|
|
660
|
+
action,
|
|
661
|
+
changed,
|
|
662
|
+
instance: after.instance,
|
|
663
|
+
origin: after.origin,
|
|
664
|
+
after,
|
|
665
|
+
});
|
|
666
|
+
}
|
package/dist/lib/tui-launcher.js
CHANGED
|
@@ -3,10 +3,6 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
const SUPPORTED_TUI_TARGETS = new Set([
|
|
6
|
-
'darwin-arm64',
|
|
7
|
-
'darwin-x64',
|
|
8
|
-
'linux-arm64',
|
|
9
|
-
'linux-x64',
|
|
10
6
|
'win32-x64',
|
|
11
7
|
]);
|
|
12
8
|
function packageRootFromModule() {
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aopslabs/aops",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AOPS CLI and terminal setup application.",
|
|
6
|
+
"aopsDockerServerVersion": "0.2.2",
|
|
6
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"test:agent-assets-content": "node --test test/agent-assets-content.test.mjs",
|
|
20
21
|
"test:cockpit": "node --test test/cockpit.test.mjs",
|
|
21
22
|
"test:setup-install": "node --test test/setup-install.test.mjs",
|
|
23
|
+
"test:setup-docker": "node --test test/setup-docker-server.test.mjs",
|
|
22
24
|
"test:native-lifecycle": "node --test test/community-native-lifecycle.test.mjs",
|
|
23
25
|
"test:operation-journal": "node --test test/community-operation-journal.test.mjs",
|
|
24
26
|
"test:commercial-artifact": "node --test test/commercial-artifact.test.mjs",
|