@crossdelta/infrastructure 0.9.0 → 0.10.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 +65 -99
- package/bin/generate-env.mjs +3 -1
- package/bin/generate-env.ts +5 -3
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/runtimes/doks/types.d.ts +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,17 +3,58 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@crossdelta/infrastructure)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Pulumi abstractions that turn per-service config objects into complete Kubernetes deployments. You describe **what** each service needs (ports, env, secrets, health checks), the package handles **how** (Deployments, Services, Ingress, TLS, probes, pull secrets).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**What you skip writing:**
|
|
9
|
+
- Boilerplate K8s YAML / Pulumi resource declarations per service
|
|
10
|
+
- NATS JetStream + cert-manager + NGINX Ingress setup
|
|
11
|
+
- Docker registry secrets, health probes, rolling update strategies
|
|
12
|
+
- Multi-environment concerns (shared cluster, per-stack namespaces)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
9
15
|
|
|
10
16
|
```bash
|
|
11
17
|
npm install @crossdelta/infrastructure @pulumi/pulumi @pulumi/kubernetes
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
##
|
|
20
|
+
## End-to-End Example
|
|
21
|
+
|
|
22
|
+
One `index.ts` that provisions a cluster, runtime, and all services:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
createDOKSCluster,
|
|
27
|
+
createVPC,
|
|
28
|
+
createNamespace,
|
|
29
|
+
deployRuntime,
|
|
30
|
+
deployK8sServices,
|
|
31
|
+
discoverServiceConfigs,
|
|
32
|
+
} from '@crossdelta/infrastructure'
|
|
33
|
+
|
|
34
|
+
// 1. Cluster
|
|
35
|
+
const vpc = createVPC({ name: 'my-vpc', region: 'fra1' })
|
|
36
|
+
const { provider } = createDOKSCluster({
|
|
37
|
+
name: 'my-cluster',
|
|
38
|
+
vpcUuid: vpc.id,
|
|
39
|
+
nodePool: { name: 'default', size: 's-4vcpu-8gb', nodeCount: 1 },
|
|
40
|
+
})
|
|
41
|
+
createNamespace(provider, 'my-namespace')
|
|
42
|
+
|
|
43
|
+
// 2. Runtime (toggle what you need)
|
|
44
|
+
const runtime = deployRuntime(provider, 'my-namespace', {
|
|
45
|
+
nats: { enabled: true, config: { replicas: 1, jetstream: { enabled: true } } },
|
|
46
|
+
ingress: { enabled: true },
|
|
47
|
+
certManager: { enabled: true, config: { email: 'ops@example.com' } },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// 3. Services (auto-discovered from infra/services/*.ts)
|
|
51
|
+
const configs = discoverServiceConfigs('services')
|
|
52
|
+
deployK8sServices(provider, 'my-namespace', configs)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Service Config
|
|
15
56
|
|
|
16
|
-
|
|
57
|
+
Each file in `infra/services/` exports one config. The package derives Deployment, Service, Ingress, Secret, and probes from it:
|
|
17
58
|
|
|
18
59
|
```typescript
|
|
19
60
|
import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
|
|
@@ -21,14 +62,9 @@ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
|
|
|
21
62
|
const config: K8sServiceConfig = {
|
|
22
63
|
name: 'my-service',
|
|
23
64
|
ports: ports().http(4000).public().build(),
|
|
24
|
-
|
|
25
|
-
healthCheck: {
|
|
26
|
-
httpPath: '/health',
|
|
27
|
-
readinessPath: '/health/ready',
|
|
28
|
-
},
|
|
65
|
+
healthCheck: { httpPath: '/health' },
|
|
29
66
|
ingress: { path: '/api', host: 'example.com' },
|
|
30
67
|
env: { DATABASE_URL: dbUrl },
|
|
31
|
-
containerEnv: { NODE_OPTIONS: '--max-old-space-size=384' },
|
|
32
68
|
secrets: { API_KEY: apiKey },
|
|
33
69
|
resources: {
|
|
34
70
|
requests: { cpu: '50m', memory: '64Mi' },
|
|
@@ -39,103 +75,33 @@ const config: K8sServiceConfig = {
|
|
|
39
75
|
export default config
|
|
40
76
|
```
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
See `K8sServiceConfig` type for all available fields (replicas, volumes, strategy, containerEnv, etc.).
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
import { deployK8sServices, discoverServiceConfigs } from '@crossdelta/infrastructure'
|
|
80
|
+
## Shared Cluster (Multi-Stack)
|
|
46
81
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
deployK8sServices({
|
|
50
|
-
provider,
|
|
51
|
-
namespace,
|
|
52
|
-
serviceConfigs,
|
|
53
|
-
env: { NATS_URL: natsUrl },
|
|
54
|
-
})
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### Deploy runtime components
|
|
82
|
+
When multiple Pulumi stacks share one cluster, use `clusterName`/`vpcName` to pin the DigitalOcean resource name (default appends the stack name):
|
|
58
83
|
|
|
59
84
|
```typescript
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
config: {
|
|
67
|
-
nginxConfig: {
|
|
68
|
-
'proxy-buffer-size': '16k', // Increase for large auth cookies/JWTs
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
certManager: { enabled: true, config: { email: 'ops@example.com' } },
|
|
85
|
+
// Stack A (stage) — owns the cluster
|
|
86
|
+
const { provider, kubeconfig } = createDOKSCluster({
|
|
87
|
+
name: 'my-cluster',
|
|
88
|
+
clusterName: 'my-cluster',
|
|
89
|
+
vpcUuid: vpc.id,
|
|
90
|
+
nodePool: { name: 'default', size: 's-4vcpu-8gb', nodeCount: 1 },
|
|
73
91
|
})
|
|
92
|
+
export const clusterKubeconfig = kubeconfig
|
|
93
|
+
|
|
94
|
+
// Stack B (production) — references the shared cluster
|
|
95
|
+
const stageStack = new StackReference('org/project/stage')
|
|
96
|
+
const provider = createK8sProviderFromKubeconfig(
|
|
97
|
+
'production',
|
|
98
|
+
stageStack.getOutput('clusterKubeconfig'),
|
|
99
|
+
)
|
|
74
100
|
```
|
|
75
101
|
|
|
76
|
-
##
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
interface K8sServiceConfig {
|
|
80
|
-
name: string
|
|
81
|
-
ports: PortConfig
|
|
82
|
-
replicas?: number
|
|
83
|
-
resources?: {
|
|
84
|
-
requests?: { cpu: string; memory: string }
|
|
85
|
-
limits?: { cpu: string; memory: string }
|
|
86
|
-
}
|
|
87
|
-
healthCheck?: {
|
|
88
|
-
httpPath: string
|
|
89
|
-
readinessPath?: string // Falls back to httpPath
|
|
90
|
-
initialDelaySeconds?: number // Default: 10
|
|
91
|
-
periodSeconds?: number // Default: 10
|
|
92
|
-
failureThreshold?: number // Default: 3
|
|
93
|
-
}
|
|
94
|
-
ingress?: {
|
|
95
|
-
path: string
|
|
96
|
-
host?: string
|
|
97
|
-
}
|
|
98
|
-
env?: Record<string, pulumi.Input<string>>
|
|
99
|
-
containerEnv?: Record<string, pulumi.Input<string>>
|
|
100
|
-
secrets?: Record<string, pulumi.Input<string>>
|
|
101
|
-
image?: string
|
|
102
|
-
skip?: boolean
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Fluent Ports API
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
import { ports } from '@crossdelta/infrastructure'
|
|
110
|
-
|
|
111
|
-
ports().http(4000).build() // Simple HTTP
|
|
112
|
-
ports().http(4000).public().build() // Public (ingress)
|
|
113
|
-
ports().grpc(50051).build() // gRPC
|
|
114
|
-
ports().http(4000).addHttp(8080, 'metrics').build() // Multiple ports
|
|
115
|
-
```
|
|
102
|
+
## generate-env
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
|--------|-------------|
|
|
119
|
-
| `.http(port)` | HTTP primary port |
|
|
120
|
-
| `.grpc(port)` | gRPC primary port |
|
|
121
|
-
| `.primary(port, name?)` | Custom primary port |
|
|
122
|
-
| `.add(port, name?, protocol?)` | Additional port |
|
|
123
|
-
| `.addHttp(port, name?)` | Additional HTTP port |
|
|
124
|
-
| `.addGrpc(port, name?)` | Additional gRPC port |
|
|
125
|
-
| `.public()` | Expose via ingress |
|
|
126
|
-
| `.protocol(type)` | Set protocol (TCP, UDP, SCTP) |
|
|
127
|
-
| `.build()` | Build config |
|
|
128
|
-
|
|
129
|
-
## API Reference
|
|
130
|
-
|
|
131
|
-
| Function | Description |
|
|
132
|
-
|----------|-------------|
|
|
133
|
-
| `deployRuntime(provider, ns, config)` | Deploy runtime components |
|
|
134
|
-
| `discoverServiceConfigs(dir, opts?)` | Auto-discover service configs from filesystem |
|
|
135
|
-
| `deployK8sServices(opts)` | Deploy services to cluster |
|
|
136
|
-
| `createNamespace(provider, name)` | Create k8s namespace |
|
|
137
|
-
| `createImagePullSecret(...)` | Create registry pull secret |
|
|
138
|
-
| `ports()` | Fluent port builder |
|
|
104
|
+
Generates `.env.local` from Pulumi secrets (default: `stage` stack) and discovered services. Override with `--stack=production` or `--no-pulumi`.
|
|
139
105
|
|
|
140
106
|
## License
|
|
141
107
|
|
package/bin/generate-env.mjs
CHANGED
|
@@ -162,12 +162,14 @@ var discoverServices = (servicesDirectory) => {
|
|
|
162
162
|
};
|
|
163
163
|
var main = async () => {
|
|
164
164
|
const noPulumi = process.argv.includes("--no-pulumi");
|
|
165
|
+
const stackArg = process.argv.find((arg) => arg.startsWith("--stack="));
|
|
166
|
+
const stack = stackArg?.split("=")[1] ?? "stage";
|
|
165
167
|
const workspaceRootDirectory = findWorkspaceRoot();
|
|
166
168
|
const infraDirectory = join(workspaceRootDirectory, "infra");
|
|
167
169
|
const servicesDirectory = join(infraDirectory, "services");
|
|
168
170
|
const envLines = ["# Generated .env.local"];
|
|
169
171
|
if (!noPulumi && existsSync(join(infraDirectory, "Pulumi.yaml"))) {
|
|
170
|
-
const pulumiEntries = await loadPulumiConfig(infraDirectory,
|
|
172
|
+
const pulumiEntries = await loadPulumiConfig(infraDirectory, stack);
|
|
171
173
|
if (pulumiEntries.length > 0) {
|
|
172
174
|
envLines.push("", "# Pulumi Secrets");
|
|
173
175
|
envLines.push(...pulumiEntries);
|
package/bin/generate-env.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Generate environment variables for local development.
|
|
4
4
|
*
|
|
5
5
|
* This script:
|
|
6
|
-
* 1. Loads secrets from Pulumi config (
|
|
6
|
+
* 1. Loads secrets from Pulumi config (defaults to stage stack) if available
|
|
7
7
|
* 2. Discovers services from infra/services/*.ts (parses files without importing)
|
|
8
8
|
* 3. Generates service URLs and ports for localhost
|
|
9
9
|
* 4. Writes .env.local to the workspace root
|
|
10
10
|
*
|
|
11
|
-
* Usage: generate-env [--no-pulumi]
|
|
11
|
+
* Usage: generate-env [--stack=<name>] [--no-pulumi]
|
|
12
12
|
*/
|
|
13
13
|
import { execSync } from 'node:child_process'
|
|
14
14
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -177,13 +177,15 @@ const discoverServices = (servicesDirectory: string): MinimalServiceConfig[] =>
|
|
|
177
177
|
|
|
178
178
|
const main = async () => {
|
|
179
179
|
const noPulumi = process.argv.includes('--no-pulumi')
|
|
180
|
+
const stackArg = process.argv.find((arg) => arg.startsWith('--stack='))
|
|
181
|
+
const stack = stackArg?.split('=')[1] ?? 'stage'
|
|
180
182
|
const workspaceRootDirectory = findWorkspaceRoot()
|
|
181
183
|
const infraDirectory = join(workspaceRootDirectory, 'infra')
|
|
182
184
|
const servicesDirectory = join(infraDirectory, 'services')
|
|
183
185
|
const envLines: string[] = ['# Generated .env.local']
|
|
184
186
|
|
|
185
187
|
if (!noPulumi && existsSync(join(infraDirectory, 'Pulumi.yaml'))) {
|
|
186
|
-
const pulumiEntries = await loadPulumiConfig(infraDirectory,
|
|
188
|
+
const pulumiEntries = await loadPulumiConfig(infraDirectory, stack)
|
|
187
189
|
if (pulumiEntries.length > 0) {
|
|
188
190
|
envLines.push('', '# Pulumi Secrets')
|
|
189
191
|
envLines.push(...pulumiEntries)
|
package/dist/index.cjs
CHANGED
|
@@ -1139,7 +1139,7 @@ function createDOKSCluster(config) {
|
|
|
1139
1139
|
const stack = pulumi4.getStack();
|
|
1140
1140
|
const region = config.region ?? "fra1";
|
|
1141
1141
|
const cluster = new digitalocean.KubernetesCluster(config.name, {
|
|
1142
|
-
name: `${config.name}-${stack}`,
|
|
1142
|
+
name: config.clusterName ?? `${config.name}-${stack}`,
|
|
1143
1143
|
region,
|
|
1144
1144
|
version: config.version ?? "1.32.10-do.0",
|
|
1145
1145
|
vpcUuid: config.vpcUuid,
|
|
@@ -1190,7 +1190,7 @@ function createVPC(config) {
|
|
|
1190
1190
|
const stack = pulumi5.getStack();
|
|
1191
1191
|
const region = config.region ?? "fra1";
|
|
1192
1192
|
return new digitalocean2.Vpc(config.name, {
|
|
1193
|
-
name: `${config.name}-${stack}`,
|
|
1193
|
+
name: config.vpcName ?? `${config.name}-${stack}`,
|
|
1194
1194
|
region,
|
|
1195
1195
|
description: config.description ?? `VPC for ${config.name}`,
|
|
1196
1196
|
ipRange: config.ipRange
|
package/dist/index.js
CHANGED
|
@@ -1045,7 +1045,7 @@ function createDOKSCluster(config) {
|
|
|
1045
1045
|
const stack = pulumi4.getStack();
|
|
1046
1046
|
const region = config.region ?? "fra1";
|
|
1047
1047
|
const cluster = new digitalocean.KubernetesCluster(config.name, {
|
|
1048
|
-
name: `${config.name}-${stack}`,
|
|
1048
|
+
name: config.clusterName ?? `${config.name}-${stack}`,
|
|
1049
1049
|
region,
|
|
1050
1050
|
version: config.version ?? "1.32.10-do.0",
|
|
1051
1051
|
vpcUuid: config.vpcUuid,
|
|
@@ -1096,7 +1096,7 @@ function createVPC(config) {
|
|
|
1096
1096
|
const stack = pulumi5.getStack();
|
|
1097
1097
|
const region = config.region ?? "fra1";
|
|
1098
1098
|
return new digitalocean2.Vpc(config.name, {
|
|
1099
|
-
name: `${config.name}-${stack}`,
|
|
1099
|
+
name: config.vpcName ?? `${config.name}-${stack}`,
|
|
1100
1100
|
region,
|
|
1101
1101
|
description: config.description ?? `VPC for ${config.name}`,
|
|
1102
1102
|
ipRange: config.ipRange
|
|
@@ -28,8 +28,10 @@ export type Region = digitalocean.Region;
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export interface DOKSClusterConfig {
|
|
31
|
-
/**
|
|
31
|
+
/** Pulumi resource name (logical, for state tracking) */
|
|
32
32
|
name: string;
|
|
33
|
+
/** Actual cluster name in DigitalOcean. Defaults to `${name}-${stack}` */
|
|
34
|
+
clusterName?: string;
|
|
33
35
|
/** DigitalOcean region (defaults to 'fra1') */
|
|
34
36
|
region?: digitalocean.Region | string;
|
|
35
37
|
/** Kubernetes version - use `doctl kubernetes options versions` to list available */
|
|
@@ -65,8 +67,10 @@ export interface DOKSClusterConfig {
|
|
|
65
67
|
* Simplified VPC configuration with stack-aware naming.
|
|
66
68
|
*/
|
|
67
69
|
export interface VPCConfig {
|
|
68
|
-
/**
|
|
70
|
+
/** Pulumi resource name (logical, for state tracking) */
|
|
69
71
|
name: string;
|
|
72
|
+
/** Actual VPC name in DigitalOcean. Defaults to `${name}-${stack}` */
|
|
73
|
+
vpcName?: string;
|
|
70
74
|
/** DigitalOcean region (defaults to 'fra1') */
|
|
71
75
|
region?: digitalocean.Region | string;
|
|
72
76
|
/** Optional description */
|