openlattice-cloudrun 0.0.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 +137 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +130 -0
- package/dist/cloudrun-provider.d.ts +28 -0
- package/dist/cloudrun-provider.js +641 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/package.json +37 -0
- package/src/auth.ts +152 -0
- package/src/cloudrun-provider.ts +867 -0
- package/src/index.ts +2 -0
- package/src/types.ts +96 -0
- package/tests/cloudrun-provider.test.ts +1081 -0
- package/tests/conformance.test.ts +26 -0
- package/tests/integration.test.ts +89 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# openlattice-cloudrun
|
|
2
|
+
|
|
3
|
+
Google Cloud Run compute provider for [OpenLattice](../../README.md). Deploys containers as fully managed Cloud Run services, with exec via Cloud Run Jobs and logs via Cloud Logging.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install openlattice openlattice-cloudrun
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { OpenLattice } from "openlattice";
|
|
15
|
+
import { CloudRunProvider } from "openlattice-cloudrun";
|
|
16
|
+
|
|
17
|
+
const lattice = new OpenLattice({
|
|
18
|
+
providers: [
|
|
19
|
+
new CloudRunProvider({
|
|
20
|
+
projectId: "my-gcp-project",
|
|
21
|
+
region: "us-central1",
|
|
22
|
+
authMethod: "metadata", // or "service-account" / "token"
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const node = await lattice.provision(
|
|
28
|
+
{
|
|
29
|
+
runtime: { image: "gcr.io/my-project/my-app:latest" },
|
|
30
|
+
cpu: { cores: 1 },
|
|
31
|
+
memory: { sizeGiB: 0.5 },
|
|
32
|
+
network: { ports: [{ port: 8080 }] },
|
|
33
|
+
},
|
|
34
|
+
{ id: "agent-1", type: "agent" }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = await lattice.exec(node.id, ["python", "-c", "print('hello')"]);
|
|
38
|
+
console.log(result.stdout);
|
|
39
|
+
|
|
40
|
+
await lattice.destroy(node.id);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Description |
|
|
46
|
+
|--------|------|---------|-------------|
|
|
47
|
+
| `projectId` | `string` | *required* | GCP project ID |
|
|
48
|
+
| `region` | `string` | `"us-central1"` | Cloud Run region |
|
|
49
|
+
| `authMethod` | `"metadata" \| "service-account" \| "token"` | `"metadata"` | Authentication method |
|
|
50
|
+
| `serviceAccountKeyPath` | `string` | — | Path to SA key JSON (when `authMethod` is `"service-account"`) |
|
|
51
|
+
| `accessToken` | `string` | — | Static token (when `authMethod` is `"token"`). Also reads `GOOGLE_ACCESS_TOKEN` env var |
|
|
52
|
+
| `maxInstances` | `number` | `1` | Max instance count per service |
|
|
53
|
+
| `minInstances` | `number` | `0` | Min instances (0 = scale-to-zero) |
|
|
54
|
+
| `concurrency` | `number` | `80` | Request concurrency per instance |
|
|
55
|
+
| `serviceAccount` | `string` | — | Service account email for the Cloud Run service identity |
|
|
56
|
+
| `vpcConnector` | `string` | — | VPC connector name for private networking |
|
|
57
|
+
| `defaultLabels` | `Record<string, string>` | — | Labels applied to all services |
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
Three methods are supported:
|
|
62
|
+
|
|
63
|
+
**Metadata server** (default) — for code running on GCP (GCE, GKE, Cloud Run, etc.):
|
|
64
|
+
```typescript
|
|
65
|
+
new CloudRunProvider({ projectId: "my-project" })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Service account key** — for local development or CI:
|
|
69
|
+
```typescript
|
|
70
|
+
new CloudRunProvider({
|
|
71
|
+
projectId: "my-project",
|
|
72
|
+
authMethod: "service-account",
|
|
73
|
+
serviceAccountKeyPath: "/path/to/key.json",
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Static token** — for short-lived scripts or testing:
|
|
78
|
+
```typescript
|
|
79
|
+
new CloudRunProvider({
|
|
80
|
+
projectId: "my-project",
|
|
81
|
+
authMethod: "token",
|
|
82
|
+
accessToken: "ya29.a0...",
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Tokens are cached with a 5-minute expiry buffer and refreshed automatically.
|
|
87
|
+
|
|
88
|
+
## Capabilities
|
|
89
|
+
|
|
90
|
+
| Capability | Supported |
|
|
91
|
+
|------------|-----------|
|
|
92
|
+
| Provision / Exec / Destroy | Yes |
|
|
93
|
+
| Stop / Start | Yes (via traffic routing) |
|
|
94
|
+
| Pause / Resume | No |
|
|
95
|
+
| Snapshots | No |
|
|
96
|
+
| GPU | No |
|
|
97
|
+
| Logs | Yes (Cloud Logging, with follow mode) |
|
|
98
|
+
| Tailscale | No |
|
|
99
|
+
| File Operations | No |
|
|
100
|
+
| Persistent Storage | No |
|
|
101
|
+
|
|
102
|
+
## How It Works
|
|
103
|
+
|
|
104
|
+
### Provision
|
|
105
|
+
Creates a Cloud Run service via the Admin API v2. Maps `ComputeSpec` fields to the service template (image, CPU, memory, ports, env, scaling). Configures startup and liveness probes on `/health`. Polls the long-running operation until the service is ready, then returns the service's HTTPS endpoint.
|
|
106
|
+
|
|
107
|
+
### Exec
|
|
108
|
+
Cloud Run services don't support shell exec. Instead, the provider creates a one-off **Cloud Run Job** using the same container image, runs it, retrieves stdout/stderr from **Cloud Logging**, then cleans up the job. Supports `cwd`, `env`, and `timeoutMs` options.
|
|
109
|
+
|
|
110
|
+
### Stop / Start
|
|
111
|
+
Implemented via traffic routing. Stop patches traffic to 0% (the service still exists but receives no requests). Start restores traffic to 100%.
|
|
112
|
+
|
|
113
|
+
### Logs
|
|
114
|
+
Queries the Cloud Logging API for entries matching the service. Supports `tail`, `since`, and `follow` options. Follow mode polls every 2 seconds for new entries.
|
|
115
|
+
|
|
116
|
+
### Cost Estimation
|
|
117
|
+
`getCost()` returns a rough estimate based on Cloud Run per-second pricing for vCPU and memory, computed from the service's resource limits and uptime since creation.
|
|
118
|
+
|
|
119
|
+
## Required GCP Permissions
|
|
120
|
+
|
|
121
|
+
The authenticated principal needs these IAM roles (or equivalent permissions):
|
|
122
|
+
|
|
123
|
+
- `roles/run.admin` — create, update, delete Cloud Run services and jobs
|
|
124
|
+
- `roles/logging.viewer` — read logs from Cloud Logging
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm run build # Compile TypeScript
|
|
130
|
+
npm run test # Run unit tests (mocked, no GCP needed)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Integration tests require a real GCP project:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
CLOUDRUN_PROJECT_ID=my-project CLOUDRUN_ACCESS_TOKEN=ya29... npm run test:integration
|
|
137
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages GCP access token acquisition and caching.
|
|
3
|
+
* Supports metadata server, service account key, and static token methods.
|
|
4
|
+
*/
|
|
5
|
+
export declare class GcpAuthManager {
|
|
6
|
+
private readonly authMethod;
|
|
7
|
+
private readonly serviceAccountKeyPath?;
|
|
8
|
+
private readonly staticToken?;
|
|
9
|
+
private cachedToken;
|
|
10
|
+
/** Buffer before token expiry to trigger refresh (5 minutes). */
|
|
11
|
+
private static readonly EXPIRY_BUFFER_MS;
|
|
12
|
+
constructor(opts: {
|
|
13
|
+
authMethod?: "metadata" | "service-account" | "token";
|
|
14
|
+
serviceAccountKeyPath?: string;
|
|
15
|
+
accessToken?: string;
|
|
16
|
+
});
|
|
17
|
+
/** Returns a valid access token, refreshing if necessary. */
|
|
18
|
+
getToken(): Promise<string>;
|
|
19
|
+
private fetchFromMetadata;
|
|
20
|
+
private fetchFromServiceAccount;
|
|
21
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GcpAuthManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
/**
|
|
40
|
+
* Manages GCP access token acquisition and caching.
|
|
41
|
+
* Supports metadata server, service account key, and static token methods.
|
|
42
|
+
*/
|
|
43
|
+
class GcpAuthManager {
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
this.cachedToken = null;
|
|
46
|
+
this.authMethod = opts.authMethod ?? "metadata";
|
|
47
|
+
this.serviceAccountKeyPath = opts.serviceAccountKeyPath;
|
|
48
|
+
this.staticToken =
|
|
49
|
+
opts.accessToken ?? process.env.GOOGLE_ACCESS_TOKEN ?? undefined;
|
|
50
|
+
if (this.authMethod === "service-account" && !this.serviceAccountKeyPath) {
|
|
51
|
+
throw new Error("[cloudrun] serviceAccountKeyPath is required when authMethod is 'service-account'");
|
|
52
|
+
}
|
|
53
|
+
if (this.authMethod === "token" && !this.staticToken) {
|
|
54
|
+
throw new Error("[cloudrun] accessToken or GOOGLE_ACCESS_TOKEN env var is required when authMethod is 'token'");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Returns a valid access token, refreshing if necessary. */
|
|
58
|
+
async getToken() {
|
|
59
|
+
if (this.authMethod === "token") {
|
|
60
|
+
return this.staticToken;
|
|
61
|
+
}
|
|
62
|
+
if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
|
|
63
|
+
return this.cachedToken.token;
|
|
64
|
+
}
|
|
65
|
+
if (this.authMethod === "metadata") {
|
|
66
|
+
return this.fetchFromMetadata();
|
|
67
|
+
}
|
|
68
|
+
return this.fetchFromServiceAccount();
|
|
69
|
+
}
|
|
70
|
+
async fetchFromMetadata() {
|
|
71
|
+
const url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
headers: { "Metadata-Flavor": "Google" },
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`[cloudrun] metadata token request failed (${response.status}): ${await response.text()}`);
|
|
77
|
+
}
|
|
78
|
+
const data = (await response.json());
|
|
79
|
+
this.cachedToken = {
|
|
80
|
+
token: data.access_token,
|
|
81
|
+
expiresAt: Date.now() +
|
|
82
|
+
data.expires_in * 1000 -
|
|
83
|
+
GcpAuthManager.EXPIRY_BUFFER_MS,
|
|
84
|
+
};
|
|
85
|
+
return data.access_token;
|
|
86
|
+
}
|
|
87
|
+
async fetchFromServiceAccount() {
|
|
88
|
+
const keyData = JSON.parse(fs.readFileSync(this.serviceAccountKeyPath, "utf-8"));
|
|
89
|
+
const now = Math.floor(Date.now() / 1000);
|
|
90
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
91
|
+
const payload = {
|
|
92
|
+
iss: keyData.client_email,
|
|
93
|
+
scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
94
|
+
aud: keyData.token_uri,
|
|
95
|
+
iat: now,
|
|
96
|
+
exp: now + 3600,
|
|
97
|
+
};
|
|
98
|
+
const encodedHeader = base64url(JSON.stringify(header));
|
|
99
|
+
const encodedPayload = base64url(JSON.stringify(payload));
|
|
100
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
101
|
+
const sign = crypto.createSign("RSA-SHA256");
|
|
102
|
+
sign.update(signatureInput);
|
|
103
|
+
const signature = sign.sign(keyData.private_key);
|
|
104
|
+
const encodedSignature = base64url(signature);
|
|
105
|
+
const jwt = `${signatureInput}.${encodedSignature}`;
|
|
106
|
+
const response = await fetch(keyData.token_uri, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
109
|
+
body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}`,
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`[cloudrun] service account token exchange failed (${response.status}): ${await response.text()}`);
|
|
113
|
+
}
|
|
114
|
+
const data = (await response.json());
|
|
115
|
+
this.cachedToken = {
|
|
116
|
+
token: data.access_token,
|
|
117
|
+
expiresAt: Date.now() +
|
|
118
|
+
data.expires_in * 1000 -
|
|
119
|
+
GcpAuthManager.EXPIRY_BUFFER_MS,
|
|
120
|
+
};
|
|
121
|
+
return data.access_token;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.GcpAuthManager = GcpAuthManager;
|
|
125
|
+
/** Buffer before token expiry to trigger refresh (5 minutes). */
|
|
126
|
+
GcpAuthManager.EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
127
|
+
function base64url(input) {
|
|
128
|
+
const buf = typeof input === "string" ? Buffer.from(input) : input;
|
|
129
|
+
return buf.toString("base64url");
|
|
130
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComputeProvider, ComputeSpec, ExecOpts, ExecResult, ExtensionMap, HealthStatus, LogEntry, LogOpts, ProviderCapabilities, ProviderNode, ProviderNodeStatus } from "openlattice";
|
|
2
|
+
import type { CloudRunProviderConfig } from "./types";
|
|
3
|
+
export declare class CloudRunProvider implements ComputeProvider {
|
|
4
|
+
readonly name = "cloudrun";
|
|
5
|
+
readonly capabilities: ProviderCapabilities;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly region;
|
|
8
|
+
private readonly auth;
|
|
9
|
+
constructor(config: CloudRunProviderConfig);
|
|
10
|
+
provision(spec: ComputeSpec): Promise<ProviderNode>;
|
|
11
|
+
exec(externalId: string, command: string[], opts?: ExecOpts): Promise<ExecResult>;
|
|
12
|
+
destroy(externalId: string): Promise<void>;
|
|
13
|
+
inspect(externalId: string): Promise<ProviderNodeStatus>;
|
|
14
|
+
stop(externalId: string): Promise<void>;
|
|
15
|
+
start(externalId: string): Promise<void>;
|
|
16
|
+
logs(externalId: string, opts?: LogOpts): AsyncIterable<LogEntry>;
|
|
17
|
+
healthCheck(): Promise<HealthStatus>;
|
|
18
|
+
getCost(externalId: string): Promise<{
|
|
19
|
+
totalUsd: number;
|
|
20
|
+
}>;
|
|
21
|
+
getExtension<K extends keyof ExtensionMap>(externalId: string, extension: K): ExtensionMap[K] | undefined;
|
|
22
|
+
private cloudRunRequest;
|
|
23
|
+
private cloudRunJobRequest;
|
|
24
|
+
private pollOperation;
|
|
25
|
+
private fetchLogs;
|
|
26
|
+
private fetchJobLogs;
|
|
27
|
+
private createNetworkExtension;
|
|
28
|
+
}
|