@miosa/sdk 1.2.2 → 1.2.4
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 +42 -0
- package/dist/index.d.ts +166 -26
- package/dist/index.js +384 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +5 -0
- package/src/index.ts +15 -0
- package/src/resources/deployments.test.ts +139 -0
- package/src/resources/deployments.ts +254 -0
- package/src/resources/docker-deploy.test.ts +102 -0
- package/src/resources/docker-deploy.ts +183 -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/tenant-events.ts +32 -0
- package/src/resources/tenant.ts +101 -19
- package/src/resources/webhooks.ts +39 -54
- package/src/resources/workspaces.ts +285 -0
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
|
@@ -136,6 +136,10 @@ export type {
|
|
|
136
136
|
DeploymentVersionKind,
|
|
137
137
|
DeploymentVersionState,
|
|
138
138
|
DockerDeployCreateParams,
|
|
139
|
+
DockerDeployDoctorCheck,
|
|
140
|
+
DockerDeployDoctorParams,
|
|
141
|
+
DockerDeployDoctorProbe,
|
|
142
|
+
DockerDeployDoctorResult,
|
|
139
143
|
ExternalAttribution,
|
|
140
144
|
PublishFromSandboxParams,
|
|
141
145
|
PublishParams,
|
|
@@ -147,6 +151,17 @@ export type {
|
|
|
147
151
|
RuntimeLogsResult,
|
|
148
152
|
VersionListParams,
|
|
149
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";
|
|
150
165
|
export type {
|
|
151
166
|
SnapshotData,
|
|
152
167
|
SnapshotStatus,
|
|
@@ -3,15 +3,18 @@ import type { HttpClient } from "../http.js";
|
|
|
3
3
|
import { Deployments } from "./deployments.js";
|
|
4
4
|
|
|
5
5
|
const mockRequest = vi.fn();
|
|
6
|
+
const mockGet = vi.fn();
|
|
6
7
|
|
|
7
8
|
function makeHttp(): HttpClient {
|
|
8
9
|
const http = {} as HttpClient;
|
|
9
10
|
http.request = mockRequest;
|
|
11
|
+
http.get = mockGet;
|
|
10
12
|
return http;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
beforeEach(() => {
|
|
14
16
|
vi.clearAllMocks();
|
|
17
|
+
vi.unstubAllGlobals();
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
describe("Deployments", () => {
|
|
@@ -52,4 +55,140 @@ describe("Deployments", () => {
|
|
|
52
55
|
expect(deployment.deployment_product).toBe("docker_deploy");
|
|
53
56
|
expect(deployment.docker_deploy_host_id).toBe("ddh_123");
|
|
54
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
|
+
docker_deploy: {
|
|
76
|
+
app_id: "miosa-clinic-intake",
|
|
77
|
+
container_id: "container_123",
|
|
78
|
+
status: "running",
|
|
79
|
+
url: "http://127.0.0.1:23906",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (path === "/docker-deploy/hosts/ddh_123") {
|
|
87
|
+
return {
|
|
88
|
+
data: {
|
|
89
|
+
id: "ddh_123",
|
|
90
|
+
tenant_id: "ten_123",
|
|
91
|
+
workspace_id: "ws_123",
|
|
92
|
+
status: "active",
|
|
93
|
+
size: "medium",
|
|
94
|
+
region: "us",
|
|
95
|
+
appliance_status: "healthy",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error(`unexpected path ${path}`);
|
|
101
|
+
});
|
|
102
|
+
vi.stubGlobal(
|
|
103
|
+
"fetch",
|
|
104
|
+
vi.fn().mockResolvedValue({ ok: true, status: 200 }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const result = await new Deployments(makeHttp()).doctorDockerDeploy(
|
|
108
|
+
"dep_123",
|
|
109
|
+
{ probePath: "/health" },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(result.ok).toBe(true);
|
|
113
|
+
expect(mockGet).toHaveBeenCalledWith("/deployments/dep_123");
|
|
114
|
+
expect(mockGet).toHaveBeenCalledWith("/docker-deploy/hosts/ddh_123");
|
|
115
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
116
|
+
"https://clinic-intake.osa.miosa.app/health",
|
|
117
|
+
expect.objectContaining({ method: "GET" }),
|
|
118
|
+
);
|
|
119
|
+
expect(result.checks.map((check) => check.name)).toEqual([
|
|
120
|
+
"deployment_product",
|
|
121
|
+
"docker_deploy_host_id",
|
|
122
|
+
"docker_deploy_host_health",
|
|
123
|
+
"docker_deploy_app",
|
|
124
|
+
"runtime_route",
|
|
125
|
+
"public_url_probe",
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("doctorDockerDeploy fails when route metadata does not point at the Docker container port", async () => {
|
|
130
|
+
mockGet.mockImplementation(async (path: string) => {
|
|
131
|
+
if (path === "/deployments/dep_123") {
|
|
132
|
+
return {
|
|
133
|
+
data: {
|
|
134
|
+
id: "dep_123",
|
|
135
|
+
tenant_id: "ten_123",
|
|
136
|
+
name: "Clinic Intake",
|
|
137
|
+
slug: "clinic-intake",
|
|
138
|
+
state: "running",
|
|
139
|
+
deployment_product: "docker_deploy",
|
|
140
|
+
docker_deploy_host_id: "ddh_123",
|
|
141
|
+
public_url: "https://clinic-intake.osa.miosa.app",
|
|
142
|
+
metadata: {
|
|
143
|
+
deployment_product: "docker_deploy",
|
|
144
|
+
runtime: { ip: "172.16.74.246", port: 8080 },
|
|
145
|
+
docker_deploy: {
|
|
146
|
+
app_id: "miosa-clinic-intake",
|
|
147
|
+
container_id: "container_123",
|
|
148
|
+
status: "running",
|
|
149
|
+
url: "http://127.0.0.1:23906",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (path === "/docker-deploy/hosts/ddh_123") {
|
|
157
|
+
return {
|
|
158
|
+
data: {
|
|
159
|
+
id: "ddh_123",
|
|
160
|
+
tenant_id: "ten_123",
|
|
161
|
+
workspace_id: "ws_123",
|
|
162
|
+
status: "active",
|
|
163
|
+
size: "medium",
|
|
164
|
+
region: "us",
|
|
165
|
+
appliance_status: "healthy",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(`unexpected path ${path}`);
|
|
171
|
+
});
|
|
172
|
+
vi.stubGlobal(
|
|
173
|
+
"fetch",
|
|
174
|
+
vi.fn().mockResolvedValue({ ok: true, status: 200 }),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = await new Deployments(makeHttp()).doctorDockerDeploy(
|
|
178
|
+
"dep_123",
|
|
179
|
+
{ probePath: "/health" },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result.ok).toBe(false);
|
|
183
|
+
expect(result.checks).toEqual(
|
|
184
|
+
expect.arrayContaining([
|
|
185
|
+
expect.objectContaining({
|
|
186
|
+
name: "runtime_route",
|
|
187
|
+
ok: false,
|
|
188
|
+
message:
|
|
189
|
+
"Deployment route port 8080 does not match Docker container host port 23906.",
|
|
190
|
+
}),
|
|
191
|
+
]),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
55
194
|
});
|
|
@@ -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
|
|
|
@@ -120,6 +121,35 @@ export interface DeploymentData {
|
|
|
120
121
|
updated_at?: string;
|
|
121
122
|
}
|
|
122
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
|
+
|
|
123
153
|
export type DeploymentDatabaseRequest =
|
|
124
154
|
| boolean
|
|
125
155
|
| {
|
|
@@ -436,6 +466,62 @@ function dockerDeployMetadata(
|
|
|
436
466
|
};
|
|
437
467
|
}
|
|
438
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 dockerDeployApp(deployment: DeploymentData): Record<string, unknown> | null {
|
|
485
|
+
const app = deployment.metadata?.["docker_deploy"];
|
|
486
|
+
if (!app || typeof app !== "object" || Array.isArray(app)) return null;
|
|
487
|
+
return app as Record<string, unknown>;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function dockerDeployAppPort(app: Record<string, unknown> | null): number | null {
|
|
491
|
+
const url = app?.["url"];
|
|
492
|
+
if (typeof url !== "string") return null;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const parsed = new URL(url);
|
|
496
|
+
const port = Number.parseInt(parsed.port, 10);
|
|
497
|
+
return Number.isInteger(port) ? port : null;
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function addDoctorCheck(
|
|
504
|
+
checks: DockerDeployDoctorCheck[],
|
|
505
|
+
name: string,
|
|
506
|
+
ok: boolean,
|
|
507
|
+
message: string,
|
|
508
|
+
details?: Record<string, unknown>,
|
|
509
|
+
): void {
|
|
510
|
+
checks.push({ name, ok, message, ...(details ? { details } : {}) });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function hostHealthy(host: DockerDeployHostData | undefined): boolean {
|
|
514
|
+
return Boolean(
|
|
515
|
+
host && host.status === "active" && host.appliance_status === "healthy",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function probeUrl(publicUrl: string, probePath: string): string {
|
|
520
|
+
const url = new URL(publicUrl);
|
|
521
|
+
url.pathname = probePath.startsWith("/") ? probePath : `/${probePath}`;
|
|
522
|
+
return url.toString();
|
|
523
|
+
}
|
|
524
|
+
|
|
439
525
|
// ── Sub-resources ──────────────────────────────────────────────────────────
|
|
440
526
|
|
|
441
527
|
export class DeploymentVersions {
|
|
@@ -656,6 +742,174 @@ export class Deployments {
|
|
|
656
742
|
});
|
|
657
743
|
}
|
|
658
744
|
|
|
745
|
+
/**
|
|
746
|
+
* Verify a Docker Deploy deployment before telling a user or agent it is
|
|
747
|
+
* live. Checks product markers, appliance host health, route metadata, and
|
|
748
|
+
* optionally probes the public URL.
|
|
749
|
+
*/
|
|
750
|
+
async doctorDockerDeploy(
|
|
751
|
+
deploymentId: string,
|
|
752
|
+
params: DockerDeployDoctorParams = {},
|
|
753
|
+
): Promise<DockerDeployDoctorResult> {
|
|
754
|
+
const checks: DockerDeployDoctorCheck[] = [];
|
|
755
|
+
const deployment = await this.get(deploymentId);
|
|
756
|
+
const metadata = deployment.metadata ?? {};
|
|
757
|
+
const product = dockerDeployProduct(deployment);
|
|
758
|
+
const hostId = dockerDeployHostId(deployment);
|
|
759
|
+
|
|
760
|
+
addDoctorCheck(
|
|
761
|
+
checks,
|
|
762
|
+
"deployment_product",
|
|
763
|
+
product === "docker_deploy",
|
|
764
|
+
product === "docker_deploy"
|
|
765
|
+
? "Deployment is marked for Docker Deploy."
|
|
766
|
+
: `Expected deployment_product=docker_deploy, got ${String(product ?? "missing")}.`,
|
|
767
|
+
{ deployment_product: product ?? null },
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
addDoctorCheck(
|
|
771
|
+
checks,
|
|
772
|
+
"docker_deploy_host_id",
|
|
773
|
+
Boolean(hostId),
|
|
774
|
+
hostId
|
|
775
|
+
? "Deployment has a Docker Deploy host id."
|
|
776
|
+
: "Deployment has no docker_deploy_host_id.",
|
|
777
|
+
{ docker_deploy_host_id: hostId },
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
let host: DockerDeployHostData | undefined;
|
|
781
|
+
if (hostId) {
|
|
782
|
+
try {
|
|
783
|
+
const rawHost = await this.http.get<unknown>(
|
|
784
|
+
`/docker-deploy/hosts/${hostId}`,
|
|
785
|
+
);
|
|
786
|
+
host = unwrap(rawHost) as DockerDeployHostData;
|
|
787
|
+
addDoctorCheck(
|
|
788
|
+
checks,
|
|
789
|
+
"docker_deploy_host_health",
|
|
790
|
+
hostHealthy(host),
|
|
791
|
+
hostHealthy(host)
|
|
792
|
+
? "Docker Deploy host is active and healthy."
|
|
793
|
+
: `Docker Deploy host status=${host.status} appliance=${host.appliance_status}.`,
|
|
794
|
+
{
|
|
795
|
+
status: host.status,
|
|
796
|
+
appliance_status: host.appliance_status,
|
|
797
|
+
},
|
|
798
|
+
);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
addDoctorCheck(
|
|
801
|
+
checks,
|
|
802
|
+
"docker_deploy_host_health",
|
|
803
|
+
false,
|
|
804
|
+
error instanceof Error ? error.message : String(error),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const app = dockerDeployApp(deployment);
|
|
810
|
+
const appPort = dockerDeployAppPort(app);
|
|
811
|
+
const appRunning = app?.["status"] === "running" && appPort !== null;
|
|
812
|
+
addDoctorCheck(
|
|
813
|
+
checks,
|
|
814
|
+
"docker_deploy_app",
|
|
815
|
+
appRunning,
|
|
816
|
+
appRunning
|
|
817
|
+
? "Docker Deploy app metadata points at a running container."
|
|
818
|
+
: "Deployment is missing running Docker Deploy app metadata.",
|
|
819
|
+
app
|
|
820
|
+
? {
|
|
821
|
+
app_id: app["app_id"],
|
|
822
|
+
container_id: app["container_id"],
|
|
823
|
+
status: app["status"],
|
|
824
|
+
url: app["url"],
|
|
825
|
+
expected_port: appPort,
|
|
826
|
+
}
|
|
827
|
+
: undefined,
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const runtime = metadata["runtime"];
|
|
831
|
+
const hasRuntimeRoute =
|
|
832
|
+
typeof runtime === "object" &&
|
|
833
|
+
runtime !== null &&
|
|
834
|
+
typeof (runtime as Record<string, unknown>)["ip"] === "string" &&
|
|
835
|
+
typeof (runtime as Record<string, unknown>)["port"] === "number";
|
|
836
|
+
const runtimeRecord =
|
|
837
|
+
hasRuntimeRoute && typeof runtime === "object"
|
|
838
|
+
? (runtime as Record<string, unknown>)
|
|
839
|
+
: undefined;
|
|
840
|
+
const routeMatchesContainerPort =
|
|
841
|
+
hasRuntimeRoute &&
|
|
842
|
+
(appPort === null || runtimeRecord?.["port"] === appPort);
|
|
843
|
+
addDoctorCheck(
|
|
844
|
+
checks,
|
|
845
|
+
"runtime_route",
|
|
846
|
+
hasRuntimeRoute && routeMatchesContainerPort,
|
|
847
|
+
hasRuntimeRoute && routeMatchesContainerPort
|
|
848
|
+
? "Deployment route points at the Docker container host port."
|
|
849
|
+
: hasRuntimeRoute && appPort !== null
|
|
850
|
+
? `Deployment route port ${String(runtimeRecord?.["port"])} does not match Docker container host port ${appPort}.`
|
|
851
|
+
: "Deployment is missing appliance runtime route metadata.",
|
|
852
|
+
runtimeRecord
|
|
853
|
+
? {
|
|
854
|
+
...runtimeRecord,
|
|
855
|
+
expected_port: appPort,
|
|
856
|
+
docker_deploy_url: app?.["url"],
|
|
857
|
+
}
|
|
858
|
+
: undefined,
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
let probe: DockerDeployDoctorProbe | undefined;
|
|
862
|
+
const publicUrl = deployment.public_url;
|
|
863
|
+
const path = params.probePath ?? params.probe_path ?? "/";
|
|
864
|
+
if (publicUrl && typeof fetch === "function") {
|
|
865
|
+
const url = probeUrl(publicUrl, path);
|
|
866
|
+
const controller = new AbortController();
|
|
867
|
+
const timeout = setTimeout(
|
|
868
|
+
() => controller.abort(),
|
|
869
|
+
params.timeoutMs ?? params.timeout_ms ?? 10_000,
|
|
870
|
+
);
|
|
871
|
+
try {
|
|
872
|
+
const response = await fetch(url, {
|
|
873
|
+
method: "GET",
|
|
874
|
+
signal: controller.signal,
|
|
875
|
+
});
|
|
876
|
+
probe = { url, ok: response.ok, status: response.status };
|
|
877
|
+
addDoctorCheck(
|
|
878
|
+
checks,
|
|
879
|
+
"public_url_probe",
|
|
880
|
+
response.ok,
|
|
881
|
+
response.ok
|
|
882
|
+
? `Public URL returned HTTP ${response.status}.`
|
|
883
|
+
: `Public URL returned HTTP ${response.status}.`,
|
|
884
|
+
{ url, status: response.status },
|
|
885
|
+
);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
probe = {
|
|
888
|
+
url,
|
|
889
|
+
ok: false,
|
|
890
|
+
error: error instanceof Error ? error.message : String(error),
|
|
891
|
+
};
|
|
892
|
+
addDoctorCheck(
|
|
893
|
+
checks,
|
|
894
|
+
"public_url_probe",
|
|
895
|
+
false,
|
|
896
|
+
probe.error ?? "Public URL probe failed.",
|
|
897
|
+
{ url },
|
|
898
|
+
);
|
|
899
|
+
} finally {
|
|
900
|
+
clearTimeout(timeout);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
ok: checks.every((check) => check.ok),
|
|
906
|
+
deployment,
|
|
907
|
+
...(host ? { host } : {}),
|
|
908
|
+
checks,
|
|
909
|
+
...(probe ? { probe } : {}),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
659
913
|
async update(
|
|
660
914
|
deploymentId: string,
|
|
661
915
|
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
|
+
});
|