@zincapp/znvault-plugin-payara 1.0.6 → 1.3.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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +656 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +209 -5
- package/dist/index.js.map +1 -1
- package/dist/payara-manager.d.ts +15 -0
- package/dist/payara-manager.d.ts.map +1 -1
- package/dist/payara-manager.js +75 -9
- package/dist/payara-manager.js.map +1 -1
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,98 @@
|
|
|
1
1
|
// Path: src/cli.ts
|
|
2
2
|
// CLI commands for Payara plugin
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { stat } from 'node:fs/promises';
|
|
4
|
+
import { stat, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { homedir, hostname } from 'node:os';
|
|
5
8
|
import AdmZip from 'adm-zip';
|
|
9
|
+
// Config file path
|
|
10
|
+
const CONFIG_DIR = join(homedir(), '.znvault');
|
|
11
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'deploy-configs.json');
|
|
12
|
+
/**
|
|
13
|
+
* Load deployment configs
|
|
14
|
+
*/
|
|
15
|
+
async function loadDeployConfigs() {
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(CONFIG_FILE)) {
|
|
18
|
+
const content = await readFile(CONFIG_FILE, 'utf-8');
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore parse errors
|
|
24
|
+
}
|
|
25
|
+
return { configs: {} };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Save deployment configs
|
|
29
|
+
*/
|
|
30
|
+
async function saveDeployConfigs(store) {
|
|
31
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
32
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
await writeFile(CONFIG_FILE, JSON.stringify(store, null, 2));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Deploy to a single host
|
|
38
|
+
*/
|
|
39
|
+
async function deployToHost(ctx, host, port, warPath, localHashes, force) {
|
|
40
|
+
try {
|
|
41
|
+
const baseUrl = host.replace(/\/$/, '');
|
|
42
|
+
// Add protocol if missing
|
|
43
|
+
const fullUrl = baseUrl.startsWith('http') ? baseUrl : `https://${baseUrl}`;
|
|
44
|
+
const pluginUrl = `${fullUrl}:${port}/plugins/payara`;
|
|
45
|
+
// Get remote hashes
|
|
46
|
+
let remoteHashes = {};
|
|
47
|
+
if (!force) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await ctx.client.get(`${pluginUrl}/hashes`);
|
|
50
|
+
remoteHashes = response.hashes;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Full deployment if can't get hashes
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Calculate diff
|
|
57
|
+
const { changed, deleted } = calculateDiff(localHashes, remoteHashes);
|
|
58
|
+
if (changed.length === 0 && deleted.length === 0) {
|
|
59
|
+
return { success: true, filesChanged: 0, filesDeleted: 0 };
|
|
60
|
+
}
|
|
61
|
+
// Prepare files for upload
|
|
62
|
+
const zip = new AdmZip(warPath);
|
|
63
|
+
const files = changed.map(path => {
|
|
64
|
+
const entry = zip.getEntry(path);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
throw new Error(`Entry not found in WAR: ${path}`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
path,
|
|
70
|
+
content: entry.getData().toString('base64'),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
// Deploy
|
|
74
|
+
const deployResponse = await ctx.client.post(`${pluginUrl}/deploy`, {
|
|
75
|
+
files,
|
|
76
|
+
deletions: deleted,
|
|
77
|
+
});
|
|
78
|
+
if (deployResponse.status === 'deployed') {
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
filesChanged: deployResponse.filesChanged,
|
|
82
|
+
filesDeleted: deployResponse.filesDeleted,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return { success: false, error: deployResponse.message };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: err instanceof Error ? err.message : String(err),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
6
96
|
/**
|
|
7
97
|
* Payara CLI plugin
|
|
8
98
|
*
|
|
@@ -11,17 +101,498 @@ import AdmZip from 'adm-zip';
|
|
|
11
101
|
export function createPayaraCLIPlugin() {
|
|
12
102
|
return {
|
|
13
103
|
name: 'payara',
|
|
14
|
-
version: '1.
|
|
104
|
+
version: '1.1.0',
|
|
15
105
|
description: 'Payara WAR deployment commands',
|
|
16
106
|
registerCommands(program, ctx) {
|
|
17
107
|
// Create deploy command group
|
|
18
108
|
const deploy = program
|
|
19
109
|
.command('deploy')
|
|
20
110
|
.description('Deploy WAR files to remote Payara servers');
|
|
21
|
-
//
|
|
111
|
+
// ========================================================================
|
|
112
|
+
// deploy <config-name> - Deploy using saved configuration
|
|
113
|
+
// ========================================================================
|
|
114
|
+
deploy
|
|
115
|
+
.command('run <configName>')
|
|
116
|
+
.alias('to')
|
|
117
|
+
.description('Deploy WAR to all hosts in a saved configuration')
|
|
118
|
+
.option('-f, --force', 'Force full deployment (no diff)')
|
|
119
|
+
.option('--dry-run', 'Show what would be deployed without deploying')
|
|
120
|
+
.option('--sequential', 'Deploy to hosts one at a time (override parallel setting)')
|
|
121
|
+
.action(async (configName, options) => {
|
|
122
|
+
try {
|
|
123
|
+
const store = await loadDeployConfigs();
|
|
124
|
+
const config = store.configs[configName];
|
|
125
|
+
if (!config) {
|
|
126
|
+
ctx.output.error(`Deployment config '${configName}' not found`);
|
|
127
|
+
ctx.output.info('Use "znvault deploy config list" to see available configs');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (config.hosts.length === 0) {
|
|
131
|
+
ctx.output.error('No hosts configured for this deployment');
|
|
132
|
+
ctx.output.info(`Use "znvault deploy config add-host ${configName} <host>" to add hosts`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
// Resolve WAR path
|
|
136
|
+
const warPath = resolve(config.warPath);
|
|
137
|
+
try {
|
|
138
|
+
await stat(warPath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
ctx.output.error(`WAR file not found: ${warPath}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
ctx.output.info(`Deploying ${configName}`);
|
|
145
|
+
ctx.output.info(` WAR: ${warPath}`);
|
|
146
|
+
ctx.output.info(` Hosts: ${config.hosts.length}`);
|
|
147
|
+
ctx.output.info(` Mode: ${options.sequential || !config.parallel ? 'sequential' : 'parallel'}`);
|
|
148
|
+
console.log();
|
|
149
|
+
// Calculate local hashes once
|
|
150
|
+
ctx.output.info('Analyzing WAR file...');
|
|
151
|
+
const localHashes = await calculateWarHashes(warPath);
|
|
152
|
+
ctx.output.info(`Found ${Object.keys(localHashes).length} files`);
|
|
153
|
+
console.log();
|
|
154
|
+
if (options.dryRun) {
|
|
155
|
+
ctx.output.info('Dry run - checking each host:');
|
|
156
|
+
for (const host of config.hosts) {
|
|
157
|
+
console.log(` - ${host}`);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const results = [];
|
|
162
|
+
const deployToHostWrapper = async (host) => {
|
|
163
|
+
ctx.output.info(`Deploying to ${host}...`);
|
|
164
|
+
const result = await deployToHost(ctx, host, config.port, warPath, localHashes, options.force ?? false);
|
|
165
|
+
results.push({
|
|
166
|
+
host,
|
|
167
|
+
success: result.success,
|
|
168
|
+
error: result.error,
|
|
169
|
+
changed: result.filesChanged,
|
|
170
|
+
deleted: result.filesDeleted,
|
|
171
|
+
});
|
|
172
|
+
if (result.success) {
|
|
173
|
+
ctx.output.success(` ✓ ${host}: ${result.filesChanged} changed, ${result.filesDeleted} deleted`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
ctx.output.error(` ✗ ${host}: ${result.error}`);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
if (options.sequential || !config.parallel) {
|
|
180
|
+
// Sequential deployment
|
|
181
|
+
for (const host of config.hosts) {
|
|
182
|
+
await deployToHostWrapper(host);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Parallel deployment
|
|
187
|
+
await Promise.all(config.hosts.map(deployToHostWrapper));
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
const successful = results.filter(r => r.success).length;
|
|
191
|
+
const failed = results.filter(r => !r.success).length;
|
|
192
|
+
if (failed === 0) {
|
|
193
|
+
ctx.output.success(`Deployment complete: ${successful}/${config.hosts.length} hosts successful`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
ctx.output.warn(`Deployment complete: ${successful}/${config.hosts.length} hosts successful, ${failed} failed`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
ctx.output.error(`Deployment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// ========================================================================
|
|
206
|
+
// deploy config - Manage deployment configurations
|
|
207
|
+
// ========================================================================
|
|
208
|
+
const configCmd = deploy
|
|
209
|
+
.command('config')
|
|
210
|
+
.description('Manage deployment configurations');
|
|
211
|
+
// deploy config create <name>
|
|
212
|
+
configCmd
|
|
213
|
+
.command('create <name>')
|
|
214
|
+
.description('Create a new deployment configuration')
|
|
215
|
+
.option('-w, --war <path>', 'Path to WAR file')
|
|
216
|
+
.option('-H, --host <host>', 'Add a host (can be used multiple times)', (val, arr) => [...arr, val], [])
|
|
217
|
+
.option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
|
|
218
|
+
.option('--parallel', 'Deploy to all hosts in parallel (default)')
|
|
219
|
+
.option('--sequential', 'Deploy to hosts one at a time')
|
|
220
|
+
.option('-d, --description <text>', 'Description for this config')
|
|
221
|
+
.action(async (name, options) => {
|
|
222
|
+
try {
|
|
223
|
+
const store = await loadDeployConfigs();
|
|
224
|
+
if (store.configs[name]) {
|
|
225
|
+
ctx.output.error(`Config '${name}' already exists. Use "znvault deploy config delete ${name}" first.`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
const config = {
|
|
229
|
+
name,
|
|
230
|
+
hosts: options.host,
|
|
231
|
+
warPath: options.war ?? '',
|
|
232
|
+
port: parseInt(options.port, 10),
|
|
233
|
+
parallel: !options.sequential,
|
|
234
|
+
description: options.description,
|
|
235
|
+
};
|
|
236
|
+
store.configs[name] = config;
|
|
237
|
+
await saveDeployConfigs(store);
|
|
238
|
+
ctx.output.success(`Created deployment config: ${name}`);
|
|
239
|
+
if (config.hosts.length === 0) {
|
|
240
|
+
ctx.output.info(`Add hosts with: znvault deploy config add-host ${name} <host>`);
|
|
241
|
+
}
|
|
242
|
+
if (!config.warPath) {
|
|
243
|
+
ctx.output.info(`Set WAR path with: znvault deploy config set ${name} war /path/to/app.war`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
ctx.output.error(`Failed to create config: ${err instanceof Error ? err.message : String(err)}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// deploy config list
|
|
252
|
+
configCmd
|
|
253
|
+
.command('list')
|
|
254
|
+
.alias('ls')
|
|
255
|
+
.description('List all deployment configurations')
|
|
256
|
+
.option('--json', 'Output as JSON')
|
|
257
|
+
.action(async (options) => {
|
|
258
|
+
try {
|
|
259
|
+
const store = await loadDeployConfigs();
|
|
260
|
+
const configs = Object.values(store.configs);
|
|
261
|
+
if (configs.length === 0) {
|
|
262
|
+
if (options.json) {
|
|
263
|
+
console.log(JSON.stringify([], null, 2));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
ctx.output.info('No deployment configurations found.');
|
|
267
|
+
ctx.output.info('Create one with: znvault deploy config create <name>');
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (options.json) {
|
|
272
|
+
console.log(JSON.stringify(configs, null, 2));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
console.log('Deployment Configurations:\n');
|
|
276
|
+
for (const config of configs) {
|
|
277
|
+
console.log(` ${config.name}`);
|
|
278
|
+
if (config.description) {
|
|
279
|
+
console.log(` ${config.description}`);
|
|
280
|
+
}
|
|
281
|
+
console.log(` Hosts: ${config.hosts.length > 0 ? config.hosts.join(', ') : '(none)'}`);
|
|
282
|
+
console.log(` WAR: ${config.warPath || '(not set)'}`);
|
|
283
|
+
console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
ctx.output.error(`Failed to list configs: ${err instanceof Error ? err.message : String(err)}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// deploy config show <name>
|
|
293
|
+
configCmd
|
|
294
|
+
.command('show <name>')
|
|
295
|
+
.description('Show deployment configuration details')
|
|
296
|
+
.option('--json', 'Output as JSON')
|
|
297
|
+
.action(async (name, options) => {
|
|
298
|
+
try {
|
|
299
|
+
const store = await loadDeployConfigs();
|
|
300
|
+
const config = store.configs[name];
|
|
301
|
+
if (!config) {
|
|
302
|
+
ctx.output.error(`Config '${name}' not found`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
if (options.json) {
|
|
306
|
+
console.log(JSON.stringify(config, null, 2));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log(`\nDeployment Config: ${config.name}\n`);
|
|
310
|
+
if (config.description) {
|
|
311
|
+
console.log(` Description: ${config.description}`);
|
|
312
|
+
}
|
|
313
|
+
console.log(` WAR Path: ${config.warPath || '(not set)'}`);
|
|
314
|
+
console.log(` Port: ${config.port}`);
|
|
315
|
+
console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
|
|
316
|
+
console.log(`\n Hosts (${config.hosts.length}):`);
|
|
317
|
+
if (config.hosts.length === 0) {
|
|
318
|
+
console.log(' (none)');
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
for (const host of config.hosts) {
|
|
322
|
+
console.log(` - ${host}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
console.log();
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
ctx.output.error(`Failed to show config: ${err instanceof Error ? err.message : String(err)}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// deploy config delete <name>
|
|
333
|
+
configCmd
|
|
334
|
+
.command('delete <name>')
|
|
335
|
+
.alias('rm')
|
|
336
|
+
.description('Delete a deployment configuration')
|
|
337
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
338
|
+
.action(async (name, options) => {
|
|
339
|
+
try {
|
|
340
|
+
const store = await loadDeployConfigs();
|
|
341
|
+
if (!store.configs[name]) {
|
|
342
|
+
ctx.output.error(`Config '${name}' not found`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
if (!options.yes) {
|
|
346
|
+
// Dynamic import of inquirer (available from znvault-cli context)
|
|
347
|
+
const inquirerModule = await import('inquirer');
|
|
348
|
+
const inquirer = inquirerModule.default;
|
|
349
|
+
const answers = await inquirer.prompt([{
|
|
350
|
+
type: 'confirm',
|
|
351
|
+
name: 'confirm',
|
|
352
|
+
message: `Delete deployment config '${name}'?`,
|
|
353
|
+
default: false,
|
|
354
|
+
}]);
|
|
355
|
+
if (!answers.confirm) {
|
|
356
|
+
ctx.output.info('Cancelled');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
delete store.configs[name];
|
|
361
|
+
await saveDeployConfigs(store);
|
|
362
|
+
ctx.output.success(`Deleted config: ${name}`);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
ctx.output.error(`Failed to delete config: ${err instanceof Error ? err.message : String(err)}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
// deploy config add-host <name> <host>
|
|
370
|
+
configCmd
|
|
371
|
+
.command('add-host <name> <host>')
|
|
372
|
+
.description('Add a host to deployment configuration')
|
|
373
|
+
.action(async (name, host) => {
|
|
374
|
+
try {
|
|
375
|
+
const store = await loadDeployConfigs();
|
|
376
|
+
const config = store.configs[name];
|
|
377
|
+
if (!config) {
|
|
378
|
+
ctx.output.error(`Config '${name}' not found`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
if (config.hosts.includes(host)) {
|
|
382
|
+
ctx.output.warn(`Host '${host}' already in config`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
config.hosts.push(host);
|
|
386
|
+
await saveDeployConfigs(store);
|
|
387
|
+
ctx.output.success(`Added host: ${host}`);
|
|
388
|
+
ctx.output.info(`Config '${name}' now has ${config.hosts.length} host(s)`);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
ctx.output.error(`Failed to add host: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// deploy config remove-host <name> <host>
|
|
396
|
+
configCmd
|
|
397
|
+
.command('remove-host <name> <host>')
|
|
398
|
+
.description('Remove a host from deployment configuration')
|
|
399
|
+
.action(async (name, host) => {
|
|
400
|
+
try {
|
|
401
|
+
const store = await loadDeployConfigs();
|
|
402
|
+
const config = store.configs[name];
|
|
403
|
+
if (!config) {
|
|
404
|
+
ctx.output.error(`Config '${name}' not found`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const index = config.hosts.indexOf(host);
|
|
408
|
+
if (index === -1) {
|
|
409
|
+
ctx.output.error(`Host '${host}' not found in config`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
config.hosts.splice(index, 1);
|
|
413
|
+
await saveDeployConfigs(store);
|
|
414
|
+
ctx.output.success(`Removed host: ${host}`);
|
|
415
|
+
ctx.output.info(`Config '${name}' now has ${config.hosts.length} host(s)`);
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
ctx.output.error(`Failed to remove host: ${err instanceof Error ? err.message : String(err)}`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
// deploy config push - Push configs to vault
|
|
423
|
+
configCmd
|
|
424
|
+
.command('push')
|
|
425
|
+
.description('Push deployment configs to vault for sharing/backup')
|
|
426
|
+
.option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
|
|
427
|
+
.action(async (options) => {
|
|
428
|
+
try {
|
|
429
|
+
const alias = options.alias ?? 'deploy/configs';
|
|
430
|
+
const store = await loadDeployConfigs();
|
|
431
|
+
if (Object.keys(store.configs).length === 0) {
|
|
432
|
+
ctx.output.error('No configs to push');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
ctx.output.info(`Pushing ${Object.keys(store.configs).length} config(s) to vault...`);
|
|
436
|
+
// Create/update secret in vault
|
|
437
|
+
const secretData = {
|
|
438
|
+
configs: store.configs,
|
|
439
|
+
pushedAt: new Date().toISOString(),
|
|
440
|
+
pushedFrom: hostname(),
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
// Try to update existing secret
|
|
444
|
+
await ctx.client.post(`/v1/secrets/by-alias/${encodeURIComponent(alias)}`, {
|
|
445
|
+
value: JSON.stringify(secretData, null, 2),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// Create new secret
|
|
450
|
+
await ctx.client.post('/v1/secrets', {
|
|
451
|
+
alias,
|
|
452
|
+
name: 'Deployment Configurations',
|
|
453
|
+
description: 'Shared deployment configs for znvault deploy command',
|
|
454
|
+
value: JSON.stringify(secretData, null, 2),
|
|
455
|
+
type: 'generic',
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
// Update local store with vault info
|
|
459
|
+
store.vaultEnabled = true;
|
|
460
|
+
store.vaultAlias = alias;
|
|
461
|
+
await saveDeployConfigs(store);
|
|
462
|
+
ctx.output.success(`Pushed to vault: ${alias}`);
|
|
463
|
+
ctx.output.info('Other users can pull with: znvault deploy config pull');
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
ctx.output.error(`Failed to push: ${err instanceof Error ? err.message : String(err)}`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// deploy config pull - Pull configs from vault
|
|
471
|
+
configCmd
|
|
472
|
+
.command('pull')
|
|
473
|
+
.description('Pull deployment configs from vault')
|
|
474
|
+
.option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
|
|
475
|
+
.option('--merge', 'Merge with local configs instead of replacing')
|
|
476
|
+
.action(async (options) => {
|
|
477
|
+
try {
|
|
478
|
+
const localStore = await loadDeployConfigs();
|
|
479
|
+
const alias = options.alias ?? localStore.vaultAlias ?? 'deploy/configs';
|
|
480
|
+
ctx.output.info(`Pulling configs from vault: ${alias}...`);
|
|
481
|
+
const response = await ctx.client.get(`/v1/secrets/by-alias/${encodeURIComponent(alias)}/value`);
|
|
482
|
+
const vaultData = JSON.parse(response.value);
|
|
483
|
+
const vaultConfigs = vaultData.configs;
|
|
484
|
+
const configCount = Object.keys(vaultConfigs).length;
|
|
485
|
+
if (options.merge) {
|
|
486
|
+
// Merge: vault configs override local on conflict
|
|
487
|
+
const newStore = {
|
|
488
|
+
configs: { ...localStore.configs, ...vaultConfigs },
|
|
489
|
+
vaultEnabled: true,
|
|
490
|
+
vaultAlias: alias,
|
|
491
|
+
};
|
|
492
|
+
await saveDeployConfigs(newStore);
|
|
493
|
+
const merged = Object.keys(newStore.configs).length;
|
|
494
|
+
ctx.output.success(`Merged ${configCount} config(s) from vault (${merged} total)`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Replace local configs
|
|
498
|
+
const newStore = {
|
|
499
|
+
configs: vaultConfigs,
|
|
500
|
+
vaultEnabled: true,
|
|
501
|
+
vaultAlias: alias,
|
|
502
|
+
};
|
|
503
|
+
await saveDeployConfigs(newStore);
|
|
504
|
+
ctx.output.success(`Pulled ${configCount} config(s) from vault`);
|
|
505
|
+
}
|
|
506
|
+
ctx.output.info(`Last pushed: ${vaultData.pushedAt} from ${vaultData.pushedFrom}`);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
if (String(err).includes('404') || String(err).includes('not found')) {
|
|
510
|
+
ctx.output.error('No configs found in vault');
|
|
511
|
+
ctx.output.info('Push configs first with: znvault deploy config push');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
ctx.output.error(`Failed to pull: ${err instanceof Error ? err.message : String(err)}`);
|
|
515
|
+
}
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
// deploy config sync - Show sync status
|
|
520
|
+
configCmd
|
|
521
|
+
.command('sync')
|
|
522
|
+
.description('Show vault sync status')
|
|
523
|
+
.action(async () => {
|
|
524
|
+
try {
|
|
525
|
+
const store = await loadDeployConfigs();
|
|
526
|
+
console.log('\nVault Sync Status:\n');
|
|
527
|
+
if (!store.vaultEnabled) {
|
|
528
|
+
console.log(' Status: Local only (not synced to vault)');
|
|
529
|
+
console.log(' Configs: ' + Object.keys(store.configs).length);
|
|
530
|
+
console.log('\n To enable vault sync: znvault deploy config push');
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
console.log(' Status: Vault sync enabled');
|
|
534
|
+
console.log(' Alias: ' + (store.vaultAlias ?? 'deploy/configs'));
|
|
535
|
+
console.log(' Configs: ' + Object.keys(store.configs).length);
|
|
536
|
+
console.log('\n Push changes: znvault deploy config push');
|
|
537
|
+
console.log(' Pull updates: znvault deploy config pull');
|
|
538
|
+
}
|
|
539
|
+
console.log();
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
ctx.output.error(`Failed to get sync status: ${err instanceof Error ? err.message : String(err)}`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// deploy config set <name> <key> <value>
|
|
547
|
+
configCmd
|
|
548
|
+
.command('set <name> <key> <value>')
|
|
549
|
+
.description('Set a configuration value (war, port, parallel, description)')
|
|
550
|
+
.action(async (name, key, value) => {
|
|
551
|
+
try {
|
|
552
|
+
const store = await loadDeployConfigs();
|
|
553
|
+
const config = store.configs[name];
|
|
554
|
+
if (!config) {
|
|
555
|
+
ctx.output.error(`Config '${name}' not found`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
switch (key.toLowerCase()) {
|
|
559
|
+
case 'war':
|
|
560
|
+
case 'warpath':
|
|
561
|
+
config.warPath = value;
|
|
562
|
+
break;
|
|
563
|
+
case 'port':
|
|
564
|
+
config.port = parseInt(value, 10);
|
|
565
|
+
if (isNaN(config.port)) {
|
|
566
|
+
ctx.output.error('Port must be a number');
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
case 'parallel':
|
|
571
|
+
config.parallel = value.toLowerCase() === 'true' || value === '1';
|
|
572
|
+
break;
|
|
573
|
+
case 'description':
|
|
574
|
+
case 'desc':
|
|
575
|
+
config.description = value;
|
|
576
|
+
break;
|
|
577
|
+
default:
|
|
578
|
+
ctx.output.error(`Unknown config key: ${key}`);
|
|
579
|
+
ctx.output.info('Valid keys: war, port, parallel, description');
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
await saveDeployConfigs(store);
|
|
583
|
+
ctx.output.success(`Set ${key} = ${value}`);
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
ctx.output.error(`Failed to set config: ${err instanceof Error ? err.message : String(err)}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
// ========================================================================
|
|
591
|
+
// deploy war <file> - Original single-host deployment
|
|
592
|
+
// ========================================================================
|
|
22
593
|
deploy
|
|
23
594
|
.command('war <warFile>')
|
|
24
|
-
.description('Deploy WAR file using diff transfer')
|
|
595
|
+
.description('Deploy WAR file using diff transfer (single host)')
|
|
25
596
|
.option('-t, --target <host>', 'Target server URL (default: from profile)')
|
|
26
597
|
.option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
|
|
27
598
|
.option('-f, --force', 'Force full deployment (no diff)')
|
|
@@ -121,51 +692,108 @@ export function createPayaraCLIPlugin() {
|
|
|
121
692
|
process.exit(1);
|
|
122
693
|
}
|
|
123
694
|
});
|
|
695
|
+
// ========================================================================
|
|
124
696
|
// deploy restart
|
|
697
|
+
// ========================================================================
|
|
125
698
|
deploy
|
|
126
|
-
.command('restart')
|
|
127
|
-
.description('Restart Payara on remote server')
|
|
128
|
-
.option('-t, --target <host>', 'Target server URL')
|
|
699
|
+
.command('restart [configName]')
|
|
700
|
+
.description('Restart Payara on remote server(s)')
|
|
701
|
+
.option('-t, --target <host>', 'Target server URL (single host mode)')
|
|
129
702
|
.option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
|
|
130
|
-
.action(async (options) => {
|
|
703
|
+
.action(async (configName, options) => {
|
|
131
704
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
705
|
+
if (configName) {
|
|
706
|
+
// Multi-host restart using config
|
|
707
|
+
const store = await loadDeployConfigs();
|
|
708
|
+
const config = store.configs[configName];
|
|
709
|
+
if (!config) {
|
|
710
|
+
ctx.output.error(`Config '${configName}' not found`);
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
ctx.output.info(`Restarting Payara on ${config.hosts.length} host(s)...`);
|
|
714
|
+
for (const host of config.hosts) {
|
|
715
|
+
const baseUrl = host.startsWith('http') ? host : `https://${host}`;
|
|
716
|
+
const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
|
|
717
|
+
try {
|
|
718
|
+
await ctx.client.post(`${pluginUrl}/restart`, {});
|
|
719
|
+
ctx.output.success(` ✓ ${host} restarted`);
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
ctx.output.error(` ✗ ${host}: ${err instanceof Error ? err.message : String(err)}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
// Single host restart
|
|
728
|
+
const target = options.target ?? ctx.getConfig().url;
|
|
729
|
+
const baseUrl = target.replace(/\/$/, '');
|
|
730
|
+
const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
|
|
731
|
+
ctx.output.info('Restarting Payara...');
|
|
732
|
+
await ctx.client.post(`${pluginUrl}/restart`, {});
|
|
733
|
+
ctx.output.success('Payara restarted');
|
|
734
|
+
}
|
|
138
735
|
}
|
|
139
736
|
catch (err) {
|
|
140
737
|
ctx.output.error(`Restart failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
738
|
process.exit(1);
|
|
142
739
|
}
|
|
143
740
|
});
|
|
741
|
+
// ========================================================================
|
|
144
742
|
// deploy status
|
|
743
|
+
// ========================================================================
|
|
145
744
|
deploy
|
|
146
|
-
.command('status')
|
|
147
|
-
.description('Get Payara status from remote server')
|
|
148
|
-
.option('-t, --target <host>', 'Target server URL')
|
|
745
|
+
.command('status [configName]')
|
|
746
|
+
.description('Get Payara status from remote server(s)')
|
|
747
|
+
.option('-t, --target <host>', 'Target server URL (single host mode)')
|
|
149
748
|
.option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
|
|
150
|
-
.action(async (options) => {
|
|
749
|
+
.action(async (configName, options) => {
|
|
151
750
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
751
|
+
if (configName) {
|
|
752
|
+
// Multi-host status using config
|
|
753
|
+
const store = await loadDeployConfigs();
|
|
754
|
+
const config = store.configs[configName];
|
|
755
|
+
if (!config) {
|
|
756
|
+
ctx.output.error(`Config '${configName}' not found`);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
console.log(`\nStatus for ${configName}:\n`);
|
|
760
|
+
for (const host of config.hosts) {
|
|
761
|
+
const baseUrl = host.startsWith('http') ? host : `https://${host}`;
|
|
762
|
+
const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
|
|
763
|
+
try {
|
|
764
|
+
const status = await ctx.client.get(`${pluginUrl}/status`);
|
|
765
|
+
const icon = status.healthy ? '✓' : status.running ? '!' : '✗';
|
|
766
|
+
const state = status.healthy ? 'healthy' : status.running ? 'degraded' : 'down';
|
|
767
|
+
console.log(` ${icon} ${host}: ${state} (${status.domain})`);
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
console.log(` ✗ ${host}: unreachable`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
console.log();
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// Single host status
|
|
777
|
+
const target = options.target ?? ctx.getConfig().url;
|
|
778
|
+
const baseUrl = target.replace(/\/$/, '');
|
|
779
|
+
const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
|
|
780
|
+
const status = await ctx.client.get(`${pluginUrl}/status`);
|
|
781
|
+
ctx.output.keyValue({
|
|
782
|
+
'Domain': status.domain,
|
|
783
|
+
'Running': status.running,
|
|
784
|
+
'Healthy': status.healthy,
|
|
785
|
+
'PID': status.pid ?? 'N/A',
|
|
786
|
+
});
|
|
787
|
+
}
|
|
162
788
|
}
|
|
163
789
|
catch (err) {
|
|
164
790
|
ctx.output.error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
|
|
165
791
|
process.exit(1);
|
|
166
792
|
}
|
|
167
793
|
});
|
|
794
|
+
// ========================================================================
|
|
168
795
|
// deploy applications
|
|
796
|
+
// ========================================================================
|
|
169
797
|
deploy
|
|
170
798
|
.command('applications')
|
|
171
799
|
.alias('apps')
|