Update documentation files
This commit is contained in:
@@ -0,0 +1,743 @@
|
||||
1) Generate Control Plane API scaffold
|
||||
Folder layout
|
||||
backend/control-plane/
|
||||
package.json
|
||||
tsconfig.json
|
||||
src/
|
||||
index.ts
|
||||
config.ts
|
||||
auth.ts
|
||||
registry.ts
|
||||
types.ts
|
||||
storage/
|
||||
firestore.ts
|
||||
gcs.ts
|
||||
routes/
|
||||
tools.ts
|
||||
runs.ts
|
||||
health.ts
|
||||
.env.example
|
||||
|
||||
backend/control-plane/package.json
|
||||
{
|
||||
"name": "@productos/control-plane",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/firestore": "^7.11.0",
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/helmet": "^12.0.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"fastify": "^4.28.1",
|
||||
"zod": "^3.23.8",
|
||||
"nanoid": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4",
|
||||
"eslint": "^9.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend/control-plane/tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
||||
backend/control-plane/.env.example
|
||||
PORT=8080
|
||||
GCP_PROJECT_ID=your-project-id
|
||||
GCS_BUCKET_ARTIFACTS=productos-artifacts-dev
|
||||
FIRESTORE_COLLECTION_RUNS=runs
|
||||
FIRESTORE_COLLECTION_TOOLS=tools
|
||||
# If you put behind IAP / OAuth later, validate ID tokens here:
|
||||
AUTH_MODE=dev # dev | oauth
|
||||
|
||||
backend/control-plane/src/config.ts
|
||||
export const config = {
|
||||
port: Number(process.env.PORT ?? 8080),
|
||||
projectId: process.env.GCP_PROJECT_ID ?? "",
|
||||
artifactsBucket: process.env.GCS_BUCKET_ARTIFACTS ?? "",
|
||||
runsCollection: process.env.FIRESTORE_COLLECTION_RUNS ?? "runs",
|
||||
toolsCollection: process.env.FIRESTORE_COLLECTION_TOOLS ?? "tools",
|
||||
authMode: process.env.AUTH_MODE ?? "dev"
|
||||
};
|
||||
|
||||
backend/control-plane/src/types.ts
|
||||
export type ToolRisk = "low" | "medium" | "high";
|
||||
|
||||
export type ToolDef = {
|
||||
name: string;
|
||||
description: string;
|
||||
risk: ToolRisk;
|
||||
executor: {
|
||||
kind: "http";
|
||||
url: string; // executor base url
|
||||
path: string; // executor endpoint path
|
||||
};
|
||||
inputSchema: unknown; // JSON Schema object
|
||||
outputSchema?: unknown; // JSON Schema object
|
||||
};
|
||||
|
||||
export type ToolInvokeRequest = {
|
||||
tool: string;
|
||||
tenant_id: string;
|
||||
workspace_id?: string;
|
||||
input: unknown;
|
||||
dry_run?: boolean;
|
||||
};
|
||||
|
||||
export type RunStatus = "queued" | "running" | "succeeded" | "failed";
|
||||
|
||||
export type RunRecord = {
|
||||
run_id: string;
|
||||
tenant_id: string;
|
||||
tool: string;
|
||||
status: RunStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
input: unknown;
|
||||
output?: unknown;
|
||||
error?: { message: string; details?: unknown };
|
||||
artifacts?: { bucket: string; prefix: string };
|
||||
};
|
||||
|
||||
backend/control-plane/src/auth.ts
|
||||
import { FastifyRequest } from "fastify";
|
||||
import { config } from "./config.js";
|
||||
|
||||
/**
|
||||
* V1: dev mode = trust caller (or a shared API key later).
|
||||
* V2: validate Google OAuth/IAP identity token and map to tenant/org.
|
||||
*/
|
||||
export async function requireAuth(req: FastifyRequest) {
|
||||
if (config.authMode === "dev") return;
|
||||
|
||||
// Placeholder for OAuth/IAP verification:
|
||||
// - read Authorization: Bearer <id_token>
|
||||
// - verify token (Google JWKS)
|
||||
// - attach req.user
|
||||
throw new Error("AUTH_MODE oauth not yet implemented");
|
||||
}
|
||||
|
||||
backend/control-plane/src/storage/firestore.ts
|
||||
import { Firestore } from "@google-cloud/firestore";
|
||||
import { config } from "../config.js";
|
||||
import type { RunRecord, ToolDef } from "../types.js";
|
||||
|
||||
const db = new Firestore({ projectId: config.projectId });
|
||||
|
||||
export async function saveRun(run: RunRecord): Promise<void> {
|
||||
await db.collection(config.runsCollection).doc(run.run_id).set(run, { merge: true });
|
||||
}
|
||||
|
||||
export async function getRun(runId: string): Promise<RunRecord | null> {
|
||||
const snap = await db.collection(config.runsCollection).doc(runId).get();
|
||||
return snap.exists ? (snap.data() as RunRecord) : null;
|
||||
}
|
||||
|
||||
export async function saveTool(tool: ToolDef): Promise<void> {
|
||||
await db.collection(config.toolsCollection).doc(tool.name).set(tool, { merge: true });
|
||||
}
|
||||
|
||||
export async function listTools(): Promise<ToolDef[]> {
|
||||
const snap = await db.collection(config.toolsCollection).get();
|
||||
return snap.docs.map(d => d.data() as ToolDef);
|
||||
}
|
||||
|
||||
backend/control-plane/src/storage/gcs.ts
|
||||
import { Storage } from "@google-cloud/storage";
|
||||
import { config } from "../config.js";
|
||||
|
||||
const storage = new Storage({ projectId: config.projectId });
|
||||
|
||||
export async function writeArtifactText(prefix: string, filename: string, content: string) {
|
||||
const bucket = storage.bucket(config.artifactsBucket);
|
||||
const file = bucket.file(`${prefix}/${filename}`);
|
||||
await file.save(content, { contentType: "text/plain" });
|
||||
return { bucket: config.artifactsBucket, path: `${prefix}/${filename}` };
|
||||
}
|
||||
|
||||
backend/control-plane/src/registry.ts
|
||||
import type { ToolDef } from "./types.js";
|
||||
import { listTools } from "./storage/firestore.js";
|
||||
|
||||
/**
|
||||
* Simple registry. V2: cache + versioning + per-tenant overrides.
|
||||
*/
|
||||
export async function getRegistry(): Promise<Record<string, ToolDef>> {
|
||||
const tools = await listTools();
|
||||
return Object.fromEntries(tools.map(t => [t.name, t]));
|
||||
}
|
||||
|
||||
backend/control-plane/src/routes/health.ts
|
||||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
export async function healthRoutes(app: FastifyInstance) {
|
||||
app.get("/healthz", async () => ({ ok: true }));
|
||||
}
|
||||
|
||||
backend/control-plane/src/routes/tools.ts
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { nanoid } from "nanoid";
|
||||
import { requireAuth } from "../auth.js";
|
||||
import { getRegistry } from "../registry.js";
|
||||
import { saveRun } from "../storage/firestore.js";
|
||||
import { writeArtifactText } from "../storage/gcs.js";
|
||||
import type { RunRecord, ToolInvokeRequest } from "../types.js";
|
||||
|
||||
async function postJson(url: string, body: unknown) {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(`Executor error ${res.status}: ${txt}`);
|
||||
}
|
||||
return res.json() as Promise<unknown>;
|
||||
}
|
||||
|
||||
export async function toolRoutes(app: FastifyInstance) {
|
||||
app.get("/tools", async (req) => {
|
||||
await requireAuth(req);
|
||||
const registry = await getRegistry();
|
||||
return { tools: Object.values(registry) };
|
||||
});
|
||||
|
||||
app.post<{ Body: ToolInvokeRequest }>("/tools/invoke", async (req) => {
|
||||
await requireAuth(req);
|
||||
|
||||
const body = req.body;
|
||||
const registry = await getRegistry();
|
||||
const tool = registry[body.tool];
|
||||
if (!tool) return app.httpErrors.notFound(`Unknown tool: ${body.tool}`);
|
||||
|
||||
const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const run: RunRecord = {
|
||||
run_id: runId,
|
||||
tenant_id: body.tenant_id,
|
||||
tool: body.tool,
|
||||
status: "queued",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
input: body.input,
|
||||
artifacts: { bucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", prefix: `runs/${runId}` }
|
||||
};
|
||||
|
||||
await saveRun(run);
|
||||
|
||||
// record input artifact
|
||||
await writeArtifactText(`runs/${runId}`, "input.json", JSON.stringify(body, null, 2));
|
||||
|
||||
// execute (sync for v1; v2: push to Cloud Tasks / Workflows)
|
||||
try {
|
||||
run.status = "running";
|
||||
run.updated_at = new Date().toISOString();
|
||||
await saveRun(run);
|
||||
|
||||
if (body.dry_run) {
|
||||
run.status = "succeeded";
|
||||
run.output = { dry_run: true };
|
||||
run.updated_at = new Date().toISOString();
|
||||
await saveRun(run);
|
||||
await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(run.output, null, 2));
|
||||
return { run_id: runId, status: run.status };
|
||||
}
|
||||
|
||||
const execUrl = `${tool.executor.url}${tool.executor.path}`;
|
||||
const output = await postJson(execUrl, {
|
||||
run_id: runId,
|
||||
tenant_id: body.tenant_id,
|
||||
workspace_id: body.workspace_id,
|
||||
input: body.input
|
||||
});
|
||||
|
||||
run.status = "succeeded";
|
||||
run.output = output;
|
||||
run.updated_at = new Date().toISOString();
|
||||
await saveRun(run);
|
||||
await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(output, null, 2));
|
||||
|
||||
return { run_id: runId, status: run.status };
|
||||
} catch (e: any) {
|
||||
run.status = "failed";
|
||||
run.error = { message: e?.message ?? "Unknown error" };
|
||||
run.updated_at = new Date().toISOString();
|
||||
await saveRun(run);
|
||||
await writeArtifactText(`runs/${runId}`, "error.json", JSON.stringify(run.error, null, 2));
|
||||
return { run_id: runId, status: run.status };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
backend/control-plane/src/routes/runs.ts
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { requireAuth } from "../auth.js";
|
||||
import { getRun } from "../storage/firestore.js";
|
||||
|
||||
export async function runRoutes(app: FastifyInstance) {
|
||||
app.get("/runs/:run_id", async (req) => {
|
||||
await requireAuth(req);
|
||||
// @ts-expect-error fastify param typing
|
||||
const runId = req.params.run_id as string;
|
||||
const run = await getRun(runId);
|
||||
if (!run) return app.httpErrors.notFound("Run not found");
|
||||
return run;
|
||||
});
|
||||
|
||||
// V1: logs are stored as artifacts in GCS; IDE can fetch by signed URL later
|
||||
app.get("/runs/:run_id/logs", async (req) => {
|
||||
await requireAuth(req);
|
||||
// stub
|
||||
return { note: "V1: logs are in GCS artifacts under runs/<run_id>/" };
|
||||
});
|
||||
}
|
||||
|
||||
backend/control-plane/src/index.ts
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import helmet from "@fastify/helmet";
|
||||
import rateLimit from "@fastify/rate-limit";
|
||||
import { config } from "./config.js";
|
||||
import { healthRoutes } from "./routes/health.js";
|
||||
import { toolRoutes } from "./routes/tools.js";
|
||||
import { runRoutes } from "./routes/runs.js";
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(cors, { origin: true });
|
||||
await app.register(helmet);
|
||||
await app.register(rateLimit, { max: 300, timeWindow: "1 minute" });
|
||||
|
||||
await app.register(healthRoutes);
|
||||
await app.register(toolRoutes);
|
||||
await app.register(runRoutes);
|
||||
|
||||
app.listen({ port: config.port, host: "0.0.0.0" }).catch((err) => {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
2) Generate Tool Registry schema
|
||||
|
||||
You want two things:
|
||||
|
||||
A human-editable YAML (source of truth)
|
||||
|
||||
A JSON Schema to validate tool definitions
|
||||
|
||||
contracts/tool-registry.yaml (example)
|
||||
version: 1
|
||||
tools:
|
||||
cloudrun.deploy_service:
|
||||
description: Deploy a Cloud Run service via Cloud Build.
|
||||
risk: medium
|
||||
executor:
|
||||
kind: http
|
||||
url: https://deploy-executor-xxxxx.a.run.app
|
||||
path: /execute/deploy
|
||||
inputSchema:
|
||||
type: object
|
||||
required: [service_name, repo, ref, env]
|
||||
properties:
|
||||
service_name: { type: string }
|
||||
repo: { type: string, description: "Git repo URL" }
|
||||
ref: { type: string, description: "Branch, tag, or commit SHA" }
|
||||
env: { type: string, enum: ["dev", "staging", "prod"] }
|
||||
outputSchema:
|
||||
type: object
|
||||
properties:
|
||||
service_url: { type: string }
|
||||
revision: { type: string }
|
||||
|
||||
analytics.get_funnel_summary:
|
||||
description: Return funnel metrics for a tenant and time window.
|
||||
risk: low
|
||||
executor:
|
||||
kind: http
|
||||
url: https://analytics-executor-xxxxx.a.run.app
|
||||
path: /execute/funnel
|
||||
inputSchema:
|
||||
type: object
|
||||
required: [range_days]
|
||||
properties:
|
||||
range_days: { type: integer, minimum: 1, maximum: 365 }
|
||||
segment: { type: object }
|
||||
|
||||
contracts/tool-registry.schema.json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://productos.dev/schemas/tool-registry.schema.json",
|
||||
"type": "object",
|
||||
"required": ["version", "tools"],
|
||||
"properties": {
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/ToolDef" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"ToolDef": {
|
||||
"type": "object",
|
||||
"required": ["description", "risk", "executor", "inputSchema"],
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"risk": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"executor": { "$ref": "#/$defs/Executor" },
|
||||
"inputSchema": { "type": "object" },
|
||||
"outputSchema": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Executor": {
|
||||
"type": "object",
|
||||
"required": ["kind", "url", "path"],
|
||||
"properties": {
|
||||
"kind": { "type": "string", "enum": ["http"] },
|
||||
"url": { "type": "string" },
|
||||
"path": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3) Generate VSCodium extension skeleton
|
||||
Folder layout
|
||||
client-ide/extensions/gcp-productos/
|
||||
package.json
|
||||
tsconfig.json
|
||||
src/extension.ts
|
||||
src/api.ts
|
||||
src/ui.ts
|
||||
media/icon.png (optional)
|
||||
|
||||
client-ide/extensions/gcp-productos/package.json
|
||||
{
|
||||
"name": "gcp-productos",
|
||||
"displayName": "GCP Product OS",
|
||||
"description": "Product-centric panels (Code, Marketing, Analytics, Growth...) and backend tool invocation.",
|
||||
"version": "0.0.1",
|
||||
"publisher": "productos",
|
||||
"engines": { "vscode": "^1.90.0" },
|
||||
"categories": ["Other"],
|
||||
"activationEvents": ["onStartupFinished"],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{ "command": "productos.configure", "title": "Product OS: Configure Backend" },
|
||||
{ "command": "productos.tools.list", "title": "Product OS: List Tools" },
|
||||
{ "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool" },
|
||||
{ "command": "productos.runs.open", "title": "Product OS: Open Run" }
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Product OS",
|
||||
"properties": {
|
||||
"productos.backendUrl": {
|
||||
"type": "string",
|
||||
"default": "http://localhost:8080",
|
||||
"description": "Control Plane API base URL"
|
||||
},
|
||||
"productos.tenantId": {
|
||||
"type": "string",
|
||||
"default": "t_dev",
|
||||
"description": "Tenant ID for tool calls"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"watch": "tsc -w -p tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/vscode": "^1.90.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
client-ide/extensions/gcp-productos/tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
client-ide/extensions/gcp-productos/src/api.ts
|
||||
import * as vscode from "vscode";
|
||||
|
||||
function cfg<T>(key: string): T {
|
||||
return vscode.workspace.getConfiguration("productos").get<T>(key)!;
|
||||
}
|
||||
|
||||
export function backendUrl() {
|
||||
return cfg<string>("backendUrl");
|
||||
}
|
||||
|
||||
export function tenantId() {
|
||||
return cfg<string>("tenantId");
|
||||
}
|
||||
|
||||
export async function listTools(): Promise<any[]> {
|
||||
const res = await fetch(`${backendUrl()}/tools`);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const json = await res.json();
|
||||
return json.tools ?? [];
|
||||
}
|
||||
|
||||
export async function invokeTool(tool: string, input: any) {
|
||||
const res = await fetch(`${backendUrl()}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
tool,
|
||||
tenant_id: tenantId(),
|
||||
input
|
||||
})
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getRun(runId: string) {
|
||||
const res = await fetch(`${backendUrl()}/runs/${runId}`);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
}
|
||||
|
||||
client-ide/extensions/gcp-productos/src/ui.ts
|
||||
import * as vscode from "vscode";
|
||||
import { getRun } from "./api";
|
||||
|
||||
export async function showJson(title: string, obj: any) {
|
||||
const doc = await vscode.workspace.openTextDocument({
|
||||
content: JSON.stringify(obj, null, 2),
|
||||
language: "json"
|
||||
});
|
||||
await vscode.window.showTextDocument(doc, { preview: false });
|
||||
vscode.window.setStatusBarMessage(title, 3000);
|
||||
}
|
||||
|
||||
export async function openRun(runId: string) {
|
||||
const run = await getRun(runId);
|
||||
await showJson(`Run ${runId}`, run);
|
||||
}
|
||||
|
||||
client-ide/extensions/gcp-productos/src/extension.ts
|
||||
import * as vscode from "vscode";
|
||||
import { invokeTool, listTools } from "./api";
|
||||
import { openRun, showJson } from "./ui";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("productos.configure", async () => {
|
||||
const backendUrl = await vscode.window.showInputBox({ prompt: "Control Plane backend URL" });
|
||||
if (!backendUrl) return;
|
||||
await vscode.workspace.getConfiguration("productos").update("backendUrl", backendUrl, vscode.ConfigurationTarget.Global);
|
||||
vscode.window.showInformationMessage(`Product OS backend set: ${backendUrl}`);
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("productos.tools.list", async () => {
|
||||
const tools = await listTools();
|
||||
await showJson("Tools", tools);
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("productos.tools.invoke", async () => {
|
||||
const tools = await listTools();
|
||||
const pick = await vscode.window.showQuickPick(
|
||||
tools.map((t: any) => ({ label: t.name, description: t.description })),
|
||||
{ placeHolder: "Select a tool to invoke" }
|
||||
);
|
||||
if (!pick) return;
|
||||
|
||||
const inputText = await vscode.window.showInputBox({
|
||||
prompt: "Tool input JSON",
|
||||
value: "{}"
|
||||
});
|
||||
if (!inputText) return;
|
||||
|
||||
const input = JSON.parse(inputText);
|
||||
const result = await invokeTool(pick.label, input);
|
||||
|
||||
await showJson("Invoke Result", result);
|
||||
|
||||
if (result?.run_id) {
|
||||
const open = await vscode.window.showInformationMessage(`Run started: ${result.run_id}`, "Open Run");
|
||||
if (open === "Open Run") await openRun(result.run_id);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("productos.runs.open", async () => {
|
||||
const runId = await vscode.window.showInputBox({ prompt: "Run ID" });
|
||||
if (!runId) return;
|
||||
await openRun(runId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
|
||||
4) Generate Terraform base
|
||||
|
||||
This is a minimal hub-style baseline:
|
||||
|
||||
GCS bucket for artifacts
|
||||
|
||||
Firestore (Native) for runs/tools
|
||||
|
||||
Cloud Run service for Control Plane
|
||||
|
||||
Service accounts + IAM
|
||||
|
||||
Placeholders for executor services
|
||||
|
||||
Folder layout
|
||||
infra/terraform/
|
||||
providers.tf
|
||||
variables.tf
|
||||
outputs.tf
|
||||
main.tf
|
||||
iam.tf
|
||||
|
||||
infra/terraform/providers.tf
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.30"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
infra/terraform/variables.tf
|
||||
variable "project_id" { type = string }
|
||||
variable "region" { type = string default = "us-central1" }
|
||||
variable "artifact_bucket_name" { type = string }
|
||||
variable "control_plane_image" {
|
||||
type = string
|
||||
description = "Container image URI for control-plane (Artifact Registry)."
|
||||
}
|
||||
|
||||
infra/terraform/main.tf
|
||||
resource "google_storage_bucket" "artifacts" {
|
||||
name = var.artifact_bucket_name
|
||||
location = var.region
|
||||
uniform_bucket_level_access = true
|
||||
versioning { enabled = true }
|
||||
}
|
||||
|
||||
# Firestore (Native mode) – requires enabling in console once per project (or via API depending on org policy).
|
||||
resource "google_firestore_database" "default" {
|
||||
name = "(default)"
|
||||
location_id = var.region
|
||||
type = "FIRESTORE_NATIVE"
|
||||
}
|
||||
|
||||
resource "google_service_account" "control_plane_sa" {
|
||||
account_id = "sa-control-plane"
|
||||
display_name = "Product OS Control Plane"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service" "control_plane" {
|
||||
name = "control-plane"
|
||||
location = var.region
|
||||
|
||||
template {
|
||||
service_account = google_service_account.control_plane_sa.email
|
||||
|
||||
containers {
|
||||
image = var.control_plane_image
|
||||
env {
|
||||
name = "GCP_PROJECT_ID"
|
||||
value = var.project_id
|
||||
}
|
||||
env {
|
||||
name = "GCS_BUCKET_ARTIFACTS"
|
||||
value = google_storage_bucket.artifacts.name
|
||||
}
|
||||
env {
|
||||
name = "AUTH_MODE"
|
||||
value = "dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Public access optional; prefer IAM auth in production.
|
||||
resource "google_cloud_run_v2_service_iam_member" "control_plane_public" {
|
||||
name = google_cloud_run_v2_service.control_plane.name
|
||||
location = var.region
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
infra/terraform/iam.tf
|
||||
# Allow control-plane to write artifacts in GCS
|
||||
resource "google_storage_bucket_iam_member" "control_plane_bucket_writer" {
|
||||
bucket = google_storage_bucket.artifacts.name
|
||||
role = "roles/storage.objectAdmin"
|
||||
member = "serviceAccount:${google_service_account.control_plane_sa.email}"
|
||||
}
|
||||
|
||||
# Firestore access for run/tool metadata
|
||||
resource "google_project_iam_member" "control_plane_firestore" {
|
||||
project = var.project_id
|
||||
role = "roles/datastore.user"
|
||||
member = "serviceAccount:${google_service_account.control_plane_sa.email}"
|
||||
}
|
||||
|
||||
# Placeholder: executor services will each have their own service accounts.
|
||||
# Control-plane should be granted roles/run.invoker on each executor service once created.
|
||||
|
||||
infra/terraform/outputs.tf
|
||||
output "control_plane_url" {
|
||||
value = google_cloud_run_v2_service.control_plane.uri
|
||||
}
|
||||
|
||||
output "artifact_bucket" {
|
||||
value = google_storage_bucket.artifacts.name
|
||||
}
|
||||
Reference in New Issue
Block a user