@miosa/sdk 1.2.1 → 1.2.3
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 +43 -1
- package/dist/index.d.ts +224 -8
- package/dist/index.js +398 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +5 -0
- package/src/index.ts +24 -2
- package/src/resources/admin.ts +11 -0
- package/src/resources/api-keys.ts +16 -0
- package/src/resources/custom_domains.ts +1 -1
- package/src/resources/deployments.test.ts +121 -0
- package/src/resources/deployments.ts +231 -1
- package/src/resources/docker-deploy.test.ts +102 -0
- package/src/resources/docker-deploy.ts +183 -0
- package/src/resources/governance.test.ts +355 -0
- package/src/resources/governance.ts +528 -0
- package/src/resources/phase1.test.ts +187 -0
- package/src/resources/quotas.ts +77 -0
- package/src/resources/sandbox-processes.ts +112 -0
- package/src/resources/sandbox-shares.ts +83 -0
- package/src/resources/sandboxes.test.ts +26 -0
- package/src/resources/sandboxes.ts +31 -0
- package/src/resources/tenant-events.ts +32 -0
- package/src/resources/tenant.ts +111 -2
- package/src/resources/webhooks.ts +54 -1
- package/src/resources/workspaces.ts +285 -0
- package/src/types.ts +5 -5
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { CronJobs } from "./resources/cron-jobs.js";
|
|
|
15
15
|
import { Dashboard } from "./resources/dashboard.js";
|
|
16
16
|
import { Databases } from "./resources/databases.js";
|
|
17
17
|
import { Deployments } from "./resources/deployments.js";
|
|
18
|
+
import { DockerDeploy } from "./resources/docker-deploy.js";
|
|
18
19
|
import { EgressAudit } from "./resources/egressAudit.js";
|
|
19
20
|
import { EgressNetwork } from "./resources/egressNetwork.js";
|
|
20
21
|
import { EgressSecrets } from "./resources/egressSecrets.js";
|
|
@@ -130,6 +131,9 @@ export class Miosa {
|
|
|
130
131
|
*/
|
|
131
132
|
readonly deployments: Deployments;
|
|
132
133
|
|
|
134
|
+
/** Docker Deploy appliance hosts — one always-on workspace host, many apps. */
|
|
135
|
+
readonly dockerDeploy: DockerDeploy;
|
|
136
|
+
|
|
133
137
|
/** Credit balance and usage. */
|
|
134
138
|
readonly credits: Credits;
|
|
135
139
|
|
|
@@ -257,6 +261,7 @@ export class Miosa {
|
|
|
257
261
|
this.computers = new Computers(this.http);
|
|
258
262
|
this.sandboxes = new Sandboxes(this.http);
|
|
259
263
|
this.deployments = new Deployments(this.http);
|
|
264
|
+
this.dockerDeploy = new DockerDeploy(this.http);
|
|
260
265
|
this.credits = new Credits(this.http);
|
|
261
266
|
this.admin = new Admin(this.http);
|
|
262
267
|
this.openComputers = new OpenComputers(this.http);
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,7 @@ export type {
|
|
|
122
122
|
DeploymentData,
|
|
123
123
|
DeploymentId,
|
|
124
124
|
DeploymentListParams,
|
|
125
|
+
DeploymentProduct,
|
|
125
126
|
DeploymentReleaseData,
|
|
126
127
|
DeploymentReleaseId,
|
|
127
128
|
DeploymentServiceData,
|
|
@@ -134,6 +135,11 @@ export type {
|
|
|
134
135
|
DeploymentVersionId,
|
|
135
136
|
DeploymentVersionKind,
|
|
136
137
|
DeploymentVersionState,
|
|
138
|
+
DockerDeployCreateParams,
|
|
139
|
+
DockerDeployDoctorCheck,
|
|
140
|
+
DockerDeployDoctorParams,
|
|
141
|
+
DockerDeployDoctorProbe,
|
|
142
|
+
DockerDeployDoctorResult,
|
|
137
143
|
ExternalAttribution,
|
|
138
144
|
PublishFromSandboxParams,
|
|
139
145
|
PublishParams,
|
|
@@ -145,6 +151,17 @@ export type {
|
|
|
145
151
|
RuntimeLogsResult,
|
|
146
152
|
VersionListParams,
|
|
147
153
|
} from "./resources/deployments.js";
|
|
154
|
+
export { DockerDeploy } from "./resources/docker-deploy.js";
|
|
155
|
+
export type {
|
|
156
|
+
DockerDeployApplianceStatus,
|
|
157
|
+
DockerDeployHostData,
|
|
158
|
+
DockerDeployHostEnsureParams,
|
|
159
|
+
DockerDeployHostId,
|
|
160
|
+
DockerDeployHostListParams,
|
|
161
|
+
DockerDeployHostListResponse,
|
|
162
|
+
DockerDeployHostResponse,
|
|
163
|
+
DockerDeployHostStatus,
|
|
164
|
+
} from "./resources/docker-deploy.js";
|
|
148
165
|
export type {
|
|
149
166
|
SnapshotData,
|
|
150
167
|
SnapshotStatus,
|
|
@@ -216,7 +233,7 @@ export type {
|
|
|
216
233
|
HealthCheckCreateParams,
|
|
217
234
|
HealthCheckUpdateParams,
|
|
218
235
|
} from "./resources/health-checks.js";
|
|
219
|
-
export { Webhooks } from "./resources/webhooks.js";
|
|
236
|
+
export { verifySignature, Webhooks } from "./resources/webhooks.js";
|
|
220
237
|
export type {
|
|
221
238
|
WebhookId,
|
|
222
239
|
WebhookDeliveryId,
|
|
@@ -285,7 +302,12 @@ export type {
|
|
|
285
302
|
|
|
286
303
|
// P2 resources
|
|
287
304
|
export { Tenant } from "./resources/tenant.js";
|
|
288
|
-
export type {
|
|
305
|
+
export type {
|
|
306
|
+
BrandingData,
|
|
307
|
+
PreviewDomainData,
|
|
308
|
+
TenantBrandingUpdateParams,
|
|
309
|
+
TenantPlan,
|
|
310
|
+
} from "./resources/tenant.js";
|
|
289
311
|
export { Regions } from "./resources/regions.js";
|
|
290
312
|
export type {
|
|
291
313
|
RegionData,
|
package/src/resources/admin.ts
CHANGED
|
@@ -345,4 +345,15 @@ export class Admin {
|
|
|
345
345
|
model_id: modelId,
|
|
346
346
|
});
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
/** POST /api/v1/admin/impersonate — returns {token, expires_at}. */
|
|
350
|
+
impersonate(
|
|
351
|
+
externalUserId: string,
|
|
352
|
+
options: { ttlSec?: number } = {},
|
|
353
|
+
): Promise<{ token: string; expires_at: string }> {
|
|
354
|
+
return this.http.post("/admin/impersonate", {
|
|
355
|
+
external_user_id: externalUserId,
|
|
356
|
+
ttl_sec: options.ttlSec ?? 3600,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
348
359
|
}
|
|
@@ -113,6 +113,22 @@ export class ApiKeys {
|
|
|
113
113
|
return unwrap<ApiKeyCreateResult>(data);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** POST /api/v1/api-keys/scoped — L2 delegation token bound to one external user. */
|
|
117
|
+
async createScoped(params: {
|
|
118
|
+
externalUserId: string;
|
|
119
|
+
scopes: string[];
|
|
120
|
+
expiresAt?: string;
|
|
121
|
+
}): Promise<ApiKeyCreateResult> {
|
|
122
|
+
const body = stripUndefined({
|
|
123
|
+
external_user_id: params.externalUserId,
|
|
124
|
+
scopes: params.scopes,
|
|
125
|
+
expires_at: params.expiresAt,
|
|
126
|
+
});
|
|
127
|
+
return unwrap<ApiKeyCreateResult>(
|
|
128
|
+
await this.http.post<unknown>("/api-keys/scoped", body),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
async delete(keyId: string): Promise<void> {
|
|
117
133
|
await this.http.delete<unknown>(`/api-keys/${keyId}`);
|
|
118
134
|
}
|
|
@@ -41,7 +41,7 @@ export interface CustomDomainRegisterParams {
|
|
|
41
41
|
* // 1. Register the domain
|
|
42
42
|
* const domain = await computer.domains.register("app.example.com");
|
|
43
43
|
* console.log(domain.instructions);
|
|
44
|
-
* // => "Add a CNAME record: app.example.com → <slug>.sandbox
|
|
44
|
+
* // => "Add a CNAME record: app.example.com → <slug>.sandbox.miosa.ai"
|
|
45
45
|
*
|
|
46
46
|
* // 2. Add the CNAME in your DNS registrar, then...
|
|
47
47
|
*
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { HttpClient } from "../http.js";
|
|
3
|
+
import { Deployments } from "./deployments.js";
|
|
4
|
+
|
|
5
|
+
const mockRequest = vi.fn();
|
|
6
|
+
const mockGet = vi.fn();
|
|
7
|
+
|
|
8
|
+
function makeHttp(): HttpClient {
|
|
9
|
+
const http = {} as HttpClient;
|
|
10
|
+
http.request = mockRequest;
|
|
11
|
+
http.get = mockGet;
|
|
12
|
+
return http;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
vi.unstubAllGlobals();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("Deployments", () => {
|
|
21
|
+
it("createDockerDeploy() marks the deployment for Docker Deploy", async () => {
|
|
22
|
+
mockRequest.mockResolvedValue({
|
|
23
|
+
data: {
|
|
24
|
+
id: "dep_123",
|
|
25
|
+
tenant_id: "ten_123",
|
|
26
|
+
name: "Clinic Intake",
|
|
27
|
+
slug: "clinic-intake",
|
|
28
|
+
state: "pending",
|
|
29
|
+
deployment_product: "docker_deploy",
|
|
30
|
+
docker_deploy_host_id: "ddh_123",
|
|
31
|
+
metadata: { deployment_product: "docker_deploy", client: "clinic-iq" },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const deployment = await new Deployments(makeHttp()).createDockerDeploy({
|
|
36
|
+
name: "Clinic Intake",
|
|
37
|
+
repoUrl: "https://github.com/clinic-iq/intake",
|
|
38
|
+
externalWorkspaceId: "dr-smith",
|
|
39
|
+
externalProjectId: "lead-magnet",
|
|
40
|
+
metadata: { client: "clinic-iq" },
|
|
41
|
+
idempotencyKey: "idem-123",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(mockRequest).toHaveBeenCalledWith("/deployments", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: {
|
|
47
|
+
name: "Clinic Intake",
|
|
48
|
+
repo_url: "https://github.com/clinic-iq/intake",
|
|
49
|
+
metadata: { client: "clinic-iq", deployment_product: "docker_deploy" },
|
|
50
|
+
external_workspace_id: "dr-smith",
|
|
51
|
+
external_project_id: "lead-magnet",
|
|
52
|
+
},
|
|
53
|
+
headers: { "Idempotency-Key": "idem-123" },
|
|
54
|
+
});
|
|
55
|
+
expect(deployment.deployment_product).toBe("docker_deploy");
|
|
56
|
+
expect(deployment.docker_deploy_host_id).toBe("ddh_123");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("doctorDockerDeploy verifies host, route metadata, and public URL", async () => {
|
|
60
|
+
mockGet.mockImplementation(async (path: string) => {
|
|
61
|
+
if (path === "/deployments/dep_123") {
|
|
62
|
+
return {
|
|
63
|
+
data: {
|
|
64
|
+
id: "dep_123",
|
|
65
|
+
tenant_id: "ten_123",
|
|
66
|
+
name: "Clinic Intake",
|
|
67
|
+
slug: "clinic-intake",
|
|
68
|
+
state: "running",
|
|
69
|
+
deployment_product: "docker_deploy",
|
|
70
|
+
docker_deploy_host_id: "ddh_123",
|
|
71
|
+
public_url: "https://clinic-intake.osa.miosa.app",
|
|
72
|
+
metadata: {
|
|
73
|
+
deployment_product: "docker_deploy",
|
|
74
|
+
runtime: { ip: "172.16.74.246", port: 23906 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (path === "/docker-deploy/hosts/ddh_123") {
|
|
81
|
+
return {
|
|
82
|
+
data: {
|
|
83
|
+
id: "ddh_123",
|
|
84
|
+
tenant_id: "ten_123",
|
|
85
|
+
workspace_id: "ws_123",
|
|
86
|
+
status: "active",
|
|
87
|
+
size: "medium",
|
|
88
|
+
region: "us",
|
|
89
|
+
appliance_status: "healthy",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new Error(`unexpected path ${path}`);
|
|
95
|
+
});
|
|
96
|
+
vi.stubGlobal(
|
|
97
|
+
"fetch",
|
|
98
|
+
vi.fn().mockResolvedValue({ ok: true, status: 200 }),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = await new Deployments(makeHttp()).doctorDockerDeploy(
|
|
102
|
+
"dep_123",
|
|
103
|
+
{ probePath: "/health" },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(result.ok).toBe(true);
|
|
107
|
+
expect(mockGet).toHaveBeenCalledWith("/deployments/dep_123");
|
|
108
|
+
expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts/ddh_123");
|
|
109
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
110
|
+
"https://clinic-intake.osa.miosa.app/health",
|
|
111
|
+
expect.objectContaining({ method: "GET" }),
|
|
112
|
+
);
|
|
113
|
+
expect(result.checks.map((check) => check.name)).toEqual([
|
|
114
|
+
"deployment_product",
|
|
115
|
+
"docker_deploy_host_id",
|
|
116
|
+
"docker_deploy_host_health",
|
|
117
|
+
"runtime_route",
|
|
118
|
+
"public_url_probe",
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
|
|
15
15
|
import type { HttpClient } from "../http.js";
|
|
16
|
+
import type { DockerDeployHostData } from "./docker-deploy.js";
|
|
16
17
|
|
|
17
18
|
// ── Branded IDs ─────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -50,6 +51,8 @@ export type DeploymentVersionState =
|
|
|
50
51
|
|
|
51
52
|
export type DeploymentSourceType = "repo" | "sandbox" | "upload";
|
|
52
53
|
|
|
54
|
+
export type DeploymentProduct = "miosa_deploy" | "docker_deploy";
|
|
55
|
+
|
|
53
56
|
export type DeploymentServiceType =
|
|
54
57
|
| "static_web"
|
|
55
58
|
| "web"
|
|
@@ -107,6 +110,8 @@ export interface DeploymentData {
|
|
|
107
110
|
auto_deploy?: boolean;
|
|
108
111
|
custom_domain_id?: string | null;
|
|
109
112
|
linked_database_id?: string | null;
|
|
113
|
+
deployment_product?: DeploymentProduct | string | null;
|
|
114
|
+
docker_deploy_host_id?: string | null;
|
|
110
115
|
metadata?: Record<string, unknown>;
|
|
111
116
|
external_workspace_id?: string | null;
|
|
112
117
|
external_user_id?: string | null;
|
|
@@ -116,10 +121,39 @@ export interface DeploymentData {
|
|
|
116
121
|
updated_at?: string;
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
export interface DockerDeployDoctorCheck {
|
|
125
|
+
name: string;
|
|
126
|
+
ok: boolean;
|
|
127
|
+
message: string;
|
|
128
|
+
details?: Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface DockerDeployDoctorProbe {
|
|
132
|
+
url: string;
|
|
133
|
+
ok: boolean;
|
|
134
|
+
status?: number;
|
|
135
|
+
error?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface DockerDeployDoctorResult {
|
|
139
|
+
ok: boolean;
|
|
140
|
+
deployment: DeploymentData;
|
|
141
|
+
host?: DockerDeployHostData;
|
|
142
|
+
checks: DockerDeployDoctorCheck[];
|
|
143
|
+
probe?: DockerDeployDoctorProbe;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface DockerDeployDoctorParams {
|
|
147
|
+
probePath?: string;
|
|
148
|
+
probe_path?: string;
|
|
149
|
+
timeoutMs?: number;
|
|
150
|
+
timeout_ms?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
export type DeploymentDatabaseRequest =
|
|
120
154
|
| boolean
|
|
121
155
|
| {
|
|
122
|
-
engine?: "postgresql" | "mysql" | "redis";
|
|
156
|
+
engine?: "postgresql" | "mysql" | "redis" | "qdrant";
|
|
123
157
|
size?: "xs" | "small" | "medium" | "large";
|
|
124
158
|
storage_mb?: number;
|
|
125
159
|
region?: string;
|
|
@@ -281,6 +315,8 @@ export interface DeploymentCreateParams extends ExternalAttribution {
|
|
|
281
315
|
idempotencyKey?: string;
|
|
282
316
|
}
|
|
283
317
|
|
|
318
|
+
export interface DockerDeployCreateParams extends DeploymentCreateParams {}
|
|
319
|
+
|
|
284
320
|
export interface DeploymentUpdateParams {
|
|
285
321
|
name?: string;
|
|
286
322
|
branch?: string;
|
|
@@ -421,6 +457,52 @@ function stripUndefined(
|
|
|
421
457
|
);
|
|
422
458
|
}
|
|
423
459
|
|
|
460
|
+
function dockerDeployMetadata(
|
|
461
|
+
metadata: Record<string, unknown> | undefined,
|
|
462
|
+
): Record<string, unknown> {
|
|
463
|
+
return {
|
|
464
|
+
...(metadata ?? {}),
|
|
465
|
+
deployment_product: "docker_deploy",
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function dockerDeployProduct(deployment: DeploymentData): unknown {
|
|
470
|
+
return (
|
|
471
|
+
deployment.deployment_product ??
|
|
472
|
+
deployment.metadata?.["deployment_product"]
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function dockerDeployHostId(deployment: DeploymentData): string | null {
|
|
477
|
+
const metadataHost = deployment.metadata?.["docker_deploy_host_id"];
|
|
478
|
+
return (
|
|
479
|
+
deployment.docker_deploy_host_id ??
|
|
480
|
+
(typeof metadataHost === "string" ? metadataHost : null)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function addDoctorCheck(
|
|
485
|
+
checks: DockerDeployDoctorCheck[],
|
|
486
|
+
name: string,
|
|
487
|
+
ok: boolean,
|
|
488
|
+
message: string,
|
|
489
|
+
details?: Record<string, unknown>,
|
|
490
|
+
): void {
|
|
491
|
+
checks.push({ name, ok, message, ...(details ? { details } : {}) });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function hostHealthy(host: DockerDeployHostData | undefined): boolean {
|
|
495
|
+
return Boolean(
|
|
496
|
+
host && host.status === "active" && host.appliance_status === "healthy",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function probeUrl(publicUrl: string, probePath: string): string {
|
|
501
|
+
const url = new URL(publicUrl);
|
|
502
|
+
url.pathname = probePath.startsWith("/") ? probePath : `/${probePath}`;
|
|
503
|
+
return url.toString();
|
|
504
|
+
}
|
|
505
|
+
|
|
424
506
|
// ── Sub-resources ──────────────────────────────────────────────────────────
|
|
425
507
|
|
|
426
508
|
export class DeploymentVersions {
|
|
@@ -627,6 +709,154 @@ export class Deployments {
|
|
|
627
709
|
return unwrap(data) as DeploymentData;
|
|
628
710
|
}
|
|
629
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Create a deployment that runs on the workspace's dedicated Docker Deploy
|
|
714
|
+
* runtime. It uses the same /deployments API as MIOSA Deploy, but marks the
|
|
715
|
+
* deployment so the control plane attaches it to the workspace Docker host.
|
|
716
|
+
*/
|
|
717
|
+
async createDockerDeploy(
|
|
718
|
+
params: DockerDeployCreateParams,
|
|
719
|
+
): Promise<DeploymentData> {
|
|
720
|
+
return this.create({
|
|
721
|
+
...params,
|
|
722
|
+
metadata: dockerDeployMetadata(params.metadata),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Verify a Docker Deploy deployment before telling a user or agent it is
|
|
728
|
+
* live. Checks product markers, appliance host health, route metadata, and
|
|
729
|
+
* optionally probes the public URL.
|
|
730
|
+
*/
|
|
731
|
+
async doctorDockerDeploy(
|
|
732
|
+
deploymentId: string,
|
|
733
|
+
params: DockerDeployDoctorParams = {},
|
|
734
|
+
): Promise<DockerDeployDoctorResult> {
|
|
735
|
+
const checks: DockerDeployDoctorCheck[] = [];
|
|
736
|
+
const deployment = await this.get(deploymentId);
|
|
737
|
+
const metadata = deployment.metadata ?? {};
|
|
738
|
+
const product = dockerDeployProduct(deployment);
|
|
739
|
+
const hostId = dockerDeployHostId(deployment);
|
|
740
|
+
|
|
741
|
+
addDoctorCheck(
|
|
742
|
+
checks,
|
|
743
|
+
"deployment_product",
|
|
744
|
+
product === "docker_deploy",
|
|
745
|
+
product === "docker_deploy"
|
|
746
|
+
? "Deployment is marked for Docker Deploy."
|
|
747
|
+
: `Expected deployment_product=docker_deploy, got ${String(product ?? "missing")}.`,
|
|
748
|
+
{ deployment_product: product ?? null },
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
addDoctorCheck(
|
|
752
|
+
checks,
|
|
753
|
+
"docker_deploy_host_id",
|
|
754
|
+
Boolean(hostId),
|
|
755
|
+
hostId
|
|
756
|
+
? "Deployment has a Docker Deploy host id."
|
|
757
|
+
: "Deployment has no docker_deploy_host_id.",
|
|
758
|
+
{ docker_deploy_host_id: hostId },
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
let host: DockerDeployHostData | undefined;
|
|
762
|
+
if (hostId) {
|
|
763
|
+
try {
|
|
764
|
+
const rawHost = await this.http.get<unknown>(
|
|
765
|
+
`/docker-deploy/hosts/${hostId}`,
|
|
766
|
+
);
|
|
767
|
+
host = unwrap(rawHost) as DockerDeployHostData;
|
|
768
|
+
addDoctorCheck(
|
|
769
|
+
checks,
|
|
770
|
+
"docker_deploy_host_health",
|
|
771
|
+
hostHealthy(host),
|
|
772
|
+
hostHealthy(host)
|
|
773
|
+
? "Docker Deploy host is active and healthy."
|
|
774
|
+
: `Docker Deploy host status=${host.status} appliance=${host.appliance_status}.`,
|
|
775
|
+
{
|
|
776
|
+
status: host.status,
|
|
777
|
+
appliance_status: host.appliance_status,
|
|
778
|
+
},
|
|
779
|
+
);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
addDoctorCheck(
|
|
782
|
+
checks,
|
|
783
|
+
"docker_deploy_host_health",
|
|
784
|
+
false,
|
|
785
|
+
error instanceof Error ? error.message : String(error),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const runtime = metadata["runtime"];
|
|
791
|
+
const hasRuntimeRoute =
|
|
792
|
+
typeof runtime === "object" &&
|
|
793
|
+
runtime !== null &&
|
|
794
|
+
typeof (runtime as Record<string, unknown>)["ip"] === "string" &&
|
|
795
|
+
typeof (runtime as Record<string, unknown>)["port"] === "number";
|
|
796
|
+
addDoctorCheck(
|
|
797
|
+
checks,
|
|
798
|
+
"runtime_route",
|
|
799
|
+
hasRuntimeRoute,
|
|
800
|
+
hasRuntimeRoute
|
|
801
|
+
? "Deployment has appliance runtime route metadata."
|
|
802
|
+
: "Deployment is missing appliance runtime route metadata.",
|
|
803
|
+
typeof runtime === "object" && runtime !== null
|
|
804
|
+
? (runtime as Record<string, unknown>)
|
|
805
|
+
: undefined,
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
let probe: DockerDeployDoctorProbe | undefined;
|
|
809
|
+
const publicUrl = deployment.public_url;
|
|
810
|
+
const path = params.probePath ?? params.probe_path ?? "/";
|
|
811
|
+
if (publicUrl && typeof fetch === "function") {
|
|
812
|
+
const url = probeUrl(publicUrl, path);
|
|
813
|
+
const controller = new AbortController();
|
|
814
|
+
const timeout = setTimeout(
|
|
815
|
+
() => controller.abort(),
|
|
816
|
+
params.timeoutMs ?? params.timeout_ms ?? 10_000,
|
|
817
|
+
);
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetch(url, {
|
|
820
|
+
method: "GET",
|
|
821
|
+
signal: controller.signal,
|
|
822
|
+
});
|
|
823
|
+
probe = { url, ok: response.ok, status: response.status };
|
|
824
|
+
addDoctorCheck(
|
|
825
|
+
checks,
|
|
826
|
+
"public_url_probe",
|
|
827
|
+
response.ok,
|
|
828
|
+
response.ok
|
|
829
|
+
? `Public URL returned HTTP ${response.status}.`
|
|
830
|
+
: `Public URL returned HTTP ${response.status}.`,
|
|
831
|
+
{ url, status: response.status },
|
|
832
|
+
);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
probe = {
|
|
835
|
+
url,
|
|
836
|
+
ok: false,
|
|
837
|
+
error: error instanceof Error ? error.message : String(error),
|
|
838
|
+
};
|
|
839
|
+
addDoctorCheck(
|
|
840
|
+
checks,
|
|
841
|
+
"public_url_probe",
|
|
842
|
+
false,
|
|
843
|
+
probe.error ?? "Public URL probe failed.",
|
|
844
|
+
{ url },
|
|
845
|
+
);
|
|
846
|
+
} finally {
|
|
847
|
+
clearTimeout(timeout);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
ok: checks.every((check) => check.ok),
|
|
853
|
+
deployment,
|
|
854
|
+
...(host ? { host } : {}),
|
|
855
|
+
checks,
|
|
856
|
+
...(probe ? { probe } : {}),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
630
860
|
async update(
|
|
631
861
|
deploymentId: string,
|
|
632
862
|
params: DeploymentUpdateParams,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { HttpClient } from "../http.js";
|
|
3
|
+
import { DockerDeploy } from "./docker-deploy.js";
|
|
4
|
+
|
|
5
|
+
const mockGet = vi.fn();
|
|
6
|
+
const mockPost = vi.fn();
|
|
7
|
+
|
|
8
|
+
function makeHttp(): HttpClient {
|
|
9
|
+
const http = {} as HttpClient;
|
|
10
|
+
http.get = mockGet;
|
|
11
|
+
http.post = mockPost;
|
|
12
|
+
return http;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("DockerDeploy", () => {
|
|
20
|
+
it("lists Docker Deploy hosts by workspace", async () => {
|
|
21
|
+
mockGet.mockResolvedValue({
|
|
22
|
+
data: [
|
|
23
|
+
{
|
|
24
|
+
id: "ddh_123",
|
|
25
|
+
tenant_id: "ten_123",
|
|
26
|
+
workspace_id: "ws_123",
|
|
27
|
+
status: "bootstrapping",
|
|
28
|
+
appliance_status: "starting",
|
|
29
|
+
size: "medium",
|
|
30
|
+
region: "us",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const hosts = await new DockerDeploy(makeHttp()).listHosts({
|
|
36
|
+
workspaceId: "ws_123",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts", {
|
|
40
|
+
workspace_id: "ws_123",
|
|
41
|
+
});
|
|
42
|
+
expect(hosts[0]?.id).toBe("ddh_123");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("ensures a workspace Docker Deploy host", async () => {
|
|
46
|
+
mockPost.mockResolvedValue({
|
|
47
|
+
host: {
|
|
48
|
+
id: "ddh_123",
|
|
49
|
+
tenant_id: "ten_123",
|
|
50
|
+
workspace_id: "ws_123",
|
|
51
|
+
status: "pending",
|
|
52
|
+
appliance_status: "not_installed",
|
|
53
|
+
size: "medium",
|
|
54
|
+
region: "us",
|
|
55
|
+
},
|
|
56
|
+
queued: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await new DockerDeploy(makeHttp()).ensureHost({
|
|
60
|
+
workspaceId: "ws_123",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(mockPost).toHaveBeenCalledWith("/docker-deploy/hosts/ensure", {
|
|
64
|
+
workspace_id: "ws_123",
|
|
65
|
+
});
|
|
66
|
+
expect(result.queued).toBe(true);
|
|
67
|
+
expect(result.host.status).toBe("pending");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("lists Docker Deploy templates", async () => {
|
|
71
|
+
mockGet.mockResolvedValue({
|
|
72
|
+
data: [
|
|
73
|
+
{
|
|
74
|
+
id: "nextjs-app",
|
|
75
|
+
name: "Next.js app",
|
|
76
|
+
description: "App router starter",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const templates = await new DockerDeploy(makeHttp()).listTemplates();
|
|
82
|
+
|
|
83
|
+
expect(mockGet).toHaveBeenCalledWith("/docker-deploy/templates");
|
|
84
|
+
expect(templates[0]?.id).toBe("nextjs-app");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("gets a Docker Deploy template", async () => {
|
|
88
|
+
mockGet.mockResolvedValue({
|
|
89
|
+
template: {
|
|
90
|
+
id: "compose-full-stack",
|
|
91
|
+
name: "Compose full stack",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const template = await new DockerDeploy(makeHttp()).getTemplate("compose-full-stack");
|
|
96
|
+
|
|
97
|
+
expect(mockGet).toHaveBeenCalledWith(
|
|
98
|
+
"/docker-deploy/templates/compose-full-stack",
|
|
99
|
+
);
|
|
100
|
+
expect(template.id).toBe("compose-full-stack");
|
|
101
|
+
});
|
|
102
|
+
});
|