@jacebenson/jsn 4.3.7-dev.22 → 4.3.7-dev.24
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/package.json +1 -1
- package/src/commands/midservers.js +44 -10
- package/src/midserver-install.js +41 -14
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { confirmDelete, formatRecordForDisplay, getStringField, interactiveList,
|
|
|
3
3
|
import { FormatAuto } from '../output.js';
|
|
4
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
5
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
6
|
-
import {
|
|
6
|
+
import { detectDocker, installDockerMidServer, validateMidCredentials } from '../midserver-install.js';
|
|
7
7
|
|
|
8
8
|
const AGENT_TABLE = 'ecc_agent';
|
|
9
9
|
const QUEUE_TABLE = 'ecc_queue';
|
|
@@ -235,23 +235,30 @@ export function midserversCmd(wrap) {
|
|
|
235
235
|
message: 'MID Server name:',
|
|
236
236
|
validate: value => value.trim() ? true : 'MID Server name is required',
|
|
237
237
|
})).trim();
|
|
238
|
-
const
|
|
238
|
+
const dockerStatus = detectDocker();
|
|
239
|
+
const dockerAvailable = dockerStatus.available;
|
|
239
240
|
const installMethod = argv['install-method'] || await select({
|
|
240
241
|
message: dockerAvailable
|
|
241
|
-
? 'Docker and Docker Compose
|
|
242
|
-
:
|
|
242
|
+
? 'Docker and Docker Compose are ready. How should the MID Server be installed?'
|
|
243
|
+
: dockerStatus.installed && dockerStatus.compose
|
|
244
|
+
? 'Docker is installed, but this user cannot access the Docker daemon. How should the MID Server be installed?'
|
|
245
|
+
: 'Docker was not detected. How should the MID Server be installed?',
|
|
243
246
|
choices: dockerAvailable
|
|
244
247
|
? [
|
|
245
248
|
{ name: 'Docker (recommended)', value: 'docker' },
|
|
246
249
|
{ name: 'Direct install (not available yet)', value: 'direct' },
|
|
247
250
|
]
|
|
251
|
+
: dockerStatus.installed && dockerStatus.compose
|
|
252
|
+
? [
|
|
253
|
+
{ name: 'Prepare files now, run Docker later', value: 'prepare' },
|
|
254
|
+
{ name: 'Direct install (not available yet)', value: 'direct' },
|
|
255
|
+
]
|
|
248
256
|
: [
|
|
249
|
-
{ name: '
|
|
257
|
+
{ name: 'Prepare files now, run Docker later', value: 'prepare' },
|
|
250
258
|
{ name: 'Direct install (not available yet)', value: 'direct' },
|
|
251
259
|
],
|
|
252
260
|
});
|
|
253
261
|
if (installMethod === 'direct') throw new Error('Direct MID Server installation is not implemented yet. Choose Docker.');
|
|
254
|
-
if (installMethod === 'install-docker') throw new Error('Automatic Docker installation is not implemented yet. Install Docker and Compose, then run this command again.');
|
|
255
262
|
|
|
256
263
|
const installation = await installDockerMidServer({
|
|
257
264
|
instanceURL: app.getEffectiveInstance(),
|
|
@@ -260,7 +267,12 @@ export function midserversCmd(wrap) {
|
|
|
260
267
|
name,
|
|
261
268
|
releaseName: credentials.releaseName,
|
|
262
269
|
directory: argv.directory,
|
|
270
|
+
runNow: installMethod !== 'prepare',
|
|
263
271
|
});
|
|
272
|
+
if (installation.prepared) {
|
|
273
|
+
app.ok(installation, { summary: `Prepared MID Server files in ${installation.installDir}. Run: ${installation.runCommand}` });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
264
276
|
let record;
|
|
265
277
|
try {
|
|
266
278
|
record = await waitForMidServerUp(app, installation.name, Math.max(1, argv['wait-timeout']), Math.max(500, argv['wait-interval']));
|
|
@@ -277,14 +289,36 @@ export function midserversCmd(wrap) {
|
|
|
277
289
|
}),
|
|
278
290
|
})
|
|
279
291
|
.command({
|
|
280
|
-
command: 'remove
|
|
292
|
+
command: 'remove [identifier]',
|
|
281
293
|
aliases: ['delete'],
|
|
282
|
-
describe: 'Remove a MID Server record',
|
|
294
|
+
describe: 'Remove a MID Server record (interactive when no identifier is given)',
|
|
283
295
|
builder: (b) => b.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
284
296
|
handler: wrap(async (argv, app) => {
|
|
285
|
-
|
|
297
|
+
let record;
|
|
298
|
+
if (argv.identifier) {
|
|
299
|
+
record = await resolveMidServer(app.sdk, argv.identifier);
|
|
300
|
+
} else {
|
|
301
|
+
if (!canPrompt() || app.output.getFormat() !== FormatAuto) {
|
|
302
|
+
throw new Error('An identifier is required outside an interactive terminal');
|
|
303
|
+
}
|
|
304
|
+
record = await interactiveList({
|
|
305
|
+
app,
|
|
306
|
+
table: AGENT_TABLE,
|
|
307
|
+
singular: 'MID Server',
|
|
308
|
+
columns: LIST_FIELDS,
|
|
309
|
+
limit: 50,
|
|
310
|
+
labelField: 'name',
|
|
311
|
+
message: 'Select a MID Server to remove',
|
|
312
|
+
formatLabel: (candidate) => {
|
|
313
|
+
const name = getStringField(candidate, 'name') || getStringField(candidate, 'sys_id');
|
|
314
|
+
const status = getStringField(candidate, 'status');
|
|
315
|
+
return status ? `${name} [${status}]` : name;
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
if (!record) return;
|
|
319
|
+
}
|
|
286
320
|
const sysId = unwrapSysId(record);
|
|
287
|
-
const name = getStringField(record, 'name') || argv.identifier;
|
|
321
|
+
const name = getStringField(record, 'name') || argv.identifier || sysId;
|
|
288
322
|
await confirmDelete(app, argv, `Remove MID Server '${name}'`);
|
|
289
323
|
await app.sdk.delete(AGENT_TABLE, sysId);
|
|
290
324
|
app.ok({ name, sys_id: sysId, deleted: true }, { summary: `Removed MID Server '${name}'` });
|
package/src/midserver-install.js
CHANGED
|
@@ -63,18 +63,30 @@ export async function validateMidCredentials(instanceURL, username, password, fe
|
|
|
63
63
|
return { releaseName };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export function detectDocker(run = defaultRun) {
|
|
67
|
+
const status = { installed: false, compose: false, daemon: false, available: false };
|
|
68
|
+
try { run('docker', ['--version'], { stdio: 'ignore' }); status.installed = true; } catch { return status; }
|
|
69
|
+
try { run('docker', ['compose', 'version'], { stdio: 'ignore' }); status.compose = true; } catch { return status; }
|
|
70
|
+
try { run('docker', ['info'], { stdio: 'ignore' }); status.daemon = true; } catch { return status; }
|
|
71
|
+
status.available = true;
|
|
72
|
+
return status;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
export function dockerComposeAvailable(run = defaultRun) {
|
|
67
|
-
|
|
68
|
-
run('docker', ['info'], { stdio: 'ignore' });
|
|
69
|
-
run('docker', ['compose', 'version'], { stdio: 'ignore' });
|
|
70
|
-
return true;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
76
|
+
return detectDocker(run).available;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function composeFile({ serviceName }) {
|
|
77
|
-
return `services:\n ${serviceName}:\n container_name: ${serviceName}\n image: ${serviceName}:\${MID_RELEASE}\n restart: unless-stopped\n volumes:\n - ./export:/opt/snc_mid_server/agent/export\n environment:\n MID_INSTANCE_URL: \${MID_INSTANCE_URL}\n MID_INSTANCE_USERNAME: \${MID_INSTANCE_USERNAME}\n MID_INSTANCE_PASSWORD: \${MID_INSTANCE_PASSWORD}\n MID_SERVER_NAME: \${MID_SERVER_NAME}\n`;
|
|
80
|
+
return `services:\n ${serviceName}:\n container_name: ${serviceName}\n build: ./recipe\n image: ${serviceName}:\${MID_RELEASE}\n restart: unless-stopped\n volumes:\n - ./export:/opt/snc_mid_server/agent/export\n environment:\n MID_INSTANCE_URL: \${MID_INSTANCE_URL}\n MID_INSTANCE_USERNAME: \${MID_INSTANCE_USERNAME}\n MID_INSTANCE_PASSWORD: \${MID_INSTANCE_PASSWORD}\n MID_SERVER_NAME: \${MID_SERVER_NAME}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function envFile({ instanceURL, username, password, displayName, releaseName }) {
|
|
84
|
+
return `MID_RELEASE=${releaseName}\nMID_INSTANCE_URL=${String(instanceURL).replace(/\/$/, '')}/\nMID_INSTANCE_USERNAME=${username}\nMID_INSTANCE_PASSWORD=${password}\nMID_SERVER_NAME=${displayName}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function instanceName(instanceURL) {
|
|
88
|
+
const hostname = new URL(instanceURL).hostname;
|
|
89
|
+
return hostname.replace(/\.service-now\.com$|\.servicenowservices\.com$/i, '').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
export async function installDockerMidServer({
|
|
@@ -84,17 +96,17 @@ export async function installDockerMidServer({
|
|
|
84
96
|
name,
|
|
85
97
|
directory,
|
|
86
98
|
releaseName: suppliedReleaseName,
|
|
99
|
+
runNow = true,
|
|
87
100
|
fetchImpl = fetch,
|
|
88
101
|
run = defaultRun,
|
|
89
102
|
}) {
|
|
90
|
-
|
|
91
|
-
throw new Error('Docker and Docker Compose are required for Docker MID Server installation');
|
|
92
|
-
}
|
|
103
|
+
const dockerAvailable = detectDocker(run).available;
|
|
93
104
|
const shortName = normalizeMidName(name);
|
|
94
105
|
const displayName = `mid.server.${shortName}`;
|
|
95
106
|
const releaseName = suppliedReleaseName || (await validateMidCredentials(instanceURL, username, password, fetchImpl)).releaseName;
|
|
96
107
|
const recipeURL = buildMidRecipeUrl(releaseName);
|
|
97
|
-
const
|
|
108
|
+
const serviceNowInstance = instanceName(instanceURL);
|
|
109
|
+
const installDir = directory || path.join(os.homedir(), '.servicenow', 'mids', serviceNowInstance, shortName);
|
|
98
110
|
fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
|
|
99
111
|
const exportDir = path.join(installDir, 'export');
|
|
100
112
|
fs.mkdirSync(exportDir, { recursive: true, mode: 0o700 });
|
|
@@ -107,9 +119,8 @@ export async function installDockerMidServer({
|
|
|
107
119
|
fs.mkdirSync(recipeDir, { recursive: true, mode: 0o700 });
|
|
108
120
|
run('unzip', ['-q', archive, '-d', recipeDir]);
|
|
109
121
|
const composePath = path.join(installDir, 'docker-compose.yaml');
|
|
122
|
+
const persistedRecipeDir = path.join(installDir, 'recipe');
|
|
110
123
|
const serviceName = `jsn_mid_${shortName.replace(/[^A-Za-z0-9_.-]/g, '_')}`;
|
|
111
|
-
run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, recipeDir]);
|
|
112
|
-
fs.writeFileSync(composePath, composeFile({ serviceName, instanceURL, displayName }), { mode: 0o600 });
|
|
113
124
|
const env = {
|
|
114
125
|
MID_RELEASE: releaseName,
|
|
115
126
|
MID_INSTANCE_URL: `${String(instanceURL).replace(/\/$/, '')}/`,
|
|
@@ -117,6 +128,22 @@ export async function installDockerMidServer({
|
|
|
117
128
|
MID_INSTANCE_PASSWORD: password,
|
|
118
129
|
MID_SERVER_NAME: displayName,
|
|
119
130
|
};
|
|
131
|
+
fs.rmSync(persistedRecipeDir, { recursive: true, force: true });
|
|
132
|
+
fs.cpSync(recipeDir, persistedRecipeDir, { recursive: true });
|
|
133
|
+
fs.writeFileSync(composePath, composeFile({ serviceName }), { mode: 0o600 });
|
|
134
|
+
fs.writeFileSync(path.join(installDir, '.env'), envFile({ instanceURL, username, password, displayName, releaseName }), { mode: 0o600 });
|
|
135
|
+
if (!dockerAvailable || !runNow) {
|
|
136
|
+
return {
|
|
137
|
+
name: displayName,
|
|
138
|
+
releaseName,
|
|
139
|
+
installDir,
|
|
140
|
+
composePath,
|
|
141
|
+
serviceName,
|
|
142
|
+
prepared: true,
|
|
143
|
+
runCommand: `docker compose -f ${composePath} up -d`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, persistedRecipeDir]);
|
|
120
147
|
run('docker', ['compose', '-f', composePath, 'up', '-d'], { cwd: installDir, env });
|
|
121
148
|
return {
|
|
122
149
|
name: displayName,
|