@crossdelta/infrastructure 0.7.6 → 0.8.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 +2 -0
- package/bin/generate-env.mjs +88 -66
- package/bin/generate-env.ts +100 -72
- package/dist/index.cjs +9 -2
- package/dist/index.js +9 -2
- package/dist/runtimes/doks/types.d.ts +15 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ const config: K8sServiceConfig = {
|
|
|
28
28
|
},
|
|
29
29
|
ingress: { path: '/api', host: 'example.com' },
|
|
30
30
|
env: { DATABASE_URL: dbUrl },
|
|
31
|
+
containerEnv: { NODE_OPTIONS: '--max-old-space-size=384' },
|
|
31
32
|
secrets: { API_KEY: apiKey },
|
|
32
33
|
resources: {
|
|
33
34
|
requests: { cpu: '50m', memory: '64Mi' },
|
|
@@ -95,6 +96,7 @@ interface K8sServiceConfig {
|
|
|
95
96
|
host?: string
|
|
96
97
|
}
|
|
97
98
|
env?: Record<string, pulumi.Input<string>>
|
|
99
|
+
containerEnv?: Record<string, pulumi.Input<string>>
|
|
98
100
|
secrets?: Record<string, pulumi.Input<string>>
|
|
99
101
|
image?: string
|
|
100
102
|
skip?: boolean
|
package/bin/generate-env.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
var LOCK_FILES = ["bun.lock", "bun.lockb", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"];
|
|
7
8
|
var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
|
|
8
9
|
var resolveNumber = (value, fileContent) => {
|
|
9
10
|
const literal = Number.parseInt(value, 10);
|
|
@@ -22,28 +23,84 @@ var extract = {
|
|
|
22
23
|
const match = content.match(pattern)?.[1];
|
|
23
24
|
if (!match)
|
|
24
25
|
return;
|
|
25
|
-
const numbers = match.split(",").map((
|
|
26
|
+
const numbers = match.split(",").map((value) => resolveNumber(value.trim(), content)).filter((num) => num !== undefined);
|
|
26
27
|
return numbers.length > 0 ? numbers : undefined;
|
|
27
28
|
}
|
|
28
29
|
};
|
|
29
|
-
var
|
|
30
|
-
|
|
30
|
+
var extractEnvLiterals = (content) => {
|
|
31
|
+
const envBlockMatch = content.match(/(?<!\w)env:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/);
|
|
32
|
+
if (!envBlockMatch?.[1])
|
|
33
|
+
return;
|
|
34
|
+
const literals = {};
|
|
35
|
+
const literalPattern = /^\s*(\w+):\s*['"]([^'"]+)['"]/gm;
|
|
36
|
+
for (const [, key, value] of envBlockMatch[1].matchAll(literalPattern)) {
|
|
37
|
+
if (key && value)
|
|
38
|
+
literals[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(literals).length > 0 ? literals : undefined;
|
|
41
|
+
};
|
|
42
|
+
var getServicePort = (config) => {
|
|
43
|
+
if (config.primaryPort)
|
|
44
|
+
return config.primaryPort;
|
|
45
|
+
if (config.containerPort)
|
|
46
|
+
return config.containerPort;
|
|
47
|
+
if (config.httpPort)
|
|
48
|
+
return config.httpPort;
|
|
49
|
+
if (config.internalPorts?.[0])
|
|
50
|
+
return config.internalPorts[0];
|
|
51
|
+
return;
|
|
52
|
+
};
|
|
53
|
+
var getLocalUrl = (config) => {
|
|
54
|
+
const port = getServicePort(config);
|
|
55
|
+
if (!port)
|
|
56
|
+
return;
|
|
57
|
+
if (config.internalUrl) {
|
|
58
|
+
const protocolMatch = config.internalUrl.match(/^(\w+):\/\//);
|
|
59
|
+
if (protocolMatch) {
|
|
60
|
+
return `${protocolMatch[1]}://localhost:${port}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `http://localhost:${port}`;
|
|
64
|
+
};
|
|
65
|
+
var buildServiceEnvLines = (serviceConfigs) => {
|
|
66
|
+
const lines = [];
|
|
67
|
+
lines.push("", "# Service URLs");
|
|
68
|
+
for (const config of serviceConfigs) {
|
|
69
|
+
const url = getLocalUrl(config);
|
|
70
|
+
if (url)
|
|
71
|
+
lines.push(`${toEnvKey(config.name)}_URL=${url}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push("", "# Service Ports");
|
|
74
|
+
for (const config of serviceConfigs) {
|
|
75
|
+
const port = getServicePort(config);
|
|
76
|
+
if (port)
|
|
77
|
+
lines.push(`${toEnvKey(config.name)}_PORT=${port}`);
|
|
78
|
+
}
|
|
79
|
+
const envLiteralConfigs = serviceConfigs.filter((config) => config.envLiterals !== undefined);
|
|
80
|
+
if (envLiteralConfigs.length > 0) {
|
|
81
|
+
lines.push("", "# Service Environment");
|
|
82
|
+
for (const config of envLiteralConfigs) {
|
|
83
|
+
for (const [key, value] of Object.entries(config.envLiterals)) {
|
|
84
|
+
lines.push(`${key}=${value}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
};
|
|
90
|
+
var hasLockFile = (directory) => LOCK_FILES.some((file) => existsSync(join(directory, file)));
|
|
31
91
|
var findWorkspaceRoot = () => {
|
|
32
|
-
let
|
|
33
|
-
while (!hasLockFile(
|
|
34
|
-
const parent = join(
|
|
35
|
-
if (parent ===
|
|
92
|
+
let directory = process.cwd();
|
|
93
|
+
while (!hasLockFile(directory)) {
|
|
94
|
+
const parent = join(directory, "..");
|
|
95
|
+
if (parent === directory)
|
|
36
96
|
throw new Error("Could not locate workspace root");
|
|
37
|
-
|
|
97
|
+
directory = parent;
|
|
38
98
|
}
|
|
39
|
-
return
|
|
99
|
+
return directory;
|
|
40
100
|
};
|
|
41
|
-
var loadPulumiConfig = async (
|
|
101
|
+
var loadPulumiConfig = async (infraDirectory, stack) => {
|
|
42
102
|
try {
|
|
43
|
-
const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${
|
|
44
|
-
encoding: "utf-8",
|
|
45
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
46
|
-
});
|
|
103
|
+
const stdout = execSync(`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDirectory}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
47
104
|
const config = JSON.parse(stdout);
|
|
48
105
|
return Object.entries(config).filter(([key]) => key.includes(":")).filter(([, entry]) => entry.value !== undefined && entry.value !== null && entry.value !== "undefined").map(([fullKey, entry]) => {
|
|
49
106
|
const [, rawKey] = fullKey.split(":");
|
|
@@ -53,12 +110,12 @@ var loadPulumiConfig = async (infraDir, stack) => {
|
|
|
53
110
|
return [];
|
|
54
111
|
}
|
|
55
112
|
};
|
|
56
|
-
var discoverServices = (
|
|
57
|
-
if (!existsSync(
|
|
113
|
+
var discoverServices = (servicesDirectory) => {
|
|
114
|
+
if (!existsSync(servicesDirectory))
|
|
58
115
|
return [];
|
|
59
|
-
const files = readdirSync(
|
|
116
|
+
const files = readdirSync(servicesDirectory).filter((file) => file.endsWith(".ts") && file !== "index.ts");
|
|
60
117
|
return files.map((file) => {
|
|
61
|
-
const content = readFileSync(join(
|
|
118
|
+
const content = readFileSync(join(servicesDirectory, file), "utf-8");
|
|
62
119
|
const portsApiMatch = content.match(/ports\(\)\.(?:http|https|grpc|primary)\((\d+)\)/) || content.match(/ports\(\)\.add\((\d+)/);
|
|
63
120
|
return {
|
|
64
121
|
name: extract.string(content, /name:\s*['"]([^'"]+)['"]/) ?? file.replace(".ts", ""),
|
|
@@ -66,68 +123,33 @@ var discoverServices = (servicesDir) => {
|
|
|
66
123
|
containerPort: extract.number(content, /containerPort:\s*(\w+)/),
|
|
67
124
|
httpPort: extract.number(content, /httpPort:\s*(\w+)/),
|
|
68
125
|
internalPorts: extract.numberArray(content, /internalPorts:\s*\[([^\]]+)\]/),
|
|
69
|
-
internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/)
|
|
126
|
+
internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/),
|
|
127
|
+
envLiterals: extractEnvLiterals(content)
|
|
70
128
|
};
|
|
71
129
|
});
|
|
72
130
|
};
|
|
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
131
|
var main = async () => {
|
|
97
132
|
const noPulumi = process.argv.includes("--no-pulumi");
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
133
|
+
const workspaceRootDirectory = findWorkspaceRoot();
|
|
134
|
+
const infraDirectory = join(workspaceRootDirectory, "infra");
|
|
135
|
+
const servicesDirectory = join(infraDirectory, "services");
|
|
101
136
|
const envLines = ["# Generated .env.local"];
|
|
102
|
-
if (!noPulumi && existsSync(join(
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
137
|
+
if (!noPulumi && existsSync(join(infraDirectory, "Pulumi.yaml"))) {
|
|
138
|
+
const pulumiEntries = await loadPulumiConfig(infraDirectory, "dev");
|
|
139
|
+
if (pulumiEntries.length > 0) {
|
|
105
140
|
envLines.push("", "# Pulumi Secrets");
|
|
106
|
-
envLines.push(...
|
|
141
|
+
envLines.push(...pulumiEntries);
|
|
107
142
|
}
|
|
108
143
|
}
|
|
109
|
-
const serviceConfigs = discoverServices(
|
|
144
|
+
const serviceConfigs = discoverServices(servicesDirectory);
|
|
110
145
|
if (serviceConfigs.length > 0) {
|
|
111
|
-
envLines.push(
|
|
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
|
-
}
|
|
146
|
+
envLines.push(...buildServiceEnvLines(serviceConfigs));
|
|
125
147
|
console.log(`✅ Discovered ${serviceConfigs.length} services`);
|
|
126
148
|
}
|
|
127
|
-
writeFileSync(join(
|
|
149
|
+
writeFileSync(join(workspaceRootDirectory, ".env.local"), `${envLines.join(`
|
|
128
150
|
`)}
|
|
129
151
|
`);
|
|
130
|
-
console.log(`✅ .env.local generated at ${
|
|
152
|
+
console.log(`✅ .env.local generated at ${workspaceRootDirectory}`);
|
|
131
153
|
};
|
|
132
154
|
main().catch((err) => {
|
|
133
155
|
console.error("❌", err.message);
|
package/bin/generate-env.ts
CHANGED
|
@@ -26,8 +26,11 @@ interface MinimalServiceConfig {
|
|
|
26
26
|
httpPort?: number
|
|
27
27
|
internalPorts?: number[]
|
|
28
28
|
internalUrl?: string
|
|
29
|
+
envLiterals?: Record<string, string>
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
const LOCK_FILES = ['bun.lock', 'bun.lockb', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']
|
|
33
|
+
|
|
31
34
|
const toEnvKey = (name: string): string => name.toUpperCase().replace(/-/g, '_')
|
|
32
35
|
|
|
33
36
|
const resolveNumber = (value: string, fileContent: string): number | undefined => {
|
|
@@ -52,33 +55,97 @@ const extract = {
|
|
|
52
55
|
|
|
53
56
|
const numbers = match
|
|
54
57
|
.split(',')
|
|
55
|
-
.map((
|
|
56
|
-
.filter((
|
|
58
|
+
.map((value) => resolveNumber(value.trim(), content))
|
|
59
|
+
.filter((num): num is number => num !== undefined)
|
|
57
60
|
|
|
58
61
|
return numbers.length > 0 ? numbers : undefined
|
|
59
62
|
},
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
const
|
|
65
|
+
const extractEnvLiterals = (content: string): Record<string, string> | undefined => {
|
|
66
|
+
const envBlockMatch = content.match(/(?<!\w)env:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/)
|
|
67
|
+
if (!envBlockMatch?.[1]) return undefined
|
|
68
|
+
|
|
69
|
+
const literals: Record<string, string> = {}
|
|
70
|
+
const literalPattern = /^\s*(\w+):\s*['"]([^'"]+)['"]/gm
|
|
71
|
+
for (const [, key, value] of envBlockMatch[1].matchAll(literalPattern)) {
|
|
72
|
+
if (key && value) literals[key] = value
|
|
73
|
+
}
|
|
74
|
+
return Object.keys(literals).length > 0 ? literals : undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getServicePort = (config: MinimalServiceConfig): number | undefined => {
|
|
78
|
+
if (config.primaryPort) return config.primaryPort
|
|
79
|
+
if (config.containerPort) return config.containerPort
|
|
80
|
+
if (config.httpPort) return config.httpPort
|
|
81
|
+
if (config.internalPorts?.[0]) return config.internalPorts[0]
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const getLocalUrl = (config: MinimalServiceConfig): string | undefined => {
|
|
86
|
+
const port = getServicePort(config)
|
|
87
|
+
if (!port) return undefined
|
|
63
88
|
|
|
64
|
-
|
|
89
|
+
if (config.internalUrl) {
|
|
90
|
+
const protocolMatch = config.internalUrl.match(/^(\w+):\/\//)
|
|
91
|
+
if (protocolMatch) {
|
|
92
|
+
return `${protocolMatch[1]}://localhost:${port}`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `http://localhost:${port}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const buildServiceEnvLines = (serviceConfigs: MinimalServiceConfig[]): string[] => {
|
|
100
|
+
const lines: string[] = []
|
|
101
|
+
|
|
102
|
+
lines.push('', '# Service URLs')
|
|
103
|
+
for (const config of serviceConfigs) {
|
|
104
|
+
const url = getLocalUrl(config)
|
|
105
|
+
if (url) lines.push(`${toEnvKey(config.name)}_URL=${url}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push('', '# Service Ports')
|
|
109
|
+
for (const config of serviceConfigs) {
|
|
110
|
+
const port = getServicePort(config)
|
|
111
|
+
if (port) lines.push(`${toEnvKey(config.name)}_PORT=${port}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const envLiteralConfigs = serviceConfigs.filter(
|
|
115
|
+
(config): config is MinimalServiceConfig & { envLiterals: Record<string, string> } =>
|
|
116
|
+
config.envLiterals !== undefined,
|
|
117
|
+
)
|
|
118
|
+
if (envLiteralConfigs.length > 0) {
|
|
119
|
+
lines.push('', '# Service Environment')
|
|
120
|
+
for (const config of envLiteralConfigs) {
|
|
121
|
+
for (const [key, value] of Object.entries(config.envLiterals)) {
|
|
122
|
+
lines.push(`${key}=${value}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hasLockFile = (directory: string): boolean =>
|
|
131
|
+
LOCK_FILES.some((file) => existsSync(join(directory, file)))
|
|
65
132
|
|
|
66
133
|
const findWorkspaceRoot = (): string => {
|
|
67
|
-
let
|
|
68
|
-
while (!hasLockFile(
|
|
69
|
-
const parent = join(
|
|
70
|
-
if (parent ===
|
|
71
|
-
|
|
134
|
+
let directory = process.cwd()
|
|
135
|
+
while (!hasLockFile(directory)) {
|
|
136
|
+
const parent = join(directory, '..')
|
|
137
|
+
if (parent === directory) throw new Error('Could not locate workspace root')
|
|
138
|
+
directory = parent
|
|
72
139
|
}
|
|
73
|
-
return
|
|
140
|
+
return directory
|
|
74
141
|
}
|
|
75
142
|
|
|
76
|
-
const loadPulumiConfig = async (
|
|
143
|
+
const loadPulumiConfig = async (infraDirectory: string, stack: string): Promise<string[]> => {
|
|
77
144
|
try {
|
|
78
|
-
const stdout = execSync(
|
|
79
|
-
|
|
80
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
-
|
|
145
|
+
const stdout = execSync(
|
|
146
|
+
`pulumi config --show-secrets --json --stack ${stack} --cwd ${infraDirectory}`,
|
|
147
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
148
|
+
)
|
|
82
149
|
|
|
83
150
|
const config = JSON.parse(stdout) as Record<string, PulumiConfigEntry>
|
|
84
151
|
|
|
@@ -94,15 +161,15 @@ const loadPulumiConfig = async (infraDir: string, stack: string): Promise<string
|
|
|
94
161
|
}
|
|
95
162
|
}
|
|
96
163
|
|
|
97
|
-
const discoverServices = (
|
|
98
|
-
if (!existsSync(
|
|
164
|
+
const discoverServices = (servicesDirectory: string): MinimalServiceConfig[] => {
|
|
165
|
+
if (!existsSync(servicesDirectory)) return []
|
|
99
166
|
|
|
100
|
-
const files = readdirSync(
|
|
167
|
+
const files = readdirSync(servicesDirectory).filter((file) => file.endsWith('.ts') && file !== 'index.ts')
|
|
101
168
|
|
|
102
169
|
return files.map((file) => {
|
|
103
|
-
const content = readFileSync(join(
|
|
170
|
+
const content = readFileSync(join(servicesDirectory, file), 'utf-8')
|
|
104
171
|
|
|
105
|
-
// Extract port from
|
|
172
|
+
// Extract port from ports() API: ports().http(4001) or ports().primary(4222)
|
|
106
173
|
const portsApiMatch =
|
|
107
174
|
content.match(/ports\(\)\.(?:http|https|grpc|primary)\((\d+)\)/) ||
|
|
108
175
|
content.match(/ports\(\)\.add\((\d+)/)
|
|
@@ -114,74 +181,35 @@ const discoverServices = (servicesDir: string): MinimalServiceConfig[] => {
|
|
|
114
181
|
httpPort: extract.number(content, /httpPort:\s*(\w+)/),
|
|
115
182
|
internalPorts: extract.numberArray(content, /internalPorts:\s*\[([^\]]+)\]/),
|
|
116
183
|
internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/),
|
|
184
|
+
envLiterals: extractEnvLiterals(content),
|
|
117
185
|
}
|
|
118
186
|
})
|
|
119
187
|
}
|
|
120
188
|
|
|
121
|
-
const getServicePort = (config: MinimalServiceConfig): number | undefined => {
|
|
122
|
-
if (config.primaryPort) return config.primaryPort // New ports() API
|
|
123
|
-
if (config.containerPort) return config.containerPort
|
|
124
|
-
if (config.httpPort) return config.httpPort
|
|
125
|
-
if (config.internalPorts?.[0]) return config.internalPorts[0]
|
|
126
|
-
return undefined
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const getLocalUrl = (config: MinimalServiceConfig): string | undefined => {
|
|
130
|
-
const port = getServicePort(config)
|
|
131
|
-
if (!port) return undefined
|
|
132
|
-
|
|
133
|
-
if (config.internalUrl) {
|
|
134
|
-
const protocolMatch = config.internalUrl.match(/^(\w+):\/\//)
|
|
135
|
-
if (protocolMatch) {
|
|
136
|
-
return `${protocolMatch[1]}://localhost:${port}`
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return `http://localhost:${port}`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
189
|
const main = async () => {
|
|
144
190
|
const noPulumi = process.argv.includes('--no-pulumi')
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
191
|
+
const workspaceRootDirectory = findWorkspaceRoot()
|
|
192
|
+
const infraDirectory = join(workspaceRootDirectory, 'infra')
|
|
193
|
+
const servicesDirectory = join(infraDirectory, 'services')
|
|
149
194
|
const envLines: string[] = ['# Generated .env.local']
|
|
150
195
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (pulumiEnvs.length > 0) {
|
|
196
|
+
if (!noPulumi && existsSync(join(infraDirectory, 'Pulumi.yaml'))) {
|
|
197
|
+
const pulumiEntries = await loadPulumiConfig(infraDirectory, 'dev')
|
|
198
|
+
if (pulumiEntries.length > 0) {
|
|
155
199
|
envLines.push('', '# Pulumi Secrets')
|
|
156
|
-
envLines.push(...
|
|
200
|
+
envLines.push(...pulumiEntries)
|
|
157
201
|
}
|
|
158
202
|
}
|
|
159
203
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
const serviceConfigs = discoverServices(servicesDirectory)
|
|
205
|
+
|
|
163
206
|
if (serviceConfigs.length > 0) {
|
|
164
|
-
envLines.push(
|
|
165
|
-
for (const config of serviceConfigs) {
|
|
166
|
-
const url = getLocalUrl(config)
|
|
167
|
-
if (url) {
|
|
168
|
-
envLines.push(`${toEnvKey(config.name)}_URL=${url}`)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
envLines.push('', '# Service Ports')
|
|
173
|
-
for (const config of serviceConfigs) {
|
|
174
|
-
const port = getServicePort(config)
|
|
175
|
-
if (port) {
|
|
176
|
-
envLines.push(`${toEnvKey(config.name)}_PORT=${port}`)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
207
|
+
envLines.push(...buildServiceEnvLines(serviceConfigs))
|
|
180
208
|
console.log(`✅ Discovered ${serviceConfigs.length} services`)
|
|
181
209
|
}
|
|
182
210
|
|
|
183
|
-
writeFileSync(join(
|
|
184
|
-
console.log(`✅ .env.local generated at ${
|
|
211
|
+
writeFileSync(join(workspaceRootDirectory, '.env.local'), `${envLines.join('\n')}\n`)
|
|
212
|
+
console.log(`✅ .env.local generated at ${workspaceRootDirectory}`)
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
main().catch((err) => {
|
package/dist/index.cjs
CHANGED
|
@@ -1266,10 +1266,10 @@ var createImagePullSecret = (provider, namespace, name, config) => {
|
|
|
1266
1266
|
var getImagePullPolicy = (image, explicit) => explicit ?? (image.endsWith(":latest") ? "Always" : "IfNotPresent");
|
|
1267
1267
|
var buildEnvVars = (config) => {
|
|
1268
1268
|
const portEnv = { name: "PORT", value: String(config.ports.primary.port) };
|
|
1269
|
-
const plainEnvVars = config.env
|
|
1269
|
+
const plainEnvVars = [...Object.entries(config.env ?? {}), ...Object.entries(config.containerEnv ?? {})].map(([key, value]) => ({
|
|
1270
1270
|
name: key,
|
|
1271
1271
|
value: pulumi6.output(value)
|
|
1272
|
-
}))
|
|
1272
|
+
}));
|
|
1273
1273
|
const secretEnvVars = config.secrets ? Object.keys(config.secrets).map((key) => ({
|
|
1274
1274
|
name: key,
|
|
1275
1275
|
valueFrom: {
|
|
@@ -1435,6 +1435,13 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1435
1435
|
},
|
|
1436
1436
|
spec: {
|
|
1437
1437
|
replicas,
|
|
1438
|
+
strategy: {
|
|
1439
|
+
type: "RollingUpdate",
|
|
1440
|
+
rollingUpdate: {
|
|
1441
|
+
maxSurge: normalizedConfig.strategy?.maxSurge ?? 0,
|
|
1442
|
+
maxUnavailable: normalizedConfig.strategy?.maxUnavailable ?? 1
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1438
1445
|
selector: {
|
|
1439
1446
|
matchLabels: { app: normalizedConfig.name }
|
|
1440
1447
|
},
|
package/dist/index.js
CHANGED
|
@@ -1172,10 +1172,10 @@ var createImagePullSecret = (provider, namespace, name, config) => {
|
|
|
1172
1172
|
var getImagePullPolicy = (image, explicit) => explicit ?? (image.endsWith(":latest") ? "Always" : "IfNotPresent");
|
|
1173
1173
|
var buildEnvVars = (config) => {
|
|
1174
1174
|
const portEnv = { name: "PORT", value: String(config.ports.primary.port) };
|
|
1175
|
-
const plainEnvVars = config.env
|
|
1175
|
+
const plainEnvVars = [...Object.entries(config.env ?? {}), ...Object.entries(config.containerEnv ?? {})].map(([key, value]) => ({
|
|
1176
1176
|
name: key,
|
|
1177
1177
|
value: pulumi6.output(value)
|
|
1178
|
-
}))
|
|
1178
|
+
}));
|
|
1179
1179
|
const secretEnvVars = config.secrets ? Object.keys(config.secrets).map((key) => ({
|
|
1180
1180
|
name: key,
|
|
1181
1181
|
valueFrom: {
|
|
@@ -1341,6 +1341,13 @@ var deployK8sService = (provider, namespace, config) => {
|
|
|
1341
1341
|
},
|
|
1342
1342
|
spec: {
|
|
1343
1343
|
replicas,
|
|
1344
|
+
strategy: {
|
|
1345
|
+
type: "RollingUpdate",
|
|
1346
|
+
rollingUpdate: {
|
|
1347
|
+
maxSurge: normalizedConfig.strategy?.maxSurge ?? 0,
|
|
1348
|
+
maxUnavailable: normalizedConfig.strategy?.maxUnavailable ?? 1
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1344
1351
|
selector: {
|
|
1345
1352
|
matchLabels: { app: normalizedConfig.name }
|
|
1346
1353
|
},
|
|
@@ -244,8 +244,10 @@ export interface K8sServiceConfig {
|
|
|
244
244
|
containerPort?: number;
|
|
245
245
|
/** Number of replicas (defaults to 1) */
|
|
246
246
|
replicas?: number;
|
|
247
|
-
/** Environment variables (plain values) */
|
|
247
|
+
/** Environment variables (plain values, available locally and in containers) */
|
|
248
248
|
env?: Record<string, pulumi.Input<string>>;
|
|
249
|
+
/** Environment variables applied only inside the container (excluded from generate-env) */
|
|
250
|
+
containerEnv?: Record<string, pulumi.Input<string>>;
|
|
249
251
|
/** Secret environment variables (stored in K8s Secret) */
|
|
250
252
|
secrets?: Record<string, pulumi.Input<string>>;
|
|
251
253
|
/** Ingress configuration (set to enable public access) */
|
|
@@ -254,6 +256,18 @@ export interface K8sServiceConfig {
|
|
|
254
256
|
healthCheck?: K8sHealthCheck;
|
|
255
257
|
/** Resource requests and limits */
|
|
256
258
|
resources?: K8sResourceConfig;
|
|
259
|
+
/**
|
|
260
|
+
* Deployment strategy for rolling updates.
|
|
261
|
+
*
|
|
262
|
+
* Defaults to `maxSurge: 0, maxUnavailable: 1` which avoids memory spikes
|
|
263
|
+
* during rollouts by replacing pods one-at-a-time without surge.
|
|
264
|
+
*/
|
|
265
|
+
strategy?: {
|
|
266
|
+
/** Max extra pods during rollout (default: 0) */
|
|
267
|
+
maxSurge?: number;
|
|
268
|
+
/** Max unavailable pods during rollout (default: 1) */
|
|
269
|
+
maxUnavailable?: number;
|
|
270
|
+
};
|
|
257
271
|
/** Volume mounts for persistent storage */
|
|
258
272
|
volumes?: K8sVolumeMount[];
|
|
259
273
|
/** Command to run (overrides container entrypoint) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossdelta/infrastructure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@crossdelta/cloudevents": "0.7.
|
|
38
|
+
"@crossdelta/cloudevents": "^0.7.14"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@pulumi/digitalocean": "^4.0.0",
|