@toshicloud/sdk 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -30
- package/dist/client.d.ts +30 -4
- package/dist/client.js +19 -6
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +136 -0
- package/dist/execute.js +15 -0
- package/dist/gen/toshi/permission_pb.d.ts +39 -0
- package/dist/gen/toshi/permission_pb.js +42 -0
- package/dist/gen/toshi/workspace/v1/artifacts_pb.d.ts +1 -1
- package/dist/gen/toshi/workspace/v1/artifacts_pb.js +3 -2
- package/dist/gen/toshi/workspace/v1/audit_events_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/channels_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/discovery_pb.d.ts +676 -0
- package/dist/gen/toshi/workspace/v1/discovery_pb.js +182 -0
- package/dist/gen/toshi/workspace/v1/display_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/fillers_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/hostjobs_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/hosts_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/products_pb.d.ts +703 -0
- package/dist/gen/toshi/workspace/v1/products_pb.js +106 -0
- package/dist/gen/toshi/workspace/v1/projects_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/recordings_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/secrets_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/storage_providers_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/tokens_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/volumes_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/webhook_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/workloads_pb.js +2 -1
- package/dist/gen/toshi/workspace/v1/workspace_pb.d.ts +2 -2
- package/dist/gen/toshi/workspace/v1/workspace_pb.js +1 -1
- package/dist/gen/toshi/workspace/v1/workspace_service_pb.js +2 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/interceptors.d.ts +17 -2
- package/dist/interceptors.js +69 -4
- package/dist/node/client.d.ts +35 -0
- package/dist/node/client.js +178 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.js +4 -0
- package/dist/output.js +6 -1
- package/dist/paginate.d.ts +36 -0
- package/dist/paginate.js +37 -0
- package/dist/retry.js +4 -0
- package/dist/run.js +3 -2
- package/dist/stream.d.ts +8 -1
- package/dist/stream.js +24 -2
- package/dist/test/fake.d.ts +88 -0
- package/dist/test/fake.js +845 -0
- package/dist/test/index.d.ts +58 -0
- package/dist/test/index.js +57 -0
- package/dist/test/procedures.d.ts +18 -0
- package/dist/test/procedures.js +219 -0
- package/dist/timeout.d.ts +29 -0
- package/dist/timeout.js +67 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/wait.js +15 -12
- package/dist/watch.js +15 -2
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -4,40 +4,26 @@ A TypeScript client for the Toshi Workspace API, `toshi.workspace.v1`,
|
|
|
4
4
|
published as ES modules with type declarations.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npm install @toshicloud/sdk @connectrpc/connect
|
|
7
|
+
npm install @toshicloud/sdk @connectrpc/connect
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
The browser entry needs a transport such as `@connectrpc/connect-web`; the
|
|
11
|
+
Node entry needs `@connectrpc/connect-node`. Both are peer dependencies, so
|
|
12
|
+
install the ones your application uses.
|
|
13
|
+
|
|
10
14
|
Versions up to 0.1.2 were published as `@toshicloud/toshi-ts`.
|
|
11
15
|
|
|
12
16
|
## Using the client
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function, and that function builds a transport with them. Every call then
|
|
17
|
-
gets the same token, scope and retries: the helpers below and the
|
|
18
|
-
generated clients alike.
|
|
19
|
-
|
|
20
|
-
In Node, with `@connectrpc/connect-node`:
|
|
18
|
+
In Node, `@toshicloud/sdk/node` builds the transport and fills the server
|
|
19
|
+
and the token from the environment:
|
|
21
20
|
|
|
22
21
|
<!-- examples/quick-start.ts -->
|
|
23
22
|
|
|
24
23
|
```ts
|
|
25
|
-
import {
|
|
26
|
-
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
27
|
-
import { createToshiClient } from "@toshicloud/sdk";
|
|
24
|
+
import { createToshiClient } from "@toshicloud/sdk/node";
|
|
28
25
|
|
|
29
|
-
const toshi = createToshiClient({
|
|
30
|
-
transport: (interceptors) =>
|
|
31
|
-
createConnectTransport({
|
|
32
|
-
baseUrl: "https://toshi.example.com",
|
|
33
|
-
httpVersion: "2",
|
|
34
|
-
interceptors,
|
|
35
|
-
// Only for a server whose certificate a private CA signed.
|
|
36
|
-
nodeOptions: { ca: readFileSync("ca.pem") },
|
|
37
|
-
}),
|
|
38
|
-
token: process.env.TOSHI_TOKEN,
|
|
39
|
-
project: "default",
|
|
40
|
-
});
|
|
26
|
+
const toshi = createToshiClient({ project: "default" });
|
|
41
27
|
|
|
42
28
|
const { projects } = await toshi.project.listProjects({});
|
|
43
29
|
console.log(projects.length);
|
|
@@ -62,14 +48,21 @@ export const toshi = createToshiClient({
|
|
|
62
48
|
});
|
|
63
49
|
```
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
`interceptors
|
|
51
|
+
Interceptors of your own go before the client's. The Node entry takes
|
|
52
|
+
`interceptors`; with a transport of your own, put them first:
|
|
53
|
+
`interceptors: [...mine, ...interceptors]`. Each unary call enters them
|
|
54
|
+
once, with its retries inside, and they never see the Authorization
|
|
55
|
+
header: the client sets it on a copy of the request below them.
|
|
67
56
|
|
|
68
|
-
The
|
|
69
|
-
|
|
57
|
+
The browser entry reads no environment variable and no file. The Node
|
|
58
|
+
entry reads `TOSHI_SERVER`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE` and
|
|
59
|
+
`TOSHI_CA_FILE`; see Node options.
|
|
70
60
|
|
|
71
61
|
### Options
|
|
72
62
|
|
|
63
|
+
- `tokenSource`: a function that gives each request attempt its token, so a
|
|
64
|
+
refreshed token is used without a new client; it wins over `token`. See
|
|
65
|
+
Tokens that change.
|
|
73
66
|
- `token`: sent as `Authorization: Bearer <token>` on every call.
|
|
74
67
|
- `project`, `environment`: fill the request's `project` and
|
|
75
68
|
`environment` fields when they are empty. A value in the request wins.
|
|
@@ -77,14 +70,73 @@ server and any CA come from the caller.
|
|
|
77
70
|
- `retry`: `{ maxAttempts, initialBackoffMs, maxBackoffMs }`, by default
|
|
78
71
|
5, 200 ms and 10 s, with full jitter. `retry: false` turns off retries
|
|
79
72
|
and watch reconnects.
|
|
73
|
+
- `defaultTimeoutMs`: bounds each unary call except the Wait calls,
|
|
74
|
+
which their request's timeout bounds; streams ignore it.
|
|
75
|
+
|
|
76
|
+
A call may set `timeoutMs` instead: it bounds the whole call, retries
|
|
77
|
+
included, and on a watch it bounds the open. It composes with `signal`.
|
|
78
|
+
|
|
79
|
+
### Node options
|
|
80
|
+
|
|
81
|
+
`@toshicloud/sdk/node` builds a connect-node transport over HTTP/2 and
|
|
82
|
+
fills what is missing from the environment:
|
|
83
|
+
|
|
84
|
+
- `server`, else `TOSHI_SERVER`: the control plane's URL. Without either,
|
|
85
|
+
the client refuses to start.
|
|
86
|
+
- `tokenSource`, `token`, `tokenFile`, `TOSHI_TOKEN`, `TOSHI_TOKEN_FILE`, in
|
|
87
|
+
that order. A token file is trimmed, and an empty file is an error.
|
|
88
|
+
- `caFile`, else `TOSHI_CA_FILE`: a PEM bundle trusted besides Node's
|
|
89
|
+
built-in roots.
|
|
90
|
+
- `userAgent`: put in front of the User-Agent, for example "myapp/1.2".
|
|
91
|
+
- `interceptors`: run before the client's own, in the order given.
|
|
92
|
+
- `defaultTimeoutMs`: bounds each unary call except the Wait calls,
|
|
93
|
+
which their request's timeout bounds; streams ignore it.
|
|
94
|
+
- `transport`: a transport of your own; then `server` and `caFile` are not
|
|
95
|
+
required.
|
|
96
|
+
|
|
97
|
+
Every request carries `User-Agent: toshi-ts/<version> (node/<node
|
|
98
|
+
version>)`, with `userAgent` and one space in front when it is set. The
|
|
99
|
+
root entry sends no User-Agent: browsers own it.
|
|
100
|
+
|
|
101
|
+
### Tokens that change
|
|
102
|
+
|
|
103
|
+
`tokenSource` gives the client a function it calls for the token of every
|
|
104
|
+
request attempt, so a token that is refreshed, for example from a secret
|
|
105
|
+
manager, is used without a new client. It wins over every other token
|
|
106
|
+
option. The client asks it once per attempt, a retry included, and may
|
|
107
|
+
ask it concurrently, so it must be fast: keep the token somewhere and
|
|
108
|
+
refresh it outside the call. A source that throws, or that answers an
|
|
109
|
+
empty token, fails the call with `UNAUTHENTICATED`, with the error as the
|
|
110
|
+
cause, and the call is not retried.
|
|
111
|
+
|
|
112
|
+
<!-- examples/token-source.ts -->
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// A token a refresh loop replaces; the client asks for it on every call.
|
|
116
|
+
let token = "first-token";
|
|
117
|
+
|
|
118
|
+
export const toshi = createToshiClient({
|
|
119
|
+
project: "default",
|
|
120
|
+
tokenSource: () => token,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export function rotate(next: string): void {
|
|
124
|
+
token = next;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A token file, from `tokenFile` or `TOSHI_TOKEN_FILE`, is read again
|
|
129
|
+
before a request when its modification time or size has changed, so a
|
|
130
|
+
rotated token, such as a Kubernetes projected service account token, is
|
|
131
|
+
picked up without a new client.
|
|
80
132
|
|
|
81
133
|
### The client's shape
|
|
82
134
|
|
|
83
135
|
The client has one field per Workspace API service: `project`, `secret`,
|
|
84
136
|
`webhook`, `display`, `channel`, `recording`, `volume`, `artifact`,
|
|
85
137
|
`artifactUpload`, `workspace`, `hostJob`, `host`, `token`, `auditEvent`,
|
|
86
|
-
`filler` and `
|
|
87
|
-
client.
|
|
138
|
+
`filler`, `storageProvider`, `product` and `discovery`, each holding that
|
|
139
|
+
service's generated client.
|
|
88
140
|
`workload` holds `WorkloadService`'s calls, grouped by the type they
|
|
89
141
|
name: `workload.jobs`, `workload.sandboxes` and `workload.services`, one
|
|
90
142
|
method for each call (`create`, `get`, `list`, `update`, `delete`,
|
|
@@ -102,6 +154,43 @@ const { job } = await toshi.workload.jobs.get({ id: jobs[0].id });
|
|
|
102
154
|
console.log(job?.id);
|
|
103
155
|
```
|
|
104
156
|
|
|
157
|
+
### Lists
|
|
158
|
+
|
|
159
|
+
`workload.jobs.all`, `workload.sandboxes.all`, `workload.services.all`,
|
|
160
|
+
`workload.services.allRuns` and `workload.allEvents` take the request their
|
|
161
|
+
`list` method takes and yield every item, one page after another. Each
|
|
162
|
+
response's `nextPageToken` becomes the next request's `pageToken`, and a
|
|
163
|
+
`pageToken` in the request resumes from that page. A service with no
|
|
164
|
+
`all` method is one `paginate` call away: it takes the list call and a
|
|
165
|
+
function that picks a response's items.
|
|
166
|
+
|
|
167
|
+
<!-- examples/lists.ts -->
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { paginate } from "@toshicloud/sdk";
|
|
171
|
+
|
|
172
|
+
import { toshi } from "./client.js";
|
|
173
|
+
|
|
174
|
+
// Every job that matches the request, one page after another.
|
|
175
|
+
for await (const job of toshi.workload.jobs.all({ pageSize: 10 })) {
|
|
176
|
+
console.log(job.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// A service with no all() method: paginate takes its list call and items.
|
|
180
|
+
for await (const project of paginate(
|
|
181
|
+
{},
|
|
182
|
+
(req, opts) => toshi.project.listProjects(req, opts),
|
|
183
|
+
(resp) => resp.projects,
|
|
184
|
+
)) {
|
|
185
|
+
console.log(project.name);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`paginate` sends a copy of the request, so the caller's request keeps its
|
|
190
|
+
fields. Every page is a call of its own, so `opts` goes to each one, and
|
|
191
|
+
`timeoutMs` bounds a page and not the whole iteration. An error throws once
|
|
192
|
+
and ends the iteration; leaving the loop sends no further request.
|
|
193
|
+
|
|
105
194
|
### Retries
|
|
106
195
|
|
|
107
196
|
The client retries a read, and a write that carries a request key, when
|
|
@@ -216,13 +305,83 @@ await run.wait(); // rejects with JobExitError on a non-zero exit
|
|
|
216
305
|
Stopping the loop stops only the output stream: the job keeps running,
|
|
217
306
|
and `workload.jobs.cancel` ends it.
|
|
218
307
|
|
|
308
|
+
## Products that are off
|
|
309
|
+
|
|
310
|
+
A call for a product that is off for the workspace, or that turns on an
|
|
311
|
+
outside product without accepting the access it asks for, rejects with a
|
|
312
|
+
`ProductError`, `wait`, the watches and `execute` included.
|
|
313
|
+
`isProductError(err)` tells it apart. It is still a `ConnectError` with the
|
|
314
|
+
same `code`, and it has the product's ID (`product`), the `reason`
|
|
315
|
+
(`ProductReason.NotInstalled`, `Disabled`, `NotEntitled`, `TurnedOff` or
|
|
316
|
+
`AccessNotAccepted`), the setting that turns the product on (`feature`,
|
|
317
|
+
such as `runners.enabled`), how far a product the deployment disables has
|
|
318
|
+
wound down (`state`) and, for `AccessNotAccepted`, the access to accept
|
|
319
|
+
(`access`).
|
|
320
|
+
|
|
321
|
+
<!-- examples/product-error.ts -->
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
|
|
325
|
+
// Turning an outside product on installs it, and the API asks the caller to
|
|
326
|
+
// accept the access it wants first.
|
|
327
|
+
const enable = { id: "acme_ci", enabled: true, acceptedAccess: [] as string[] };
|
|
328
|
+
try {
|
|
329
|
+
try {
|
|
330
|
+
await toshi.product.updateProduct(enable);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (!isProductError(err) || err.reason !== ProductReason.AccessNotAccepted) {
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
console.log(err.product, err.reason, err.access);
|
|
336
|
+
// Accept exactly the access the product asks for.
|
|
337
|
+
await toshi.product.updateProduct({ ...enable, acceptedAccess: err.access });
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (!isProductError(err)) {
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
// Off for the workspace: say which product and why.
|
|
344
|
+
console.log(err.product, err.reason, err.feature);
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Testing with `@toshicloud/sdk/test`
|
|
349
|
+
|
|
350
|
+
`createFake()` serves the whole Workspace API in memory: no server, no port,
|
|
351
|
+
nothing to close, and no Node module, so it runs in the same runtimes as the
|
|
352
|
+
client. `fake.client()` returns a `ToshiClient` wired to it, with the token
|
|
353
|
+
`test-token` and the scope `default`/`production`, which is enough to drive
|
|
354
|
+
`run`, the watchers and the generated clients from a test.
|
|
355
|
+
|
|
356
|
+
<!-- examples/testing.ts -->
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { createFake } from "@toshicloud/sdk/test";
|
|
360
|
+
|
|
361
|
+
const fake = createFake();
|
|
362
|
+
fake.job("job-1").stdout("deployed\n").exitCode(0);
|
|
363
|
+
|
|
364
|
+
const toshi = fake.client();
|
|
365
|
+
const run = await toshi.run({ spec: { command: { argv: ["./deploy.sh"] } } });
|
|
366
|
+
for await (const chunk of run) {
|
|
367
|
+
process[chunk.stream].write(chunk.data);
|
|
368
|
+
}
|
|
369
|
+
await run.wait();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`fake.on(method, impl)` replaces the state machine for one method with a hook
|
|
373
|
+
typed by that method: one call for unary, server-stream and bidirectional
|
|
374
|
+
methods alike. `fake.calls()` returns every call the fake received, oldest
|
|
375
|
+
first, with the procedure, the request and the headers.
|
|
376
|
+
|
|
219
377
|
## Generated code
|
|
220
378
|
|
|
221
379
|
`src/gen/toshi/workspace/v1/` is generated by
|
|
222
380
|
[`protoc-gen-es`](https://github.com/bufbuild/protobuf-es) from the protos
|
|
223
381
|
in `proto/toshi/workspace/v1` of the
|
|
224
382
|
[toshi-api](https://github.com/toshicloud/toshi-api) repository, one
|
|
225
|
-
`*_pb.ts` file per proto
|
|
383
|
+
`*_pb.ts` file per proto, and `src/gen/toshi/permission_pb.ts` from
|
|
384
|
+
`proto/toshi/permission.proto`, which they import. The generated code is committed and imports
|
|
226
385
|
only `@bufbuild/protobuf`, so installing the package needs no generator.
|
|
227
386
|
|
|
228
387
|
`pin.env` names the toshi-api commit (or, once one exists, the release
|
package/dist/client.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { Client, Interceptor, Transport } from "@connectrpc/connect";
|
|
|
3
3
|
import { ArtifactService, ArtifactUploadService } from "./gen/toshi/workspace/v1/artifacts_pb.js";
|
|
4
4
|
import { AuditEventService } from "./gen/toshi/workspace/v1/audit_events_pb.js";
|
|
5
5
|
import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
|
|
6
|
+
import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
|
|
6
7
|
import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
|
|
7
8
|
import { FillerService } from "./gen/toshi/workspace/v1/fillers_pb.js";
|
|
8
9
|
import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
|
|
9
10
|
import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
|
|
11
|
+
import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
|
|
10
12
|
import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
|
|
11
13
|
import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
|
|
12
14
|
import { SecretService } from "./gen/toshi/workspace/v1/secrets_pb.js";
|
|
@@ -18,21 +20,32 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
|
|
|
18
20
|
import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
19
21
|
import type { CancelJobResponse, CreateJobResponse, CreateSandboxResponse, CreateServiceResponse, DeleteJobResponse, DeleteSandboxResponse, DeleteServiceResponse, GetJobResponse, GetSandboxResponse, GetServiceResponse, ListJobsResponse, ListSandboxesResponse, ListServicesResponse, UpdateJobResponse, UpdateSandboxResponse, UpdateServiceResponse, WaitJobResponse, WaitSandboxResponse, WaitServiceResponse, WatchJobsResponse, WatchSandboxesResponse, WatchServicesResponse } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
20
22
|
import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
21
|
-
import type { GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
23
|
+
import type { Event, GetOperationResponse, HoldServiceResponse, Job, ListEventsResponse, ListServiceRunsResponse, Operation, ReleaseServiceResponse, Sandbox, Service, ServiceRun, SetServiceModeResponse, StartServiceResponse, StopServiceResponse, WakeServiceResponse, WatchEventsResponse, WatchOutputResponse } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
22
24
|
import type { RetryPolicy } from "./retry.js";
|
|
23
25
|
/** How the client is configured; the caller builds the transport. */
|
|
24
26
|
export interface ToshiOptions {
|
|
25
27
|
/** Builds the caller's transport (connect-node or connect-web) with the client's interceptors. */
|
|
26
28
|
transport: (interceptors: Interceptor[]) => Transport;
|
|
27
29
|
token?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Gives each request attempt its token, so a refreshed token is used without
|
|
32
|
+
* a new client; it wins over `token` and a retry asks it again. A source that
|
|
33
|
+
* throws or answers an empty token ends the call with Code.Unauthenticated,
|
|
34
|
+
* with its error as the cause, and the call is not retried.
|
|
35
|
+
*/
|
|
36
|
+
tokenSource?: (signal: AbortSignal | undefined) => string | Promise<string>;
|
|
28
37
|
project?: string;
|
|
29
38
|
environment?: string;
|
|
30
39
|
/** false turns off retries and watch reconnects. */
|
|
31
40
|
retry?: Partial<RetryPolicy> | false;
|
|
41
|
+
/** Bounds each unary call except the Wait calls, which their request's timeout bounds; streams ignore it. */
|
|
42
|
+
defaultTimeoutMs?: number;
|
|
32
43
|
}
|
|
33
44
|
/** Per-call options. */
|
|
34
45
|
export interface CallOptions {
|
|
35
46
|
signal?: AbortSignal;
|
|
47
|
+
/** Bounds the whole call, retries included; on a watch, the open. */
|
|
48
|
+
timeoutMs?: number;
|
|
36
49
|
}
|
|
37
50
|
/** One client per Workspace API service, plus the helpers that live on top of them. */
|
|
38
51
|
export interface ToshiClient {
|
|
@@ -53,6 +66,8 @@ export interface ToshiClient {
|
|
|
53
66
|
readonly auditEvent: Client<typeof AuditEventService>;
|
|
54
67
|
readonly filler: Client<typeof FillerService>;
|
|
55
68
|
readonly storageProvider: Client<typeof StorageProviderService>;
|
|
69
|
+
readonly product: Client<typeof ProductService>;
|
|
70
|
+
readonly discovery: Client<typeof DiscoveryService>;
|
|
56
71
|
run(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<JobRun>;
|
|
57
72
|
}
|
|
58
73
|
/** WorkloadService's calls, grouped by the type they name. */
|
|
@@ -63,6 +78,8 @@ export interface WorkloadClient {
|
|
|
63
78
|
waitOperation(req: MessageInitShape<typeof WaitOperationRequestSchema>, opts?: CallOptions): Promise<Operation>;
|
|
64
79
|
/** Lists the events after a cursor, a page at a time; `watchEvents` is the live form. */
|
|
65
80
|
listEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): Promise<ListEventsResponse>;
|
|
81
|
+
/** Yields every event listEvents lists, following the page token from one page to the next. */
|
|
82
|
+
allEvents(req: MessageInitShape<typeof ListEventsRequestSchema>, opts?: CallOptions): AsyncIterable<Event>;
|
|
66
83
|
watchEvents(req: MessageInitShape<typeof WatchEventsRequestSchema>, opts?: CallOptions): Promise<EventWatch>;
|
|
67
84
|
/** Returns one operation: the id a long call hands back, asked for again. */
|
|
68
85
|
getOperation(req: MessageInitShape<typeof GetOperationRequestSchema>, opts?: CallOptions): Promise<GetOperationResponse>;
|
|
@@ -72,6 +89,8 @@ export interface JobsClient {
|
|
|
72
89
|
create(req: MessageInitShape<typeof CreateJobRequestSchema>, opts?: CallOptions): Promise<CreateJobResponse>;
|
|
73
90
|
get(req: MessageInitShape<typeof GetJobRequestSchema>, opts?: CallOptions): Promise<GetJobResponse>;
|
|
74
91
|
list(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): Promise<ListJobsResponse>;
|
|
92
|
+
/** Yields every job that matches every filter that is set, following the page token from one page to the next. */
|
|
93
|
+
all(req: MessageInitShape<typeof ListJobsRequestSchema>, opts?: CallOptions): AsyncIterable<Job>;
|
|
75
94
|
update(req: MessageInitShape<typeof UpdateJobRequestSchema>, opts?: CallOptions): Promise<UpdateJobResponse>;
|
|
76
95
|
delete(req: MessageInitShape<typeof DeleteJobRequestSchema>, opts?: CallOptions): Promise<DeleteJobResponse>;
|
|
77
96
|
cancel(req: MessageInitShape<typeof CancelJobRequestSchema>, opts?: CallOptions): Promise<CancelJobResponse>;
|
|
@@ -84,6 +103,8 @@ export interface SandboxesClient {
|
|
|
84
103
|
create(req: MessageInitShape<typeof CreateSandboxRequestSchema>, opts?: CallOptions): Promise<CreateSandboxResponse>;
|
|
85
104
|
get(req: MessageInitShape<typeof GetSandboxRequestSchema>, opts?: CallOptions): Promise<GetSandboxResponse>;
|
|
86
105
|
list(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): Promise<ListSandboxesResponse>;
|
|
106
|
+
/** Yields every sandbox that matches every filter that is set, following the page token from one page to the next. */
|
|
107
|
+
all(req: MessageInitShape<typeof ListSandboxesRequestSchema>, opts?: CallOptions): AsyncIterable<Sandbox>;
|
|
87
108
|
update(req: MessageInitShape<typeof UpdateSandboxRequestSchema>, opts?: CallOptions): Promise<UpdateSandboxResponse>;
|
|
88
109
|
delete(req: MessageInitShape<typeof DeleteSandboxRequestSchema>, opts?: CallOptions): Promise<DeleteSandboxResponse>;
|
|
89
110
|
wait(req: MessageInitShape<typeof WaitSandboxRequestSchema>, opts?: CallOptions): Promise<WaitSandboxResponse>;
|
|
@@ -95,6 +116,8 @@ export interface ServicesClient {
|
|
|
95
116
|
create(req: MessageInitShape<typeof CreateServiceRequestSchema>, opts?: CallOptions): Promise<CreateServiceResponse>;
|
|
96
117
|
get(req: MessageInitShape<typeof GetServiceRequestSchema>, opts?: CallOptions): Promise<GetServiceResponse>;
|
|
97
118
|
list(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): Promise<ListServicesResponse>;
|
|
119
|
+
/** Yields every service that matches every filter that is set, following the page token from one page to the next. */
|
|
120
|
+
all(req: MessageInitShape<typeof ListServicesRequestSchema>, opts?: CallOptions): AsyncIterable<Service>;
|
|
98
121
|
update(req: MessageInitShape<typeof UpdateServiceRequestSchema>, opts?: CallOptions): Promise<UpdateServiceResponse>;
|
|
99
122
|
delete(req: MessageInitShape<typeof DeleteServiceRequestSchema>, opts?: CallOptions): Promise<DeleteServiceResponse>;
|
|
100
123
|
wait(req: MessageInitShape<typeof WaitServiceRequestSchema>, opts?: CallOptions): Promise<WaitServiceResponse>;
|
|
@@ -106,6 +129,8 @@ export interface ServicesClient {
|
|
|
106
129
|
wake(req: MessageInitShape<typeof WakeServiceRequestSchema>, opts?: CallOptions): Promise<WakeServiceResponse>;
|
|
107
130
|
hold(req: MessageInitShape<typeof HoldServiceRequestSchema>, opts?: CallOptions): Promise<HoldServiceResponse>;
|
|
108
131
|
listRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): Promise<ListServiceRunsResponse>;
|
|
132
|
+
/** Yields every run listRuns returns, the newest first, following the page token from one page to the next. */
|
|
133
|
+
allRuns(req: MessageInitShape<typeof ListServiceRunsRequestSchema>, opts?: CallOptions): AsyncIterable<ServiceRun>;
|
|
109
134
|
}
|
|
110
135
|
/** A live stream of a job's changes. */
|
|
111
136
|
export interface JobWatch extends AsyncIterable<WatchJobsResponse> {
|
|
@@ -160,9 +185,10 @@ export interface JobRun extends AsyncIterable<OutputChunk> {
|
|
|
160
185
|
wait(): Promise<void>;
|
|
161
186
|
}
|
|
162
187
|
/**
|
|
163
|
-
* Creates the client: one normalized retry setting, the
|
|
164
|
-
* retry interceptors (
|
|
165
|
-
*
|
|
188
|
+
* Creates the client: one normalized retry setting, the product, scope, key,
|
|
189
|
+
* retry and auth interceptors (product outermost, so a product's refusal
|
|
190
|
+
* reaches the caller as a ProductError; auth innermost, so no interceptor sees
|
|
191
|
+
* the Authorization header), one transport for every client and helper.
|
|
166
192
|
*
|
|
167
193
|
* Since 0.2.0 the workload calls are grouped by the type they name under
|
|
168
194
|
* `workload`; every other service, WorkspaceService's included, is handed out
|
package/dist/client.js
CHANGED
|
@@ -4,10 +4,12 @@ import { executeCommand } from "./execute.js";
|
|
|
4
4
|
import { ArtifactService, ArtifactUploadService } from "./gen/toshi/workspace/v1/artifacts_pb.js";
|
|
5
5
|
import { AuditEventService } from "./gen/toshi/workspace/v1/audit_events_pb.js";
|
|
6
6
|
import { ChannelService } from "./gen/toshi/workspace/v1/channels_pb.js";
|
|
7
|
+
import { DiscoveryService } from "./gen/toshi/workspace/v1/discovery_pb.js";
|
|
7
8
|
import { DisplayService } from "./gen/toshi/workspace/v1/display_pb.js";
|
|
8
9
|
import { FillerService } from "./gen/toshi/workspace/v1/fillers_pb.js";
|
|
9
10
|
import { HostJobService } from "./gen/toshi/workspace/v1/hostjobs_pb.js";
|
|
10
11
|
import { HostService } from "./gen/toshi/workspace/v1/hosts_pb.js";
|
|
12
|
+
import { ProductService } from "./gen/toshi/workspace/v1/products_pb.js";
|
|
11
13
|
import { ProjectService } from "./gen/toshi/workspace/v1/projects_pb.js";
|
|
12
14
|
import { RecordingService } from "./gen/toshi/workspace/v1/recordings_pb.js";
|
|
13
15
|
import { SecretService } from "./gen/toshi/workspace/v1/secrets_pb.js";
|
|
@@ -19,15 +21,18 @@ import { WorkspaceService } from "./gen/toshi/workspace/v1/workspace_service_pb.
|
|
|
19
21
|
import { CancelJobRequestSchema, CreateJobRequestSchema, CreateSandboxRequestSchema, CreateServiceRequestSchema, DeleteJobRequestSchema, DeleteSandboxRequestSchema, DeleteServiceRequestSchema, GetJobRequestSchema, GetSandboxRequestSchema, GetServiceRequestSchema, ListJobsRequestSchema, ListSandboxesRequestSchema, ListServicesRequestSchema, UpdateJobRequestSchema, UpdateSandboxRequestSchema, UpdateServiceRequestSchema, WaitJobRequestSchema, WaitSandboxRequestSchema, WaitServiceRequestSchema, WatchJobsRequestSchema, WatchSandboxesRequestSchema, WatchServicesRequestSchema, WorkloadService, } from "./gen/toshi/workspace/v1/workloads_pb.js";
|
|
20
22
|
import { ExecuteStartSchema, GetOperationRequestSchema, HoldServiceRequestSchema, ListEventsRequestSchema, ListServiceRunsRequestSchema, ReleaseServiceRequestSchema, SetServiceModeRequestSchema, StartServiceRequestSchema, StopServiceRequestSchema, WaitOperationRequestSchema, WakeServiceRequestSchema, WatchEventsRequestSchema, WatchOutputRequestSchema, } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
21
23
|
import { openOutputWatch } from "./output.js";
|
|
22
|
-
import {
|
|
24
|
+
import { paginate } from "./paginate.js";
|
|
25
|
+
import { authInterceptor, keyInterceptor, productInterceptor, retryInterceptor, scopeInterceptor, } from "./interceptors.js";
|
|
23
26
|
import { normalizeRetry } from "./retry.js";
|
|
27
|
+
import { withDefaultUnaryTimeout } from "./timeout.js";
|
|
24
28
|
import { openEventWatch, openJobWatch, openSandboxWatch, openServiceWatch } from "./watch.js";
|
|
25
29
|
import { runJob } from "./run.js";
|
|
26
30
|
import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
|
|
27
31
|
/**
|
|
28
|
-
* Creates the client: one normalized retry setting, the
|
|
29
|
-
* retry interceptors (
|
|
30
|
-
*
|
|
32
|
+
* Creates the client: one normalized retry setting, the product, scope, key,
|
|
33
|
+
* retry and auth interceptors (product outermost, so a product's refusal
|
|
34
|
+
* reaches the caller as a ProductError; auth innermost, so no interceptor sees
|
|
35
|
+
* the Authorization header), one transport for every client and helper.
|
|
31
36
|
*
|
|
32
37
|
* Since 0.2.0 the workload calls are grouped by the type they name under
|
|
33
38
|
* `workload`; every other service, WorkspaceService's included, is handed out
|
|
@@ -36,12 +41,13 @@ import { waitJob, waitOperation, waitSandbox, waitService } from "./wait.js";
|
|
|
36
41
|
export function createToshiClient(options) {
|
|
37
42
|
const retry = normalizeRetry(options.retry);
|
|
38
43
|
const interceptors = [
|
|
39
|
-
|
|
44
|
+
productInterceptor(),
|
|
40
45
|
scopeInterceptor(options.project ?? "", options.environment ?? ""),
|
|
41
46
|
keyInterceptor(),
|
|
42
47
|
retryInterceptor(retry),
|
|
48
|
+
authInterceptor(options.token ?? "", options.tokenSource),
|
|
43
49
|
];
|
|
44
|
-
const transport = options.transport(interceptors);
|
|
50
|
+
const transport = withDefaultUnaryTimeout(options.transport(interceptors), options.defaultTimeoutMs);
|
|
45
51
|
const workloadService = createClient(WorkloadService, transport);
|
|
46
52
|
return {
|
|
47
53
|
project: createClient(ProjectService, transport),
|
|
@@ -58,6 +64,7 @@ export function createToshiClient(options) {
|
|
|
58
64
|
create: (req, opts) => workloadService.createJob(req, opts),
|
|
59
65
|
get: (req, opts) => workloadService.getJob(req, opts),
|
|
60
66
|
list: (req, opts) => workloadService.listJobs(req, opts),
|
|
67
|
+
all: (req, opts) => paginate(req, workloadService.listJobs, (resp) => resp.jobs, opts),
|
|
61
68
|
update: (req, opts) => workloadService.updateJob(req, opts),
|
|
62
69
|
delete: (req, opts) => workloadService.deleteJob(req, opts),
|
|
63
70
|
cancel: (req, opts) => workloadService.cancelJob(req, opts),
|
|
@@ -69,6 +76,7 @@ export function createToshiClient(options) {
|
|
|
69
76
|
create: (req, opts) => workloadService.createSandbox(req, opts),
|
|
70
77
|
get: (req, opts) => workloadService.getSandbox(req, opts),
|
|
71
78
|
list: (req, opts) => workloadService.listSandboxes(req, opts),
|
|
79
|
+
all: (req, opts) => paginate(req, workloadService.listSandboxes, (resp) => resp.sandboxes, opts),
|
|
72
80
|
update: (req, opts) => workloadService.updateSandbox(req, opts),
|
|
73
81
|
delete: (req, opts) => workloadService.deleteSandbox(req, opts),
|
|
74
82
|
wait: (req, opts) => waitSandbox(workloadService, create(WaitSandboxRequestSchema, req), opts),
|
|
@@ -79,6 +87,7 @@ export function createToshiClient(options) {
|
|
|
79
87
|
create: (req, opts) => workloadService.createService(req, opts),
|
|
80
88
|
get: (req, opts) => workloadService.getService(req, opts),
|
|
81
89
|
list: (req, opts) => workloadService.listServices(req, opts),
|
|
90
|
+
all: (req, opts) => paginate(req, workloadService.listServices, (resp) => resp.services, opts),
|
|
82
91
|
update: (req, opts) => workloadService.updateService(req, opts),
|
|
83
92
|
delete: (req, opts) => workloadService.deleteService(req, opts),
|
|
84
93
|
wait: (req, opts) => waitService(workloadService, create(WaitServiceRequestSchema, req), opts),
|
|
@@ -90,9 +99,11 @@ export function createToshiClient(options) {
|
|
|
90
99
|
wake: (req, opts) => workloadService.wakeService(req, opts),
|
|
91
100
|
hold: (req, opts) => workloadService.holdService(req, opts),
|
|
92
101
|
listRuns: (req, opts) => workloadService.listServiceRuns(req, opts),
|
|
102
|
+
allRuns: (req, opts) => paginate(req, workloadService.listServiceRuns, (resp) => resp.runs, opts),
|
|
93
103
|
},
|
|
94
104
|
waitOperation: (req, opts) => waitOperation(workloadService, create(WaitOperationRequestSchema, req), opts),
|
|
95
105
|
listEvents: (req, opts) => workloadService.listEvents(req, opts),
|
|
106
|
+
allEvents: (req, opts) => paginate(req, workloadService.listEvents, (resp) => resp.events, opts),
|
|
96
107
|
watchEvents: (req, opts) => openEventWatch(workloadService, retry, create(WatchEventsRequestSchema, req), opts),
|
|
97
108
|
getOperation: (req, opts) => workloadService.getOperation(req, opts),
|
|
98
109
|
},
|
|
@@ -104,5 +115,7 @@ export function createToshiClient(options) {
|
|
|
104
115
|
auditEvent: createClient(AuditEventService, transport),
|
|
105
116
|
filler: createClient(FillerService, transport),
|
|
106
117
|
storageProvider: createClient(StorageProviderService, transport),
|
|
118
|
+
product: createClient(ProductService, transport),
|
|
119
|
+
discovery: createClient(DiscoveryService, transport),
|
|
107
120
|
};
|
|
108
121
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectError } from "@connectrpc/connect";
|
|
1
2
|
import type { ExecutionExit, OutputEnd } from "./gen/toshi/workspace/v1/workspace_pb.js";
|
|
2
3
|
/** A command that ended non-zero, or was cancelled before it had a status (errors.go:9-26). */
|
|
3
4
|
export declare class ExitError extends Error {
|
|
@@ -26,3 +27,49 @@ export declare class StreamLostError extends Error {
|
|
|
26
27
|
readonly requestKey: string;
|
|
27
28
|
constructor(executionId: string, requestKey: string, cause?: unknown);
|
|
28
29
|
}
|
|
30
|
+
/** Why the API refused a call for a product: the reason of the refusal's ErrorInfo detail. */
|
|
31
|
+
export type ProductReason = "PRODUCT_NOT_INSTALLED" | "PRODUCT_DISABLED" | "PRODUCT_NOT_ENTITLED" | "PRODUCT_TURNED_OFF" | "ACCESS_NOT_ACCEPTED";
|
|
32
|
+
/** The reasons a call is refused for a product. */
|
|
33
|
+
export declare const ProductReason: {
|
|
34
|
+
/** The deployment's build does not include the product; the code is Unimplemented. */
|
|
35
|
+
readonly NotInstalled: "PRODUCT_NOT_INSTALLED";
|
|
36
|
+
/** The deployment turned the product off; the code is FailedPrecondition. */
|
|
37
|
+
readonly Disabled: "PRODUCT_DISABLED";
|
|
38
|
+
/** The workspace's entitlements leave the product out; the code is PermissionDenied. */
|
|
39
|
+
readonly NotEntitled: "PRODUCT_NOT_ENTITLED";
|
|
40
|
+
/**
|
|
41
|
+
* The workspace's settings turn the product off, or an outside product's
|
|
42
|
+
* installation is turned off; the code is FailedPrecondition.
|
|
43
|
+
*/
|
|
44
|
+
readonly TurnedOff: "PRODUCT_TURNED_OFF";
|
|
45
|
+
/**
|
|
46
|
+
* Turning an outside product on installs it, and the call did not accept the
|
|
47
|
+
* access it asks for; the code is FailedPrecondition.
|
|
48
|
+
*/
|
|
49
|
+
readonly AccessNotAccepted: "ACCESS_NOT_ACCEPTED";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The API's refusal of a call for a product: one that is off for the
|
|
53
|
+
* workspace, or an outside product whose access the call did not accept.
|
|
54
|
+
* Every call the client makes throws it in place of the ConnectError, wait,
|
|
55
|
+
* watch and execute included. It is a ConnectError, so `instanceof
|
|
56
|
+
* ConnectError` and `code` read as before; test for it with
|
|
57
|
+
* {@link isProductError}.
|
|
58
|
+
*/
|
|
59
|
+
export declare class ProductError extends ConnectError {
|
|
60
|
+
/** The product's ID, such as `runners`. */
|
|
61
|
+
readonly product: string;
|
|
62
|
+
readonly reason: ProductReason;
|
|
63
|
+
/** The setting that turns the product on, such as `runners.enabled`; empty for ACCESS_NOT_ACCEPTED. */
|
|
64
|
+
readonly feature: string;
|
|
65
|
+
/** How far a product the deployment disables has wound down: `no_new_work`, `draining` or `stopped`; empty otherwise. */
|
|
66
|
+
readonly state: string;
|
|
67
|
+
/** The access an outside product asks for, for ACCESS_NOT_ACCEPTED: accept exactly this list to install it. */
|
|
68
|
+
readonly access: string[];
|
|
69
|
+
constructor(err: ConnectError, reason: ProductReason, metadata: ReadonlyMap<string, string>);
|
|
70
|
+
static [Symbol.hasInstance](v: unknown): boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Reports whether err is a product's refusal. */
|
|
73
|
+
export declare function isProductError(err: unknown): err is ProductError;
|
|
74
|
+
/** err as a ProductError when it is a product's refusal, and err unchanged otherwise. */
|
|
75
|
+
export declare function asProductError(err: unknown): unknown;
|