@di-framework/tenant-cli 0.0.0-stage → 6.0.9
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 +116 -2
- package/api/v1/openapi.yaml +1 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +419 -0
- package/dist/client/index.d.ts +135 -0
- package/dist/client/index.js +131 -0
- package/dist/client/schema.d.ts +1213 -0
- package/dist/client/sse.d.ts +12 -0
- package/dist/client/sse.js +61 -0
- package/dist/credentials.d.ts +42 -0
- package/dist/credentials.js +65 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.js +41 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +25 -0
- package/dist/oidc.d.ts +54 -0
- package/dist/oidc.js +110 -0
- package/package.json +46 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { components, operations } from './schema.js';
|
|
2
|
+
export { events, type SseEvent } from './sse.js';
|
|
3
|
+
type Json<Op extends keyof operations, Status extends number> = operations[Op]['responses'] extends {
|
|
4
|
+
[S in Status]: {
|
|
5
|
+
content: {
|
|
6
|
+
'application/json': infer T;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
} ? T : never;
|
|
10
|
+
type Body<Op extends keyof operations> = operations[Op] extends {
|
|
11
|
+
requestBody: {
|
|
12
|
+
content: {
|
|
13
|
+
'application/json': infer T;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
} ? T : never;
|
|
17
|
+
type Query<Op extends keyof operations> = operations[Op]['parameters'] extends {
|
|
18
|
+
query: infer Q;
|
|
19
|
+
} ? Q : never;
|
|
20
|
+
export type Env = Query<'deployments'>['env'];
|
|
21
|
+
export type AuthInfo = components['schemas']['AuthInfo'];
|
|
22
|
+
export type Principal = components['schemas']['Principal'];
|
|
23
|
+
export type DeployBundle = Body<'deploy'>;
|
|
24
|
+
export type DeployPlan = Json<'previewDeploy', 200>;
|
|
25
|
+
export type Deployment = Json<'deploy', 202>;
|
|
26
|
+
export type DeploymentList = Json<'deployments', 200>;
|
|
27
|
+
export type DeploymentStats = Json<'deploymentStats', 200>;
|
|
28
|
+
export type RollbackRequest = Body<'rollback'>;
|
|
29
|
+
export type CreateServiceRequest = Body<'createService'>;
|
|
30
|
+
export type Service = Json<'createService', 201>;
|
|
31
|
+
export type LogEvent = Json<'logs', 200>;
|
|
32
|
+
export type LogsQuery = Query<'logs'>;
|
|
33
|
+
export type SecretList = Json<'secrets', 200>;
|
|
34
|
+
export type ConfigList = Json<'vars', 200>;
|
|
35
|
+
export type ProxyRequest = Body<'proxy'>;
|
|
36
|
+
export type ProxySession = Json<'proxy', 201>;
|
|
37
|
+
export type Problem = components['schemas']['Problem'];
|
|
38
|
+
/** A non-2xx answer from the controller, carrying its problem details when it sent any. */
|
|
39
|
+
export declare class ControllerError extends Error {
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly problem: Partial<Problem>;
|
|
42
|
+
constructor(status: number, problem: Partial<Problem>);
|
|
43
|
+
}
|
|
44
|
+
export interface ClientOptions {
|
|
45
|
+
/** The controller's base URL, without a trailing slash. */
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
/** Identity-server access token or tenant API key; absent only for `authInfo`. */
|
|
48
|
+
token?: string;
|
|
49
|
+
fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
export declare function createClient(options: ClientOptions): {
|
|
52
|
+
authInfo: () => Promise<{
|
|
53
|
+
account: string;
|
|
54
|
+
issuer: string;
|
|
55
|
+
clientId: string;
|
|
56
|
+
}>;
|
|
57
|
+
whoami: () => Promise<{
|
|
58
|
+
user: string;
|
|
59
|
+
account: string;
|
|
60
|
+
role: "developer" | "viewer";
|
|
61
|
+
via: "identity" | "api-key";
|
|
62
|
+
credentialId?: string;
|
|
63
|
+
}>;
|
|
64
|
+
logout: () => Promise<void>;
|
|
65
|
+
previewDeploy: (bundle: DeployBundle) => Promise<{
|
|
66
|
+
env: "prod" | "staging";
|
|
67
|
+
service: string;
|
|
68
|
+
changes: components["schemas"]["DeployChange"][];
|
|
69
|
+
}>;
|
|
70
|
+
deploy: (bundle: DeployBundle) => Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
service: string;
|
|
73
|
+
env: "prod" | "staging";
|
|
74
|
+
status: "pending" | "rolling" | "ready" | "failed" | "rolled-back";
|
|
75
|
+
component: components["schemas"]["ComponentReference"];
|
|
76
|
+
createdAt: string;
|
|
77
|
+
readyAt?: string;
|
|
78
|
+
}>;
|
|
79
|
+
createService: (request: CreateServiceRequest) => Promise<{
|
|
80
|
+
name: string;
|
|
81
|
+
env: "prod" | "staging";
|
|
82
|
+
type: "keyvalue" | "messaging" | "blobstore" | "postgres" | "egress";
|
|
83
|
+
className: string;
|
|
84
|
+
parameters?: {
|
|
85
|
+
storage?: string;
|
|
86
|
+
memory?: string;
|
|
87
|
+
cpu?: string;
|
|
88
|
+
};
|
|
89
|
+
deletionPolicy?: "Retain" | "Delete";
|
|
90
|
+
destinations?: string[];
|
|
91
|
+
createdAt: string;
|
|
92
|
+
}>;
|
|
93
|
+
proxy: (service: string, request: ProxyRequest) => Promise<{
|
|
94
|
+
url: string;
|
|
95
|
+
port: number;
|
|
96
|
+
expiresAt: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** Yields one LogEvent per `log` event until the server sends `end` or the stream closes. */
|
|
99
|
+
logs(service: string, query: LogsQuery): AsyncGenerator<LogEvent>;
|
|
100
|
+
deployments: (query: Query<"deployments">) => Promise<{
|
|
101
|
+
items: components["schemas"]["Deployment"][];
|
|
102
|
+
}>;
|
|
103
|
+
deploymentStats: (env: Env) => Promise<{
|
|
104
|
+
env: "prod" | "staging";
|
|
105
|
+
services: number;
|
|
106
|
+
deployments: number;
|
|
107
|
+
ready: number;
|
|
108
|
+
failed: number;
|
|
109
|
+
lastDeployedAt?: string;
|
|
110
|
+
}>;
|
|
111
|
+
rollback: (request: RollbackRequest) => Promise<{
|
|
112
|
+
id: string;
|
|
113
|
+
service: string;
|
|
114
|
+
env: "prod" | "staging";
|
|
115
|
+
status: "pending" | "rolling" | "ready" | "failed" | "rolled-back";
|
|
116
|
+
component: components["schemas"]["ComponentReference"];
|
|
117
|
+
createdAt: string;
|
|
118
|
+
readyAt?: string;
|
|
119
|
+
}>;
|
|
120
|
+
secrets: (env: Env) => Promise<{
|
|
121
|
+
env: "prod" | "staging";
|
|
122
|
+
items: components["schemas"]["SecretEntry"][];
|
|
123
|
+
}>;
|
|
124
|
+
setSecret: (env: Env, name: string, value: string) => Promise<void>;
|
|
125
|
+
updateSecret: (env: Env, name: string, value: string) => Promise<void>;
|
|
126
|
+
unsetSecret: (env: Env, name: string) => Promise<void>;
|
|
127
|
+
vars: (env: Env) => Promise<{
|
|
128
|
+
env: "prod" | "staging";
|
|
129
|
+
items: components["schemas"]["ConfigEntry"][];
|
|
130
|
+
}>;
|
|
131
|
+
setVar: (env: Env, name: string, value: string) => Promise<void>;
|
|
132
|
+
updateVar: (env: Env, name: string, value: string) => Promise<void>;
|
|
133
|
+
unsetVar: (env: Env, name: string) => Promise<void>;
|
|
134
|
+
};
|
|
135
|
+
export type TenantClient = ReturnType<typeof createClient>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { events } from './sse.js';
|
|
2
|
+
export { events } from './sse.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every path the client calls, checked against the generated `paths` at compile time: an
|
|
5
|
+
* operation that leaves the contract stops the client from building.
|
|
6
|
+
*/
|
|
7
|
+
const PATHS = {
|
|
8
|
+
authInfo: '/v1/auth/info',
|
|
9
|
+
whoami: '/v1/auth/whoami',
|
|
10
|
+
logout: '/v1/auth/logout',
|
|
11
|
+
previewDeploy: '/v1/deploy/preview',
|
|
12
|
+
deploy: '/v1/deploy',
|
|
13
|
+
createService: '/v1/services',
|
|
14
|
+
logs: '/v1/services/{service}/logs',
|
|
15
|
+
proxy: '/v1/services/{service}/proxy',
|
|
16
|
+
deployments: '/v1/deployments',
|
|
17
|
+
deploymentStats: '/v1/deployments/stats',
|
|
18
|
+
rollback: '/v1/deployments/rollback',
|
|
19
|
+
secrets: '/v1/secrets',
|
|
20
|
+
secret: '/v1/secrets/{name}',
|
|
21
|
+
vars: '/v1/vars',
|
|
22
|
+
var: '/v1/vars/{name}',
|
|
23
|
+
};
|
|
24
|
+
/** A non-2xx answer from the controller, carrying its problem details when it sent any. */
|
|
25
|
+
export class ControllerError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
problem;
|
|
28
|
+
constructor(status, problem) {
|
|
29
|
+
super(problem.detail ?? problem.title ?? `controller answered ${status}`);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.problem = problem;
|
|
32
|
+
this.name = 'ControllerError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function createClient(options) {
|
|
36
|
+
const base = options.baseUrl.replace(/\/$/, '');
|
|
37
|
+
const doFetch = options.fetch ?? fetch;
|
|
38
|
+
const url = (call) => {
|
|
39
|
+
const path = call.path.replace(/\{(\w+)\}/g, (_, key) => encodeURIComponent(call.params?.[key] ?? ''));
|
|
40
|
+
const search = new URLSearchParams();
|
|
41
|
+
for (const [key, value] of Object.entries(call.query ?? {})) {
|
|
42
|
+
if (value !== undefined)
|
|
43
|
+
search.set(key, String(value));
|
|
44
|
+
}
|
|
45
|
+
const qs = search.toString();
|
|
46
|
+
return `${base}${path}${qs ? `?${qs}` : ''}`;
|
|
47
|
+
};
|
|
48
|
+
const send = async (call) => {
|
|
49
|
+
const headers = new Headers({ accept: call.accept ?? 'application/json' });
|
|
50
|
+
if (options.token)
|
|
51
|
+
headers.set('authorization', `Bearer ${options.token}`);
|
|
52
|
+
if (call.body !== undefined)
|
|
53
|
+
headers.set('content-type', 'application/json');
|
|
54
|
+
const response = await doFetch(url(call), {
|
|
55
|
+
method: call.method,
|
|
56
|
+
headers,
|
|
57
|
+
body: call.body === undefined ? undefined : JSON.stringify(call.body),
|
|
58
|
+
});
|
|
59
|
+
if (response.ok)
|
|
60
|
+
return response;
|
|
61
|
+
let problem = {};
|
|
62
|
+
try {
|
|
63
|
+
problem = (await response.json());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Not a problem document; the status alone carries the error.
|
|
67
|
+
}
|
|
68
|
+
throw new ControllerError(response.status, problem);
|
|
69
|
+
};
|
|
70
|
+
const json = async (call) => (await send(call)).json();
|
|
71
|
+
const empty = async (call) => {
|
|
72
|
+
await send(call);
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
authInfo: () => json({ method: 'GET', path: PATHS.authInfo }),
|
|
76
|
+
whoami: () => json({ method: 'GET', path: PATHS.whoami }),
|
|
77
|
+
logout: () => empty({ method: 'POST', path: PATHS.logout }),
|
|
78
|
+
previewDeploy: (bundle) => json({ method: 'POST', path: PATHS.previewDeploy, body: bundle }),
|
|
79
|
+
deploy: (bundle) => json({ method: 'POST', path: PATHS.deploy, body: bundle }),
|
|
80
|
+
createService: (request) => json({ method: 'POST', path: PATHS.createService, body: request }),
|
|
81
|
+
proxy: (service, request) => json({ method: 'POST', path: PATHS.proxy, params: { service }, body: request }),
|
|
82
|
+
/** Yields one LogEvent per `log` event until the server sends `end` or the stream closes. */
|
|
83
|
+
async *logs(service, query) {
|
|
84
|
+
const response = await send({
|
|
85
|
+
method: 'GET',
|
|
86
|
+
path: PATHS.logs,
|
|
87
|
+
params: { service },
|
|
88
|
+
query,
|
|
89
|
+
accept: 'text/event-stream',
|
|
90
|
+
});
|
|
91
|
+
if (!response.body)
|
|
92
|
+
return;
|
|
93
|
+
for await (const event of events(response.body)) {
|
|
94
|
+
if (event.event === 'end')
|
|
95
|
+
return;
|
|
96
|
+
if (event.event === undefined || event.event === 'log') {
|
|
97
|
+
yield JSON.parse(event.data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
deployments: (query) => json({ method: 'GET', path: PATHS.deployments, query }),
|
|
102
|
+
deploymentStats: (env) => json({ method: 'GET', path: PATHS.deploymentStats, query: { env } }),
|
|
103
|
+
rollback: (request) => json({ method: 'POST', path: PATHS.rollback, body: request }),
|
|
104
|
+
secrets: (env) => json({ method: 'GET', path: PATHS.secrets, query: { env } }),
|
|
105
|
+
setSecret: (env, name, value) => empty({
|
|
106
|
+
method: 'PUT',
|
|
107
|
+
path: PATHS.secret,
|
|
108
|
+
params: { name },
|
|
109
|
+
query: { env },
|
|
110
|
+
body: { value },
|
|
111
|
+
}),
|
|
112
|
+
updateSecret: (env, name, value) => empty({
|
|
113
|
+
method: 'PATCH',
|
|
114
|
+
path: PATHS.secret,
|
|
115
|
+
params: { name },
|
|
116
|
+
query: { env },
|
|
117
|
+
body: { value },
|
|
118
|
+
}),
|
|
119
|
+
unsetSecret: (env, name) => empty({ method: 'DELETE', path: PATHS.secret, params: { name }, query: { env } }),
|
|
120
|
+
vars: (env) => json({ method: 'GET', path: PATHS.vars, query: { env } }),
|
|
121
|
+
setVar: (env, name, value) => empty({ method: 'PUT', path: PATHS.var, params: { name }, query: { env }, body: { value } }),
|
|
122
|
+
updateVar: (env, name, value) => empty({
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
path: PATHS.var,
|
|
125
|
+
params: { name },
|
|
126
|
+
query: { env },
|
|
127
|
+
body: { value },
|
|
128
|
+
}),
|
|
129
|
+
unsetVar: (env, name) => empty({ method: 'DELETE', path: PATHS.var, params: { name }, query: { env } }),
|
|
130
|
+
};
|
|
131
|
+
}
|