@sandbox-kit/modal 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/checks/provider_checks.d.ts +2 -0
- package/dist/checks/provider_checks.js +86 -0
- package/dist/config/provider_config.d.ts +8 -0
- package/dist/config/provider_config.js +45 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mappings/mappings.d.ts +16 -0
- package/dist/mappings/mappings.js +315 -0
- package/dist/mappings/provider_mappings.d.ts +14 -0
- package/dist/mappings/provider_mappings.js +194 -0
- package/dist/mappings.d.ts +17 -0
- package/dist/mappings.js +317 -0
- package/dist/modal.d.ts +22 -0
- package/dist/modal.js +106 -0
- package/dist/provider.checks.gen.d.ts +3 -0
- package/dist/provider.checks.gen.js +101 -0
- package/dist/provider.config.gen.d.ts +9 -0
- package/dist/provider.config.gen.js +40 -0
- package/dist/provider.d.ts +13 -0
- package/dist/provider.errors.gen.d.ts +3 -0
- package/dist/provider.errors.gen.js +36 -0
- package/dist/provider.gen.d.ts +14 -0
- package/dist/provider.gen.js +11 -0
- package/dist/provider.js +10 -0
- package/dist/provider.mappings.gen.d.ts +11 -0
- package/dist/provider.mappings.gen.js +161 -0
- package/dist/provider_checks.d.ts +3 -0
- package/dist/provider_checks.js +106 -0
- package/dist/provider_config.d.ts +9 -0
- package/dist/provider_config.js +46 -0
- package/dist/provider_mappings.d.ts +15 -0
- package/dist/provider_mappings.js +179 -0
- package/package.json +74 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SandboxInfo, type Scope } from "@sandbox-kit/sandbox";
|
|
2
|
+
import type { CreateOptions, OAuthCredentials, Resources, TokenPairCredentials } from "@sandbox-kit/sandbox";
|
|
3
|
+
import type { ModalClientParams as NativeModalClientParams, Sandbox as NativeSandbox, SandboxCreateParams as NativeSandboxCreateParams } from "modal";
|
|
4
|
+
type PublicLifetime = NonNullable<CreateOptions["lifetime"]>;
|
|
5
|
+
type PublicIdleTerminate = NonNullable<PublicLifetime["idleTerminate"]>;
|
|
6
|
+
export declare function mapSandboxIdentity(source: NativeSandbox | undefined, operation?: string): Partial<SandboxInfo>;
|
|
7
|
+
export declare function mapClientScope(source: Scope | undefined, operation?: string): NativeModalClientParams;
|
|
8
|
+
export declare function mapCommonCreateFields(source: CreateOptions | undefined, operation?: string): NativeSandboxCreateParams;
|
|
9
|
+
export declare function mapResources(source: Resources | undefined, operation?: string): NativeSandboxCreateParams;
|
|
10
|
+
export declare function mapLifetimeDuration(source: PublicLifetime | undefined, operation?: string): NativeSandboxCreateParams;
|
|
11
|
+
export declare function mapIdleDuration(source: PublicIdleTerminate | undefined, operation?: string): NativeSandboxCreateParams;
|
|
12
|
+
export declare function mapTokenPairCredentials(source: TokenPairCredentials | undefined, operation?: string): NativeModalClientParams;
|
|
13
|
+
export declare function mapOAuthAuth(source: OAuthCredentials | undefined, operation?: string): NativeModalClientParams;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Provider field mappings between shared CreateOptions/Config and the native SDK.
|
|
2
|
+
// Inventoried in specs/providers/modal.yaml groups; kept in sync by hand (not emitted).
|
|
3
|
+
import { ErrorKind, ValueOrigin, CreateFieldResourcesMemoryLimitMib, CreateFieldResourcesMemoryMib, InfoFieldId, CreateFieldLifetimeIdleTerminateAfter, CreateFieldLifetimeMaximumLifetime, CreateFieldResourcesCpuCores, CreateFieldResourcesCpuLimitCores, } from "@sandbox-kit/sandbox";
|
|
4
|
+
import { providerError } from "#errors.js";
|
|
5
|
+
export function mapSandboxIdentity(source, operation = "create") {
|
|
6
|
+
const target = {};
|
|
7
|
+
if (source === undefined)
|
|
8
|
+
return target;
|
|
9
|
+
const origins = {};
|
|
10
|
+
target.origins = origins;
|
|
11
|
+
{
|
|
12
|
+
if (source.sandboxId !== undefined) {
|
|
13
|
+
const value = source.sandboxId;
|
|
14
|
+
target.id = value;
|
|
15
|
+
origins[InfoFieldId] = ValueOrigin.Provider;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
target.provider = "modal";
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
export function mapClientScope(source, operation = "initialize") {
|
|
22
|
+
const target = {};
|
|
23
|
+
if (source === undefined)
|
|
24
|
+
return target;
|
|
25
|
+
{
|
|
26
|
+
if (source.environment !== undefined) {
|
|
27
|
+
const value = source.environment;
|
|
28
|
+
target.environment = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return target;
|
|
32
|
+
}
|
|
33
|
+
export function mapCommonCreateFields(source, operation = "create") {
|
|
34
|
+
const target = {};
|
|
35
|
+
if (source === undefined)
|
|
36
|
+
return target;
|
|
37
|
+
{
|
|
38
|
+
if (source.name !== undefined) {
|
|
39
|
+
const value = source.name;
|
|
40
|
+
target.name = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
{
|
|
44
|
+
if (source.environment !== undefined) {
|
|
45
|
+
const value = source.environment;
|
|
46
|
+
target.env = { ...value };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
{
|
|
50
|
+
if (source.labels !== undefined) {
|
|
51
|
+
const value = source.labels;
|
|
52
|
+
target.tags = { ...value };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
export function mapResources(source, operation = "create") {
|
|
58
|
+
const target = {};
|
|
59
|
+
if (source === undefined)
|
|
60
|
+
return target;
|
|
61
|
+
{
|
|
62
|
+
if (source.cpuCores !== undefined) {
|
|
63
|
+
target.cpu = scaledCpuCores(source.cpuCores, CreateFieldResourcesCpuCores, operation);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
{
|
|
67
|
+
if (source.cpuLimitCores !== undefined) {
|
|
68
|
+
target.cpuLimit = scaledCpuCores(source.cpuLimitCores, CreateFieldResourcesCpuLimitCores, operation);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
if (source.memoryMib !== undefined) {
|
|
73
|
+
const value = source.memoryMib;
|
|
74
|
+
if (value > 2147483647n)
|
|
75
|
+
mappingError({
|
|
76
|
+
kind: ErrorKind.InvalidArgument,
|
|
77
|
+
field: CreateFieldResourcesMemoryMib,
|
|
78
|
+
message: "memory_mib cannot be represented",
|
|
79
|
+
operation,
|
|
80
|
+
});
|
|
81
|
+
target.memoryMiB = Number(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
{
|
|
85
|
+
if (source.memoryLimitMib !== undefined) {
|
|
86
|
+
const value = source.memoryLimitMib;
|
|
87
|
+
if (value > 2147483647n)
|
|
88
|
+
mappingError({
|
|
89
|
+
kind: ErrorKind.InvalidArgument,
|
|
90
|
+
field: CreateFieldResourcesMemoryLimitMib,
|
|
91
|
+
message: "memory_limit_mib cannot be represented",
|
|
92
|
+
operation,
|
|
93
|
+
});
|
|
94
|
+
target.memoryLimitMiB = Number(value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return target;
|
|
98
|
+
}
|
|
99
|
+
export function mapLifetimeDuration(source, operation = "create") {
|
|
100
|
+
const target = {};
|
|
101
|
+
if (source === undefined)
|
|
102
|
+
return target;
|
|
103
|
+
{
|
|
104
|
+
if (source.maximumLifetime !== undefined) {
|
|
105
|
+
target.timeoutMs = durationSecondsMs(source.maximumLifetime, CreateFieldLifetimeMaximumLifetime, 86_400, operation);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
export function mapIdleDuration(source, operation = "create") {
|
|
111
|
+
const target = {};
|
|
112
|
+
if (source === undefined)
|
|
113
|
+
return target;
|
|
114
|
+
{
|
|
115
|
+
if (source.after !== undefined) {
|
|
116
|
+
target.idleTimeoutMs = durationSecondsMs(source.after, CreateFieldLifetimeIdleTerminateAfter, 4_294_967_295, operation);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return target;
|
|
120
|
+
}
|
|
121
|
+
/** Modal CPU fields are millis-scaled: value must equal round(value*1000)/1000. */
|
|
122
|
+
function scaledCpuCores(value, field, operation) {
|
|
123
|
+
const scaled = Math.round(value * 1000);
|
|
124
|
+
if (!Number.isFinite(value) ||
|
|
125
|
+
value < 0 ||
|
|
126
|
+
value !== scaled / 1000 ||
|
|
127
|
+
scaled > 4_294_967_295) {
|
|
128
|
+
mappingError({
|
|
129
|
+
kind: ErrorKind.InvalidArgument,
|
|
130
|
+
field,
|
|
131
|
+
message: field.slice(field.lastIndexOf(".") + 1) + " cannot be represented",
|
|
132
|
+
operation,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return value * 1000 < scaled ? scaled / 1000 : value;
|
|
136
|
+
}
|
|
137
|
+
/** Modal lifetime/idle timeouts are whole seconds expressed as milliseconds. */
|
|
138
|
+
function durationSecondsMs(value, field, maximumSeconds, operation) {
|
|
139
|
+
if (!Number.isFinite(value) ||
|
|
140
|
+
value <= 0 ||
|
|
141
|
+
value % 1000 !== 0 ||
|
|
142
|
+
value / 1000 > maximumSeconds) {
|
|
143
|
+
mappingError({
|
|
144
|
+
kind: ErrorKind.InvalidArgument,
|
|
145
|
+
field,
|
|
146
|
+
message: field.slice(field.lastIndexOf(".") + 1) + " cannot be represented",
|
|
147
|
+
operation,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
export function mapTokenPairCredentials(source, operation = "initialize") {
|
|
153
|
+
const target = {};
|
|
154
|
+
if (source === undefined)
|
|
155
|
+
return target;
|
|
156
|
+
{
|
|
157
|
+
const value = source.id;
|
|
158
|
+
target.tokenId = value;
|
|
159
|
+
}
|
|
160
|
+
{
|
|
161
|
+
const value = source.secret;
|
|
162
|
+
target.tokenSecret = value;
|
|
163
|
+
}
|
|
164
|
+
return target;
|
|
165
|
+
}
|
|
166
|
+
export function mapOAuthAuth(source, operation = "create") {
|
|
167
|
+
const target = {};
|
|
168
|
+
if (source === undefined)
|
|
169
|
+
return target;
|
|
170
|
+
{
|
|
171
|
+
const value = source.refreshToken;
|
|
172
|
+
target.oauthRefreshToken = value;
|
|
173
|
+
}
|
|
174
|
+
{
|
|
175
|
+
const value = source.clientId;
|
|
176
|
+
target.oauthClientId = value;
|
|
177
|
+
}
|
|
178
|
+
{
|
|
179
|
+
if (source.clientSecret !== undefined) {
|
|
180
|
+
const value = source.clientSecret;
|
|
181
|
+
target.oauthClientSecret = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
{
|
|
185
|
+
if (source.jwtKey !== undefined) {
|
|
186
|
+
const value = source.jwtKey;
|
|
187
|
+
target.oauthJwtKey = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return target;
|
|
191
|
+
}
|
|
192
|
+
function mappingError(input) {
|
|
193
|
+
providerError(input);
|
|
194
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ModalClientParams, type Sandbox, type SandboxCreateParams } from "modal";
|
|
2
|
+
import { type Config, type CreateOptions, type SandboxInfo } from "@sandbox-kit/sandbox";
|
|
3
|
+
import type { ProviderState } from "./provider_config.js";
|
|
4
|
+
export interface ModalCreatePlan {
|
|
5
|
+
readonly app: string;
|
|
6
|
+
readonly environment?: string;
|
|
7
|
+
readonly image: string;
|
|
8
|
+
readonly params: SandboxCreateParams;
|
|
9
|
+
readonly secretNames: readonly string[];
|
|
10
|
+
readonly proxyName?: string;
|
|
11
|
+
readonly waitUntilReady: boolean;
|
|
12
|
+
readonly readyTimeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function mapClientConfig(config: Config): ModalClientParams;
|
|
15
|
+
export declare function mapCreateRequest(request: CreateOptions, config: ProviderState): ModalCreatePlan;
|
|
16
|
+
export declare function mapSandboxInfo(value: Sandbox, request?: CreateOptions): SandboxInfo;
|
|
17
|
+
//# sourceMappingURL=mappings.d.ts.map
|
package/dist/mappings.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Provider-specific orchestration around generated portable field mappings.
|
|
2
|
+
import { Probe, } from "modal";
|
|
3
|
+
import { EgressMode, ErrorKind, IsolationKind, PolicyMode, PortTransport, SecretInjection, SandboxState, ValueOrigin, WaitCondition, sandboxError } from "@sandbox-kit/sandbox";
|
|
4
|
+
import { mapCommonCreateFields as generatedMapCommonCreateFields, mapIdleDuration as generatedMapIdleDuration, mapLifetimeDuration as generatedMapLifetimeDuration, mapResources as generatedMapResources, mapSandboxIdentity as generatedMapSandboxIdentity, } from "./provider_mappings.js";
|
|
5
|
+
import { validateProviderCreate } from "./provider_checks.js";
|
|
6
|
+
import { mapGeneratedClientConfig } from "./provider_config.js";
|
|
7
|
+
export function mapClientConfig(config) {
|
|
8
|
+
const result = mapGeneratedClientConfig(config);
|
|
9
|
+
if (config.timeout !== undefined)
|
|
10
|
+
result.timeoutMs = finiteDurationMs(config.timeout, "timeout", true);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export function mapCreateRequest(request, config) {
|
|
14
|
+
validateProviderCreate(request);
|
|
15
|
+
const app = config.scope?.appName === undefined
|
|
16
|
+
? invalid("scope.appName", "Modal app name is required")
|
|
17
|
+
: nonblank(config.scope.appName, "scope.appName");
|
|
18
|
+
if (request.source?.image === undefined)
|
|
19
|
+
unsupported("source", "Modal requires an explicit image source");
|
|
20
|
+
const params = generatedMapCommonCreateFields(request);
|
|
21
|
+
if (request.name !== undefined)
|
|
22
|
+
params.name = nonblank(request.name, "name");
|
|
23
|
+
if (config.region !== undefined)
|
|
24
|
+
params.regions = [nonblankConfig(config.region, "region")];
|
|
25
|
+
mapRuntime(request, params);
|
|
26
|
+
mapIsolation(request, params);
|
|
27
|
+
mapResources(request, params);
|
|
28
|
+
mapPlacement(request, params);
|
|
29
|
+
mapNetwork(request, params);
|
|
30
|
+
const secretNames = mapSecurity(request, params);
|
|
31
|
+
mapLifetime(request, params);
|
|
32
|
+
mapReadiness(request, params);
|
|
33
|
+
const wait = mapProvisioning(request, params);
|
|
34
|
+
mapObservability(request, params);
|
|
35
|
+
if (request.providerOptions !== undefined)
|
|
36
|
+
unsupported("providerOptions", "Modal provider options are not mapped");
|
|
37
|
+
return {
|
|
38
|
+
app,
|
|
39
|
+
...(config.scope?.environment === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { environment: config.scope.environment }),
|
|
42
|
+
image: nonblank(request.source.image.reference, "source.image.reference"),
|
|
43
|
+
params,
|
|
44
|
+
secretNames,
|
|
45
|
+
...(request.network?.proxyReference === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: {
|
|
48
|
+
proxyName: nonblank(request.network.proxyReference, "network.proxyReference"),
|
|
49
|
+
}),
|
|
50
|
+
...wait,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function mapRuntime(r, p) {
|
|
54
|
+
const v = r.runtime;
|
|
55
|
+
if (!v)
|
|
56
|
+
return;
|
|
57
|
+
if (v.workingDirectory !== undefined)
|
|
58
|
+
p.workdir = v.workingDirectory;
|
|
59
|
+
if (v.entrypoint !== undefined)
|
|
60
|
+
p.command = [...v.entrypoint];
|
|
61
|
+
if (v.pty !== undefined)
|
|
62
|
+
p.pty = v.pty;
|
|
63
|
+
if (v.language !== undefined ||
|
|
64
|
+
v.version !== undefined ||
|
|
65
|
+
v.user !== undefined)
|
|
66
|
+
unsupported("runtime", "language, version, and user must come from the Modal image");
|
|
67
|
+
}
|
|
68
|
+
function mapIsolation(r, p) {
|
|
69
|
+
const v = r.isolation;
|
|
70
|
+
if (!v)
|
|
71
|
+
return;
|
|
72
|
+
const kind = v.kind ?? IsolationKind.Default;
|
|
73
|
+
if (kind === IsolationKind.LinuxVm)
|
|
74
|
+
p.runtime = "vm";
|
|
75
|
+
else if (kind === IsolationKind.Container)
|
|
76
|
+
p.runtime = "gvisor";
|
|
77
|
+
else if (kind !== IsolationKind.Default)
|
|
78
|
+
unsupported("isolation.kind", "requested Modal isolation is unsupported");
|
|
79
|
+
if (v.providerKind !== undefined || v.nestedVirtualization !== undefined)
|
|
80
|
+
unsupported("isolation", "provider kind and nested virtualization are unsupported");
|
|
81
|
+
}
|
|
82
|
+
function mapResources(r, p) {
|
|
83
|
+
const v = r.resources;
|
|
84
|
+
if (!v)
|
|
85
|
+
return;
|
|
86
|
+
if (v.cpuLimitCores !== undefined)
|
|
87
|
+
positive(v.cpuLimitCores, "resources.cpuLimitCores");
|
|
88
|
+
if (v.memoryMib !== undefined)
|
|
89
|
+
integer(v.memoryMib, "resources.memoryMib", 128);
|
|
90
|
+
if (v.memoryLimitMib !== undefined)
|
|
91
|
+
integer(v.memoryLimitMib, "resources.memoryLimitMib", 1);
|
|
92
|
+
if (v.diskMib !== undefined)
|
|
93
|
+
unsupported("resources.diskMib", "Modal disk override is unsupported");
|
|
94
|
+
Object.assign(p, generatedMapResources(v));
|
|
95
|
+
}
|
|
96
|
+
function mapPlacement(r, p) {
|
|
97
|
+
const v = r.placement;
|
|
98
|
+
if (!v)
|
|
99
|
+
return;
|
|
100
|
+
if (v.cloud !== undefined)
|
|
101
|
+
p.cloud = v.cloud;
|
|
102
|
+
if (v.regions !== undefined)
|
|
103
|
+
p.regions = [...v.regions];
|
|
104
|
+
if (v.target !== undefined)
|
|
105
|
+
unsupported("placement.target", "Modal placement target is unsupported");
|
|
106
|
+
}
|
|
107
|
+
function mapNetwork(r, p) {
|
|
108
|
+
const v = r.network;
|
|
109
|
+
if (!v)
|
|
110
|
+
return;
|
|
111
|
+
const egress = v.egress ?? EgressMode.Default;
|
|
112
|
+
if (egress === EgressMode.BlockAll)
|
|
113
|
+
p.blockNetwork = true;
|
|
114
|
+
else if (egress !== EgressMode.Default &&
|
|
115
|
+
egress !== EgressMode.AllowAll &&
|
|
116
|
+
egress !== EgressMode.Restricted)
|
|
117
|
+
invalid("network.egress", "unknown egress mode");
|
|
118
|
+
if (v.outboundCidrs !== undefined)
|
|
119
|
+
p.outboundCidrAllowlist = [...v.outboundCidrs];
|
|
120
|
+
if (v.outboundDomains !== undefined)
|
|
121
|
+
p.outboundDomainAllowlist = [...v.outboundDomains];
|
|
122
|
+
if (v.inboundCidrs !== undefined) {
|
|
123
|
+
if (!v.inboundCidrs.length)
|
|
124
|
+
unsupported("network.inboundCidrs", "empty inbound deny-all is not representable");
|
|
125
|
+
p.inboundCidrAllowlist = [...v.inboundCidrs];
|
|
126
|
+
}
|
|
127
|
+
if (v.privateNetwork !== undefined)
|
|
128
|
+
p.i6pn = v.privateNetwork;
|
|
129
|
+
if (v.customDomain !== undefined)
|
|
130
|
+
p.customDomain = v.customDomain;
|
|
131
|
+
if (v.ports) {
|
|
132
|
+
p.encryptedPorts = [];
|
|
133
|
+
p.h2Ports = [];
|
|
134
|
+
p.unencryptedPorts = [];
|
|
135
|
+
for (const x of v.ports) {
|
|
136
|
+
const port = x.port;
|
|
137
|
+
if (port === undefined)
|
|
138
|
+
invalid("network.ports.port", "port is required");
|
|
139
|
+
const transport = x.transport ?? PortTransport.Default;
|
|
140
|
+
if (transport === PortTransport.Tls)
|
|
141
|
+
p.encryptedPorts.push(port);
|
|
142
|
+
else if (transport === PortTransport.Http2Tls)
|
|
143
|
+
p.h2Ports.push(port);
|
|
144
|
+
else if (transport === PortTransport.Plain)
|
|
145
|
+
p.unencryptedPorts.push(port);
|
|
146
|
+
else
|
|
147
|
+
invalid("network.ports.transport", "explicit transport is required");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (v.linkedSandbox !== undefined || v.outboundProxyUrl !== undefined)
|
|
151
|
+
unsupported("network", "linked sandbox and outbound proxy URL mappings are unsupported");
|
|
152
|
+
}
|
|
153
|
+
function mapSecurity(r, p) {
|
|
154
|
+
const v = r.security;
|
|
155
|
+
if (!v)
|
|
156
|
+
return [];
|
|
157
|
+
if (v.workloadIdentity !== undefined)
|
|
158
|
+
p.includeOidcIdentityToken = v.workloadIdentity;
|
|
159
|
+
if (v.publicAccess !== undefined)
|
|
160
|
+
unsupported("security.publicAccess", "Modal public access is represented by ports");
|
|
161
|
+
const names = [];
|
|
162
|
+
for (const s of v.secrets ?? []) {
|
|
163
|
+
if (s.environmentVariable !== undefined)
|
|
164
|
+
unsupported("security.secrets.environmentVariable", "per-variable secret renaming is unsupported");
|
|
165
|
+
const injection = s.injection ?? SecretInjection.Default;
|
|
166
|
+
if (injection !== SecretInjection.Default &&
|
|
167
|
+
injection !== SecretInjection.Environment)
|
|
168
|
+
unsupported("security.secrets.injection", "egress-placeholder injection is unsupported");
|
|
169
|
+
names.push(nonblank(s.reference, "security.secrets.reference"));
|
|
170
|
+
}
|
|
171
|
+
return names;
|
|
172
|
+
}
|
|
173
|
+
function mapLifetime(r, p) {
|
|
174
|
+
const v = r.lifetime;
|
|
175
|
+
if (!v)
|
|
176
|
+
return;
|
|
177
|
+
if (v.maximumLifetime !== undefined)
|
|
178
|
+
finiteDurationMs(v.maximumLifetime, "lifetime.maximumLifetime", false);
|
|
179
|
+
Object.assign(p, generatedMapLifetimeDuration(v));
|
|
180
|
+
if (v.idleTerminate) {
|
|
181
|
+
const mode = v.idleTerminate.mode ?? PolicyMode.Default;
|
|
182
|
+
if (mode === PolicyMode.Disabled)
|
|
183
|
+
p.idleTimeoutMs = 0;
|
|
184
|
+
else if (mode === PolicyMode.After) {
|
|
185
|
+
if (v.idleTerminate.after === undefined)
|
|
186
|
+
invalid("lifetime.idleTerminate.after", "duration is required");
|
|
187
|
+
finiteDurationMs(v.idleTerminate.after, "lifetime.idleTerminate.after", false);
|
|
188
|
+
Object.assign(p, generatedMapIdleDuration(v.idleTerminate));
|
|
189
|
+
}
|
|
190
|
+
else if (mode !== PolicyMode.Default)
|
|
191
|
+
invalid("lifetime.idleTerminate.mode", "unknown policy mode");
|
|
192
|
+
}
|
|
193
|
+
if (v.ephemeral !== undefined ||
|
|
194
|
+
v.idleStop !== undefined ||
|
|
195
|
+
v.idlePause !== undefined ||
|
|
196
|
+
v.stoppedArchive !== undefined ||
|
|
197
|
+
v.stoppedDelete !== undefined)
|
|
198
|
+
unsupported("lifetime", "requested lifecycle policy is unsupported by Modal");
|
|
199
|
+
}
|
|
200
|
+
function mapReadiness(r, p) {
|
|
201
|
+
const v = r.readiness;
|
|
202
|
+
if (!v)
|
|
203
|
+
return;
|
|
204
|
+
if (v.tcpPort !== undefined) {
|
|
205
|
+
p.readinessProbe = Probe.withTcp(v.tcpPort);
|
|
206
|
+
}
|
|
207
|
+
else if (v.command !== undefined)
|
|
208
|
+
p.readinessProbe = Probe.withExec([...(v.command.argv ?? [])]);
|
|
209
|
+
else
|
|
210
|
+
invalid("readiness", "probe is required");
|
|
211
|
+
}
|
|
212
|
+
function mapProvisioning(r, _p) {
|
|
213
|
+
const v = r.provisioning;
|
|
214
|
+
if (!v)
|
|
215
|
+
return { waitUntilReady: false };
|
|
216
|
+
if (v.queueTimeout !== undefined)
|
|
217
|
+
unsupported("provisioning.queueTimeout", "Modal queue timeout is unsupported");
|
|
218
|
+
const waitFor = v.waitFor ?? WaitCondition.Default;
|
|
219
|
+
if (waitFor === WaitCondition.Default || waitFor === WaitCondition.Scheduled)
|
|
220
|
+
return { waitUntilReady: false };
|
|
221
|
+
if (waitFor === WaitCondition.Ready) {
|
|
222
|
+
if (v.timeout === undefined || v.timeout <= 0)
|
|
223
|
+
invalid("provisioning.timeout", "ready wait requires a positive timeout");
|
|
224
|
+
if (!r.readiness)
|
|
225
|
+
invalid("readiness", "ready wait requires a probe");
|
|
226
|
+
return { waitUntilReady: true, readyTimeoutMs: v.timeout };
|
|
227
|
+
}
|
|
228
|
+
unsupported("provisioning.waitFor", "requested wait condition is unsupported");
|
|
229
|
+
}
|
|
230
|
+
function mapObservability(r, p) {
|
|
231
|
+
const v = r.observability;
|
|
232
|
+
if (!v)
|
|
233
|
+
return;
|
|
234
|
+
if (v.verbose !== undefined)
|
|
235
|
+
p.verbose = v.verbose;
|
|
236
|
+
if (v.telemetryEndpoint !== undefined)
|
|
237
|
+
unsupported("observability.telemetryEndpoint", "per-sandbox telemetry endpoint is unsupported");
|
|
238
|
+
}
|
|
239
|
+
export function mapSandboxInfo(value, request) {
|
|
240
|
+
const mapped = generatedMapSandboxIdentity(value);
|
|
241
|
+
const origins = { ...(mapped.origins ?? {}) };
|
|
242
|
+
const result = {
|
|
243
|
+
id: mapped.id ?? value.sandboxId,
|
|
244
|
+
provider: "modal",
|
|
245
|
+
state: SandboxState.Running,
|
|
246
|
+
labels: {},
|
|
247
|
+
origins,
|
|
248
|
+
};
|
|
249
|
+
if (request?.name !== undefined) {
|
|
250
|
+
mutable(result).name = request.name;
|
|
251
|
+
origins.name = ValueOrigin.Request;
|
|
252
|
+
}
|
|
253
|
+
if (request?.labels !== undefined) {
|
|
254
|
+
mutable(result).labels = { ...request.labels };
|
|
255
|
+
origins.labels = ValueOrigin.Request;
|
|
256
|
+
}
|
|
257
|
+
if (request?.isolation !== undefined) {
|
|
258
|
+
mutable(result).isolation = { ...request.isolation };
|
|
259
|
+
origins.isolation = ValueOrigin.Request;
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
function mutable(v) {
|
|
264
|
+
return v;
|
|
265
|
+
}
|
|
266
|
+
function nonblank(v, f) {
|
|
267
|
+
if (!v.trim())
|
|
268
|
+
invalid(f, "must not be blank");
|
|
269
|
+
return v;
|
|
270
|
+
}
|
|
271
|
+
function nonblankConfig(v, f) {
|
|
272
|
+
if (!v.trim())
|
|
273
|
+
configInvalid(f, "must not be blank");
|
|
274
|
+
return v;
|
|
275
|
+
}
|
|
276
|
+
function positive(v, f) {
|
|
277
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
278
|
+
invalid(f, "must be positive");
|
|
279
|
+
return v;
|
|
280
|
+
}
|
|
281
|
+
function integer(v, f, min) {
|
|
282
|
+
if (v < BigInt(min) || v > 2147483647n)
|
|
283
|
+
invalid(f, "is outside the supported range");
|
|
284
|
+
return Number(v);
|
|
285
|
+
}
|
|
286
|
+
function finiteDurationMs(v, f, zero) {
|
|
287
|
+
if (!Number.isFinite(v) || v < 0 || (!zero && v === 0))
|
|
288
|
+
invalid(f, "must be a finite positive duration");
|
|
289
|
+
return v;
|
|
290
|
+
}
|
|
291
|
+
function invalid(field, message) {
|
|
292
|
+
throw sandboxError({
|
|
293
|
+
kind: ErrorKind.InvalidArgument,
|
|
294
|
+
provider: "modal",
|
|
295
|
+
operation: "create",
|
|
296
|
+
field,
|
|
297
|
+
message: "sandbox-kit modal: " + message,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function unsupported(field, message) {
|
|
301
|
+
throw sandboxError({
|
|
302
|
+
kind: ErrorKind.Unsupported,
|
|
303
|
+
provider: "modal",
|
|
304
|
+
operation: "create",
|
|
305
|
+
field,
|
|
306
|
+
message: "sandbox-kit modal: " + message,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function configInvalid(field, message) {
|
|
310
|
+
throw sandboxError({
|
|
311
|
+
kind: ErrorKind.InvalidArgument,
|
|
312
|
+
provider: "modal",
|
|
313
|
+
operation: "initialize",
|
|
314
|
+
field,
|
|
315
|
+
message: "sandbox-kit modal: " + message,
|
|
316
|
+
});
|
|
317
|
+
}
|
package/dist/modal.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ModalClient, type ModalClientParams } from "modal";
|
|
2
|
+
import { type Provider } from "@sandbox-kit/sandbox";
|
|
3
|
+
import type { Backend, Config } from "@sandbox-kit/sandbox";
|
|
4
|
+
import { ProviderBase } from "./provider.js";
|
|
5
|
+
interface NativeModal {
|
|
6
|
+
readonly apps: Pick<ModalClient["apps"], "fromName">;
|
|
7
|
+
readonly images: Pick<ModalClient["images"], "fromRegistry">;
|
|
8
|
+
readonly proxies: Pick<ModalClient["proxies"], "fromName">;
|
|
9
|
+
readonly sandboxes: Pick<ModalClient["sandboxes"], "create" | "fromId">;
|
|
10
|
+
readonly secrets: Pick<ModalClient["secrets"], "fromName">;
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
type NativeFactory = (config: ModalClientParams) => NativeModal;
|
|
14
|
+
/** @internal Exported for adapter boundary tests, but not from the package root. */
|
|
15
|
+
export declare class ModalProvider extends ProviderBase {
|
|
16
|
+
private readonly nativeFactory;
|
|
17
|
+
constructor(nativeFactory?: NativeFactory);
|
|
18
|
+
initialize(config: Config): Backend;
|
|
19
|
+
}
|
|
20
|
+
/** Selects Modal as the provider for the shared Sandbox Kit client. */
|
|
21
|
+
export declare function modal(): Provider;
|
|
22
|
+
export {};
|
package/dist/modal.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ModalClient, } from "modal";
|
|
2
|
+
import { LifecycleCapability, } from "@sandbox-kit/sandbox";
|
|
3
|
+
import { mapClientConfig, mapCreateRequest, mapSandboxInfo, } from "#mappings/mappings.js";
|
|
4
|
+
import { mapModalError } from "#errors.js";
|
|
5
|
+
import { BackendBase, ProviderBase } from "./provider.js";
|
|
6
|
+
import { captureProviderState, } from "#config/provider_config.js";
|
|
7
|
+
/** @internal Exported for adapter boundary tests, but not from the package root. */
|
|
8
|
+
export class ModalProvider extends ProviderBase {
|
|
9
|
+
nativeFactory;
|
|
10
|
+
constructor(nativeFactory = (config) => new ModalClient(config)) {
|
|
11
|
+
super();
|
|
12
|
+
this.nativeFactory = nativeFactory;
|
|
13
|
+
}
|
|
14
|
+
initialize(config) {
|
|
15
|
+
try {
|
|
16
|
+
return new ModalBackend(this.nativeFactory(mapClientConfig(config)), captureProviderState(config));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw mapModalError(error, "initialize");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class ModalBackend extends BackendBase {
|
|
24
|
+
native;
|
|
25
|
+
config;
|
|
26
|
+
constructor(native, config) {
|
|
27
|
+
super();
|
|
28
|
+
this.native = native;
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
async create(request, options) {
|
|
32
|
+
try {
|
|
33
|
+
options.signal?.throwIfAborted();
|
|
34
|
+
const plan = mapCreateRequest(request, this.config);
|
|
35
|
+
const app = await this.native.apps.fromName(plan.app, {
|
|
36
|
+
...(plan.environment === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { environment: plan.environment }),
|
|
39
|
+
});
|
|
40
|
+
const secrets = await Promise.all(plan.secretNames.map((name) => this.native.secrets.fromName(name, {
|
|
41
|
+
...(plan.environment === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { environment: plan.environment }),
|
|
44
|
+
})));
|
|
45
|
+
const proxy = plan.proxyName === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: await this.native.proxies.fromName(plan.proxyName, {
|
|
48
|
+
...(plan.environment === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { environment: plan.environment }),
|
|
51
|
+
});
|
|
52
|
+
const image = this.native.images.fromRegistry(plan.image);
|
|
53
|
+
const sandbox = await this.native.sandboxes.create(app, image, {
|
|
54
|
+
...plan.params,
|
|
55
|
+
...(secrets.length === 0 ? {} : { secrets }),
|
|
56
|
+
...(proxy === undefined ? {} : { proxy }),
|
|
57
|
+
});
|
|
58
|
+
if (plan.waitUntilReady) {
|
|
59
|
+
const readyTimeoutMs = plan.readyTimeoutMs ?? options.timeout;
|
|
60
|
+
await sandbox.waitUntilReady(readyTimeoutMs);
|
|
61
|
+
}
|
|
62
|
+
return { sandbox: mapSandboxInfo(sandbox, request) };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw mapModalError(error, "create");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async attach(id) {
|
|
69
|
+
try {
|
|
70
|
+
return mapSandboxInfo(await this.native.sandboxes.fromId(id));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw mapModalError(error, "attach");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
capabilities() {
|
|
77
|
+
return [LifecycleCapability.Terminate];
|
|
78
|
+
}
|
|
79
|
+
async terminate(id, request) {
|
|
80
|
+
try {
|
|
81
|
+
const sandbox = await this.native.sandboxes.fromId(id);
|
|
82
|
+
if (request.wait === true)
|
|
83
|
+
return { exitCode: await sandbox.terminate({ wait: true }) };
|
|
84
|
+
await sandbox.terminate();
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw mapModalError(error, "terminate");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async closeSandbox(id) {
|
|
92
|
+
try {
|
|
93
|
+
(await this.native.sandboxes.fromId(id)).detach();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw mapModalError(error, "close");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async close() {
|
|
100
|
+
this.native.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Selects Modal as the provider for the shared Sandbox Kit client. */
|
|
104
|
+
export function modal() {
|
|
105
|
+
return new ModalProvider();
|
|
106
|
+
}
|