@alienplatform/core 1.8.0 → 1.10.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/.turbo/turbo-build.log +10 -10
- package/dist/index.d.ts +772 -89
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +199 -33
- package/dist/index.js.map +1 -1
- package/dist/stack.js +579 -44
- package/dist/stack.js.map +1 -1
- package/dist/tests/index.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/stack.test.ts.snap +10 -4
- package/src/__tests__/stack.test.ts +185 -2
- package/src/compute-cluster.ts +213 -0
- package/src/container.ts +38 -26
- package/src/daemon.ts +79 -0
- package/src/generated/index.ts +42 -2
- package/src/generated/schemas/architecture.json +1 -0
- package/src/generated/schemas/capacityGroup.json +1 -0
- package/src/generated/schemas/capacityGroupScalePolicy.json +1 -0
- package/src/generated/schemas/computeChoiceRange.json +1 -0
- package/src/generated/schemas/computeCluster.json +1 -0
- package/src/generated/schemas/computePoolSelection.json +1 -0
- package/src/generated/schemas/computeSettings.json +1 -0
- package/src/generated/schemas/container.json +1 -1
- package/src/generated/schemas/containerOutputs.json +1 -1
- package/src/generated/schemas/containerPort.json +1 -1
- package/src/generated/schemas/daemon.json +1 -1
- package/src/generated/schemas/daemonOutputs.json +1 -1
- package/src/generated/schemas/daemonRuntime.json +1 -0
- package/src/generated/schemas/daemonRuntimeMount.json +1 -0
- package/src/generated/schemas/exposeProtocol.json +1 -1
- package/src/generated/schemas/gpuSpec.json +1 -0
- package/src/generated/schemas/machineProfile.json +1 -0
- package/src/generated/schemas/publicEndpoint.json +1 -0
- package/src/generated/schemas/publicEndpointOutput.json +1 -0
- package/src/generated/schemas/stack.json +1 -1
- package/src/generated/schemas/stackImportRequest.json +1 -1
- package/src/generated/schemas/stackImportResponse.json +1 -1
- package/src/generated/schemas/stackInputDefaultValue.json +1 -0
- package/src/generated/schemas/stackInputDefinition.json +1 -0
- package/src/generated/schemas/stackInputEnvironmentMapping.json +1 -0
- package/src/generated/schemas/stackInputEnvironmentVariableType.json +1 -0
- package/src/generated/schemas/stackInputKind.json +1 -0
- package/src/generated/schemas/stackInputProvider.json +1 -0
- package/src/generated/schemas/stackInputValidation.json +1 -0
- package/src/generated/schemas/stackSettings.json +1 -1
- package/src/generated/schemas/worker.json +1 -1
- package/src/generated/schemas/workerOutputs.json +1 -1
- package/src/generated/schemas/workerPublicEndpoint.json +1 -0
- package/src/generated/zod/architecture-schema.ts +13 -0
- package/src/generated/zod/capacity-group-scale-policy-schema.ts +27 -0
- package/src/generated/zod/capacity-group-schema.ts +27 -0
- package/src/generated/zod/compute-choice-range-schema.ts +17 -0
- package/src/generated/zod/compute-cluster-schema.ts +20 -0
- package/src/generated/zod/compute-pool-selection-schema.ts +22 -0
- package/src/generated/zod/compute-settings-schema.ts +18 -0
- package/src/generated/zod/container-outputs-schema.ts +5 -6
- package/src/generated/zod/container-port-schema.ts +1 -5
- package/src/generated/zod/container-schema.ts +7 -3
- package/src/generated/zod/daemon-outputs-schema.ts +4 -0
- package/src/generated/zod/daemon-runtime-mount-schema.ts +14 -0
- package/src/generated/zod/daemon-runtime-schema.ts +19 -0
- package/src/generated/zod/daemon-schema.ts +14 -2
- package/src/generated/zod/expose-protocol-schema.ts +2 -2
- package/src/generated/zod/gpu-spec-schema.ts +16 -0
- package/src/generated/zod/index.ts +42 -2
- package/src/generated/zod/machine-profile-schema.ts +25 -0
- package/src/generated/zod/public-endpoint-output-schema.ts +21 -0
- package/src/generated/zod/public-endpoint-schema.ts +22 -0
- package/src/generated/zod/stack-import-request-schema.ts +3 -0
- package/src/generated/zod/stack-input-default-value-schema.ts +25 -0
- package/src/generated/zod/stack-input-definition-schema.ts +43 -0
- package/src/generated/zod/stack-input-environment-mapping-schema.ts +20 -0
- package/src/generated/zod/stack-input-environment-variable-type-schema.ts +13 -0
- package/src/generated/zod/stack-input-kind-schema.ts +13 -0
- package/src/generated/zod/stack-input-provider-schema.ts +13 -0
- package/src/generated/zod/stack-input-validation-schema.ts +23 -0
- package/src/generated/zod/stack-schema.ts +4 -0
- package/src/generated/zod/stack-settings-schema.ts +5 -1
- package/src/generated/zod/worker-outputs-schema.ts +4 -5
- package/src/generated/zod/worker-public-endpoint-schema.ts +17 -0
- package/src/generated/zod/worker-schema.ts +4 -4
- package/src/index.ts +9 -0
- package/src/input.ts +380 -0
- package/src/stack.ts +19 -0
- package/src/worker.ts +24 -14
- package/src/generated/schemas/ingress.json +0 -1
- package/src/generated/zod/ingress-schema.ts +0 -13
package/dist/tests/index.js
CHANGED
package/package.json
CHANGED
|
@@ -31,10 +31,10 @@ exports[`ArtifactRegistry resource configuration > can be used in stack permissi
|
|
|
31
31
|
"commandsEnabled": false,
|
|
32
32
|
"environment": {},
|
|
33
33
|
"id": "registry-user",
|
|
34
|
-
"ingress": "private",
|
|
35
34
|
"links": [],
|
|
36
35
|
"memoryMb": 256,
|
|
37
36
|
"permissions": "execution",
|
|
37
|
+
"publicEndpoints": [],
|
|
38
38
|
"timeoutSeconds": 180,
|
|
39
39
|
"triggers": [],
|
|
40
40
|
"type": "worker",
|
|
@@ -145,10 +145,10 @@ exports[`Permissions system > creates a stack with custom permission sets 1`] =
|
|
|
145
145
|
"commandsEnabled": false,
|
|
146
146
|
"environment": {},
|
|
147
147
|
"id": "test-worker",
|
|
148
|
-
"ingress": "private",
|
|
149
148
|
"links": [],
|
|
150
149
|
"memoryMb": 256,
|
|
151
150
|
"permissions": "execution",
|
|
151
|
+
"publicEndpoints": [],
|
|
152
152
|
"timeoutSeconds": 180,
|
|
153
153
|
"triggers": [],
|
|
154
154
|
"type": "worker",
|
|
@@ -206,7 +206,6 @@ exports[`Stack builder validation > builds and validates a complex stack with pe
|
|
|
206
206
|
"RUST_LOG": "info,alien_runtime_test_server=debug,alien_runtime=debug",
|
|
207
207
|
},
|
|
208
208
|
"id": "my-test-worker",
|
|
209
|
-
"ingress": "public",
|
|
210
209
|
"links": [
|
|
211
210
|
{
|
|
212
211
|
"id": "my-test-bucket",
|
|
@@ -215,6 +214,13 @@ exports[`Stack builder validation > builds and validates a complex stack with pe
|
|
|
215
214
|
],
|
|
216
215
|
"memoryMb": 512,
|
|
217
216
|
"permissions": "execution",
|
|
217
|
+
"publicEndpoints": [
|
|
218
|
+
{
|
|
219
|
+
"hostLabel": undefined,
|
|
220
|
+
"name": "api",
|
|
221
|
+
"wildcardSubdomains": false,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
218
224
|
"timeoutSeconds": 30,
|
|
219
225
|
"triggers": [],
|
|
220
226
|
"type": "worker",
|
|
@@ -332,10 +338,10 @@ exports[`Stack builder validation > builds and validates a stack with worker sou
|
|
|
332
338
|
"commandsEnabled": false,
|
|
333
339
|
"environment": {},
|
|
334
340
|
"id": "my-source-worker",
|
|
335
|
-
"ingress": "private",
|
|
336
341
|
"links": [],
|
|
337
342
|
"memoryMb": 256,
|
|
338
343
|
"permissions": "execution",
|
|
344
|
+
"publicEndpoints": [],
|
|
339
345
|
"timeoutSeconds": 15,
|
|
340
346
|
"triggers": [],
|
|
341
347
|
"type": "worker",
|
|
@@ -7,6 +7,128 @@ import * as alien from "../index.js"
|
|
|
7
7
|
const SHARED_IMAGE = "docker.io/library/rust:latest"
|
|
8
8
|
|
|
9
9
|
describe("Stack builder validation", () => {
|
|
10
|
+
it("builds compute pools from portable requirements only", () => {
|
|
11
|
+
const compute = new alien.ComputeCluster("runtime")
|
|
12
|
+
.pool("nested", {
|
|
13
|
+
requirements: {
|
|
14
|
+
cpu: 4,
|
|
15
|
+
memory: "16Gi",
|
|
16
|
+
architecture: "x86_64",
|
|
17
|
+
nestedVirtualization: true,
|
|
18
|
+
},
|
|
19
|
+
scale: {
|
|
20
|
+
type: "fixed",
|
|
21
|
+
machines: { min: 2, max: 4, default: 2 },
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
.build()
|
|
25
|
+
|
|
26
|
+
expect(compute.config.capacityGroups).toEqual([
|
|
27
|
+
{
|
|
28
|
+
groupId: "nested",
|
|
29
|
+
profile: {
|
|
30
|
+
cpu: "4",
|
|
31
|
+
memoryBytes: 17179869184,
|
|
32
|
+
ephemeralStorageBytes: 21474836480,
|
|
33
|
+
gpu: undefined,
|
|
34
|
+
},
|
|
35
|
+
minSize: 2,
|
|
36
|
+
maxSize: 2,
|
|
37
|
+
nestedVirtualization: true,
|
|
38
|
+
},
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("builds stack input definitions for deployment forms", () => {
|
|
43
|
+
const stackInputs = alien.inputs({
|
|
44
|
+
apiBaseUrl: alien.string({
|
|
45
|
+
providedBy: ["developer", "deployer"],
|
|
46
|
+
required: true,
|
|
47
|
+
label: "API base URL",
|
|
48
|
+
description: "Public URL used by the runtime service.",
|
|
49
|
+
placeholder: "https://api.example.com",
|
|
50
|
+
format: "url",
|
|
51
|
+
env: "API_BASE_URL",
|
|
52
|
+
}),
|
|
53
|
+
accessKey: alien.secret({
|
|
54
|
+
providedBy: "deployer",
|
|
55
|
+
required: true,
|
|
56
|
+
label: "Access key",
|
|
57
|
+
description: "Secret token used by the runtime service.",
|
|
58
|
+
minLength: 1,
|
|
59
|
+
env: {
|
|
60
|
+
name: "ACCESS_KEY",
|
|
61
|
+
targetResources: ["my-test-worker"],
|
|
62
|
+
type: "secret",
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
deploymentTier: alien.enum(["starter", "enterprise"], {
|
|
66
|
+
providedBy: "developer",
|
|
67
|
+
required: false,
|
|
68
|
+
label: "Deployment tier",
|
|
69
|
+
description: "Controls default service sizing.",
|
|
70
|
+
default: "starter",
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const worker = new alien.Worker("my-test-worker")
|
|
75
|
+
.code({ type: "image", image: SHARED_IMAGE })
|
|
76
|
+
.permissions("execution")
|
|
77
|
+
.build()
|
|
78
|
+
|
|
79
|
+
const stack = new alien.Stack("my-test-stack").inputs(stackInputs).add(worker, "live").build()
|
|
80
|
+
const inputs = stack.inputs
|
|
81
|
+
expect(inputs).toBeDefined()
|
|
82
|
+
if (!inputs) {
|
|
83
|
+
throw new Error("expected stack inputs to be defined")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
expect(inputs).toHaveLength(3)
|
|
87
|
+
expect(inputs.map(input => input.id)).toEqual(["apiBaseUrl", "accessKey", "deploymentTier"])
|
|
88
|
+
expect(inputs.find(input => input.id === "apiBaseUrl")).toMatchObject({
|
|
89
|
+
kind: "string",
|
|
90
|
+
providedBy: ["developer", "deployer"],
|
|
91
|
+
required: true,
|
|
92
|
+
validation: { format: "url" },
|
|
93
|
+
env: [{ name: "API_BASE_URL" }],
|
|
94
|
+
})
|
|
95
|
+
expect(inputs.find(input => input.id === "accessKey")).toMatchObject({
|
|
96
|
+
kind: "secret",
|
|
97
|
+
providedBy: ["deployer"],
|
|
98
|
+
env: [
|
|
99
|
+
{
|
|
100
|
+
name: "ACCESS_KEY",
|
|
101
|
+
targetResources: ["my-test-worker"],
|
|
102
|
+
type: "secret",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
expect(inputs.find(input => input.id === "deploymentTier")).toMatchObject({
|
|
107
|
+
kind: "enum",
|
|
108
|
+
default: {
|
|
109
|
+
type: "string",
|
|
110
|
+
value: "starter",
|
|
111
|
+
},
|
|
112
|
+
validation: {
|
|
113
|
+
values: ["starter", "enterprise"],
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("rejects non-portable stack input regex patterns", () => {
|
|
119
|
+
expect(() =>
|
|
120
|
+
alien.inputs({
|
|
121
|
+
apiBaseUrl: alien.string({
|
|
122
|
+
providedBy: "deployer",
|
|
123
|
+
required: true,
|
|
124
|
+
label: "API base URL",
|
|
125
|
+
description: "Public URL used by the runtime service.",
|
|
126
|
+
pattern: "(?=https://).*",
|
|
127
|
+
}),
|
|
128
|
+
}),
|
|
129
|
+
).toThrow(/not portable/)
|
|
130
|
+
})
|
|
131
|
+
|
|
10
132
|
it("builds a stateful container with persistent storage options", () => {
|
|
11
133
|
const postgres = new alien.Container("postgres")
|
|
12
134
|
.code({ type: "image", image: "postgres:16-alpine" })
|
|
@@ -26,6 +148,68 @@ describe("Stack builder validation", () => {
|
|
|
26
148
|
})
|
|
27
149
|
})
|
|
28
150
|
|
|
151
|
+
it("builds container and daemon wildcard public endpoint options", () => {
|
|
152
|
+
const container = new alien.Container("router")
|
|
153
|
+
.code({ type: "image", image: "nginx:latest" })
|
|
154
|
+
.cpu(0.25)
|
|
155
|
+
.memory("256Mi")
|
|
156
|
+
.permissions("execution")
|
|
157
|
+
.publicEndpoint("api", 8080, {
|
|
158
|
+
protocol: "http",
|
|
159
|
+
hostLabel: "edge",
|
|
160
|
+
wildcardSubdomains: true,
|
|
161
|
+
})
|
|
162
|
+
.build()
|
|
163
|
+
|
|
164
|
+
expect(container.config.ports).toEqual([
|
|
165
|
+
{
|
|
166
|
+
port: 8080,
|
|
167
|
+
},
|
|
168
|
+
])
|
|
169
|
+
expect(container.config.publicEndpoints).toEqual([
|
|
170
|
+
{
|
|
171
|
+
name: "api",
|
|
172
|
+
port: 8080,
|
|
173
|
+
protocol: "http",
|
|
174
|
+
hostLabel: "edge",
|
|
175
|
+
wildcardSubdomains: true,
|
|
176
|
+
},
|
|
177
|
+
])
|
|
178
|
+
|
|
179
|
+
const daemon = new alien.Daemon("gateway")
|
|
180
|
+
.code({ type: "image", image: "registry.example.com/gateway:latest" })
|
|
181
|
+
.cluster("compute")
|
|
182
|
+
.permissions("execution")
|
|
183
|
+
.publicEndpoint("api", 8080, {
|
|
184
|
+
protocol: "http",
|
|
185
|
+
hostLabel: "public",
|
|
186
|
+
wildcardSubdomains: true,
|
|
187
|
+
})
|
|
188
|
+
.healthCheck({
|
|
189
|
+
path: "/health",
|
|
190
|
+
method: "GET",
|
|
191
|
+
timeoutSeconds: 1,
|
|
192
|
+
failureThreshold: 3,
|
|
193
|
+
})
|
|
194
|
+
.build()
|
|
195
|
+
|
|
196
|
+
expect(daemon.config.publicEndpoints).toEqual([
|
|
197
|
+
{
|
|
198
|
+
name: "api",
|
|
199
|
+
port: 8080,
|
|
200
|
+
protocol: "http",
|
|
201
|
+
hostLabel: "public",
|
|
202
|
+
wildcardSubdomains: true,
|
|
203
|
+
},
|
|
204
|
+
])
|
|
205
|
+
expect(daemon.config.healthCheck).toEqual({
|
|
206
|
+
path: "/health",
|
|
207
|
+
method: "GET",
|
|
208
|
+
timeoutSeconds: 1,
|
|
209
|
+
failureThreshold: 3,
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
29
213
|
it("builds and validates a complex stack with permissions", () => {
|
|
30
214
|
// Storage bucket
|
|
31
215
|
const storage = new alien.Storage("my-test-bucket").publicRead(true).build()
|
|
@@ -36,7 +220,7 @@ describe("Stack builder validation", () => {
|
|
|
36
220
|
.memoryMb(512)
|
|
37
221
|
.timeoutSeconds(30)
|
|
38
222
|
.permissions("execution")
|
|
39
|
-
.
|
|
223
|
+
.publicEndpoint("api")
|
|
40
224
|
.environment({
|
|
41
225
|
RUST_LOG: "info,alien_runtime_test_server=debug,alien_runtime=debug",
|
|
42
226
|
})
|
|
@@ -140,7 +324,6 @@ describe("Stack builder validation", () => {
|
|
|
140
324
|
})
|
|
141
325
|
.memoryMb(256)
|
|
142
326
|
.timeoutSeconds(15)
|
|
143
|
-
.ingress("private")
|
|
144
327
|
.permissions("execution")
|
|
145
328
|
.build()
|
|
146
329
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComputeCluster as ComputeClusterConfig,
|
|
3
|
+
ComputeClusterSchema,
|
|
4
|
+
type MachineProfile,
|
|
5
|
+
type ResourceType,
|
|
6
|
+
} from "./generated/index.js"
|
|
7
|
+
import { Resource } from "./resource.js"
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
ComputeCluster as ComputeClusterConfig,
|
|
11
|
+
CapacityGroup,
|
|
12
|
+
CapacityGroupScalePolicy,
|
|
13
|
+
ComputeChoiceRange as GeneratedComputeChoiceRange,
|
|
14
|
+
MachineProfile,
|
|
15
|
+
} from "./generated/index.js"
|
|
16
|
+
export {
|
|
17
|
+
ComputeClusterSchema as ComputeClusterConfigSchema,
|
|
18
|
+
CapacityGroupSchema,
|
|
19
|
+
CapacityGroupScalePolicySchema,
|
|
20
|
+
ComputeChoiceRangeSchema,
|
|
21
|
+
MachineProfileSchema,
|
|
22
|
+
} from "./generated/index.js"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hardware requirements for a compute pool.
|
|
26
|
+
*/
|
|
27
|
+
export type ComputePoolRequirements = {
|
|
28
|
+
cpu: number | string
|
|
29
|
+
memory: string
|
|
30
|
+
ephemeralStorage?: string
|
|
31
|
+
architecture?: "arm64" | "x86_64"
|
|
32
|
+
nestedVirtualization?: boolean
|
|
33
|
+
accelerators?: Array<{
|
|
34
|
+
type: string
|
|
35
|
+
count: number
|
|
36
|
+
}>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ComputeChoiceRange =
|
|
40
|
+
| number
|
|
41
|
+
| {
|
|
42
|
+
min: number
|
|
43
|
+
max: number
|
|
44
|
+
default: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ComputePoolScale =
|
|
48
|
+
| {
|
|
49
|
+
type: "fixed"
|
|
50
|
+
machines: ComputeChoiceRange
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
type: "autoscale"
|
|
54
|
+
min: ComputeChoiceRange
|
|
55
|
+
max: ComputeChoiceRange
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ComputePoolInput = {
|
|
59
|
+
requirements: ComputePoolRequirements
|
|
60
|
+
scale: ComputePoolScale
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Declares a ComputeCluster — the setup-owned machine boundary for daemons and
|
|
65
|
+
* containers. Each capacity group inside the cluster becomes a separate
|
|
66
|
+
* Auto Scaling Group (AWS), Managed Instance Group (GCP), or VM Scale Set
|
|
67
|
+
* (Azure). Daemons reference a cluster via `daemon.cluster(...)` and (when
|
|
68
|
+
* the cluster has more than one capacity group) a specific group via
|
|
69
|
+
* `daemon.pool(...)`.
|
|
70
|
+
*
|
|
71
|
+
* Application source declares portable pool requirements. Provider machine
|
|
72
|
+
* names are selected later through deployment settings.
|
|
73
|
+
*/
|
|
74
|
+
export class ComputeCluster {
|
|
75
|
+
private _config: Partial<ComputeClusterConfig> = {
|
|
76
|
+
capacityGroups: [],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
constructor(id: string) {
|
|
80
|
+
this._config.id = id
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the resource type for permission targets that apply to all
|
|
85
|
+
* compute-cluster resources.
|
|
86
|
+
*/
|
|
87
|
+
public static any(): ResourceType {
|
|
88
|
+
return "compute-cluster"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public pool(groupId: string, config: ComputePoolInput): this {
|
|
92
|
+
const { minSize, maxSize } = selectedScaleBounds(config.scale)
|
|
93
|
+
this._config.capacityGroups!.push({
|
|
94
|
+
groupId,
|
|
95
|
+
profile: machineProfileFromRequirements(config.requirements),
|
|
96
|
+
minSize,
|
|
97
|
+
maxSize,
|
|
98
|
+
scalePolicy: scalePolicyFromInput(config.scale),
|
|
99
|
+
nestedVirtualization: config.requirements.nestedVirtualization,
|
|
100
|
+
})
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sets the container CIDR block used for inter-container networking inside
|
|
106
|
+
* the cluster. Each machine gets a /24 subnet carved from this range.
|
|
107
|
+
* Defaults to 10.244.0.0/16 if not specified.
|
|
108
|
+
*/
|
|
109
|
+
public containerCidr(cidr: string): this {
|
|
110
|
+
this._config.containerCidr = cidr
|
|
111
|
+
return this
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Builds and validates the cluster configuration.
|
|
116
|
+
*/
|
|
117
|
+
public build(): Resource {
|
|
118
|
+
const config = ComputeClusterSchema.parse(this._config)
|
|
119
|
+
return new Resource({
|
|
120
|
+
type: "compute-cluster",
|
|
121
|
+
...config,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function selectedScaleBounds(scale: ComputePoolScale): { minSize: number; maxSize: number } {
|
|
127
|
+
if (scale.type === "fixed") {
|
|
128
|
+
const machines = defaultChoice(scale.machines)
|
|
129
|
+
return { minSize: machines, maxSize: machines }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
minSize: defaultChoice(scale.min),
|
|
134
|
+
maxSize: defaultChoice(scale.max),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scalePolicyFromInput(
|
|
139
|
+
scale: ComputePoolScale,
|
|
140
|
+
): ComputeClusterConfig["capacityGroups"][number]["scalePolicy"] {
|
|
141
|
+
if (scale.type === "fixed") {
|
|
142
|
+
return {
|
|
143
|
+
type: "fixed",
|
|
144
|
+
machines: choiceRange(scale.machines),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: "autoscale",
|
|
150
|
+
min: choiceRange(scale.min),
|
|
151
|
+
max: choiceRange(scale.max),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function choiceRange(choice: ComputeChoiceRange): { min: number; max: number; default: number } {
|
|
156
|
+
if (typeof choice === "number") {
|
|
157
|
+
return { min: choice, max: choice, default: choice }
|
|
158
|
+
}
|
|
159
|
+
return choice
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function defaultChoice(choice: ComputeChoiceRange): number {
|
|
163
|
+
if (typeof choice === "number") {
|
|
164
|
+
return choice
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return choice.default
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function machineProfileFromRequirements(requirements: ComputePoolRequirements): MachineProfile {
|
|
171
|
+
return {
|
|
172
|
+
cpu: typeof requirements.cpu === "number" ? `${requirements.cpu}` : requirements.cpu,
|
|
173
|
+
memoryBytes: parseQuantityBytes(requirements.memory),
|
|
174
|
+
ephemeralStorageBytes: parseQuantityBytes(requirements.ephemeralStorage ?? "20Gi"),
|
|
175
|
+
architecture: requirements.architecture,
|
|
176
|
+
gpu: requirements.accelerators?.[0]
|
|
177
|
+
? {
|
|
178
|
+
type: requirements.accelerators[0].type,
|
|
179
|
+
count: requirements.accelerators[0].count,
|
|
180
|
+
}
|
|
181
|
+
: undefined,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseQuantityBytes(value: string): number {
|
|
186
|
+
const match = value.match(/^([0-9]+(?:\.[0-9]+)?)(Ki|Mi|Gi|Ti|k|M|G|T)?$/)
|
|
187
|
+
if (!match) {
|
|
188
|
+
throw new Error(`Invalid memory/storage quantity: ${value}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const amount = Number(match[1])
|
|
192
|
+
const suffix = match[2]
|
|
193
|
+
const multiplier =
|
|
194
|
+
suffix === "Ti"
|
|
195
|
+
? 1024 ** 4
|
|
196
|
+
: suffix === "Gi"
|
|
197
|
+
? 1024 ** 3
|
|
198
|
+
: suffix === "Mi"
|
|
199
|
+
? 1024 ** 2
|
|
200
|
+
: suffix === "Ki"
|
|
201
|
+
? 1024
|
|
202
|
+
: suffix === "T"
|
|
203
|
+
? 1000 ** 4
|
|
204
|
+
: suffix === "G"
|
|
205
|
+
? 1000 ** 3
|
|
206
|
+
: suffix === "M"
|
|
207
|
+
? 1000 ** 2
|
|
208
|
+
: suffix === "k"
|
|
209
|
+
? 1000
|
|
210
|
+
: 1
|
|
211
|
+
|
|
212
|
+
return Math.round(amount * multiplier)
|
|
213
|
+
}
|
package/src/container.ts
CHANGED
|
@@ -6,11 +6,21 @@ import {
|
|
|
6
6
|
ContainerSchema,
|
|
7
7
|
type HealthCheck,
|
|
8
8
|
type PersistentStorage,
|
|
9
|
+
type PublicEndpoint,
|
|
9
10
|
type ResourceSpec,
|
|
10
11
|
type ResourceType,
|
|
11
12
|
} from "./generated/index.js"
|
|
12
13
|
import { Resource } from "./resource.js"
|
|
13
14
|
|
|
15
|
+
export type PublicEndpointOptions =
|
|
16
|
+
| "http"
|
|
17
|
+
| "tcp"
|
|
18
|
+
| {
|
|
19
|
+
protocol: "http" | "tcp"
|
|
20
|
+
hostLabel?: string
|
|
21
|
+
wildcardSubdomains?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export type {
|
|
15
25
|
Container as ContainerConfig,
|
|
16
26
|
ContainerOutputs,
|
|
@@ -18,6 +28,7 @@ export type {
|
|
|
18
28
|
ContainerAutoscaling,
|
|
19
29
|
ContainerPort,
|
|
20
30
|
ExposeProtocol,
|
|
31
|
+
PublicEndpoint,
|
|
21
32
|
ContainerGpuSpec,
|
|
22
33
|
ContainerStatus,
|
|
23
34
|
HealthCheck,
|
|
@@ -53,6 +64,7 @@ export class Container {
|
|
|
53
64
|
private _config: Partial<ContainerConfig> = {
|
|
54
65
|
links: [],
|
|
55
66
|
ports: [],
|
|
67
|
+
publicEndpoints: [],
|
|
56
68
|
environment: {},
|
|
57
69
|
stateful: false,
|
|
58
70
|
// cluster is optional - if not set, ComputeClusterMutation will auto-assign
|
|
@@ -231,40 +243,40 @@ export class Container {
|
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
/**
|
|
234
|
-
* Exposes a
|
|
246
|
+
* Exposes a named public endpoint for a container port.
|
|
247
|
+
*
|
|
248
|
+
* Endpoint names are the stable contract for URLs, DNS, and runtime
|
|
249
|
+
* environment injection.
|
|
250
|
+
*
|
|
251
|
+
* @param name Endpoint name, unique within this container.
|
|
235
252
|
* @param port Port number to expose.
|
|
236
|
-
* @param
|
|
253
|
+
* @param options "http"/"tcp" or endpoint options.
|
|
237
254
|
* @returns The Container builder instance.
|
|
238
255
|
*/
|
|
239
|
-
public
|
|
256
|
+
public publicEndpoint(name: string, port: number, options: PublicEndpointOptions = "http"): this {
|
|
240
257
|
if (!this._config.ports) {
|
|
241
258
|
this._config.ports = []
|
|
242
259
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const existingPort = this._config.ports.find(p => p.port === port)
|
|
246
|
-
if (existingPort) {
|
|
247
|
-
existingPort.expose = protocol
|
|
248
|
-
} else {
|
|
249
|
-
this._config.ports.push({ port, expose: protocol })
|
|
250
|
-
}
|
|
251
|
-
return this
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Convenience method to expose the first/primary port publicly.
|
|
256
|
-
* Must be called after .port() or .ports().
|
|
257
|
-
* @param protocol "http" for HTTPS with TLS termination, "tcp" for TCP passthrough.
|
|
258
|
-
* @returns The Container builder instance.
|
|
259
|
-
*/
|
|
260
|
-
public expose(protocol: "http" | "tcp"): this {
|
|
261
|
-
if (!this._config.ports || this._config.ports.length === 0) {
|
|
262
|
-
throw new Error("Cannot expose port: no ports defined. Call .port() first.")
|
|
260
|
+
if (!this._config.publicEndpoints) {
|
|
261
|
+
this._config.publicEndpoints = []
|
|
263
262
|
}
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
const endpoint =
|
|
264
|
+
typeof options === "string"
|
|
265
|
+
? { protocol: options, hostLabel: undefined, wildcardSubdomains: false }
|
|
266
|
+
: options
|
|
267
|
+
|
|
268
|
+
if (!this._config.ports.some(p => p.port === port)) {
|
|
269
|
+
this._config.ports.push({
|
|
270
|
+
port,
|
|
271
|
+
})
|
|
266
272
|
}
|
|
267
|
-
this._config.
|
|
273
|
+
this._config.publicEndpoints.push({
|
|
274
|
+
name,
|
|
275
|
+
port,
|
|
276
|
+
protocol: endpoint.protocol,
|
|
277
|
+
hostLabel: endpoint.hostLabel,
|
|
278
|
+
wildcardSubdomains: endpoint.wildcardSubdomains ?? false,
|
|
279
|
+
} satisfies PublicEndpoint)
|
|
268
280
|
return this
|
|
269
281
|
}
|
|
270
282
|
|