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 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
+ }