@pulumix/core 0.1.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.
Files changed (66) hide show
  1. package/dist/dev/env-builder.d.ts +9 -0
  2. package/dist/dev/env-builder.d.ts.map +1 -0
  3. package/dist/dev/env-builder.js +67 -0
  4. package/dist/dev/env-builder.js.map +1 -0
  5. package/dist/dev/index.d.ts +20 -0
  6. package/dist/dev/index.d.ts.map +1 -0
  7. package/dist/dev/index.js +272 -0
  8. package/dist/dev/index.js.map +1 -0
  9. package/dist/dev/local-runner.d.ts +13 -0
  10. package/dist/dev/local-runner.d.ts.map +1 -0
  11. package/dist/dev/local-runner.js +100 -0
  12. package/dist/dev/local-runner.js.map +1 -0
  13. package/dist/dev/port-forward.d.ts +10 -0
  14. package/dist/dev/port-forward.d.ts.map +1 -0
  15. package/dist/dev/port-forward.js +146 -0
  16. package/dist/dev/port-forward.js.map +1 -0
  17. package/dist/dev/service-swap.d.ts +27 -0
  18. package/dist/dev/service-swap.d.ts.map +1 -0
  19. package/dist/dev/service-swap.js +272 -0
  20. package/dist/dev/service-swap.js.map +1 -0
  21. package/dist/dev/types.d.ts +60 -0
  22. package/dist/dev/types.d.ts.map +1 -0
  23. package/dist/dev/types.js +28 -0
  24. package/dist/dev/types.js.map +1 -0
  25. package/dist/docker.d.ts +55 -0
  26. package/dist/docker.d.ts.map +1 -0
  27. package/dist/docker.js +331 -0
  28. package/dist/docker.js.map +1 -0
  29. package/dist/index.d.ts +12 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +29 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/orchestrator/events.d.ts +28 -0
  34. package/dist/orchestrator/events.d.ts.map +1 -0
  35. package/dist/orchestrator/events.js +142 -0
  36. package/dist/orchestrator/events.js.map +1 -0
  37. package/dist/orchestrator/index.d.ts +32 -0
  38. package/dist/orchestrator/index.d.ts.map +1 -0
  39. package/dist/orchestrator/index.js +722 -0
  40. package/dist/orchestrator/index.js.map +1 -0
  41. package/dist/types/errors.d.ts +76 -0
  42. package/dist/types/errors.d.ts.map +1 -0
  43. package/dist/types/errors.js +271 -0
  44. package/dist/types/errors.js.map +1 -0
  45. package/dist/types/events.d.ts +100 -0
  46. package/dist/types/events.d.ts.map +1 -0
  47. package/dist/types/events.js +40 -0
  48. package/dist/types/events.js.map +1 -0
  49. package/dist/types/manifest.d.ts +121 -0
  50. package/dist/types/manifest.d.ts.map +1 -0
  51. package/dist/types/manifest.js +3 -0
  52. package/dist/types/manifest.js.map +1 -0
  53. package/dist/types/service.d.ts +35 -0
  54. package/dist/types/service.d.ts.map +1 -0
  55. package/dist/types/service.js +3 -0
  56. package/dist/types/service.js.map +1 -0
  57. package/dist/utils/kubernetes.d.ts +10 -0
  58. package/dist/utils/kubernetes.d.ts.map +1 -0
  59. package/dist/utils/kubernetes.js +38 -0
  60. package/dist/utils/kubernetes.js.map +1 -0
  61. package/dist/validation/manifest.d.ts +5 -0
  62. package/dist/validation/manifest.d.ts.map +1 -0
  63. package/dist/validation/manifest.js +114 -0
  64. package/dist/validation/manifest.js.map +1 -0
  65. package/package.json +49 -0
  66. package/src/schemas/service-manifest.schema.json +267 -0
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createOrchestrator = exports.Orchestrator = exports.resolveStackConfig = exports.sortByDependencies = exports.discoverPublishedServices = exports.discoverServices = void 0;
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const child_process_1 = require("child_process");
43
+ const fast_glob_1 = __importDefault(require("fast-glob"));
44
+ const EitherAsync_1 = require("purify-ts/EitherAsync");
45
+ const Either_1 = require("purify-ts/Either");
46
+ const Maybe_1 = require("purify-ts/Maybe");
47
+ const jiti_1 = require("jiti");
48
+ const yaml = __importStar(require("yaml"));
49
+ const errors_1 = require("../types/errors");
50
+ const manifest_1 = require("../validation/manifest");
51
+ const events_1 = require("./events");
52
+ const automation_1 = require("@pulumi/pulumi/automation");
53
+ const docker_1 = require("../docker");
54
+ const jiti = (0, jiti_1.createJiti)(__filename, {
55
+ interopDefault: true
56
+ });
57
+ const DEFAULT_BACKEND = {
58
+ type: 'file',
59
+ path: 'dist/'
60
+ };
61
+ const resolveBackendUrl = (rootPath, projectConfig, stackName) => {
62
+ const stackConfig = projectConfig.stacks?.[stackName];
63
+ const backend = stackConfig?.backend ?? projectConfig.backend ?? DEFAULT_BACKEND;
64
+ switch (backend.type) {
65
+ case 'file': {
66
+ const backendPath = backend.path ?? 'dist/';
67
+ const absolutePath = path.isAbsolute(backendPath)
68
+ ? backendPath
69
+ : path.join(rootPath, backendPath);
70
+ return `file://${absolutePath}`;
71
+ }
72
+ case 's3': {
73
+ const prefix = backend.prefix ? `/${backend.prefix.replace(/^\//, '')}` : '';
74
+ const region = backend.region ? `?region=${backend.region}` : '';
75
+ return `s3://${backend.bucket}${prefix}${region}`;
76
+ }
77
+ case 'gcs': {
78
+ const prefix = backend.prefix ? `/${backend.prefix.replace(/^\//, '')}` : '';
79
+ return `gs://${backend.bucket}${prefix}`;
80
+ }
81
+ case 'azblob': {
82
+ const prefix = backend.prefix ? `/${backend.prefix.replace(/^\//, '')}` : '';
83
+ return `azblob://${backend.container}${prefix}`;
84
+ }
85
+ case 'pulumi': {
86
+ if (backend.org) {
87
+ return `https://app.pulumi.com/${backend.org}`;
88
+ }
89
+ return 'https://app.pulumi.com';
90
+ }
91
+ default: {
92
+ const defaultPath = path.join(rootPath, 'dist/');
93
+ return `file://${defaultPath}`;
94
+ }
95
+ }
96
+ };
97
+ const resolveWorkDir = (rootPath, projectConfig, stackName) => {
98
+ const stackConfig = projectConfig.stacks?.[stackName];
99
+ const backend = stackConfig?.backend ?? projectConfig.backend ?? DEFAULT_BACKEND;
100
+ if (backend.type === 'file') {
101
+ const backendPath = backend.path ?? 'dist/';
102
+ return path.isAbsolute(backendPath)
103
+ ? backendPath
104
+ : path.join(rootPath, backendPath);
105
+ }
106
+ return path.join(rootPath, 'dist/');
107
+ };
108
+ const isValidShellArg = (value) => /^[a-zA-Z0-9_\-.:]+$/.test(value);
109
+ const parseYamlFile = (filePath) => {
110
+ try {
111
+ if (!fs.existsSync(filePath)) {
112
+ return (0, Either_1.Right)({});
113
+ }
114
+ const content = fs.readFileSync(filePath, 'utf-8');
115
+ const parsed = yaml.parse(content);
116
+ return (0, Either_1.Right)(parsed ?? {});
117
+ }
118
+ catch (err) {
119
+ const message = err instanceof Error ? err.message : 'Unknown error';
120
+ return (0, Either_1.Left)((0, errors_1.createDiscoveryError)('InvalidYaml', `Failed to parse YAML file ${filePath}: ${message}`, filePath, { filePath, parseError: message }, err instanceof Error ? err : undefined));
121
+ }
122
+ };
123
+ const getConfigValue = (config, key) => config[key] !== undefined ? (0, Maybe_1.Just)(config[key]) : Maybe_1.Nothing;
124
+ const parseServiceMetadata = (rawConfig, serviceName) => {
125
+ const metadata = rawConfig.metadata ?? {};
126
+ return {
127
+ name: metadata.name ?? serviceName,
128
+ version: metadata.version ?? '0.0.0',
129
+ description: metadata.description,
130
+ team: metadata.team,
131
+ owner: metadata.owner,
132
+ repository: metadata.repository,
133
+ documentation: metadata.documentation,
134
+ tags: metadata.tags,
135
+ sla: metadata.sla,
136
+ support: metadata.support,
137
+ contract: metadata.contract
138
+ };
139
+ };
140
+ const extractDependenciesFromPackageJson = (packageJsonPath, allServiceNames) => {
141
+ if (!fs.existsSync(packageJsonPath)) {
142
+ return [];
143
+ }
144
+ try {
145
+ const content = fs.readFileSync(packageJsonPath, 'utf-8');
146
+ const pkg = JSON.parse(content);
147
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
148
+ return Object.keys(deps).filter(dep => {
149
+ const serviceName = dep.includes('/') ? dep.split('/').pop() : dep;
150
+ return serviceName && allServiceNames.has(serviceName);
151
+ }).map(dep => {
152
+ const serviceName = dep.includes('/') ? dep.split('/').pop() : dep;
153
+ return serviceName;
154
+ });
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ };
160
+ const discoverServices = async (rootPath) => {
161
+ try {
162
+ const yamlFiles = await (0, fast_glob_1.default)('**/pulumix.yaml', {
163
+ cwd: rootPath,
164
+ ignore: ['node_modules/**', '.git/**', '**/dist/**', '**/build/**', '**/.pulumi/**'],
165
+ absolute: false
166
+ });
167
+ const services = [];
168
+ const serviceNames = new Set();
169
+ for (const yamlFile of yamlFiles) {
170
+ const servicePath = path.join(rootPath, path.dirname(yamlFile));
171
+ const deployPath = path.join(servicePath, 'pulumix.ts');
172
+ if (!fs.existsSync(deployPath)) {
173
+ continue;
174
+ }
175
+ const configPath = path.join(servicePath, 'pulumix.yaml');
176
+ const configResult = parseYamlFile(configPath);
177
+ if (configResult.isLeft()) {
178
+ return configResult;
179
+ }
180
+ const rawConfig = configResult.unsafeCoerce();
181
+ const validationResult = (0, manifest_1.validateServiceManifest)(rawConfig, configPath);
182
+ if (validationResult.isLeft()) {
183
+ return validationResult;
184
+ }
185
+ const metadata = parseServiceMetadata(rawConfig, path.basename(servicePath));
186
+ serviceNames.add(metadata.name);
187
+ }
188
+ for (const yamlFile of yamlFiles) {
189
+ const servicePath = path.join(rootPath, path.dirname(yamlFile));
190
+ const deployPath = path.join(servicePath, 'pulumix.ts');
191
+ if (!fs.existsSync(deployPath)) {
192
+ continue;
193
+ }
194
+ const configPath = path.join(servicePath, 'pulumix.yaml');
195
+ const dockerPath = path.join(servicePath, 'Dockerfile');
196
+ const packageJsonPath = path.join(servicePath, 'package.json');
197
+ const configResult = parseYamlFile(configPath);
198
+ if (configResult.isLeft()) {
199
+ return configResult;
200
+ }
201
+ const rawConfig = configResult.unsafeCoerce();
202
+ const metadata = parseServiceMetadata(rawConfig, path.basename(servicePath));
203
+ const observability = rawConfig.observability;
204
+ const security = rawConfig.security;
205
+ const dependencies = extractDependenciesFromPackageJson(packageJsonPath, serviceNames);
206
+ services.push({
207
+ name: metadata.name,
208
+ path: servicePath,
209
+ deployPath,
210
+ configPath,
211
+ dependencies,
212
+ hasDockerfile: fs.existsSync(dockerPath),
213
+ rawConfig,
214
+ metadata,
215
+ observability,
216
+ security
217
+ });
218
+ }
219
+ return (0, Either_1.Right)(services);
220
+ }
221
+ catch (err) {
222
+ const message = err instanceof Error ? err.message : 'Unknown error';
223
+ return (0, Either_1.Left)((0, errors_1.createDiscoveryError)('InvalidGlobPattern', `Failed to discover services: ${message}`, rootPath, { rootPath }, err instanceof Error ? err : undefined));
224
+ }
225
+ };
226
+ exports.discoverServices = discoverServices;
227
+ const matchesAllowlist = (packageName, allowlist) => {
228
+ return allowlist.some(pattern => {
229
+ const regexPattern = pattern
230
+ .replace(/\*/g, '.*')
231
+ .replace(/\?/g, '.')
232
+ .replace(/\//g, '\\/');
233
+ const regex = new RegExp(`^${regexPattern}$`);
234
+ return regex.test(packageName);
235
+ });
236
+ };
237
+ const discoverPublishedServices = async (rootPath, allowlist) => {
238
+ try {
239
+ const nodeModulesPath = path.join(rootPath, 'node_modules');
240
+ if (!fs.existsSync(nodeModulesPath)) {
241
+ return (0, Either_1.Right)([]);
242
+ }
243
+ const yamlFiles = await (0, fast_glob_1.default)('**/pulumix.yaml', {
244
+ cwd: nodeModulesPath,
245
+ ignore: ['**/node_modules/**'],
246
+ absolute: false
247
+ });
248
+ const services = [];
249
+ const serviceNames = new Set();
250
+ for (const yamlFile of yamlFiles) {
251
+ const servicePath = path.join(nodeModulesPath, path.dirname(yamlFile));
252
+ const deployPath = path.join(servicePath, 'pulumix.ts');
253
+ if (!fs.existsSync(deployPath)) {
254
+ continue;
255
+ }
256
+ const packageName = path.dirname(yamlFile).replace(/\\/g, '/');
257
+ if (!matchesAllowlist(packageName, allowlist)) {
258
+ continue;
259
+ }
260
+ const configPath = path.join(servicePath, 'pulumix.yaml');
261
+ const configResult = parseYamlFile(configPath);
262
+ if (configResult.isLeft()) {
263
+ return configResult;
264
+ }
265
+ const rawConfig = configResult.unsafeCoerce();
266
+ const validationResult = (0, manifest_1.validateServiceManifest)(rawConfig, configPath);
267
+ if (validationResult.isLeft()) {
268
+ return validationResult;
269
+ }
270
+ const metadata = parseServiceMetadata(rawConfig, path.basename(servicePath));
271
+ serviceNames.add(metadata.name);
272
+ }
273
+ for (const yamlFile of yamlFiles) {
274
+ const servicePath = path.join(nodeModulesPath, path.dirname(yamlFile));
275
+ const deployPath = path.join(servicePath, 'pulumix.ts');
276
+ if (!fs.existsSync(deployPath)) {
277
+ continue;
278
+ }
279
+ const packageName = path.dirname(yamlFile).replace(/\\/g, '/');
280
+ if (!matchesAllowlist(packageName, allowlist)) {
281
+ continue;
282
+ }
283
+ const configPath = path.join(servicePath, 'pulumix.yaml');
284
+ const dockerPath = path.join(servicePath, 'Dockerfile');
285
+ const packageJsonPath = path.join(servicePath, 'package.json');
286
+ const configResult = parseYamlFile(configPath);
287
+ if (configResult.isLeft()) {
288
+ return configResult;
289
+ }
290
+ const rawConfig = configResult.unsafeCoerce();
291
+ const metadata = parseServiceMetadata(rawConfig, path.basename(servicePath));
292
+ const observability = rawConfig.observability;
293
+ const security = rawConfig.security;
294
+ const dependencies = extractDependenciesFromPackageJson(packageJsonPath, serviceNames);
295
+ services.push({
296
+ name: metadata.name,
297
+ path: servicePath,
298
+ deployPath,
299
+ configPath,
300
+ dependencies,
301
+ hasDockerfile: fs.existsSync(dockerPath),
302
+ rawConfig,
303
+ metadata,
304
+ observability,
305
+ security
306
+ });
307
+ }
308
+ return (0, Either_1.Right)(services);
309
+ }
310
+ catch (err) {
311
+ const message = err instanceof Error ? err.message : 'Unknown error';
312
+ return (0, Either_1.Left)((0, errors_1.createDiscoveryError)('InvalidGlobPattern', `Failed to discover published services: ${message}`, rootPath, { rootPath, allowlist }, err instanceof Error ? err : undefined));
313
+ }
314
+ };
315
+ exports.discoverPublishedServices = discoverPublishedServices;
316
+ const sortByDependencies = (services) => {
317
+ const sorted = [];
318
+ const visited = new Set();
319
+ const serviceMap = new Map(services.map(s => [s.name, s]));
320
+ const visit = (name) => {
321
+ if (visited.has(name))
322
+ return;
323
+ visited.add(name);
324
+ const service = serviceMap.get(name);
325
+ if (!service)
326
+ return;
327
+ for (const dep of service.dependencies) {
328
+ visit(dep);
329
+ }
330
+ sorted.push(service);
331
+ };
332
+ for (const service of services) {
333
+ visit(service.name);
334
+ }
335
+ return sorted;
336
+ };
337
+ exports.sortByDependencies = sortByDependencies;
338
+ const resolveStackConfig = (service, stackName) => {
339
+ const stacks = getConfigValue(service.rawConfig, 'stacks');
340
+ const stackConfig = stacks
341
+ .chain(s => getConfigValue(s, stackName))
342
+ .orDefault({});
343
+ return {
344
+ ...service,
345
+ stackConfig
346
+ };
347
+ };
348
+ exports.resolveStackConfig = resolveStackConfig;
349
+ const filterServices = (services, names) => names?.length
350
+ ? services.filter(s => names.includes(s.name))
351
+ : services;
352
+ const runCommandWithProgress = (command, args, onProgress, options) => {
353
+ return new Promise((resolve) => {
354
+ const { spawn } = require('child_process');
355
+ const proc = spawn(command, args, {
356
+ cwd: options?.cwd,
357
+ stdio: ['ignore', 'pipe', 'pipe']
358
+ });
359
+ let stdout = '';
360
+ let stderr = '';
361
+ proc.stdout.on('data', (data) => {
362
+ const text = data.toString();
363
+ stdout += text;
364
+ const lines = text.split('\n');
365
+ for (const line of lines) {
366
+ if (line.trim()) {
367
+ onProgress(line.trim());
368
+ }
369
+ }
370
+ });
371
+ proc.stderr.on('data', (data) => {
372
+ stderr += data.toString();
373
+ });
374
+ proc.on('close', (code) => {
375
+ if (code !== 0) {
376
+ resolve((0, Either_1.Left)((0, errors_1.createDeploymentError)('PulumiFailed', `Command failed: ${command} ${args.join(' ')}\n${stderr || stdout}`, undefined, { exitCode: code })));
377
+ }
378
+ else {
379
+ resolve((0, Either_1.Right)(stdout));
380
+ }
381
+ });
382
+ proc.on('error', (err) => {
383
+ resolve((0, Either_1.Left)((0, errors_1.createDeploymentError)('PulumiFailed', `Command error: ${command} ${args.join(' ')}\n${err.message}`, undefined, { error: err })));
384
+ });
385
+ });
386
+ };
387
+ const clusterExists = (clusterName) => {
388
+ if (!isValidShellArg(clusterName)) {
389
+ return false;
390
+ }
391
+ const result = (0, child_process_1.spawnSync)('k3d', ['cluster', 'list', '-o', 'json'], {
392
+ encoding: 'utf-8',
393
+ stdio: 'pipe'
394
+ });
395
+ if (result.status !== 0) {
396
+ return false;
397
+ }
398
+ try {
399
+ const clusters = JSON.parse(result.stdout);
400
+ return clusters.some((c) => c.name === clusterName);
401
+ }
402
+ catch {
403
+ return false;
404
+ }
405
+ };
406
+ const createK3dCluster = async (clusterName, registryPort, port, eventEmitter) => {
407
+ if (!isValidShellArg(clusterName)) {
408
+ return (0, Either_1.Left)((0, errors_1.createDeploymentError)('ValidationFailed', `Invalid cluster name: ${clusterName}. Must contain only alphanumeric characters, hyphens, underscores, and periods.`));
409
+ }
410
+ if (registryPort < 1 || registryPort > 65535) {
411
+ return (0, Either_1.Left)((0, errors_1.createDeploymentError)('ValidationFailed', `Invalid registry port: ${registryPort}`));
412
+ }
413
+ const args = [
414
+ 'cluster', 'create', clusterName,
415
+ '--registry-create', `${clusterName}-registry:0.0.0.0:${registryPort}`,
416
+ '--port', `${port}:80@loadbalancer`,
417
+ '--agents', '2',
418
+ '--wait'
419
+ ];
420
+ const result = await runCommandWithProgress('k3d', args, (line) => {
421
+ if (line.includes('Creating') || line.includes('Starting') || line.includes('Waiting') || line.includes('Successfully')) {
422
+ eventEmitter.emitTaskUpdate(clusterName, line);
423
+ }
424
+ }, { cwd: undefined });
425
+ return result.map(() => undefined);
426
+ };
427
+ const buildAndPushImage = async (service, registry, eventEmitter) => {
428
+ if (!service.hasDockerfile) {
429
+ return (0, Either_1.Right)(undefined);
430
+ }
431
+ const contentHash = await (0, docker_1.hashBuildContext)(service.path);
432
+ const imageTag = `${registry}/${service.name}:${contentHash}`;
433
+ const exists = await (0, docker_1.imageExistsInRegistry)(registry, service.name, contentHash);
434
+ if (exists) {
435
+ const digest = await (0, docker_1.getImageDigestFromRegistry)(registry, service.name, contentHash);
436
+ if (digest) {
437
+ eventEmitter.emitTaskUpdate(service.name, `unchanged (${contentHash})`);
438
+ return (0, Either_1.Right)({
439
+ imageRef: `${registry}/${service.name}@${digest}`,
440
+ skipped: true,
441
+ contentHash
442
+ });
443
+ }
444
+ eventEmitter.emitTaskUpdate(service.name, `unchanged (${contentHash})`);
445
+ return (0, Either_1.Right)({
446
+ imageRef: imageTag,
447
+ skipped: true,
448
+ contentHash
449
+ });
450
+ }
451
+ eventEmitter.emitTaskUpdate(service.name, `building (${contentHash})`);
452
+ const buildResult = await (0, docker_1.buildImage)({
453
+ contextPath: service.path,
454
+ tag: imageTag,
455
+ onProgress: (progress) => {
456
+ if (progress.current && progress.total) {
457
+ eventEmitter.emitTaskUpdate(service.name, progress.message, (progress.current / progress.total) * 100);
458
+ }
459
+ else {
460
+ eventEmitter.emitTaskUpdate(service.name, progress.message);
461
+ }
462
+ }
463
+ });
464
+ if (buildResult.isLeft()) {
465
+ return buildResult;
466
+ }
467
+ const pushResult = await (0, docker_1.pushImage)({
468
+ tag: imageTag,
469
+ onProgress: (progress) => {
470
+ const msg = progress.id
471
+ ? `${progress.status} ${progress.id}${progress.progress ? ` (${progress.progress}%)` : ''}`
472
+ : progress.status;
473
+ eventEmitter.emitTaskUpdate(service.name, msg);
474
+ }
475
+ });
476
+ if (pushResult.isLeft()) {
477
+ return pushResult;
478
+ }
479
+ const push = pushResult.extract();
480
+ if (push.digest) {
481
+ return (0, Either_1.Right)({
482
+ imageRef: `${registry}/${service.name}@${push.digest}`,
483
+ skipped: false,
484
+ contentHash
485
+ });
486
+ }
487
+ return (0, Either_1.Right)({
488
+ imageRef: imageTag,
489
+ skipped: false,
490
+ contentHash
491
+ });
492
+ };
493
+ const loadDeployFunction = async (service) => {
494
+ try {
495
+ const deployModule = await jiti.import(service.deployPath);
496
+ const deployFn = deployModule.default ?? deployModule;
497
+ if (typeof deployFn !== 'function') {
498
+ return (0, Either_1.Left)((0, errors_1.createConfigError)('InvalidConfigFormat', `${service.deployPath} must export a default function`, undefined, 'default', { serviceName: service.name, deployPath: service.deployPath }));
499
+ }
500
+ return (0, Either_1.Right)(deployFn);
501
+ }
502
+ catch (err) {
503
+ const message = err instanceof Error ? err.message : 'Unknown error';
504
+ return (0, Either_1.Left)((0, errors_1.createConfigError)('InvalidConfigFormat', `Failed to load deploy function from ${service.name}: ${message}`, undefined, 'deployPath', { serviceName: service.name, deployPath: service.deployPath }, err instanceof Error ? err : undefined));
505
+ }
506
+ };
507
+ class Orchestrator {
508
+ eventEmitter;
509
+ constructor(eventEmitter) {
510
+ this.eventEmitter = eventEmitter ?? (0, events_1.createEventEmitter)();
511
+ }
512
+ getEventEmitter() {
513
+ return this.eventEmitter;
514
+ }
515
+ validateEnvironment(stackName) {
516
+ if (stackName === 'production' && !process.env.PULUMI_CONFIG_PASSPHRASE) {
517
+ return (0, Either_1.Left)((0, errors_1.createConfigError)('MissingRequiredField', 'PULUMI_CONFIG_PASSPHRASE environment variable is required for production deployments', stackName, 'PULUMI_CONFIG_PASSPHRASE', { stackName, envVar: 'PULUMI_CONFIG_PASSPHRASE' }));
518
+ }
519
+ if (!process.env.PULUMI_CONFIG_PASSPHRASE) {
520
+ process.env.PULUMI_CONFIG_PASSPHRASE = '';
521
+ }
522
+ return (0, Either_1.Right)(undefined);
523
+ }
524
+ deploy(config) {
525
+ const startTime = Date.now();
526
+ return (0, EitherAsync_1.EitherAsync)(async ({ liftEither, throwE }) => {
527
+ await liftEither(this.validateEnvironment(config.stackName));
528
+ this.eventEmitter.emitPhaseStart('configuration');
529
+ const rootConfigPath = path.join(config.rootPath, 'pulumix.yaml');
530
+ const rootConfig = await liftEither(parseYamlFile(rootConfigPath));
531
+ const stacks = rootConfig.stacks ?? {};
532
+ const globalConfig = stacks[config.stackName] ?? {};
533
+ this.eventEmitter.emitPhaseComplete('configuration');
534
+ this.eventEmitter.emitPhaseStart('discovery');
535
+ const localServices = await liftEither(await (0, exports.discoverServices)(config.rootPath));
536
+ const allowlist = rootConfig.services?.allowed ?? [];
537
+ const publishedServices = await liftEither(await (0, exports.discoverPublishedServices)(config.rootPath, allowlist));
538
+ const localServiceNames = new Set(localServices.map(s => s.name));
539
+ const mergedServices = [
540
+ ...localServices,
541
+ ...publishedServices.filter(s => !localServiceNames.has(s.name))
542
+ ];
543
+ const services = filterServices(mergedServices, config.servicesToDeploy);
544
+ for (let i = 0; i < services.length; i++) {
545
+ const service = services[i];
546
+ const isLast = i === services.length - 1;
547
+ const prefix = isLast ? '└─' : '├─';
548
+ const source = localServiceNames.has(service.name) ? 'local' : 'published';
549
+ this.eventEmitter.emitLog('info', `${prefix} ${service.name} (${source})`, undefined, 'Discovery');
550
+ }
551
+ this.eventEmitter.emitPhaseComplete('discovery');
552
+ this.eventEmitter.emitPhaseStart('dependency-analysis');
553
+ const sorted = (0, exports.sortByDependencies)(services);
554
+ for (let i = 0; i < sorted.length; i++) {
555
+ const service = sorted[i];
556
+ const isLast = i === sorted.length - 1;
557
+ const prefix = isLast ? '└─' : '├─';
558
+ const deps = service.dependencies.length > 0
559
+ ? ` (requires: ${service.dependencies.join(', ')})`
560
+ : '';
561
+ this.eventEmitter.emitLog('info', `${prefix} ${service.name}${deps}`, undefined, 'DependencyGraph');
562
+ }
563
+ this.eventEmitter.emitPhaseComplete('dependency-analysis');
564
+ const providerService = sorted.find(s => s.name === 'provider');
565
+ const providerConfig = providerService
566
+ ? (0, exports.resolveStackConfig)(providerService, config.stackName).stackConfig
567
+ : {};
568
+ const k3dConfig = getConfigValue(providerConfig, 'k3d').orDefault({});
569
+ const hostRegistry = getConfigValue(globalConfig, 'hostRegistry').orDefault('') ||
570
+ k3dConfig.hostRegistry ||
571
+ (k3dConfig.registryPort ? `localhost:${k3dConfig.registryPort}` : 'localhost:5001');
572
+ const clusterRegistry = getConfigValue(globalConfig, 'clusterRegistry').orDefault('') ||
573
+ k3dConfig.clusterRegistry ||
574
+ hostRegistry;
575
+ const servicesToBuild = sorted.filter(s => s.hasDockerfile);
576
+ if (servicesToBuild.length > 0 && k3dConfig.enabled) {
577
+ this.eventEmitter.emitPhaseStart('bootstrap');
578
+ const clusterName = k3dConfig.clusterName ?? 'pulumix-dev';
579
+ const port = k3dConfig.port ?? 80;
580
+ const registryPort = k3dConfig.registryPort ?? 5001;
581
+ if (clusterExists(clusterName)) {
582
+ this.eventEmitter.emitTaskStart(clusterName);
583
+ this.eventEmitter.emitTaskComplete(clusterName, true, true);
584
+ }
585
+ else {
586
+ this.eventEmitter.emitTaskStart(clusterName);
587
+ const createResult = await createK3dCluster(clusterName, registryPort, port, this.eventEmitter);
588
+ const success = createResult.isRight();
589
+ this.eventEmitter.emitTaskComplete(clusterName, success);
590
+ if (createResult.isLeft()) {
591
+ throw throwE(createResult.extract());
592
+ }
593
+ }
594
+ this.eventEmitter.emitPhaseComplete('bootstrap');
595
+ }
596
+ const builtImages = {};
597
+ if (servicesToBuild.length > 0) {
598
+ this.eventEmitter.emitPhaseStart('image-build');
599
+ for (const service of servicesToBuild) {
600
+ this.eventEmitter.emitTaskStart(service.name);
601
+ const buildResult = await buildAndPushImage(service, hostRegistry, this.eventEmitter);
602
+ if (buildResult.isLeft()) {
603
+ const error = buildResult.extract();
604
+ this.eventEmitter.emitTaskComplete(service.name, false);
605
+ this.eventEmitter.emitLog('warn', error.message, undefined, 'Build');
606
+ }
607
+ else {
608
+ const result = buildResult.unsafeCoerce();
609
+ if (result) {
610
+ const imageWithoutRegistry = result.imageRef.replace(`${hostRegistry}/`, '');
611
+ builtImages[service.name] = `${clusterRegistry}/${imageWithoutRegistry}`;
612
+ this.eventEmitter.emitTaskComplete(service.name, true, result.skipped, result.contentHash);
613
+ }
614
+ }
615
+ }
616
+ this.eventEmitter.emitPhaseComplete('image-build');
617
+ }
618
+ this.eventEmitter.emitPhaseStart('deployment');
619
+ const namespace = getConfigValue(globalConfig, 'namespace').orDefault(config.stackName);
620
+ const outputs = {};
621
+ const program = async () => {
622
+ for (const service of sorted) {
623
+ const resolved = (0, exports.resolveStackConfig)(service, config.stackName);
624
+ const ctx = {
625
+ stackName: config.stackName,
626
+ serviceName: service.name,
627
+ metadata: service.metadata,
628
+ observability: service.observability,
629
+ security: service.security,
630
+ config: resolved.stackConfig,
631
+ globalConfig,
632
+ namespace,
633
+ dependencies: outputs,
634
+ image: builtImages[service.name]
635
+ };
636
+ const deployFnResult = await loadDeployFunction(service);
637
+ if (deployFnResult.isLeft()) {
638
+ throw deployFnResult.extract();
639
+ }
640
+ const deployFn = deployFnResult.unsafeCoerce();
641
+ const result = await deployFn(ctx);
642
+ if (result?.outputs) {
643
+ outputs[service.name] = result.outputs;
644
+ }
645
+ }
646
+ };
647
+ const backendUrl = resolveBackendUrl(config.rootPath, rootConfig, config.stackName);
648
+ const workDir = resolveWorkDir(config.rootPath, rootConfig, config.stackName);
649
+ if (!fs.existsSync(workDir)) {
650
+ fs.mkdirSync(workDir, { recursive: true });
651
+ }
652
+ const projectName = rootConfig.name ?? 'pulumix-project';
653
+ const stack = await automation_1.LocalWorkspace.createOrSelectStack({
654
+ stackName: config.stackName,
655
+ projectName,
656
+ program
657
+ }, {
658
+ workDir,
659
+ projectSettings: {
660
+ name: projectName,
661
+ runtime: 'nodejs',
662
+ backend: { url: backendUrl }
663
+ }
664
+ });
665
+ await stack.up({
666
+ onOutput: config.onOutput || ((msg) => this.eventEmitter.emitLog('info', msg, undefined, 'Deploy')),
667
+ });
668
+ this.eventEmitter.emitPhaseComplete('deployment');
669
+ const stackOutputs = await stack.outputs();
670
+ const allOutputs = {
671
+ ...outputs,
672
+ _stack: Object.fromEntries(Object.entries(stackOutputs).map(([k, v]) => [k, v.value]))
673
+ };
674
+ return {
675
+ success: true,
676
+ stack: config.stackName,
677
+ servicesDeployed: sorted.length,
678
+ duration: Date.now() - startTime,
679
+ outputs: allOutputs
680
+ };
681
+ });
682
+ }
683
+ destroy(config) {
684
+ const startTime = Date.now();
685
+ return (0, EitherAsync_1.EitherAsync)(async ({ liftEither }) => {
686
+ await liftEither(this.validateEnvironment(config.stackName));
687
+ const rootConfigPath = path.join(config.rootPath, 'pulumix.yaml');
688
+ const rootConfig = await liftEither(parseYamlFile(rootConfigPath));
689
+ const backendUrl = resolveBackendUrl(config.rootPath, rootConfig, config.stackName);
690
+ const workDir = resolveWorkDir(config.rootPath, rootConfig, config.stackName);
691
+ const projectName = rootConfig.name ?? 'pulumix-project';
692
+ const program = async () => {
693
+ };
694
+ const stack = await automation_1.LocalWorkspace.createOrSelectStack({
695
+ stackName: config.stackName,
696
+ projectName,
697
+ program
698
+ }, {
699
+ workDir,
700
+ projectSettings: {
701
+ name: projectName,
702
+ runtime: 'nodejs',
703
+ backend: { url: backendUrl }
704
+ }
705
+ });
706
+ await stack.destroy({
707
+ onOutput: config.onOutput || ((msg) => this.eventEmitter.emitLog('info', msg, undefined, 'Deploy')),
708
+ });
709
+ return {
710
+ success: true,
711
+ stack: config.stackName,
712
+ servicesDeployed: 0,
713
+ duration: Date.now() - startTime,
714
+ outputs: {}
715
+ };
716
+ });
717
+ }
718
+ }
719
+ exports.Orchestrator = Orchestrator;
720
+ const createOrchestrator = (eventEmitter) => new Orchestrator(eventEmitter);
721
+ exports.createOrchestrator = createOrchestrator;
722
+ //# sourceMappingURL=index.js.map