Repository navigation
Expand file tree
/
Copy pathopenshell.ts
More file actions
231 lines (212 loc) · 8.8 KB
/
Copy pathopenshell.ts
File metadata and controls
231 lines (212 loc) · 8.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import type {
Backend,
Namespace,
OpenShellBackendDefinition,
OpenShellBackendConfiguration,
} from "@openclaw-enterprise/contracts";
import { isNonEmptyString, publicCaBundle, sha256Hex } from "@openclaw-enterprise/utils";
import { readFileSync } from "node:fs";
import { isAbsolute } from "node:path";
import {
GrpcOpenShellGatewayClient,
type OpenShellGatewayClient,
type OpenShellGatewayClientOptions,
} from "../drivers/sandbox/openshell-gateway-client.ts";
const DEFAULT_GATEWAY_PORT = 8080;
const OPENSHELL_MAX_WORKSPACE_NAME_LENGTH = 19;
/** Provider names OCC creates; the paired Sandbox Driver accepts only this shape. */
const OCC_PROVIDER_NAME = /^oce-cs-[0-9a-f]{24}$/;
export class OpenShellBackendConfigurationFailure extends Error {}
/** The gateway's Service name and the Kubernetes namespace its Pods run in. */
export interface OpenShellGatewayPlacement {
readonly service: string;
readonly namespace: string;
}
export interface OpenShellGatewaySelection {
/** Replaces gRPC clients for every namespace, as when a caller owns the transport. */
readonly gatewayClient?: OpenShellGatewayClient;
}
/**
* The authenticated OpenShell gateway shared by the Sandbox and Credential Gateway Drivers.
* A bare Service name resolves in each Sandbox namespace, so clients are cached per endpoint.
*/
export class OpenShellGateway {
private readonly configuration: OpenShellBackendConfiguration;
private readonly injectedClient: OpenShellGatewayClient | undefined;
private readonly clients = new Map<string, OpenShellGatewayClient>();
private readonly credentialClients = new Map<string, OpenShellGatewayClient>();
constructor(
configuration: OpenShellBackendConfiguration,
selection: OpenShellGatewaySelection = {},
) {
if (configuration.endpoint === undefined && !isNonEmptyString(configuration.serviceName)) {
throw new OpenShellBackendConfigurationFailure(
"The OpenShell Backend requires an endpoint or a gateway Service name.",
);
}
this.configuration = configuration;
this.injectedClient = selection.gatewayClient;
}
clientForNamespace(namespace: string): OpenShellGatewayClient {
if (this.injectedClient !== undefined) {
return this.injectedClient;
}
const options = this.clientOptions(namespace);
const existing = this.clients.get(options.endpoint);
if (existing !== undefined) {
return existing;
}
const created = new GrpcOpenShellGatewayClient(options);
this.clients.set(options.endpoint, created);
return created;
}
/** Export uses a distinct operator identity without broadening ordinary RPC authority. */
credentialClientForNamespace(
namespace: string,
): Pick<OpenShellGatewayClient, "getProviderCredential"> {
if (this.injectedClient !== undefined) {
return this.injectedClient;
}
const operatorTls = this.configuration.operatorTls;
if (operatorTls === undefined) {
throw new OpenShellBackendConfigurationFailure(
"OpenShell credential retrieval requires operatorTls certificate and private key paths.",
);
}
const options: OpenShellGatewayClientOptions = {
...this.clientOptions(namespace),
auth: { mode: "mutualTls", ...operatorTls },
};
const existing = this.credentialClients.get(options.endpoint);
if (existing !== undefined) {
return existing;
}
const created = new GrpcOpenShellGatewayClient(options);
this.credentialClients.set(options.endpoint, created);
return created;
}
/**
* Where the gateway runs, when the configuration names it (finding 1356). A bare Service name
* runs in each tenant `namespace`, so it needs one. A dotted Service name, or an endpoint host
* `<service>.<namespace>.svc[.<cluster domain>]`, one Pod's
* `<pod>.<service>-peer.<namespace>.svc[...]` or one port's
* `<port>.<protocol>.<service>.<namespace>.svc[...]` (finding 1369), names one namespace. An IP
* address, a node address or any other host names none.
*/
placement(namespace?: string): OpenShellGatewayPlacement | undefined {
const { endpoint, serviceName } = this.configuration;
if (endpoint !== undefined) {
return serviceDnsPlacement(
new URL(endpoint.includes("://") ? endpoint : `http://${endpoint}`).hostname,
);
}
const labels = (serviceName as string).toLowerCase().split(".");
if (labels.length === 1) {
return namespace === undefined ? undefined : { service: labels[0]!, namespace };
}
return labels.length === 2
? { service: labels[0]!, namespace: labels[1]! }
: serviceDnsPlacement(serviceName as string);
}
close(): void {
this.injectedClient?.close();
for (const client of this.clients.values()) {
client.close();
}
this.clients.clear();
for (const client of this.credentialClients.values()) {
client.close();
}
this.credentialClients.clear();
}
/** Only the configured public root may cross into a Harness endpoint contract. */
publicRootCertificate(): string | undefined {
const path = this.configuration.rootCertificatePath;
if (path === undefined) {
return undefined;
}
if (!isAbsolute(path)) {
throw new OpenShellBackendConfigurationFailure(
"The OpenShell Backend root certificate requires an absolute path.",
);
}
try {
return publicCaBundle(readFileSync(path, "utf8"));
} catch {
throw new OpenShellBackendConfigurationFailure(
"The OpenShell Backend root must be a readable public PEM CA certificate bundle.",
);
}
}
private clientOptions(namespace: string): OpenShellGatewayClientOptions {
const configuration = this.configuration;
return {
endpoint: configuration.endpoint ?? serviceEndpoint(configuration, namespace),
// TODO(namespaced OpenShell identities): the OAuth PoC deliberately shares one
// installation identity for ordinary RPCs and an operator certificate for exports.
// Namespace selection changes routing, not identity; later identity provisioning
// belongs here, while OCC continues authorizing each source operation.
...(configuration.auth === undefined ? {} : { auth: configuration.auth }),
...(configuration.requestTimeoutMs === undefined
? {}
: { requestTimeoutMs: configuration.requestTimeoutMs }),
...(configuration.rootCertificatePath === undefined
? {}
: { rootCertificatePath: configuration.rootCertificatePath }),
};
}
}
/**
* `<service>.<namespace>.svc[...]`, a StatefulSet Pod's `<pod>.<service>-peer.<namespace>.svc[...]`
* or a port's `<port>.<protocol>.<service>.<namespace>.svc[...]`, which CoreDNS answers like the
* Service (an `_` before the port and protocol is optional). A Service or namespace named `svc` is
* read as the first form that fits.
*/
function serviceDnsPlacement(host: string): OpenShellGatewayPlacement | undefined {
const labels = host.toLowerCase().split(".");
if (labels[2] === "svc") {
return { service: labels[0]!, namespace: labels[1]! };
}
if (labels[3] === "svc") {
return { service: labels[1]!.replace(/-peer$/, ""), namespace: labels[2]! };
}
return labels[4] === "svc" ? { service: labels[2]!, namespace: labels[3]! } : undefined;
}
function serviceEndpoint(configuration: OpenShellBackendConfiguration, namespace: string): string {
const serviceName = configuration.serviceName as string;
const scheme =
configuration.scheme ?? (configuration.rootCertificatePath === undefined ? "http" : "https");
const host = serviceName.includes(".") ? serviceName : `${serviceName}.${namespace}.svc`;
return `${scheme}://${host}:${configuration.port ?? DEFAULT_GATEWAY_PORT}`;
}
export function createOpenShellBackend(
definition: OpenShellBackendDefinition,
selection: OpenShellGatewaySelection = {},
): Backend<OpenShellGateway> {
return Object.freeze({
id: definition.id,
client: new OpenShellGateway(definition.configuration, selection),
drivers: Object.freeze({ ...definition.drivers }),
});
}
/** Operator-mode Workspaces reuse the exact Kubernetes namespace name. */
export function openShellWorkspaceName(namespace: Readonly<Namespace>): string {
const name = namespace.name;
if (
!isNonEmptyString(name) ||
name.length > OPENSHELL_MAX_WORKSPACE_NAME_LENGTH ||
!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)
) {
throw new OpenShellBackendConfigurationFailure(
"OpenShell operator-mode Workspace names require a Kubernetes namespace name that is a DNS-1123 label of at most 19 characters.",
);
}
return name;
}
/** One OpenShell provider per OCC credential source, named without exposing the source ID. */
export function openShellProviderName(sourceId: string): string {
return `oce-cs-${sha256Hex(sourceId, 24)}`;
}
export function isOpenShellProviderName(value: string): boolean {
return OCC_PROVIDER_NAME.test(value);
}