@crossdelta/infrastructure 0.3.2 → 0.4.1
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/README.md +17 -5
- package/bin/generate-env.mjs +135 -0
- package/bin/generate-env.ts +12 -5
- package/dist/index.cjs +18 -336981
- package/dist/index.js +13 -336977
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -89,15 +89,27 @@ The architecture is **provider-agnostic** - runtime components (NATS, Ingress, C
|
|
|
89
89
|
npm install @crossdelta/infrastructure
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
### Peer Dependencies
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
This package requires Pulumi packages as peer dependencies. Install them based on your target cloud provider:
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
|
|
97
|
+
# Required - Pulumi core and Kubernetes
|
|
98
|
+
npm install @pulumi/pulumi @pulumi/kubernetes
|
|
99
|
+
|
|
100
|
+
# Optional - DigitalOcean provider (for DOKS)
|
|
101
|
+
npm install @pulumi/digitalocean
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
| Package | Required | Description |
|
|
105
|
+
|---------|----------|-------------|
|
|
106
|
+
| `@pulumi/pulumi` | ✅ Yes | Pulumi core SDK |
|
|
107
|
+
| `@pulumi/kubernetes` | ✅ Yes | Kubernetes resources |
|
|
108
|
+
| `@pulumi/digitalocean` | Optional | DigitalOcean provider (DOKS, VPC, etc.) |
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
|
|
112
|
+
### 1. Create service configs
|
|
101
113
|
|
|
102
114
|
```typescript
|
|
103
115
|
// infra/services/orders.ts
|
|
@@ -135,7 +147,7 @@ const config: K8sServiceConfig = {
|
|
|
135
147
|
export default config
|
|
136
148
|
```
|
|
137
149
|
|
|
138
|
-
###
|
|
150
|
+
### 2. Create your Pulumi infrastructure
|
|
139
151
|
|
|
140
152
|
```typescript
|
|
141
153
|
// infra/index.ts
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/generate-env.ts
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
|
|
8
|
+
var resolveNumber = (value, fileContent) => {
|
|
9
|
+
const literal = Number.parseInt(value, 10);
|
|
10
|
+
if (!Number.isNaN(literal))
|
|
11
|
+
return literal;
|
|
12
|
+
const varMatch = fileContent.match(new RegExp(`(?:const|let|var)\\s+${value}\\s*=\\s*(\\d+)`));
|
|
13
|
+
return varMatch?.[1] ? Number.parseInt(varMatch[1], 10) : undefined;
|
|
14
|
+
};
|
|
15
|
+
var extract = {
|
|
16
|
+
string: (content, pattern) => content.match(pattern)?.[1],
|
|
17
|
+
number: (content, pattern) => {
|
|
18
|
+
const match = content.match(pattern)?.[1];
|
|
19
|
+
return match ? resolveNumber(match, content) : undefined;
|
|
20
|
+
},
|
|
21
|
+
numberArray: (content, pattern) => {
|
|
22
|
+
const match = content.match(pattern)?.[1];
|
|
23
|
+
if (!match)
|
|
24
|
+
return;
|
|
25
|
+
const numbers = match.split(",").map((v) => resolveNumber(v.trim(), content)).filter((n) => n !== undefined);
|
|
26
|
+
return numbers.length > 0 ? numbers : undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var LOCK_FILES = ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
30
|
+
var hasLockFile = (dir) => LOCK_FILES.some((file) => existsSync(join(dir, file)));
|
|
31
|
+
var findWorkspaceRoot = () => {
|
|
32
|
+
let dir = process.cwd();
|
|
33
|
+
while (!hasLockFile(dir)) {
|
|
34
|
+
const parent = join(dir, "..");
|
|
35
|
+
if (parent === dir)
|
|
36
|
+
throw new Error("Could not locate workspace root");
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
};
|
|
41
|
+
var loadPulumiConfig = async (infraDir, stack) => {
|
|
42
|
+
try {
|
|
43
|
+
const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDir}`, {
|
|
44
|
+
encoding: "utf-8",
|
|
45
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
46
|
+
});
|
|
47
|
+
const config = JSON.parse(stdout);
|
|
48
|
+
return Object.entries(config).filter(([key]) => key.includes(":")).filter(([, entry]) => entry.value !== undefined && entry.value !== null && entry.value !== "undefined").map(([fullKey, entry]) => {
|
|
49
|
+
const [, rawKey] = fullKey.split(":");
|
|
50
|
+
return `${rawKey}=${entry.value}`;
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var discoverServices = (servicesDir) => {
|
|
57
|
+
if (!existsSync(servicesDir))
|
|
58
|
+
return [];
|
|
59
|
+
const files = readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
|
|
60
|
+
return files.map((file) => {
|
|
61
|
+
const content = readFileSync(join(servicesDir, file), "utf-8");
|
|
62
|
+
const portsApiMatch = content.match(/ports\(\)\.(?:http|https|grpc|primary)\((\d+)\)/) || content.match(/ports\(\)\.add\((\d+)/);
|
|
63
|
+
return {
|
|
64
|
+
name: extract.string(content, /name:\s*['"]([^'"]+)['"]/) ?? file.replace(".ts", ""),
|
|
65
|
+
primaryPort: portsApiMatch?.[1] ? Number.parseInt(portsApiMatch[1], 10) : undefined,
|
|
66
|
+
containerPort: extract.number(content, /containerPort:\s*(\w+)/),
|
|
67
|
+
httpPort: extract.number(content, /httpPort:\s*(\w+)/),
|
|
68
|
+
internalPorts: extract.numberArray(content, /internalPorts:\s*\[([^\]]+)\]/),
|
|
69
|
+
internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/)
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
var getServicePort = (config) => {
|
|
74
|
+
if (config.primaryPort)
|
|
75
|
+
return config.primaryPort;
|
|
76
|
+
if (config.containerPort)
|
|
77
|
+
return config.containerPort;
|
|
78
|
+
if (config.httpPort)
|
|
79
|
+
return config.httpPort;
|
|
80
|
+
if (config.internalPorts?.[0])
|
|
81
|
+
return config.internalPorts[0];
|
|
82
|
+
return;
|
|
83
|
+
};
|
|
84
|
+
var getLocalUrl = (config) => {
|
|
85
|
+
const port = getServicePort(config);
|
|
86
|
+
if (!port)
|
|
87
|
+
return;
|
|
88
|
+
if (config.internalUrl) {
|
|
89
|
+
const protocolMatch = config.internalUrl.match(/^(\w+):\/\//);
|
|
90
|
+
if (protocolMatch) {
|
|
91
|
+
return `${protocolMatch[1]}://localhost:${port}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `http://localhost:${port}`;
|
|
95
|
+
};
|
|
96
|
+
var main = async () => {
|
|
97
|
+
const noPulumi = process.argv.includes("--no-pulumi");
|
|
98
|
+
const workspaceRootDir = findWorkspaceRoot();
|
|
99
|
+
const infraDir = join(workspaceRootDir, "infra");
|
|
100
|
+
const servicesDir = join(infraDir, "services");
|
|
101
|
+
const envLines = ["# Generated .env.local"];
|
|
102
|
+
if (!noPulumi && existsSync(join(infraDir, "Pulumi.yaml"))) {
|
|
103
|
+
const pulumiEnvs = await loadPulumiConfig(infraDir, "dev");
|
|
104
|
+
if (pulumiEnvs.length > 0) {
|
|
105
|
+
envLines.push("", "# Pulumi Secrets");
|
|
106
|
+
envLines.push(...pulumiEnvs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const serviceConfigs = discoverServices(servicesDir);
|
|
110
|
+
if (serviceConfigs.length > 0) {
|
|
111
|
+
envLines.push("", "# Service URLs");
|
|
112
|
+
for (const config of serviceConfigs) {
|
|
113
|
+
const url = getLocalUrl(config);
|
|
114
|
+
if (url) {
|
|
115
|
+
envLines.push(`${toEnvKey(config.name)}_URL=${url}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
envLines.push("", "# Service Ports");
|
|
119
|
+
for (const config of serviceConfigs) {
|
|
120
|
+
const port = getServicePort(config);
|
|
121
|
+
if (port) {
|
|
122
|
+
envLines.push(`${toEnvKey(config.name)}_PORT=${port}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log(`✅ Discovered ${serviceConfigs.length} services`);
|
|
126
|
+
}
|
|
127
|
+
writeFileSync(join(workspaceRootDir, ".env.local"), `${envLines.join(`
|
|
128
|
+
`)}
|
|
129
|
+
`);
|
|
130
|
+
console.log(`✅ .env.local generated at ${workspaceRootDir}`);
|
|
131
|
+
};
|
|
132
|
+
main().catch((err) => {
|
|
133
|
+
console.error("❌", err.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/bin/generate-env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Generate environment variables for local development.
|
|
4
4
|
*
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage: generate-env [--no-pulumi]
|
|
12
12
|
*/
|
|
13
|
+
import { execSync } from 'node:child_process'
|
|
13
14
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
15
|
import { join } from 'node:path'
|
|
15
16
|
|
|
@@ -58,9 +59,13 @@ const extract = {
|
|
|
58
59
|
},
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const LOCK_FILES = ['bun.lock', 'bun.lockb', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']
|
|
63
|
+
|
|
64
|
+
const hasLockFile = (dir: string): boolean => LOCK_FILES.some((file) => existsSync(join(dir, file)))
|
|
65
|
+
|
|
61
66
|
const findWorkspaceRoot = (): string => {
|
|
62
67
|
let dir = process.cwd()
|
|
63
|
-
while (!
|
|
68
|
+
while (!hasLockFile(dir)) {
|
|
64
69
|
const parent = join(dir, '..')
|
|
65
70
|
if (parent === dir) throw new Error('Could not locate workspace root')
|
|
66
71
|
dir = parent
|
|
@@ -70,10 +75,12 @@ const findWorkspaceRoot = (): string => {
|
|
|
70
75
|
|
|
71
76
|
const loadPulumiConfig = async (infraDir: string, stack: string): Promise<string[]> => {
|
|
72
77
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
78
|
+
const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDir}`, {
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
})
|
|
75
82
|
|
|
76
|
-
const config = JSON.parse(
|
|
83
|
+
const config = JSON.parse(stdout) as Record<string, PulumiConfigEntry>
|
|
77
84
|
|
|
78
85
|
return Object.entries(config)
|
|
79
86
|
.filter(([key]) => key.includes(':'))
|