@mcpg-dev/pulumi 0.1.0-beta.11
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/BUILD.bazel +24 -0
- package/README.md +169 -0
- package/bin/cluster.d.ts +44 -0
- package/bin/cluster.js +80 -0
- package/bin/config.d.ts +30 -0
- package/bin/config.js +22 -0
- package/bin/config.test.d.ts +1 -0
- package/bin/config.test.js +36 -0
- package/bin/gateway.d.ts +52 -0
- package/bin/gateway.js +90 -0
- package/bin/index.d.ts +11 -0
- package/bin/index.js +27 -0
- package/bin/mcpgStack.d.ts +18 -0
- package/bin/mcpgStack.js +58 -0
- package/bin/oci.d.ts +8 -0
- package/bin/oci.js +41 -0
- package/bin/operator.d.ts +28 -0
- package/bin/operator.js +80 -0
- package/bin/pluginSet.d.ts +16 -0
- package/bin/pluginSet.js +55 -0
- package/bin/route.d.ts +35 -0
- package/bin/route.js +64 -0
- package/bin/tenant.d.ts +32 -0
- package/bin/tenant.js +95 -0
- package/bin/tenantCr.d.ts +32 -0
- package/bin/tenantCr.js +65 -0
- package/bin/trustBootstrap.d.ts +21 -0
- package/bin/trustBootstrap.js +56 -0
- package/copy.bara.sky +98 -0
- package/package.json +22 -0
- package/src/cluster.ts +75 -0
- package/src/config.test.ts +36 -0
- package/src/config.ts +44 -0
- package/src/gateway.ts +91 -0
- package/src/index.ts +11 -0
- package/src/mcpgStack.ts +37 -0
- package/src/oci.ts +40 -0
- package/src/operator.ts +65 -0
- package/src/pluginSet.ts +33 -0
- package/src/route.ts +45 -0
- package/src/tenant.ts +79 -0
- package/src/tenantCr.ts +45 -0
- package/src/trustBootstrap.ts +40 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,56 @@
|
|
|
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.TrustBootstrap = void 0;
|
|
37
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
38
|
+
const pulumi_crds_1 = require("@mcpg-dev/pulumi-crds");
|
|
39
|
+
/** Seeds the cluster-default MCPGRevocationList and carries the signing-key ref. */
|
|
40
|
+
class TrustBootstrap extends pulumi.ComponentResource {
|
|
41
|
+
constructor(name, args = {}, opts) {
|
|
42
|
+
super("mcpg:index:TrustBootstrap", name, {}, opts);
|
|
43
|
+
const rlName = args.revocationListName ?? "cluster-default";
|
|
44
|
+
const spec = { version: 1, revocations: args.revocations ?? [] };
|
|
45
|
+
if (args.issuedAt)
|
|
46
|
+
spec.issuedAt = args.issuedAt;
|
|
47
|
+
this.revocationList = new pulumi_crds_1.mcpg.v1alpha1.MCPGRevocationList(rlName, {
|
|
48
|
+
metadata: { name: rlName },
|
|
49
|
+
spec: spec,
|
|
50
|
+
}, { parent: this });
|
|
51
|
+
this.revocationListName = pulumi.output(rlName);
|
|
52
|
+
this.signingKeySecretRef = args.signingKeySecretRef;
|
|
53
|
+
this.registerOutputs({ revocationListName: this.revocationListName });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.TrustBootstrap = TrustBootstrap;
|
package/copy.bara.sky
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# GENERATED by tools/open-source/gen-copybara.mjs — DO NOT EDIT.
|
|
2
|
+
# One-way mirror of the @mcpg-dev/pulumi npm package from the mcpg monorepo to
|
|
3
|
+
# its own public repository. Contains NO version literals — regenerate
|
|
4
|
+
# (`--write-npm`) only when the package's identity or layout changes.
|
|
5
|
+
#
|
|
6
|
+
# Licence: Apache-2.0. Repo: github.com/mcpg-dev/pulumi-mcpg.
|
|
7
|
+
# History is SQUASHED and re-authored to the bot, so no internal commit
|
|
8
|
+
# history, subjects, or contributor identities are exposed.
|
|
9
|
+
|
|
10
|
+
# Reads "version" from the package's own package.json at MIRROR time: sets
|
|
11
|
+
# the public release message and the PKG_VERSION label the destination
|
|
12
|
+
# resolves in tag_name — so no version is baked into this file. Matched at
|
|
13
|
+
# two-space indentation so a nested "version" (the CRD SDK's generated
|
|
14
|
+
# provider block carries one) can never be mistaken for the package's.
|
|
15
|
+
def _release_meta(ctx):
|
|
16
|
+
content = ctx.read_path(ctx.new_path("package.json"))
|
|
17
|
+
version = ""
|
|
18
|
+
for line in content.split("\n"):
|
|
19
|
+
if not line.startswith(' "version"'):
|
|
20
|
+
continue
|
|
21
|
+
rest = line.split(":", 1)
|
|
22
|
+
if len(rest) < 2:
|
|
23
|
+
continue
|
|
24
|
+
version = rest[1].strip().strip(",").strip('"')
|
|
25
|
+
break
|
|
26
|
+
if version == "":
|
|
27
|
+
fail("mirror: could not read top-level \"version\" from package.json")
|
|
28
|
+
ctx.set_message("Release v" + version + "\n")
|
|
29
|
+
ctx.add_label("PKG_VERSION", version, hidden = True)
|
|
30
|
+
|
|
31
|
+
core.workflow(
|
|
32
|
+
name = "default",
|
|
33
|
+
mode = "SQUASH",
|
|
34
|
+
|
|
35
|
+
origin = git.origin(
|
|
36
|
+
url = "https://github.com/mcpg-dev/source-code.git",
|
|
37
|
+
ref = "main",
|
|
38
|
+
),
|
|
39
|
+
# push the code to main AND create the v{version} tag the public repo's
|
|
40
|
+
# release.yml fires on to self-publish to npm.
|
|
41
|
+
destination = git.github_destination(
|
|
42
|
+
url = "https://github.com/mcpg-dev/pulumi-mcpg.git",
|
|
43
|
+
push = "main",
|
|
44
|
+
tag_name = "v${PKG_VERSION}",
|
|
45
|
+
tag_msg = "Release v${PKG_VERSION}",
|
|
46
|
+
),
|
|
47
|
+
|
|
48
|
+
# The package subtree, minus internal artifacts: nx project.json,
|
|
49
|
+
# CHANGELOG.md (internal commits/PRs), AI-agent working rules, build
|
|
50
|
+
# output, and installed dependencies. Sources and tests DO travel — the
|
|
51
|
+
# tests use only node:test and node:assert, so they build and run in the
|
|
52
|
+
# public repo with nothing extra.
|
|
53
|
+
origin_files = glob(["iac/pulumi/components/**"], exclude = [
|
|
54
|
+
"iac/pulumi/components/copy.bara.sky", "iac/pulumi/components/project.json",
|
|
55
|
+
"iac/pulumi/components/BUILD.bazel", "iac/pulumi/components/**/BUILD.bazel",
|
|
56
|
+
"iac/pulumi/components/CHANGELOG.md", "iac/pulumi/components/**/AGENTS.md",
|
|
57
|
+
"iac/pulumi/components/bin/**", "iac/pulumi/components/node_modules/**",
|
|
58
|
+
"iac/pulumi/components/**/*.tsbuildinfo",
|
|
59
|
+
# Destination-managed names never travel from the origin (the
|
|
60
|
+
# destination excludes them, and Copybara refuses uncovered writes).
|
|
61
|
+
"iac/pulumi/components/.gitignore", "iac/pulumi/components/CONTRIBUTING.md",
|
|
62
|
+
"iac/pulumi/components/package-lock.json", "iac/pulumi/components/.github/**",
|
|
63
|
+
"iac/pulumi/components/**/DEFERRED.md", "iac/pulumi/components/**/TODO.md",
|
|
64
|
+
"iac/pulumi/components/**/NOTES.md", "iac/pulumi/components/**/ROADMAP.md",
|
|
65
|
+
"iac/pulumi/components/**/INTERNAL*.md",
|
|
66
|
+
]),
|
|
67
|
+
|
|
68
|
+
# Repo-only scaffold (CI, release shim, CONTRIBUTING, .gitignore) is
|
|
69
|
+
# seeded at bootstrap and excluded here so the mirror never deletes or
|
|
70
|
+
# clobbers it. LICENSE is NOT excluded — the mirror manages it.
|
|
71
|
+
destination_files = glob(["**"], exclude = [
|
|
72
|
+
".github/**", ".gitignore", "CONTRIBUTING.md", "package-lock.json",
|
|
73
|
+
]),
|
|
74
|
+
|
|
75
|
+
# Re-author every commit to the bot — no real contributor identity leaks.
|
|
76
|
+
authoring = authoring.overwrite("mcpg-bot <oss-bot@mcpg.dev>"),
|
|
77
|
+
|
|
78
|
+
transformations = [
|
|
79
|
+
# 1) subdir -> repo root.
|
|
80
|
+
core.move("iac/pulumi/components", ""),
|
|
81
|
+
|
|
82
|
+
# 2) point "repository" at this package's own public repo. Left
|
|
83
|
+
# naming the monorepo, a published package sends every reader
|
|
84
|
+
# to a repository they cannot open.
|
|
85
|
+
core.transform([
|
|
86
|
+
core.replace(
|
|
87
|
+
before = "\"repository\": \"https://github.com/mcpg-dev/source-code\"",
|
|
88
|
+
after = "\"repository\": \"https://github.com/mcpg-dev/pulumi-mcpg\"",
|
|
89
|
+
paths = glob(["package.json"]),
|
|
90
|
+
),
|
|
91
|
+
], reversal = []),
|
|
92
|
+
|
|
93
|
+
# 3) release message + PKG_VERSION label from the mirrored
|
|
94
|
+
# manifest — replaces the squashed history's message entirely: NO
|
|
95
|
+
# commit list, NO authors, NO subjects.
|
|
96
|
+
_release_meta,
|
|
97
|
+
],
|
|
98
|
+
)
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcpg-dev/pulumi",
|
|
3
|
+
"version": "0.1.0-beta.11",
|
|
4
|
+
"repository": "https://github.com/mcpg-dev/source-code",
|
|
5
|
+
"description": "Opinionated Pulumi components for MCPG (operator, gateway, tenant, trust).",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "bin/index.js",
|
|
8
|
+
"types": "bin/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@mcpg-dev/pulumi-crds": "0.1.0-beta.11",
|
|
15
|
+
"@pulumi/kubernetes": "4.31.1",
|
|
16
|
+
"@pulumi/pulumi": "^3.142.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20",
|
|
20
|
+
"typescript": "^5.4.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/cluster.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import * as k8s from "@pulumi/kubernetes";
|
|
3
|
+
import { mcpg } from "@mcpg-dev/pulumi-crds";
|
|
4
|
+
|
|
5
|
+
export interface ClusterCredential {
|
|
6
|
+
/** cred://cluster/<name>. */
|
|
7
|
+
name: string;
|
|
8
|
+
secretName: string;
|
|
9
|
+
key?: string;
|
|
10
|
+
/** When set, the component CREATES the Secret in operatorNamespace; else it is assumed to pre-exist. */
|
|
11
|
+
data?: { [key: string]: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ClusterArgs {
|
|
15
|
+
/** single_node (default) | redis | nats | consul | etcd. */
|
|
16
|
+
backend?: string;
|
|
17
|
+
/** Per-backend connection config. MUST be empty for single_node, non-empty otherwise. */
|
|
18
|
+
config?: { [key: string]: any };
|
|
19
|
+
/** Optional { name } of the cluster-scoped MCPGPlugin supplying the cdylib. */
|
|
20
|
+
pluginRef?: { name: string };
|
|
21
|
+
/** Optional [{ name, secretName, key? }] referencing PRE-EXISTING Secrets, surfaced as cred://cluster/<name>. */
|
|
22
|
+
credentialRefs?: any[];
|
|
23
|
+
/** Backend credentials this component manages — creates the Secret (when `data` is set) in operatorNamespace + wires the ref. */
|
|
24
|
+
credentials?: ClusterCredential[];
|
|
25
|
+
/** Namespace the operator runs in — where credential Secrets must live. */
|
|
26
|
+
operatorNamespace?: pulumi.Input<string>;
|
|
27
|
+
labels?: { [key: string]: string };
|
|
28
|
+
resourceName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An MCPGCluster (cluster-scoped): the coordination backend a gateway's
|
|
33
|
+
* `clusterRef` points at. `single_node` takes no config; external backends
|
|
34
|
+
* require a non-empty connection block.
|
|
35
|
+
*/
|
|
36
|
+
export class Cluster extends pulumi.ComponentResource {
|
|
37
|
+
public readonly cluster: mcpg.v1alpha1.MCPGCluster;
|
|
38
|
+
public readonly name: pulumi.Output<string>;
|
|
39
|
+
|
|
40
|
+
constructor(name: string, args: ClusterArgs = {}, opts?: pulumi.ComponentResourceOptions) {
|
|
41
|
+
super("mcpg:index:Cluster", name, {}, opts);
|
|
42
|
+
|
|
43
|
+
const cName = args.resourceName ?? name;
|
|
44
|
+
const ns = args.operatorNamespace ?? "mcpg-system";
|
|
45
|
+
|
|
46
|
+
// Managed credential Secrets (operator namespace) + their refs.
|
|
47
|
+
const managedRefs: any[] = [];
|
|
48
|
+
const secretDeps: pulumi.Resource[] = [];
|
|
49
|
+
for (const c of args.credentials ?? []) {
|
|
50
|
+
if (c.data) {
|
|
51
|
+
const sec = new k8s.core.v1.Secret(`${cName}-cred-${c.name}`, {
|
|
52
|
+
metadata: { name: c.secretName, namespace: ns },
|
|
53
|
+
stringData: c.data,
|
|
54
|
+
type: "Opaque",
|
|
55
|
+
}, { parent: this });
|
|
56
|
+
secretDeps.push(sec);
|
|
57
|
+
}
|
|
58
|
+
managedRefs.push(c.key ? { name: c.name, secretName: c.secretName, key: c.key } : { name: c.name, secretName: c.secretName });
|
|
59
|
+
}
|
|
60
|
+
const credentialRefs = [...(args.credentialRefs ?? []), ...managedRefs];
|
|
61
|
+
|
|
62
|
+
const spec: { [key: string]: any } = { backend: args.backend ?? "single_node" };
|
|
63
|
+
if (args.config && Object.keys(args.config).length > 0) spec.config = args.config;
|
|
64
|
+
if (args.pluginRef) spec.pluginRef = args.pluginRef;
|
|
65
|
+
if (credentialRefs.length > 0) spec.credentialRefs = credentialRefs;
|
|
66
|
+
|
|
67
|
+
this.cluster = new mcpg.v1alpha1.MCPGCluster(cName, {
|
|
68
|
+
metadata: { name: cName, labels: args.labels },
|
|
69
|
+
spec: spec as any,
|
|
70
|
+
}, { parent: this, dependsOn: secretDeps });
|
|
71
|
+
|
|
72
|
+
this.name = pulumi.output(cName);
|
|
73
|
+
this.registerOutputs({ name: this.name });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Unit tests for the config builder. Uses node's built-in test runner +
|
|
2
|
+
// assert (no extra deps). Run: `tsc && node --test bin/config.test.js`.
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { buildConfig } from "./config";
|
|
6
|
+
|
|
7
|
+
test("typed sections are included; absent ones omitted", () => {
|
|
8
|
+
const c = buildConfig({ governance: { audit: { sinks: [{ kind: "dev.mcpg.builtin.audit.local-file" }] } } });
|
|
9
|
+
assert.equal(c.governance.audit.sinks[0].kind, "dev.mcpg.builtin.audit.local-file");
|
|
10
|
+
assert.equal("gateway" in c, false);
|
|
11
|
+
assert.equal("observability" in c, false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("server nests under config.gateway.server (not a top-level server key)", () => {
|
|
15
|
+
const c = buildConfig({ server: { listen_addr: "0.0.0.0:8080" } });
|
|
16
|
+
assert.equal("server" in c, false);
|
|
17
|
+
assert.deepEqual(c.gateway.server, { listen_addr: "0.0.0.0:8080" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("plugins included only when non-empty", () => {
|
|
21
|
+
assert.equal("plugins" in buildConfig({ plugins: [] }), false);
|
|
22
|
+
assert.deepEqual(buildConfig({ plugins: [{ id: "db.read" }] }).plugins, [{ id: "db.read" }]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("extraConfig is merged LAST and wins", () => {
|
|
26
|
+
const c = buildConfig({
|
|
27
|
+
governance: { a: 1 },
|
|
28
|
+
extraConfig: { governance: { b: 2 }, extra: true },
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(c.governance, { b: 2 }); // extraConfig wins
|
|
31
|
+
assert.equal(c.extra, true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("empty input yields empty config", () => {
|
|
35
|
+
assert.deepEqual(buildConfig({}), {});
|
|
36
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The gateway config builder: assemble spec.config (the preserve-unknown
|
|
2
|
+
// AppConfig) from typed convenience sections plus an `extraConfig` escape hatch
|
|
3
|
+
// deep-merged LAST so it wins. Mirrors the Terraform module's config builder.
|
|
4
|
+
|
|
5
|
+
export interface GatewayConfigInput {
|
|
6
|
+
/**
|
|
7
|
+
* Gateway server/listener config. Emitted at config.gateway.server (the
|
|
8
|
+
* `gateway:` AppConfig umbrella holds server/admin/control_plane), NOT a
|
|
9
|
+
* top-level `server` key — that trips AppConfig's deny_unknown_fields.
|
|
10
|
+
*/
|
|
11
|
+
server?: any;
|
|
12
|
+
/**
|
|
13
|
+
* config.governance section (access / policy / approvals / audit). Audit
|
|
14
|
+
* sinks are a LIST, not a scalar:
|
|
15
|
+
* { audit: { sinks: [{ kind: "dev.mcpg.builtin.audit.local-file" }] } }.
|
|
16
|
+
* `dev.mcpg.builtin.audit.local-file` is the canonical built-in audit sink.
|
|
17
|
+
*/
|
|
18
|
+
governance?: any;
|
|
19
|
+
/** config.observability section. */
|
|
20
|
+
observability?: any;
|
|
21
|
+
/** config.mcp section (capabilities, etc.). */
|
|
22
|
+
mcp?: any;
|
|
23
|
+
/**
|
|
24
|
+
* Ordered config.plugins list (AppConfig PluginEntryConfig). Each entry:
|
|
25
|
+
* { id, source: { path | oci }, class?, kind?, config?, enforce?, disabled? }.
|
|
26
|
+
* The toggle is `disabled` (bool, default false) — there is no `enabled`.
|
|
27
|
+
* A `source` declaring exactly one of path/oci is required.
|
|
28
|
+
*/
|
|
29
|
+
plugins?: any[];
|
|
30
|
+
/** Arbitrary AppConfig fragment merged LAST into spec.config (wins). */
|
|
31
|
+
extraConfig?: Record<string, any>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Assemble spec.config from typed sections + extraConfig (extraConfig wins). */
|
|
35
|
+
export function buildConfig(input: GatewayConfigInput): Record<string, any> {
|
|
36
|
+
const cfg: Record<string, any> = {};
|
|
37
|
+
// `server` lives under the `gateway:` umbrella, not at the top level.
|
|
38
|
+
if (input.server) cfg.gateway = { server: input.server };
|
|
39
|
+
if (input.governance) cfg.governance = input.governance;
|
|
40
|
+
if (input.observability) cfg.observability = input.observability;
|
|
41
|
+
if (input.mcp) cfg.mcp = input.mcp;
|
|
42
|
+
if (input.plugins && input.plugins.length > 0) cfg.plugins = input.plugins;
|
|
43
|
+
return { ...cfg, ...(input.extraConfig ?? {}) };
|
|
44
|
+
}
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import { mcpg } from "@mcpg-dev/pulumi-crds";
|
|
3
|
+
import { buildConfig, GatewayConfigInput } from "./config";
|
|
4
|
+
|
|
5
|
+
export interface GatewayArgs extends GatewayConfigInput {
|
|
6
|
+
namespace: pulumi.Input<string>;
|
|
7
|
+
/** Gateway image (CRD spec.image — e.g. { repository, tag }). Required. */
|
|
8
|
+
image: any;
|
|
9
|
+
replicas?: number;
|
|
10
|
+
pluginSetRef?: pulumi.Input<string>;
|
|
11
|
+
revocationListRef?: pulumi.Input<string>;
|
|
12
|
+
/** MCPGCluster coordination backend (spec.clusterRef.name) for HA gateways. */
|
|
13
|
+
clusterRef?: pulumi.Input<string>;
|
|
14
|
+
/** Soft-tenancy: namespaces whose MCPGRoutes this gateway accepts. */
|
|
15
|
+
acceptedRouteNamespaces?: string[];
|
|
16
|
+
podAnnotations?: { [key: string]: string };
|
|
17
|
+
podLabels?: { [key: string]: string };
|
|
18
|
+
// Passthrough spec blocks (CRD spec.* — same surface as the TF module).
|
|
19
|
+
resources?: any;
|
|
20
|
+
service?: any;
|
|
21
|
+
ingress?: any;
|
|
22
|
+
autoscaling?: any;
|
|
23
|
+
monitoring?: any;
|
|
24
|
+
networkPolicy?: any;
|
|
25
|
+
podDisruptionBudget?: any;
|
|
26
|
+
workloadIdentity?: any;
|
|
27
|
+
scheduling?: any;
|
|
28
|
+
probes?: any;
|
|
29
|
+
imagePullSecrets?: any;
|
|
30
|
+
/** Arbitrary spec-level fields merged LAST (for spec keys not yet typed). */
|
|
31
|
+
extraSpec?: { [key: string]: any };
|
|
32
|
+
labels?: { [key: string]: string };
|
|
33
|
+
/** Default true → annotate `pulumi.com/waitFor: condition=Ready`. */
|
|
34
|
+
waitForReady?: boolean;
|
|
35
|
+
/** MCPGGateway metadata.name (defaults to the component name). */
|
|
36
|
+
resourceName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An MCPGGateway built from typed inputs + the config builder. Readiness is the
|
|
41
|
+
* provider's built-in await via the `pulumi.com/waitFor` annotation.
|
|
42
|
+
*/
|
|
43
|
+
export class Gateway extends pulumi.ComponentResource {
|
|
44
|
+
public readonly gateway: mcpg.v1alpha1.MCPGGateway;
|
|
45
|
+
public readonly name: pulumi.Output<string>;
|
|
46
|
+
|
|
47
|
+
constructor(name: string, args: GatewayArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
48
|
+
super("mcpg:index:Gateway", name, {}, opts);
|
|
49
|
+
|
|
50
|
+
const gwName = args.resourceName ?? name;
|
|
51
|
+
const spec: { [key: string]: any } = {
|
|
52
|
+
image: args.image,
|
|
53
|
+
config: buildConfig(args),
|
|
54
|
+
};
|
|
55
|
+
if (args.replicas !== undefined) spec.replicas = args.replicas;
|
|
56
|
+
if (args.pluginSetRef) spec.pluginSetRef = { name: args.pluginSetRef };
|
|
57
|
+
if (args.revocationListRef) spec.revocationListRef = { name: args.revocationListRef };
|
|
58
|
+
if (args.clusterRef) spec.clusterRef = { name: args.clusterRef };
|
|
59
|
+
|
|
60
|
+
// Typed passthrough blocks (same spec surface as the TF gateway module).
|
|
61
|
+
const optional: { [key: string]: any } = {
|
|
62
|
+
acceptedRouteNamespaces: args.acceptedRouteNamespaces,
|
|
63
|
+
podAnnotations: args.podAnnotations,
|
|
64
|
+
podLabels: args.podLabels,
|
|
65
|
+
resources: args.resources,
|
|
66
|
+
service: args.service,
|
|
67
|
+
ingress: args.ingress,
|
|
68
|
+
autoscaling: args.autoscaling,
|
|
69
|
+
monitoring: args.monitoring,
|
|
70
|
+
networkPolicy: args.networkPolicy,
|
|
71
|
+
podDisruptionBudget: args.podDisruptionBudget,
|
|
72
|
+
workloadIdentity: args.workloadIdentity,
|
|
73
|
+
scheduling: args.scheduling,
|
|
74
|
+
probes: args.probes,
|
|
75
|
+
imagePullSecrets: args.imagePullSecrets,
|
|
76
|
+
};
|
|
77
|
+
for (const [k, v] of Object.entries(optional)) if (v !== undefined) spec[k] = v;
|
|
78
|
+
Object.assign(spec, args.extraSpec ?? {}); // escape hatch wins (matches TF extra_spec)
|
|
79
|
+
|
|
80
|
+
const annotations: { [key: string]: string } =
|
|
81
|
+
args.waitForReady === false ? {} : { "pulumi.com/waitFor": "condition=Ready" };
|
|
82
|
+
|
|
83
|
+
this.gateway = new mcpg.v1alpha1.MCPGGateway(gwName, {
|
|
84
|
+
metadata: { name: gwName, namespace: args.namespace, labels: args.labels, annotations },
|
|
85
|
+
spec: spec as any,
|
|
86
|
+
}, { parent: this });
|
|
87
|
+
|
|
88
|
+
this.name = pulumi.output(gwName);
|
|
89
|
+
this.registerOutputs({ name: this.name });
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./config";
|
|
2
|
+
export * from "./oci";
|
|
3
|
+
export * from "./operator";
|
|
4
|
+
export * from "./gateway";
|
|
5
|
+
export * from "./pluginSet";
|
|
6
|
+
export * from "./trustBootstrap";
|
|
7
|
+
export * from "./tenant";
|
|
8
|
+
export * from "./cluster";
|
|
9
|
+
export * from "./route";
|
|
10
|
+
export * from "./tenantCr";
|
|
11
|
+
export * from "./mcpgStack";
|
package/src/mcpgStack.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import { Operator, OperatorArgs } from "./operator";
|
|
3
|
+
import { Gateway, GatewayArgs } from "./gateway";
|
|
4
|
+
import { TrustBootstrap } from "./trustBootstrap";
|
|
5
|
+
|
|
6
|
+
export interface McpgStackArgs {
|
|
7
|
+
operator: OperatorArgs;
|
|
8
|
+
/** Optional single gateway for the common case. */
|
|
9
|
+
gateway?: Omit<GatewayArgs, "namespace"> & { namespace?: pulumi.Input<string> };
|
|
10
|
+
/** Seed the cluster-default revocation list (default true). */
|
|
11
|
+
trust?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Convenience composition: operator + cluster trust + one gateway. */
|
|
15
|
+
export class McpgStack extends pulumi.ComponentResource {
|
|
16
|
+
public readonly operator: Operator;
|
|
17
|
+
public readonly gateway?: Gateway;
|
|
18
|
+
|
|
19
|
+
constructor(name: string, args: McpgStackArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
20
|
+
super("mcpg:index:McpgStack", name, {}, opts);
|
|
21
|
+
|
|
22
|
+
this.operator = new Operator(`${name}-operator`, args.operator, { parent: this });
|
|
23
|
+
|
|
24
|
+
if (args.trust !== false) {
|
|
25
|
+
new TrustBootstrap(`${name}-trust`, {}, { parent: this, dependsOn: this.operator });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (args.gateway) {
|
|
29
|
+
this.gateway = new Gateway(`${name}-gateway`, {
|
|
30
|
+
...args.gateway,
|
|
31
|
+
namespace: args.gateway.namespace ?? this.operator.namespace,
|
|
32
|
+
}, { parent: this, dependsOn: this.operator });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.registerOutputs({});
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/oci.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Registry coordinates every MCPG artefact hangs off. The defaults mirror the
|
|
2
|
+
// `internal` channel of tools/release/oci-registry.json — the manifest the
|
|
3
|
+
// publish steps read — and tools/ci/selftest-oci-registry.sh asserts the two
|
|
4
|
+
// still agree.
|
|
5
|
+
//
|
|
6
|
+
// A Pulumi program overrides per component (`chartRepo`, an explicit image
|
|
7
|
+
// repository) or, for a whole stack, through the same environment contract the
|
|
8
|
+
// shell and Node publish helpers honour:
|
|
9
|
+
//
|
|
10
|
+
// MCPG_OCI_HOST registry host (default: ghcr.io)
|
|
11
|
+
// MCPG_OCI_NAMESPACE org / user segment (default: mcpg-dev)
|
|
12
|
+
// MCPG_OCI_PATH repository path segment (default: source-code)
|
|
13
|
+
//
|
|
14
|
+
// MCPG_OCI_PATH set-but-empty means "no path segment" — the shape the public
|
|
15
|
+
// channel takes — so it is read by presence, not by truthiness.
|
|
16
|
+
|
|
17
|
+
const trim = (s: string): string => s.replace(/^\/+|\/+$/g, "");
|
|
18
|
+
|
|
19
|
+
/** The resolved `<host>/<namespace>[/<path>]` base. */
|
|
20
|
+
export function ociBase(env: NodeJS.ProcessEnv = process.env): string {
|
|
21
|
+
const host = trim(env.MCPG_OCI_HOST ?? "ghcr.io");
|
|
22
|
+
const namespace = trim(env.MCPG_OCI_NAMESPACE ?? "mcpg-dev");
|
|
23
|
+
const path = trim("MCPG_OCI_PATH" in env ? (env.MCPG_OCI_PATH ?? "") : "source-code");
|
|
24
|
+
return [host, namespace, path].filter(Boolean).join("/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Helm OCI repository the charts publish to (`repositoryOpts.repo`). */
|
|
28
|
+
export function ociChartRepo(env: NodeJS.ProcessEnv = process.env): string {
|
|
29
|
+
return `oci://${ociBase(env)}/charts`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Container image repository for a project's `release.docker.imageName`. */
|
|
33
|
+
export function ociImageRepo(imageName: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
34
|
+
return `${ociBase(env)}/${trim(imageName)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Registry a bare `oci:` plugin reference resolves against. */
|
|
38
|
+
export function ociPluginRegistry(env: NodeJS.ProcessEnv = process.env): string {
|
|
39
|
+
return `${ociBase(env)}/plugins`;
|
|
40
|
+
}
|
package/src/operator.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import * as k8s from "@pulumi/kubernetes";
|
|
3
|
+
import { ociChartRepo } from "./oci";
|
|
4
|
+
|
|
5
|
+
export interface OperatorArgs {
|
|
6
|
+
namespace?: pulumi.Input<string>;
|
|
7
|
+
chartVersion: pulumi.Input<string>;
|
|
8
|
+
chartName?: pulumi.Input<string>;
|
|
9
|
+
/** Helm repository. Unset composes `oci://<ociBase()>/charts`. */
|
|
10
|
+
chartRepo?: pulumi.Input<string>;
|
|
11
|
+
/** Path to the split-by-kind CRD YAMLs (the generated schema snapshot). */
|
|
12
|
+
crdsDir?: string;
|
|
13
|
+
/** Restrict the operator to a single namespace (operator.watchNamespace → --watch-namespace). */
|
|
14
|
+
watchNamespace?: pulumi.Input<string>;
|
|
15
|
+
/** Extra chart values deep-merged last. */
|
|
16
|
+
values?: { [key: string]: any };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Installs the MCPG operator via Helm with the CRDs managed as first-class
|
|
21
|
+
* resources (chart `crd.install=false`) so they upgrade independently — closing
|
|
22
|
+
* the Helm-doesn't-upgrade-CRDs hazard. Pulumi's K8s provider does SSA by
|
|
23
|
+
* default and awaits readiness.
|
|
24
|
+
*/
|
|
25
|
+
export class Operator extends pulumi.ComponentResource {
|
|
26
|
+
public readonly namespace: pulumi.Output<string>;
|
|
27
|
+
public readonly chart: k8s.helm.v4.Chart;
|
|
28
|
+
|
|
29
|
+
constructor(name: string, args: OperatorArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
30
|
+
super("mcpg:index:Operator", name, {}, opts);
|
|
31
|
+
|
|
32
|
+
const ns = pulumi.output(args.namespace ?? "mcpg-system");
|
|
33
|
+
const crdsDir = args.crdsDir ?? "../../codegen/schemas/v1alpha1/crds";
|
|
34
|
+
|
|
35
|
+
const crds = new k8s.yaml.v2.ConfigGroup(`${name}-crds`, {
|
|
36
|
+
files: [`${crdsDir}/*.yaml`],
|
|
37
|
+
}, { parent: this });
|
|
38
|
+
|
|
39
|
+
this.chart = new k8s.helm.v4.Chart(name, {
|
|
40
|
+
chart: args.chartName ?? "mcpg-operator",
|
|
41
|
+
version: args.chartVersion,
|
|
42
|
+
namespace: ns,
|
|
43
|
+
repositoryOpts: {
|
|
44
|
+
repo: args.chartRepo ?? ociChartRepo(),
|
|
45
|
+
},
|
|
46
|
+
// The chart ships CRDs under crds/ (Helm install-only, value-ungated);
|
|
47
|
+
// the ConfigGroup above owns them as first-class resources, so skip the
|
|
48
|
+
// chart's copy to avoid two Pulumi resources managing the same CRDs.
|
|
49
|
+
skipCrds: true,
|
|
50
|
+
// Secure default: the admission webhook fails closed, so it needs real
|
|
51
|
+
// TLS. cert-manager templates a self-signed Issuer + Certificate and
|
|
52
|
+
// injects the caBundle. Requires cert-manager in the cluster; override
|
|
53
|
+
// via values.certManager (e.g. { enabled: false } for BYO TLS Secret).
|
|
54
|
+
values: {
|
|
55
|
+
crd: { install: false },
|
|
56
|
+
certManager: { enabled: true },
|
|
57
|
+
...(args.watchNamespace ? { operator: { watchNamespace: args.watchNamespace } } : {}),
|
|
58
|
+
...(args.values ?? {}),
|
|
59
|
+
},
|
|
60
|
+
}, { parent: this, dependsOn: crds });
|
|
61
|
+
|
|
62
|
+
this.namespace = ns;
|
|
63
|
+
this.registerOutputs({ namespace: this.namespace });
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/pluginSet.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import { mcpg } from "@mcpg-dev/pulumi-crds";
|
|
3
|
+
|
|
4
|
+
export interface PluginSetArgs {
|
|
5
|
+
namespace: pulumi.Input<string>;
|
|
6
|
+
entries: any[];
|
|
7
|
+
capabilityGrants?: any[];
|
|
8
|
+
labels?: { [key: string]: string };
|
|
9
|
+
resourceName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class PluginSet extends pulumi.ComponentResource {
|
|
13
|
+
public readonly pluginSet: mcpg.v1alpha1.MCPGPluginSet;
|
|
14
|
+
public readonly name: pulumi.Output<string>;
|
|
15
|
+
|
|
16
|
+
constructor(name: string, args: PluginSetArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
17
|
+
super("mcpg:index:PluginSet", name, {}, opts);
|
|
18
|
+
|
|
19
|
+
const psName = args.resourceName ?? name;
|
|
20
|
+
const spec: { [key: string]: any } = { entries: args.entries };
|
|
21
|
+
if (args.capabilityGrants && args.capabilityGrants.length > 0) {
|
|
22
|
+
spec.capabilityGrants = args.capabilityGrants;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.pluginSet = new mcpg.v1alpha1.MCPGPluginSet(psName, {
|
|
26
|
+
metadata: { name: psName, namespace: args.namespace, labels: args.labels },
|
|
27
|
+
spec: spec as any,
|
|
28
|
+
}, { parent: this });
|
|
29
|
+
|
|
30
|
+
this.name = pulumi.output(psName);
|
|
31
|
+
this.registerOutputs({ name: this.name });
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/route.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import { mcpg } from "@mcpg-dev/pulumi-crds";
|
|
3
|
+
|
|
4
|
+
export interface RouteArgs {
|
|
5
|
+
namespace: pulumi.Input<string>;
|
|
6
|
+
/** Shared gateway this route binds into: { name } (non-empty). */
|
|
7
|
+
gatewayRef: { name: string };
|
|
8
|
+
/** Tool matcher: { tools: [{ id }] } — at least one unique, non-empty id. */
|
|
9
|
+
match: { tools: { id: string }[] };
|
|
10
|
+
identityChain?: string[];
|
|
11
|
+
policyChain?: string[];
|
|
12
|
+
auditChain?: string[];
|
|
13
|
+
/** e.g. { tenant: "team-a" }. Omit only for a single-tenant gateway (else reachable by any caller). */
|
|
14
|
+
attributes?: { [key: string]: any };
|
|
15
|
+
labels?: { [key: string]: string };
|
|
16
|
+
resourceName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An MCPGRoute (namespaced): binds a tenant's tool subset into a SHARED gateway
|
|
21
|
+
* with identity/policy/audit chains — the soft-multi-tenancy primitive.
|
|
22
|
+
*/
|
|
23
|
+
export class Route extends pulumi.ComponentResource {
|
|
24
|
+
public readonly route: mcpg.v1alpha1.MCPGRoute;
|
|
25
|
+
public readonly name: pulumi.Output<string>;
|
|
26
|
+
|
|
27
|
+
constructor(name: string, args: RouteArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
28
|
+
super("mcpg:index:Route", name, {}, opts);
|
|
29
|
+
|
|
30
|
+
const rName = args.resourceName ?? name;
|
|
31
|
+
const spec: { [key: string]: any } = { gatewayRef: args.gatewayRef, match: args.match };
|
|
32
|
+
if (args.identityChain && args.identityChain.length > 0) spec.identityChain = args.identityChain;
|
|
33
|
+
if (args.policyChain && args.policyChain.length > 0) spec.policyChain = args.policyChain;
|
|
34
|
+
if (args.auditChain && args.auditChain.length > 0) spec.auditChain = args.auditChain;
|
|
35
|
+
if (args.attributes) spec.attributes = args.attributes;
|
|
36
|
+
|
|
37
|
+
this.route = new mcpg.v1alpha1.MCPGRoute(rName, {
|
|
38
|
+
metadata: { name: rName, namespace: args.namespace, labels: args.labels },
|
|
39
|
+
spec: spec as any,
|
|
40
|
+
}, { parent: this });
|
|
41
|
+
|
|
42
|
+
this.name = pulumi.output(rName);
|
|
43
|
+
this.registerOutputs({ name: this.name });
|
|
44
|
+
}
|
|
45
|
+
}
|