@edgible-team/cli 1.2.13 → 1.2.15
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/commands/stack/deploy.d.ts +6 -0
- package/dist/commands/stack/deploy.d.ts.map +1 -0
- package/dist/commands/stack/deploy.js +58 -0
- package/dist/commands/stack/diff.d.ts +7 -0
- package/dist/commands/stack/diff.d.ts.map +1 -0
- package/dist/commands/stack/diff.js +64 -0
- package/dist/commands/stack/status.d.ts +9 -0
- package/dist/commands/stack/status.d.ts.map +1 -0
- package/dist/commands/stack/status.js +53 -0
- package/dist/commands/stack/teardown.d.ts +6 -0
- package/dist/commands/stack/teardown.d.ts.map +1 -0
- package/dist/commands/stack/teardown.js +104 -0
- package/dist/commands/stack/validate.d.ts +7 -0
- package/dist/commands/stack/validate.d.ts.map +1 -0
- package/dist/commands/stack/validate.js +42 -0
- package/dist/commands/stack.d.ts +10 -0
- package/dist/commands/stack.d.ts.map +1 -0
- package/dist/commands/stack.js +112 -0
- package/dist/index.js +2 -0
- package/dist/services/instances.d.ts +23 -0
- package/dist/services/instances.d.ts.map +1 -1
- package/dist/services/instances.js +46 -1
- package/dist/services/stack/DependencyGraphManager.d.ts +69 -0
- package/dist/services/stack/DependencyGraphManager.d.ts.map +1 -0
- package/dist/services/stack/DependencyGraphManager.js +204 -0
- package/dist/services/stack/DeviceResolver.d.ts +63 -0
- package/dist/services/stack/DeviceResolver.d.ts.map +1 -0
- package/dist/services/stack/DeviceResolver.js +147 -0
- package/dist/services/stack/GatewayResolver.d.ts +84 -0
- package/dist/services/stack/GatewayResolver.d.ts.map +1 -0
- package/dist/services/stack/GatewayResolver.js +179 -0
- package/dist/services/stack/StackParser.d.ts +38 -0
- package/dist/services/stack/StackParser.d.ts.map +1 -0
- package/dist/services/stack/StackParser.js +234 -0
- package/dist/services/stack/StackService.d.ts +76 -0
- package/dist/services/stack/StackService.d.ts.map +1 -0
- package/dist/services/stack/StackService.js +476 -0
- package/dist/types/stack.d.ts +191 -0
- package/dist/types/stack.d.ts.map +1 -0
- package/dist/types/stack.js +5 -0
- package/dist/types/validation/schemas.d.ts +17 -17
- package/dist/utils/stack-errors.d.ts +103 -0
- package/dist/utils/stack-errors.d.ts.map +1 -0
- package/dist/utils/stack-errors.js +158 -0
- package/dist/validation/stack-schemas.d.ts +535 -0
- package/dist/validation/stack-schemas.d.ts.map +1 -0
- package/dist/validation/stack-schemas.js +178 -0
- package/package.json +4 -2
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stack Service
|
|
4
|
+
* Main orchestration service for application stack operations
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.StackService = void 0;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const StackParser_1 = require("./StackParser");
|
|
44
|
+
const DeviceResolver_1 = require("./DeviceResolver");
|
|
45
|
+
const GatewayResolver_1 = require("./GatewayResolver");
|
|
46
|
+
const DependencyGraphManager_1 = require("./DependencyGraphManager");
|
|
47
|
+
const stack_errors_1 = require("../../utils/stack-errors");
|
|
48
|
+
class StackService {
|
|
49
|
+
constructor(apiClient, applicationService, organizationId, logger) {
|
|
50
|
+
this.apiClient = apiClient;
|
|
51
|
+
this.applicationService = applicationService;
|
|
52
|
+
this.organizationId = organizationId;
|
|
53
|
+
this.logger = logger;
|
|
54
|
+
this.parser = new StackParser_1.StackParser(logger);
|
|
55
|
+
this.deviceResolver = new DeviceResolver_1.DeviceResolver(apiClient, organizationId, logger);
|
|
56
|
+
this.gatewayResolver = new GatewayResolver_1.GatewayResolver(apiClient, organizationId, logger);
|
|
57
|
+
this.dependencyManager = new DependencyGraphManager_1.DependencyGraphManager(logger);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate a stack file
|
|
61
|
+
* @param filePath Path to stack file
|
|
62
|
+
* @returns Validation result
|
|
63
|
+
*/
|
|
64
|
+
async validateStack(filePath) {
|
|
65
|
+
this.logger.info('Validating stack file...');
|
|
66
|
+
try {
|
|
67
|
+
const result = await this.parser.validate(filePath);
|
|
68
|
+
if (result.valid) {
|
|
69
|
+
// Additional validation: check dependency graph
|
|
70
|
+
const stack = await this.parser.parseFile(filePath);
|
|
71
|
+
this.dependencyManager.validate(stack.applications);
|
|
72
|
+
// Check device names exist (optional, might be slow)
|
|
73
|
+
// Commented out for now to keep validation fast
|
|
74
|
+
// for (const app of stack.applications) {
|
|
75
|
+
// await this.deviceResolver.resolveDevice(app.deviceName);
|
|
76
|
+
// }
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Convert any thrown errors to validation errors
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
errors: [{
|
|
85
|
+
path: 'stack',
|
|
86
|
+
message: error instanceof Error ? error.message : 'Unknown validation error',
|
|
87
|
+
}],
|
|
88
|
+
warnings: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Deploy a stack
|
|
94
|
+
* @param options Deploy options
|
|
95
|
+
* @returns Deploy result
|
|
96
|
+
*/
|
|
97
|
+
async deployStack(options) {
|
|
98
|
+
this.logger.info(`Deploying stack from: ${options.file}`);
|
|
99
|
+
const deployed = [];
|
|
100
|
+
const failed = [];
|
|
101
|
+
const skipped = [];
|
|
102
|
+
try {
|
|
103
|
+
// Parse and validate stack
|
|
104
|
+
const stack = await this.parser.parseFile(options.file);
|
|
105
|
+
this.logger.info(`Stack: ${stack.metadata.name} - ${stack.applications.length} applications`);
|
|
106
|
+
// Filter to single app if specified
|
|
107
|
+
let applicationsToDeploy = options.app
|
|
108
|
+
? stack.applications.filter(app => app.name === options.app)
|
|
109
|
+
: stack.applications;
|
|
110
|
+
if (options.app && applicationsToDeploy.length === 0) {
|
|
111
|
+
throw new Error(`Application '${options.app}' not found in stack`);
|
|
112
|
+
}
|
|
113
|
+
// Validate dependency graph
|
|
114
|
+
this.dependencyManager.validate(applicationsToDeploy);
|
|
115
|
+
// Get deployment order
|
|
116
|
+
const deploymentOrder = this.dependencyManager.getDeploymentOrder(applicationsToDeploy);
|
|
117
|
+
this.logger.info(`Deployment order: ${deploymentOrder.join(' -> ')}`);
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
this.logger.info('[DRY RUN] Would deploy applications in this order:');
|
|
120
|
+
for (const appName of deploymentOrder) {
|
|
121
|
+
const app = applicationsToDeploy.find(a => a.name === appName);
|
|
122
|
+
this.logger.info(` - ${appName} (${app.subtype}) on ${app.deviceName}`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
deployed: [],
|
|
127
|
+
failed: [],
|
|
128
|
+
skipped: deploymentOrder,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Resolve devices and gateways
|
|
132
|
+
const resolved = await this.resolveStack(stack, applicationsToDeploy);
|
|
133
|
+
// Deploy applications in order
|
|
134
|
+
const deployedSet = new Set();
|
|
135
|
+
for (const appName of deploymentOrder) {
|
|
136
|
+
const app = applicationsToDeploy.find(a => a.name === appName);
|
|
137
|
+
// Check if dependencies deployed
|
|
138
|
+
if (!options.skipDependencies && app.dependsOn) {
|
|
139
|
+
const missingDeps = app.dependsOn.filter(dep => !deployedSet.has(dep));
|
|
140
|
+
if (missingDeps.length > 0) {
|
|
141
|
+
failed.push({
|
|
142
|
+
name: appName,
|
|
143
|
+
error: `Missing dependencies: ${missingDeps.join(', ')}`,
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
this.logger.info(`Deploying application: ${appName}`);
|
|
150
|
+
const result = await this.deployApplication(app, resolved);
|
|
151
|
+
deployed.push(result);
|
|
152
|
+
deployedSet.add(appName);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
156
|
+
this.logger.error(`Failed to deploy ${appName}: ${errorMsg}`);
|
|
157
|
+
failed.push({
|
|
158
|
+
name: appName,
|
|
159
|
+
error: errorMsg,
|
|
160
|
+
});
|
|
161
|
+
// Stop deployment on first failure (fail-fast)
|
|
162
|
+
if (!options.force) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const success = failed.length === 0;
|
|
168
|
+
this.logger.info(`Deployment ${success ? 'completed successfully' : 'completed with errors'}`);
|
|
169
|
+
this.logger.info(` Deployed: ${deployed.length}`);
|
|
170
|
+
this.logger.info(` Failed: ${failed.length}`);
|
|
171
|
+
return {
|
|
172
|
+
success,
|
|
173
|
+
deployed,
|
|
174
|
+
failed,
|
|
175
|
+
skipped,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.logger.error(`Stack deployment failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Teardown a stack
|
|
185
|
+
* @param options Teardown options
|
|
186
|
+
* @returns Teardown result
|
|
187
|
+
*/
|
|
188
|
+
async teardownStack(options) {
|
|
189
|
+
this.logger.info(`Tearing down stack from: ${options.file}`);
|
|
190
|
+
const removed = [];
|
|
191
|
+
const failed = [];
|
|
192
|
+
const skipped = [];
|
|
193
|
+
try {
|
|
194
|
+
// Parse stack
|
|
195
|
+
const stack = await this.parser.parseFile(options.file);
|
|
196
|
+
// Filter to single app if specified
|
|
197
|
+
let applicationsToRemove = options.app
|
|
198
|
+
? stack.applications.filter(app => app.name === options.app)
|
|
199
|
+
: stack.applications;
|
|
200
|
+
if (options.app && applicationsToRemove.length === 0) {
|
|
201
|
+
throw new Error(`Application '${options.app}' not found in stack`);
|
|
202
|
+
}
|
|
203
|
+
// Get teardown order (reverse of deployment)
|
|
204
|
+
const teardownOrder = this.dependencyManager.getTeardownOrder(applicationsToRemove);
|
|
205
|
+
this.logger.info(`Teardown order: ${teardownOrder.join(' -> ')}`);
|
|
206
|
+
// Get deployed applications
|
|
207
|
+
const deployedApps = await this.getDeployedApplications(applicationsToRemove);
|
|
208
|
+
// Remove applications in order
|
|
209
|
+
for (const appName of teardownOrder) {
|
|
210
|
+
const deployedApp = deployedApps.find(app => app.name === appName);
|
|
211
|
+
if (!deployedApp || !deployedApp.applicationId) {
|
|
212
|
+
this.logger.debug(`Application ${appName} not deployed, skipping`);
|
|
213
|
+
skipped.push(appName);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
this.logger.info(`Removing application: ${appName}`);
|
|
218
|
+
await this.applicationService.deleteApplication(deployedApp.applicationId);
|
|
219
|
+
removed.push(appName);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
223
|
+
this.logger.error(`Failed to remove ${appName}: ${errorMsg}`);
|
|
224
|
+
failed.push({
|
|
225
|
+
name: appName,
|
|
226
|
+
error: errorMsg,
|
|
227
|
+
});
|
|
228
|
+
// Continue even on failure (best effort teardown)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const success = failed.length === 0;
|
|
232
|
+
this.logger.info(`Teardown ${success ? 'completed successfully' : 'completed with errors'}`);
|
|
233
|
+
this.logger.info(` Removed: ${removed.length}`);
|
|
234
|
+
this.logger.info(` Failed: ${failed.length}`);
|
|
235
|
+
this.logger.info(` Skipped: ${skipped.length}`);
|
|
236
|
+
return {
|
|
237
|
+
success,
|
|
238
|
+
removed,
|
|
239
|
+
failed,
|
|
240
|
+
skipped,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
this.logger.error(`Stack teardown failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get status of applications in a stack
|
|
250
|
+
* @param filePath Path to stack file
|
|
251
|
+
* @returns Stack status
|
|
252
|
+
*/
|
|
253
|
+
async getStackStatus(filePath) {
|
|
254
|
+
this.logger.info('Getting stack status...');
|
|
255
|
+
const stack = await this.parser.parseFile(filePath);
|
|
256
|
+
const applicationStatuses = await this.getDeployedApplications(stack.applications);
|
|
257
|
+
return {
|
|
258
|
+
stackName: stack.metadata.name,
|
|
259
|
+
applications: applicationStatuses,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get diff between stack definition and deployed state
|
|
264
|
+
* @param filePath Path to stack file
|
|
265
|
+
* @returns Stack diff
|
|
266
|
+
*/
|
|
267
|
+
async diffStack(filePath) {
|
|
268
|
+
this.logger.info('Computing stack diff...');
|
|
269
|
+
const stack = await this.parser.parseFile(filePath);
|
|
270
|
+
const deployed = await this.getDeployedApplications(stack.applications);
|
|
271
|
+
const toCreate = [];
|
|
272
|
+
const toUpdate = [];
|
|
273
|
+
const toDelete = [];
|
|
274
|
+
const unchanged = [];
|
|
275
|
+
// Check each application in the stack
|
|
276
|
+
for (const app of stack.applications) {
|
|
277
|
+
const deployedApp = deployed.find(d => d.name === app.name);
|
|
278
|
+
if (!deployedApp || deployedApp.status === 'not-deployed') {
|
|
279
|
+
toCreate.push(app);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// For MVP, consider as unchanged if deployed
|
|
283
|
+
// Future: implement proper field-by-field comparison
|
|
284
|
+
unchanged.push(app.name);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Check for deployed apps not in stack (to delete)
|
|
288
|
+
for (const deployedApp of deployed) {
|
|
289
|
+
if (deployedApp.status === 'deployed') {
|
|
290
|
+
const inStack = stack.applications.find(a => a.name === deployedApp.name);
|
|
291
|
+
if (!inStack) {
|
|
292
|
+
toDelete.push(deployedApp);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
toCreate,
|
|
298
|
+
toUpdate,
|
|
299
|
+
toDelete,
|
|
300
|
+
unchanged,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve stack: resolve device names and gateways
|
|
305
|
+
* @param stack Application stack
|
|
306
|
+
* @param applications Applications to resolve (filtered subset)
|
|
307
|
+
* @returns Resolved stack
|
|
308
|
+
*/
|
|
309
|
+
async resolveStack(stack, applications) {
|
|
310
|
+
this.logger.info('Resolving devices and gateways...');
|
|
311
|
+
// Get unique device names
|
|
312
|
+
const deviceNames = [...new Set(applications.map(app => app.deviceName))];
|
|
313
|
+
// Resolve devices
|
|
314
|
+
const devices = await this.deviceResolver.resolveDevices(deviceNames);
|
|
315
|
+
this.logger.info(`Resolved ${devices.size} devices`);
|
|
316
|
+
// Resolve gateways
|
|
317
|
+
const gateways = await this.gatewayResolver.resolveGateways(applications);
|
|
318
|
+
this.logger.info(`Resolved gateways for ${gateways.size} published applications`);
|
|
319
|
+
return {
|
|
320
|
+
...stack,
|
|
321
|
+
applications: stack.applications,
|
|
322
|
+
resolved: {
|
|
323
|
+
devices,
|
|
324
|
+
gateways,
|
|
325
|
+
applications: new Map(),
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Deploy a single application
|
|
331
|
+
* @param app Application to deploy
|
|
332
|
+
* @param resolved Resolved stack information
|
|
333
|
+
* @returns Deployed application info
|
|
334
|
+
*/
|
|
335
|
+
async deployApplication(app, resolved) {
|
|
336
|
+
const deviceId = resolved.resolved.devices.get(app.deviceName);
|
|
337
|
+
if (!deviceId) {
|
|
338
|
+
throw new stack_errors_1.ApplicationDeploymentError(app.name, `Device '${app.deviceName}' not found`);
|
|
339
|
+
}
|
|
340
|
+
const gatewayIds = resolved.resolved.gateways.get(app.name) || [];
|
|
341
|
+
// Map subtype to ApplicationService format
|
|
342
|
+
let serviceSubtype;
|
|
343
|
+
switch (app.subtype) {
|
|
344
|
+
case 'existing':
|
|
345
|
+
serviceSubtype = 'local-preexisting';
|
|
346
|
+
break;
|
|
347
|
+
case 'docker-compose':
|
|
348
|
+
serviceSubtype = 'docker-compose';
|
|
349
|
+
break;
|
|
350
|
+
case 'managed-process':
|
|
351
|
+
serviceSubtype = 'managed-process';
|
|
352
|
+
break;
|
|
353
|
+
default:
|
|
354
|
+
throw new stack_errors_1.ApplicationDeploymentError(app.name, `Unsupported subtype: ${app.subtype}`);
|
|
355
|
+
}
|
|
356
|
+
// Build configuration based on subtype
|
|
357
|
+
const configuration = {};
|
|
358
|
+
if (app.subtype === 'docker-compose' && app.composeFile) {
|
|
359
|
+
// Read and encode docker-compose file to base64
|
|
360
|
+
const dockerComposePath = await this.encodeDockerComposeFile(app.composeFile);
|
|
361
|
+
configuration.dockerComposePath = dockerComposePath;
|
|
362
|
+
// Also set environment variables if provided
|
|
363
|
+
if (app.environment) {
|
|
364
|
+
configuration.env = app.environment;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (app.subtype === 'managed-process') {
|
|
368
|
+
if (app.command)
|
|
369
|
+
configuration.command = app.command;
|
|
370
|
+
if (app.workingDir)
|
|
371
|
+
configuration.workingDir = app.workingDir;
|
|
372
|
+
if (app.environment)
|
|
373
|
+
configuration.environment = app.environment;
|
|
374
|
+
if (app.logFile)
|
|
375
|
+
configuration.logFile = app.logFile;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const result = await this.applicationService.createApplicationProgrammatically({
|
|
379
|
+
name: app.name,
|
|
380
|
+
description: app.description || '',
|
|
381
|
+
port: app.port,
|
|
382
|
+
protocol: app.protocol,
|
|
383
|
+
hostnames: app.hostnames,
|
|
384
|
+
deviceIds: [deviceId],
|
|
385
|
+
gatewayIds: gatewayIds.length > 0 ? gatewayIds : undefined,
|
|
386
|
+
useManagedGateway: app.published,
|
|
387
|
+
subtype: serviceSubtype,
|
|
388
|
+
configuration,
|
|
389
|
+
authModes: app.authModes,
|
|
390
|
+
allowedOrganizations: app.allowedOrganizations,
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
name: app.name,
|
|
394
|
+
applicationId: result.applicationId || result.id || 'unknown',
|
|
395
|
+
urls: result.urls || [],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
throw new stack_errors_1.ApplicationDeploymentError(app.name, error instanceof Error ? error.message : 'Unknown error', error instanceof Error ? error : undefined);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get deployed applications matching stack apps
|
|
404
|
+
* @param applications Stack applications to check
|
|
405
|
+
* @returns Array of application statuses
|
|
406
|
+
*/
|
|
407
|
+
async getDeployedApplications(applications) {
|
|
408
|
+
try {
|
|
409
|
+
// Get all applications in the organization
|
|
410
|
+
const allApps = await this.applicationService.getApplications();
|
|
411
|
+
// Match by name
|
|
412
|
+
return applications.map(stackApp => {
|
|
413
|
+
const deployedApp = allApps.find((app) => app.name === stackApp.name);
|
|
414
|
+
if (deployedApp) {
|
|
415
|
+
return {
|
|
416
|
+
name: stackApp.name,
|
|
417
|
+
status: 'deployed',
|
|
418
|
+
applicationId: deployedApp.id,
|
|
419
|
+
deviceName: stackApp.deviceName,
|
|
420
|
+
published: stackApp.published || false,
|
|
421
|
+
urls: deployedApp.urls || [],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
return {
|
|
426
|
+
name: stackApp.name,
|
|
427
|
+
status: 'not-deployed',
|
|
428
|
+
deviceName: stackApp.deviceName,
|
|
429
|
+
published: stackApp.published || false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
this.logger.error(`Failed to get deployed applications: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
436
|
+
// Return all as not-deployed on error
|
|
437
|
+
return applications.map(app => ({
|
|
438
|
+
name: app.name,
|
|
439
|
+
status: 'not-deployed',
|
|
440
|
+
deviceName: app.deviceName,
|
|
441
|
+
published: app.published || false,
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Encode docker-compose file to base64
|
|
447
|
+
* @param composeFilePath Path to docker-compose file (relative to stack file or absolute)
|
|
448
|
+
* @returns Base64-prefixed compose content ready for API submission
|
|
449
|
+
*/
|
|
450
|
+
async encodeDockerComposeFile(composeFilePath) {
|
|
451
|
+
try {
|
|
452
|
+
// Resolve path (could be absolute or relative)
|
|
453
|
+
const resolvedPath = path.isAbsolute(composeFilePath)
|
|
454
|
+
? composeFilePath
|
|
455
|
+
: path.resolve(process.cwd(), composeFilePath);
|
|
456
|
+
this.logger.debug(`Reading docker-compose file: ${resolvedPath}`);
|
|
457
|
+
// Check if file exists
|
|
458
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
459
|
+
throw new Error(`Docker compose file not found: ${resolvedPath}`);
|
|
460
|
+
}
|
|
461
|
+
// Read file contents
|
|
462
|
+
const fileContents = fs.readFileSync(resolvedPath, 'utf-8');
|
|
463
|
+
// Encode to base64
|
|
464
|
+
const base64Content = Buffer.from(fileContents, 'utf-8').toString('base64');
|
|
465
|
+
// Return with base64: prefix (matching existing pattern)
|
|
466
|
+
this.logger.debug(`Encoded docker-compose file to base64 (${base64Content.length} bytes)`);
|
|
467
|
+
return `base64:${base64Content}`;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
this.logger.error(`Failed to encode docker-compose file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
471
|
+
throw new Error(`Failed to encode docker-compose file '${composeFilePath}': ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
exports.StackService = StackService;
|
|
476
|
+
//# sourceMappingURL=StackService.js.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Stack type definitions
|
|
3
|
+
* Defines the structure for declarative application deployments
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Main application stack structure
|
|
7
|
+
*/
|
|
8
|
+
export interface ApplicationStack {
|
|
9
|
+
apiVersion: string;
|
|
10
|
+
kind: 'ApplicationStack';
|
|
11
|
+
metadata: StackMetadata;
|
|
12
|
+
applications: StackApplication[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Stack metadata
|
|
16
|
+
*/
|
|
17
|
+
export interface StackMetadata {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Application definition within a stack
|
|
23
|
+
*/
|
|
24
|
+
export interface StackApplication {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
port: number;
|
|
28
|
+
protocol: 'http' | 'https' | 'tcp' | 'udp';
|
|
29
|
+
subtype: ApplicationSubtype;
|
|
30
|
+
deviceName: string;
|
|
31
|
+
published?: boolean;
|
|
32
|
+
hostnames?: string[];
|
|
33
|
+
dependsOn?: string[];
|
|
34
|
+
authModes?: AuthMode[];
|
|
35
|
+
allowedOrganizations?: string[];
|
|
36
|
+
composeFile?: string;
|
|
37
|
+
command?: string;
|
|
38
|
+
workingDir?: string;
|
|
39
|
+
environment?: Record<string, string>;
|
|
40
|
+
logFile?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Supported application subtypes
|
|
44
|
+
*/
|
|
45
|
+
export type ApplicationSubtype = 'existing' | 'docker-compose' | 'managed-process' | 'docker' | 'podman' | 'qemu';
|
|
46
|
+
/**
|
|
47
|
+
* Authentication modes
|
|
48
|
+
*/
|
|
49
|
+
export type AuthMode = 'none' | 'org' | 'api-key';
|
|
50
|
+
/**
|
|
51
|
+
* Stack with resolved device and gateway IDs
|
|
52
|
+
*/
|
|
53
|
+
export interface ResolvedStack extends ApplicationStack {
|
|
54
|
+
resolved: {
|
|
55
|
+
devices: Map<string, string>;
|
|
56
|
+
gateways: Map<string, string[]>;
|
|
57
|
+
applications: Map<string, string>;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolved application ready for deployment
|
|
62
|
+
*/
|
|
63
|
+
export interface ResolvedApplication extends StackApplication {
|
|
64
|
+
deviceId: string;
|
|
65
|
+
gatewayIds?: string[];
|
|
66
|
+
existingApplicationId?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Stack validation result
|
|
70
|
+
*/
|
|
71
|
+
export interface StackValidationResult {
|
|
72
|
+
valid: boolean;
|
|
73
|
+
errors: StackValidationError[];
|
|
74
|
+
warnings: StackValidationWarning[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validation error
|
|
78
|
+
*/
|
|
79
|
+
export interface StackValidationError {
|
|
80
|
+
path: string;
|
|
81
|
+
message: string;
|
|
82
|
+
line?: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validation warning
|
|
86
|
+
*/
|
|
87
|
+
export interface StackValidationWarning {
|
|
88
|
+
path: string;
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Options for deploy command
|
|
93
|
+
*/
|
|
94
|
+
export interface DeployOptions {
|
|
95
|
+
file: string;
|
|
96
|
+
app?: string;
|
|
97
|
+
dryRun?: boolean;
|
|
98
|
+
skipDependencies?: boolean;
|
|
99
|
+
force?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Options for teardown command
|
|
103
|
+
*/
|
|
104
|
+
export interface TeardownOptions {
|
|
105
|
+
file: string;
|
|
106
|
+
app?: string;
|
|
107
|
+
force?: boolean;
|
|
108
|
+
keepDependencies?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Deploy operation result
|
|
112
|
+
*/
|
|
113
|
+
export interface DeployResult {
|
|
114
|
+
success: boolean;
|
|
115
|
+
deployed: DeployedApplication[];
|
|
116
|
+
failed: FailedApplication[];
|
|
117
|
+
skipped: string[];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Successfully deployed application
|
|
121
|
+
*/
|
|
122
|
+
export interface DeployedApplication {
|
|
123
|
+
name: string;
|
|
124
|
+
applicationId: string;
|
|
125
|
+
urls?: string[];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Failed application deployment
|
|
129
|
+
*/
|
|
130
|
+
export interface FailedApplication {
|
|
131
|
+
name: string;
|
|
132
|
+
error: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Teardown operation result
|
|
136
|
+
*/
|
|
137
|
+
export interface TeardownResult {
|
|
138
|
+
success: boolean;
|
|
139
|
+
removed: string[];
|
|
140
|
+
failed: FailedApplication[];
|
|
141
|
+
skipped: string[];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Stack status information
|
|
145
|
+
*/
|
|
146
|
+
export interface StackStatus {
|
|
147
|
+
stackName: string;
|
|
148
|
+
applications: ApplicationStatus[];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Individual application status
|
|
152
|
+
*/
|
|
153
|
+
export interface ApplicationStatus {
|
|
154
|
+
name: string;
|
|
155
|
+
status: 'deployed' | 'not-deployed' | 'error';
|
|
156
|
+
applicationId?: string;
|
|
157
|
+
deviceName: string;
|
|
158
|
+
deviceId?: string;
|
|
159
|
+
published: boolean;
|
|
160
|
+
urls?: string[];
|
|
161
|
+
lastUpdated?: Date;
|
|
162
|
+
error?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Stack diff result
|
|
166
|
+
*/
|
|
167
|
+
export interface StackDiff {
|
|
168
|
+
toCreate: StackApplication[];
|
|
169
|
+
toUpdate: ApplicationDiff[];
|
|
170
|
+
toDelete: ApplicationStatus[];
|
|
171
|
+
unchanged: string[];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Application diff details
|
|
175
|
+
*/
|
|
176
|
+
export interface ApplicationDiff {
|
|
177
|
+
name: string;
|
|
178
|
+
changes: {
|
|
179
|
+
field: string;
|
|
180
|
+
oldValue: any;
|
|
181
|
+
newValue: any;
|
|
182
|
+
}[];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Dependency graph node
|
|
186
|
+
*/
|
|
187
|
+
export interface DependencyNode {
|
|
188
|
+
name: string;
|
|
189
|
+
dependencies: string[];
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=stack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack.d.ts","sourceRoot":"","sources":["../../src/types/stack.ts"],"names":[],"mappings":"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"}
|