Foundation modules adopted, and the mesh-controller/foundation rename #23
@@ -0,0 +1,13 @@
|
||||
// showcase's long-running process — a `process` that stays up.
|
||||
//
|
||||
// **Runs on the machine rather than in a container**, which is the whole point of the process
|
||||
// resource: this is the mesh's own code, it needs no isolation from the mesh, and it should not
|
||||
// need an image to run.
|
||||
const greeting = process.env.SHOWCASE_GREETING ?? "hello";
|
||||
const every = Number(process.env.SHOWCASE_EVERY_SECONDS ?? "30") * 1000;
|
||||
|
||||
console.log(`[showcase] up, saying ${greeting} every ${every / 1000}s`);
|
||||
|
||||
// A daemon that stops is not a daemon, so this does not exit. The unit restarts it if it does,
|
||||
// which is the machine's job rather than this file's.
|
||||
setInterval(() => console.log(`[showcase] ${greeting}`), every);
|
||||
@@ -0,0 +1,6 @@
|
||||
# showcase's packed files
|
||||
|
||||
Packed as an `archive` artifact and unpacked onto the machine by an `archive` resource.
|
||||
|
||||
This exists to exercise the case inlining cannot serve: a tree of files that belongs on a machine
|
||||
and would make a declaration enormous if it were carried inside one.
|
||||
@@ -0,0 +1 @@
|
||||
showcase
|
||||
@@ -0,0 +1,12 @@
|
||||
// showcase's event consumer — loaded by a tool host, not run on its own.
|
||||
//
|
||||
// **This is one of the four things a module's code can be**, and the one that is easiest to
|
||||
// forget: tools are called, a provisioner is invoked, a process runs, and a consumer simply reacts.
|
||||
// It is here so the module exercises the shape rather than describing it.
|
||||
import { on, emit } from "@novox/mesh-sdk/events";
|
||||
|
||||
await on<{ who?: string }>("module.showcase.greeted", async (event) => {
|
||||
console.log(`[showcase] greeted ${event.body.who ?? "somebody"}`);
|
||||
// A consumer may emit, which is what makes an event graph rather than a list of sinks.
|
||||
await emit("module.showcase.acknowledged", { who: event.body.who ?? "somebody" });
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"module": "showcase",
|
||||
"version": "1",
|
||||
"slug": "show",
|
||||
|
||||
"capabilities": ["container-runtime"],
|
||||
|
||||
"provides": [{ "name": "greeting", "scope": "mesh" }],
|
||||
"serves": { "greeting": { "path": "/greeting" } },
|
||||
"requires": ["postgres-database"],
|
||||
"binds": { "postgres-database": "/var/lib/showcase/database.json" },
|
||||
"secrets": { "postgres-database": "/var/lib/showcase/database.secret" },
|
||||
"own-secrets": { "broker": "/var/lib/mesh/showcase/broker" },
|
||||
|
||||
"claims": [{ "name": "the-showcase", "scope": "node" }],
|
||||
|
||||
"emits": ["module.showcase.acknowledged"],
|
||||
"consumes": ["module.showcase.greeted"],
|
||||
|
||||
"listens": [
|
||||
{ "protocol": "tcp", "from": "mesh",
|
||||
"why": "the showcase daemon answers here, so the mesh can reach it" }
|
||||
],
|
||||
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{ "name": "code", "kind": "bundle", "language": "typescript",
|
||||
"entrypoints": ["index.js", "tools/index.js", "provisioner/index.js",
|
||||
"daemon/index.js", "step/index.js", "report/index.js"] },
|
||||
{ "name": "files", "kind": "archive", "from": "files" },
|
||||
{ "name": "helper", "kind": "upstream",
|
||||
"from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" }
|
||||
]
|
||||
},
|
||||
|
||||
"resources": [
|
||||
{ "id": "account", "type": "user", "name": "showcase", "shell": "/usr/bin/nologin",
|
||||
"home": "/var/lib/showcase" },
|
||||
|
||||
{ "id": "logs", "type": "access", "path": "/var/log", "mode": "0755" },
|
||||
|
||||
{ "id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/showcase", "mode": "0700" },
|
||||
{ "id": "state", "type": "directory", "path": "/var/lib/showcase", "mode": "0755" },
|
||||
|
||||
{ "id": "settings", "type": "file", "path": "/var/lib/showcase/showcase.env", "mode": "0600",
|
||||
"content": "SHOWCASE_GREETING=hello\nSHOWCASE_EVERY_SECONDS=30\nSHOWCASE_STATE=/var/lib/showcase\nSHOWCASE_DATABASE=${bound:postgres-database:at}\n" },
|
||||
|
||||
{ "id": "packed", "type": "archive", "path": "/opt/showcase", "artifact": "files" },
|
||||
|
||||
{ "id": "net", "type": "network", "name": "showcase" },
|
||||
|
||||
{ "id": "tooling", "type": "package", "package": "jq" },
|
||||
|
||||
{ "id": "migrate", "type": "process", "name": "showcase-migrate", "artifact": "code",
|
||||
"run": ["node", "step/index.js"], "run-once": true,
|
||||
"env-file": ["/var/lib/showcase/showcase.env"] },
|
||||
|
||||
{ "id": "server", "type": "process", "name": "showcase", "artifact": "code",
|
||||
"run": ["node", "daemon/index.js"], "user": "showcase",
|
||||
"env-file": ["/var/lib/showcase/showcase.env"],
|
||||
"restart-on": ["settings"] },
|
||||
|
||||
{ "id": "reporting", "type": "process", "name": "showcase-report", "artifact": "code",
|
||||
"run": ["node", "report/index.js"], "schedule": "0 3 * * *",
|
||||
"env-file": ["/var/lib/showcase/showcase.env"] },
|
||||
|
||||
{ "id": "tools", "type": "container", "name": "mesh-showcase", "artifact": "helper",
|
||||
"network": "showcase",
|
||||
"volumes": ["/var/lib/mesh/showcase/broker:/run/secrets/broker:ro"],
|
||||
"env": { "MESH_BROKER_FILE": "/run/secrets/broker" },
|
||||
"args": ["sleep", "infinity"] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@novox/module-showcase",
|
||||
"version": "0.1.0",
|
||||
"description": "showcase — a module that exercises every capability a module has, so the module system has something that proves itself rather than a claim about what it supports.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": { "@novox/mesh-sdk": "^0.1.0" },
|
||||
"devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.6.0" }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// showcase's provisioner — how a consumer is given an instance of what this module provides.
|
||||
//
|
||||
// **A provider ships the provisioner that creates instances of the resource it offers** (ADR
|
||||
// 0040). The mesh asks; this adapts that request to whatever the software actually needs, and
|
||||
// hands back what the consumer is given.
|
||||
import { provisioner } from "@novox/mesh-sdk/provisioner";
|
||||
|
||||
await provisioner({
|
||||
provision: "greeting",
|
||||
async create({ consumer }: { consumer: string }) {
|
||||
// A real provider would create something here — a database, a vhost, an account. This one has
|
||||
// nothing to create, so it returns what a consumer is told, which is the half that matters:
|
||||
// the mesh seals it and delivers it, and the consumer never sees this code.
|
||||
return { serves: { greeting: `hello ${consumer}` } };
|
||||
},
|
||||
async remove() {
|
||||
// Removal is not optional. A provider that cannot take an instance back leaves the mesh unable
|
||||
// to unassign a consumer without leaking whatever it was given.
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// showcase's scheduled process — a `process` with a schedule, fired on a cadence.
|
||||
//
|
||||
// **Not a daemon that sleeps.** A daemon that sleeps is running between fires and holds whatever
|
||||
// it held; a scheduled process starts, does its work and exits, so what it costs between fires is
|
||||
// nothing.
|
||||
import { appendFileSync, mkdirSync } from "node:fs";
|
||||
|
||||
const where = process.env.SHOWCASE_STATE ?? "/var/lib/showcase";
|
||||
mkdirSync(where, { recursive: true });
|
||||
appendFileSync(`${where}/report`, `${new Date().toISOString()} ran\n`);
|
||||
console.log("[showcase] report written");
|
||||
@@ -0,0 +1,11 @@
|
||||
// showcase's run-once step — a `process` with run-once, run to completion at install.
|
||||
//
|
||||
// **What follows it is gated on it finishing.** A migration that did not happen must not be
|
||||
// followed by the thing that needed it, which is why a step is a mode rather than a daemon that
|
||||
// exits.
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
|
||||
const where = process.env.SHOWCASE_STATE ?? "/var/lib/showcase";
|
||||
mkdirSync(where, { recursive: true });
|
||||
writeFileSync(`${where}/installed`, `${new Date().toISOString()}\n`);
|
||||
console.log(`[showcase] step complete, wrote ${where}/installed`);
|
||||
@@ -0,0 +1,25 @@
|
||||
// showcase's tools — its operator-facing surface, served over the broker.
|
||||
//
|
||||
// Two of them, because one tool proves a tool can exist and two prove a module can have a surface.
|
||||
import { tool } from "@novox/mesh-sdk/tools";
|
||||
|
||||
tool({
|
||||
name: "showcase_greet",
|
||||
description: "Greet somebody, and say which machine did it.",
|
||||
input: { type: "object", properties: { who: { type: "string" } } },
|
||||
async run({ who }: { who?: string }) {
|
||||
return { greeting: `hello ${who ?? "world"}`, from: process.env.MESH_NODE ?? "somewhere" };
|
||||
},
|
||||
});
|
||||
|
||||
tool({
|
||||
name: "showcase_state",
|
||||
description: "What this module was configured with, so a test can read it back.",
|
||||
input: { type: "object", properties: {} },
|
||||
async run() {
|
||||
return {
|
||||
greeting: process.env.SHOWCASE_GREETING ?? "",
|
||||
database: process.env.SHOWCASE_DATABASE ?? "",
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["index.ts", "tools/index.ts", "provisioner/index.ts", "daemon/index.ts", "step/index.ts", "report/index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user