@crossdelta/infrastructure 0.2.24 → 0.2.26
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/bin/generate-env.ts +178 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/otel.d.ts +62 -0
- package/dist/index.cjs +10552 -13703
- package/dist/index.js +10549 -13700
- package/package.json +6 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Generate environment variables for local development.
|
|
4
|
+
*
|
|
5
|
+
* This script:
|
|
6
|
+
* 1. Loads secrets from Pulumi config (dev stack) if available
|
|
7
|
+
* 2. Discovers services from infra/services/*.ts (parses files without importing)
|
|
8
|
+
* 3. Generates service URLs and ports for localhost
|
|
9
|
+
* 4. Writes .env.local to the workspace root
|
|
10
|
+
*
|
|
11
|
+
* Usage: generate-env [--no-pulumi]
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
type PulumiConfigEntry = {
|
|
17
|
+
value?: string
|
|
18
|
+
secret?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MinimalServiceConfig {
|
|
22
|
+
name: string
|
|
23
|
+
containerPort?: number
|
|
24
|
+
httpPort?: number
|
|
25
|
+
internalPorts?: number[]
|
|
26
|
+
internalUrl?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toEnvKey = (name: string): string => name.toUpperCase().replace(/-/g, '_')
|
|
30
|
+
|
|
31
|
+
const resolveNumber = (value: string, fileContent: string): number | undefined => {
|
|
32
|
+
const literal = Number.parseInt(value, 10)
|
|
33
|
+
if (!Number.isNaN(literal)) return literal
|
|
34
|
+
|
|
35
|
+
const varMatch = fileContent.match(new RegExp(`(?:const|let|var)\\s+${value}\\s*=\\s*(\\d+)`))
|
|
36
|
+
return varMatch?.[1] ? Number.parseInt(varMatch[1], 10) : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const extract = {
|
|
40
|
+
string: (content: string, pattern: RegExp): string | undefined =>
|
|
41
|
+
content.match(pattern)?.[1],
|
|
42
|
+
|
|
43
|
+
number: (content: string, pattern: RegExp): number | undefined => {
|
|
44
|
+
const match = content.match(pattern)?.[1]
|
|
45
|
+
return match ? resolveNumber(match, content) : undefined
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
numberArray: (content: string, pattern: RegExp): number[] | undefined => {
|
|
49
|
+
const match = content.match(pattern)?.[1]
|
|
50
|
+
if (!match) return undefined
|
|
51
|
+
|
|
52
|
+
const numbers = match
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((v) => resolveNumber(v.trim(), content))
|
|
55
|
+
.filter((n): n is number => n !== undefined)
|
|
56
|
+
|
|
57
|
+
return numbers.length > 0 ? numbers : undefined
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const findWorkspaceRoot = (): string => {
|
|
62
|
+
let dir = process.cwd()
|
|
63
|
+
while (!existsSync(join(dir, 'bun.lock')) && !existsSync(join(dir, 'package-lock.json'))) {
|
|
64
|
+
const parent = join(dir, '..')
|
|
65
|
+
if (parent === dir) throw new Error('Could not locate workspace root')
|
|
66
|
+
dir = parent
|
|
67
|
+
}
|
|
68
|
+
return dir
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const loadPulumiConfig = async (infraDir: string, stack: string): Promise<string[]> => {
|
|
72
|
+
try {
|
|
73
|
+
const result = Bun.spawnSync(['pulumi', 'config', '--show-secrets', '--json', '--stack', stack, '--cwd', infraDir])
|
|
74
|
+
if (result.exitCode !== 0) return []
|
|
75
|
+
|
|
76
|
+
const config = JSON.parse(result.stdout.toString()) as Record<string, PulumiConfigEntry>
|
|
77
|
+
|
|
78
|
+
return Object.entries(config)
|
|
79
|
+
.filter(([key]) => key.includes(':'))
|
|
80
|
+
.filter(([, entry]) => entry.value !== undefined && entry.value !== null && entry.value !== 'undefined')
|
|
81
|
+
.map(([fullKey, entry]) => {
|
|
82
|
+
const [, rawKey] = fullKey.split(':')
|
|
83
|
+
return `${rawKey}=${entry.value}`
|
|
84
|
+
})
|
|
85
|
+
} catch {
|
|
86
|
+
return []
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const discoverServices = (servicesDir: string): MinimalServiceConfig[] => {
|
|
91
|
+
if (!existsSync(servicesDir)) return []
|
|
92
|
+
|
|
93
|
+
const files = readdirSync(servicesDir).filter(
|
|
94
|
+
(file) => file.endsWith('.ts') && file !== 'index.ts'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return files.map((file) => {
|
|
98
|
+
const content = readFileSync(join(servicesDir, file), 'utf-8')
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
name: extract.string(content, /name:\s*['"]([^'"]+)['"]/) ?? file.replace('.ts', ''),
|
|
102
|
+
containerPort: extract.number(content, /containerPort:\s*(\w+)/),
|
|
103
|
+
httpPort: extract.number(content, /httpPort:\s*(\w+)/),
|
|
104
|
+
internalPorts: extract.numberArray(content, /internalPorts:\s*\[([^\]]+)\]/),
|
|
105
|
+
internalUrl: extract.string(content, /internalUrl:\s*[`'"]([^`'"]+)[`'"]/),
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const getServicePort = (config: MinimalServiceConfig): number | undefined => {
|
|
111
|
+
if (config.containerPort) return config.containerPort
|
|
112
|
+
if (config.httpPort) return config.httpPort
|
|
113
|
+
if (config.internalPorts?.[0]) return config.internalPorts[0]
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const getLocalUrl = (config: MinimalServiceConfig): string | undefined => {
|
|
118
|
+
const port = getServicePort(config)
|
|
119
|
+
if (!port) return undefined
|
|
120
|
+
|
|
121
|
+
if (config.internalUrl) {
|
|
122
|
+
const protocolMatch = config.internalUrl.match(/^(\w+):\/\//)
|
|
123
|
+
if (protocolMatch) {
|
|
124
|
+
return `${protocolMatch[1]}://localhost:${port}`
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `http://localhost:${port}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const main = async () => {
|
|
132
|
+
const noPulumi = process.argv.includes('--no-pulumi')
|
|
133
|
+
const workspaceRootDir = findWorkspaceRoot()
|
|
134
|
+
const infraDir = join(workspaceRootDir, 'infra')
|
|
135
|
+
const servicesDir = join(infraDir, 'services')
|
|
136
|
+
|
|
137
|
+
const envLines: string[] = ['# Generated .env.local']
|
|
138
|
+
|
|
139
|
+
// Load Pulumi secrets if available and not disabled
|
|
140
|
+
if (!noPulumi && existsSync(join(infraDir, 'Pulumi.yaml'))) {
|
|
141
|
+
const pulumiEnvs = await loadPulumiConfig(infraDir, 'dev')
|
|
142
|
+
if (pulumiEnvs.length > 0) {
|
|
143
|
+
envLines.push('', '# Pulumi Secrets')
|
|
144
|
+
envLines.push(...pulumiEnvs)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Discover services
|
|
149
|
+
const serviceConfigs = discoverServices(servicesDir)
|
|
150
|
+
|
|
151
|
+
if (serviceConfigs.length > 0) {
|
|
152
|
+
envLines.push('', '# Service URLs')
|
|
153
|
+
for (const config of serviceConfigs) {
|
|
154
|
+
const url = getLocalUrl(config)
|
|
155
|
+
if (url) {
|
|
156
|
+
envLines.push(`${toEnvKey(config.name)}_URL=${url}`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
envLines.push('', '# Service Ports')
|
|
161
|
+
for (const config of serviceConfigs) {
|
|
162
|
+
const port = getServicePort(config)
|
|
163
|
+
if (port) {
|
|
164
|
+
envLines.push(`${toEnvKey(config.name)}_PORT=${port}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`✅ Discovered ${serviceConfigs.length} services`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeFileSync(join(workspaceRootDir, '.env.local'), `${envLines.join('\n')}\n`)
|
|
172
|
+
console.log(`✅ .env.local generated at ${workspaceRootDir}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main().catch((err) => {
|
|
176
|
+
console.error('❌', err.message)
|
|
177
|
+
process.exit(1)
|
|
178
|
+
})
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './discover-services';
|
|
|
3
3
|
export * from './docker-hub-image';
|
|
4
4
|
export * from './droplet-builder';
|
|
5
5
|
export * from './image';
|
|
6
|
+
export * from './otel';
|
|
6
7
|
export * from './service-builder';
|
|
7
8
|
export * from './service-runtime';
|
|
8
9
|
export * from './service-urls';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type * as pulumi from '@pulumi/pulumi';
|
|
2
|
+
/**
|
|
3
|
+
* OpenTelemetry configuration for OTLP export.
|
|
4
|
+
*/
|
|
5
|
+
export interface OtelConfig {
|
|
6
|
+
/** OTLP endpoint for traces (e.g., https://in-otel.logs.betterstack.com/v1/traces) */
|
|
7
|
+
tracesEndpoint?: pulumi.Input<string>;
|
|
8
|
+
/** OTLP endpoint for metrics (e.g., https://in-otel.logs.betterstack.com/v1/metrics) */
|
|
9
|
+
metricsEndpoint?: pulumi.Input<string>;
|
|
10
|
+
/** OTLP headers (e.g., "Authorization=Bearer xxx") */
|
|
11
|
+
headers?: pulumi.Input<string>;
|
|
12
|
+
/** Deployment environment (e.g., "production", "staging") */
|
|
13
|
+
environment?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds OpenTelemetry environment variables for a service.
|
|
17
|
+
*
|
|
18
|
+
* @param serviceName - Unique service name (e.g., "orderboss-storefront")
|
|
19
|
+
* @param config - OTel configuration from Pulumi config
|
|
20
|
+
* @returns Record of environment variables for OTel SDK auto-instrumentation
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const otelConfig = {
|
|
25
|
+
* tracesEndpoint: cfg.get('otelTracesEndpoint'),
|
|
26
|
+
* metricsEndpoint: cfg.get('otelMetricsEndpoint'),
|
|
27
|
+
* headers: cfg.getSecret('otelHeaders'),
|
|
28
|
+
* environment: 'production',
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const serviceConfig = {
|
|
32
|
+
* name: 'orders',
|
|
33
|
+
* env: {
|
|
34
|
+
* ...buildOtelEnv('orderboss-orders', otelConfig),
|
|
35
|
+
* // other env vars
|
|
36
|
+
* },
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildOtelEnv(serviceName: string, config: OtelConfig): Record<string, pulumi.Input<string>>;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a function that builds OTel env vars with pre-configured endpoints.
|
|
43
|
+
* Useful for applying the same OTel config to multiple services.
|
|
44
|
+
*
|
|
45
|
+
* @param config - OTel configuration from Pulumi config
|
|
46
|
+
* @returns Function that takes a service name and returns OTel env vars
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const withOtelEnv = createOtelEnvBuilder({
|
|
51
|
+
* tracesEndpoint: cfg.get('otelTracesEndpoint'),
|
|
52
|
+
* headers: cfg.getSecret('otelHeaders'),
|
|
53
|
+
* environment: getStack(),
|
|
54
|
+
* })
|
|
55
|
+
*
|
|
56
|
+
* // In each service config:
|
|
57
|
+
* env: {
|
|
58
|
+
* ...withOtelEnv('orderboss-orders'),
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function createOtelEnvBuilder(config: OtelConfig): (serviceName: string) => Record<string, pulumi.Input<string>>;
|