@jacebenson/jsn 4.3.7-dev.23 → 4.3.7-dev.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.23",
3
+ "version": "4.3.7-dev.25",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -250,17 +250,15 @@ export function midserversCmd(wrap) {
250
250
  ]
251
251
  : dockerStatus.installed && dockerStatus.compose
252
252
  ? [
253
- { name: 'Fix Docker daemon access first', value: 'docker-unavailable' },
253
+ { name: 'Prepare files now, run Docker later', value: 'prepare' },
254
254
  { name: 'Direct install (not available yet)', value: 'direct' },
255
255
  ]
256
256
  : [
257
- { name: 'Install Docker first', value: 'install-docker' },
257
+ { name: 'Prepare files now, run Docker later', value: 'prepare' },
258
258
  { name: 'Direct install (not available yet)', value: 'direct' },
259
259
  ],
260
260
  });
261
261
  if (installMethod === 'direct') throw new Error('Direct MID Server installation is not implemented yet. Choose Docker.');
262
- if (installMethod === 'docker-unavailable') throw new Error('Docker is installed, but this user cannot access the Docker daemon. Start Docker and grant this user access to /var/run/docker.sock, then run this command again.');
263
- if (installMethod === 'install-docker') throw new Error('Automatic Docker installation is not implemented yet. Install Docker and Compose, then run this command again.');
264
262
 
265
263
  const installation = await installDockerMidServer({
266
264
  instanceURL: app.getEffectiveInstance(),
@@ -269,7 +267,12 @@ export function midserversCmd(wrap) {
269
267
  name,
270
268
  releaseName: credentials.releaseName,
271
269
  directory: argv.directory,
270
+ runNow: installMethod !== 'prepare',
272
271
  });
272
+ if (installation.prepared) {
273
+ app.ok(installation, { summary: `Prepared MID Server files in ${installation.installDir}. Run: ${installation.runCommand}` });
274
+ return;
275
+ }
273
276
  let record;
274
277
  try {
275
278
  record = await waitForMidServerUp(app, installation.name, Math.max(1, argv['wait-timeout']), Math.max(500, argv['wait-interval']));
@@ -28,7 +28,8 @@ export function normalizeMidName(name) {
28
28
  }
29
29
 
30
30
  export function buildMidRecipeUrl(releaseName) {
31
- const match = String(releaseName).match(/(\d{4})-(\d{2})-(\d{2})|(\d{2})-(\d{2})-(\d{4})/);
31
+ const matches = [...String(releaseName).matchAll(/(\d{4})-(\d{2})-(\d{2})|(\d{2})-(\d{2})-(\d{4})/g)];
32
+ const match = matches.at(-1);
32
33
  if (!match) throw new Error(`MID release name has no build date: ${releaseName}`);
33
34
  const year = match[1] || match[6];
34
35
  const month = match[2] || match[4];
@@ -77,7 +78,16 @@ export function dockerComposeAvailable(run = defaultRun) {
77
78
  }
78
79
 
79
80
  function composeFile({ serviceName }) {
80
- 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`;
81
+ 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`;
82
+ }
83
+
84
+ function envFile({ instanceURL, username, password, displayName, releaseName }) {
85
+ return `MID_RELEASE=${releaseName}\nMID_INSTANCE_URL=${String(instanceURL).replace(/\/$/, '')}/\nMID_INSTANCE_USERNAME=${username}\nMID_INSTANCE_PASSWORD=${password}\nMID_SERVER_NAME=${displayName}\n`;
86
+ }
87
+
88
+ function instanceName(instanceURL) {
89
+ const hostname = new URL(instanceURL).hostname;
90
+ return hostname.replace(/\.service-now\.com$|\.servicenowservices\.com$/i, '').replace(/[^A-Za-z0-9._-]/g, '_');
81
91
  }
82
92
 
83
93
  export async function installDockerMidServer({
@@ -87,17 +97,17 @@ export async function installDockerMidServer({
87
97
  name,
88
98
  directory,
89
99
  releaseName: suppliedReleaseName,
100
+ runNow = true,
90
101
  fetchImpl = fetch,
91
102
  run = defaultRun,
92
103
  }) {
93
- if (!dockerComposeAvailable(run)) {
94
- throw new Error('Docker and Docker Compose are required for Docker MID Server installation');
95
- }
104
+ const dockerAvailable = detectDocker(run).available;
96
105
  const shortName = normalizeMidName(name);
97
106
  const displayName = `mid.server.${shortName}`;
98
107
  const releaseName = suppliedReleaseName || (await validateMidCredentials(instanceURL, username, password, fetchImpl)).releaseName;
99
108
  const recipeURL = buildMidRecipeUrl(releaseName);
100
- const installDir = directory || path.join(os.homedir(), '.local', 'share', 'jsn', 'midservers', shortName);
109
+ const serviceNowInstance = instanceName(instanceURL);
110
+ const installDir = directory || path.join(os.homedir(), '.servicenow', 'mids', serviceNowInstance, shortName);
101
111
  fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
102
112
  const exportDir = path.join(installDir, 'export');
103
113
  fs.mkdirSync(exportDir, { recursive: true, mode: 0o700 });
@@ -110,9 +120,8 @@ export async function installDockerMidServer({
110
120
  fs.mkdirSync(recipeDir, { recursive: true, mode: 0o700 });
111
121
  run('unzip', ['-q', archive, '-d', recipeDir]);
112
122
  const composePath = path.join(installDir, 'docker-compose.yaml');
123
+ const persistedRecipeDir = path.join(installDir, 'recipe');
113
124
  const serviceName = `jsn_mid_${shortName.replace(/[^A-Za-z0-9_.-]/g, '_')}`;
114
- run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, recipeDir]);
115
- fs.writeFileSync(composePath, composeFile({ serviceName, instanceURL, displayName }), { mode: 0o600 });
116
125
  const env = {
117
126
  MID_RELEASE: releaseName,
118
127
  MID_INSTANCE_URL: `${String(instanceURL).replace(/\/$/, '')}/`,
@@ -120,6 +129,25 @@ export async function installDockerMidServer({
120
129
  MID_INSTANCE_PASSWORD: password,
121
130
  MID_SERVER_NAME: displayName,
122
131
  };
132
+ fs.rmSync(persistedRecipeDir, { recursive: true, force: true });
133
+ fs.cpSync(recipeDir, persistedRecipeDir, { recursive: true });
134
+ fs.writeFileSync(composePath, composeFile({ serviceName }), { mode: 0o600 });
135
+ fs.chmodSync(composePath, 0o600);
136
+ const envPath = path.join(installDir, '.env');
137
+ fs.writeFileSync(envPath, envFile({ instanceURL, username, password, displayName, releaseName }), { mode: 0o600 });
138
+ fs.chmodSync(envPath, 0o600);
139
+ if (!dockerAvailable || !runNow) {
140
+ return {
141
+ name: displayName,
142
+ releaseName,
143
+ installDir,
144
+ composePath,
145
+ serviceName,
146
+ prepared: true,
147
+ runCommand: `docker compose -f ${composePath} up -d`,
148
+ };
149
+ }
150
+ run('docker', ['build', '--tag', `${serviceName}:${releaseName}`, persistedRecipeDir]);
123
151
  run('docker', ['compose', '-f', composePath, 'up', '-d'], { cwd: installDir, env });
124
152
  return {
125
153
  name: displayName,