@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.
- package/dist/dev/env-builder.d.ts +9 -0
- package/dist/dev/env-builder.d.ts.map +1 -0
- package/dist/dev/env-builder.js +67 -0
- package/dist/dev/env-builder.js.map +1 -0
- package/dist/dev/index.d.ts +20 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +272 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/local-runner.d.ts +13 -0
- package/dist/dev/local-runner.d.ts.map +1 -0
- package/dist/dev/local-runner.js +100 -0
- package/dist/dev/local-runner.js.map +1 -0
- package/dist/dev/port-forward.d.ts +10 -0
- package/dist/dev/port-forward.d.ts.map +1 -0
- package/dist/dev/port-forward.js +146 -0
- package/dist/dev/port-forward.js.map +1 -0
- package/dist/dev/service-swap.d.ts +27 -0
- package/dist/dev/service-swap.d.ts.map +1 -0
- package/dist/dev/service-swap.js +272 -0
- package/dist/dev/service-swap.js.map +1 -0
- package/dist/dev/types.d.ts +60 -0
- package/dist/dev/types.d.ts.map +1 -0
- package/dist/dev/types.js +28 -0
- package/dist/dev/types.js.map +1 -0
- package/dist/docker.d.ts +55 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +331 -0
- package/dist/docker.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/events.d.ts +28 -0
- package/dist/orchestrator/events.d.ts.map +1 -0
- package/dist/orchestrator/events.js +142 -0
- package/dist/orchestrator/events.js.map +1 -0
- package/dist/orchestrator/index.d.ts +32 -0
- package/dist/orchestrator/index.d.ts.map +1 -0
- package/dist/orchestrator/index.js +722 -0
- package/dist/orchestrator/index.js.map +1 -0
- package/dist/types/errors.d.ts +76 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +271 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/events.d.ts +100 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +40 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/manifest.d.ts +121 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +3 -0
- package/dist/types/manifest.js.map +1 -0
- package/dist/types/service.d.ts +35 -0
- package/dist/types/service.d.ts.map +1 -0
- package/dist/types/service.js +3 -0
- package/dist/types/service.js.map +1 -0
- package/dist/utils/kubernetes.d.ts +10 -0
- package/dist/utils/kubernetes.d.ts.map +1 -0
- package/dist/utils/kubernetes.js +38 -0
- package/dist/utils/kubernetes.js.map +1 -0
- package/dist/validation/manifest.d.ts +5 -0
- package/dist/validation/manifest.d.ts.map +1 -0
- package/dist/validation/manifest.js +114 -0
- package/dist/validation/manifest.js.map +1 -0
- package/package.json +49 -0
- 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
|