@miosa/sdk 1.2.2 → 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 +42 -0
- package/dist/index.d.ts +166 -26
- package/dist/index.js +346 -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 +66 -0
- package/src/resources/deployments.ts +201 -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,67 @@ 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
|
+
},
|
|
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
|
+
});
|
|
55
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
|
|
|
@@ -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,43 @@ 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 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
|
+
|
|
439
506
|
// ── Sub-resources ──────────────────────────────────────────────────────────
|
|
440
507
|
|
|
441
508
|
export class DeploymentVersions {
|
|
@@ -656,6 +723,140 @@ export class Deployments {
|
|
|
656
723
|
});
|
|
657
724
|
}
|
|
658
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
|
+
|
|
659
860
|
async update(
|
|
660
861
|
deploymentId: string,
|
|
661
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
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { HttpClient } from "../http.js";
|
|
2
|
+
|
|
3
|
+
export type DockerDeployHostId = string & {
|
|
4
|
+
readonly __brand: "DockerDeployHostId";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type DockerDeployHostStatus =
|
|
8
|
+
| "pending"
|
|
9
|
+
| "provisioning"
|
|
10
|
+
| "bootstrapping"
|
|
11
|
+
| "active"
|
|
12
|
+
| "degraded"
|
|
13
|
+
| "suspended"
|
|
14
|
+
| "retired"
|
|
15
|
+
| "error";
|
|
16
|
+
|
|
17
|
+
export type DockerDeployApplianceStatus =
|
|
18
|
+
| "not_installed"
|
|
19
|
+
| "installing"
|
|
20
|
+
| "starting"
|
|
21
|
+
| "healthy"
|
|
22
|
+
| "unhealthy"
|
|
23
|
+
| "unknown";
|
|
24
|
+
|
|
25
|
+
export interface DockerDeployHostData {
|
|
26
|
+
id: DockerDeployHostId;
|
|
27
|
+
tenant_id: string;
|
|
28
|
+
workspace_id: string;
|
|
29
|
+
external_workspace_id?: string | null;
|
|
30
|
+
computer_id?: string | null;
|
|
31
|
+
fleet_node_id?: string | null;
|
|
32
|
+
status: DockerDeployHostStatus;
|
|
33
|
+
size: string;
|
|
34
|
+
region: string;
|
|
35
|
+
portal_domain?: string | null;
|
|
36
|
+
runtime_base_url?: string | null;
|
|
37
|
+
agent_base_url?: string | null;
|
|
38
|
+
appliance_image?: string | null;
|
|
39
|
+
appliance_version?: string | null;
|
|
40
|
+
appliance_status: DockerDeployApplianceStatus;
|
|
41
|
+
agent_last_seen_at?: string | null;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
created_at?: string;
|
|
44
|
+
updated_at?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DockerDeployHostListParams {
|
|
48
|
+
workspace_id?: string;
|
|
49
|
+
workspaceId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DockerDeployHostEnsureParams {
|
|
53
|
+
workspace_id?: string;
|
|
54
|
+
workspaceId?: string;
|
|
55
|
+
external_workspace_id?: string;
|
|
56
|
+
externalWorkspaceId?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DockerDeployHostListResponse {
|
|
60
|
+
data?: DockerDeployHostData[];
|
|
61
|
+
hosts?: DockerDeployHostData[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DockerDeployHostResponse {
|
|
65
|
+
data?: DockerDeployHostData;
|
|
66
|
+
host?: DockerDeployHostData;
|
|
67
|
+
queued?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DockerDeployTemplate {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
category?: string;
|
|
75
|
+
runtime?: string;
|
|
76
|
+
tags?: string[];
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DockerDeployTemplateListResponse {
|
|
82
|
+
data?: DockerDeployTemplate[];
|
|
83
|
+
templates?: DockerDeployTemplate[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DockerDeployTemplateResponse {
|
|
87
|
+
data?: DockerDeployTemplate;
|
|
88
|
+
template?: DockerDeployTemplate;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function workspaceId(params?: DockerDeployHostListParams): string | undefined {
|
|
92
|
+
return params?.workspace_id ?? params?.workspaceId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureBody(params: DockerDeployHostEnsureParams): Record<string, string> {
|
|
96
|
+
const body: Record<string, string> = {};
|
|
97
|
+
const id = params.workspace_id ?? params.workspaceId;
|
|
98
|
+
const externalId = params.external_workspace_id ?? params.externalWorkspaceId;
|
|
99
|
+
if (id) body.workspace_id = id;
|
|
100
|
+
if (externalId) body.external_workspace_id = externalId;
|
|
101
|
+
return body;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function unwrapHost(response: DockerDeployHostResponse): DockerDeployHostData {
|
|
105
|
+
const host = response.data ?? response.host;
|
|
106
|
+
if (!host) {
|
|
107
|
+
throw new Error("Docker Deploy host response was empty.");
|
|
108
|
+
}
|
|
109
|
+
return host;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unwrapTemplates(response: DockerDeployTemplateListResponse): DockerDeployTemplate[] {
|
|
113
|
+
return response.data ?? response.templates ?? [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function unwrapTemplate(response: DockerDeployTemplateResponse): DockerDeployTemplate {
|
|
117
|
+
const template = response.data ?? response.template;
|
|
118
|
+
if (!template) {
|
|
119
|
+
throw new Error("Docker Deploy template response was empty.");
|
|
120
|
+
}
|
|
121
|
+
return template;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class DockerDeploy {
|
|
125
|
+
constructor(private readonly http: HttpClient) {}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* List Docker Deploy appliance hosts scoped to the current tenant.
|
|
129
|
+
*
|
|
130
|
+
* Pass a workspace ID to inspect the dedicated always-on appliance machine
|
|
131
|
+
* for one white-label workspace.
|
|
132
|
+
*/
|
|
133
|
+
async listHosts(
|
|
134
|
+
params: DockerDeployHostListParams = {},
|
|
135
|
+
): Promise<DockerDeployHostData[]> {
|
|
136
|
+
const res = await this.http.get<DockerDeployHostListResponse>(
|
|
137
|
+
"/docker-deploy/hosts",
|
|
138
|
+
{ workspace_id: workspaceId(params) },
|
|
139
|
+
);
|
|
140
|
+
return res.data ?? res.hosts ?? [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Ensure a workspace has its dedicated Docker Deploy appliance host.
|
|
145
|
+
*
|
|
146
|
+
* The host may still be `pending`, `provisioning`, or `bootstrapping` after
|
|
147
|
+
* this call. Treat `status === "active"` and `appliance_status === "healthy"`
|
|
148
|
+
* as the ready condition before sending app/container traffic to it.
|
|
149
|
+
*/
|
|
150
|
+
async ensureHost(
|
|
151
|
+
params: DockerDeployHostEnsureParams = {},
|
|
152
|
+
): Promise<{ host: DockerDeployHostData; queued: boolean }> {
|
|
153
|
+
const res = await this.http.post<DockerDeployHostResponse>(
|
|
154
|
+
"/docker-deploy/hosts/ensure",
|
|
155
|
+
ensureBody(params),
|
|
156
|
+
);
|
|
157
|
+
return { host: unwrapHost(res), queued: res.queued ?? false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Fetch one Docker Deploy host by ID. */
|
|
161
|
+
async getHost(hostId: string): Promise<DockerDeployHostData> {
|
|
162
|
+
const res = await this.http.get<DockerDeployHostResponse>(
|
|
163
|
+
`/docker-deploy/hosts/${hostId}`,
|
|
164
|
+
);
|
|
165
|
+
return unwrapHost(res);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** List Docker Deploy starter templates. */
|
|
169
|
+
async listTemplates(): Promise<DockerDeployTemplate[]> {
|
|
170
|
+
const res = await this.http.get<DockerDeployTemplateListResponse>(
|
|
171
|
+
"/docker-deploy/templates",
|
|
172
|
+
);
|
|
173
|
+
return unwrapTemplates(res);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Fetch one Docker Deploy starter template by ID. */
|
|
177
|
+
async getTemplate(templateId: string): Promise<DockerDeployTemplate> {
|
|
178
|
+
const res = await this.http.get<DockerDeployTemplateResponse>(
|
|
179
|
+
`/docker-deploy/templates/${encodeURIComponent(templateId)}`,
|
|
180
|
+
);
|
|
181
|
+
return unwrapTemplate(res);
|
|
182
|
+
}
|
|
183
|
+
}
|