Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b09125d18 |
@@ -68,7 +68,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/api.env",
|
"path": "${dir:state}/api.env",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=mesh-novox-invoice\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "net",
|
"id": "net",
|
||||||
|
|||||||
+5
-15
@@ -303,21 +303,11 @@ export class MinioClient {
|
|||||||
|
|
||||||
// --- module-scoped helpers -------------------------------------------------
|
// --- module-scoped helpers -------------------------------------------------
|
||||||
|
|
||||||
/** A deterministic 20-char access key id from a consumer name, so removal needs no stored state:
|
// **Neither the access key nor the bucket is derived here any more.** `accessKeyFor` minted an id
|
||||||
* the provisioner recomputes the same id at teardown that it minted at creation. */
|
// of its own until the mesh took that over (ADR 0048: the login is the mesh's, handed to both
|
||||||
export function accessKeyFor(consumer: string): string {
|
// ends), and `bucketFor` derived the bucket until the mesh took that over too (ADR 0188: the rule
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
// is a line of this module's manifest, filled per consumer and delivered to both ends). Both
|
||||||
const digest = createHash("sha256").update(consumer).digest();
|
// survived with no callers, which is the state a rule comes back from; they are gone.
|
||||||
let out = "";
|
|
||||||
for (let i = 0; i < 20; i++) out += chars[digest[i] % chars.length];
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A DNS-safe bucket name derived from a consumer — the removable identity of its storage. */
|
|
||||||
export function bucketFor(consumer: string): string {
|
|
||||||
const name = consumer.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
||||||
return name.length >= 3 ? name : `mesh-${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bucketPolicy(bucket: string): string {
|
function bucketPolicy(bucket: string): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
"s3-bucket": {
|
"s3-bucket": {
|
||||||
"scheme": "http",
|
"scheme": "http",
|
||||||
"region": "eu-west",
|
"region": "eu-west",
|
||||||
"port": 9000
|
"port": 9000,
|
||||||
|
"bucket": "${consumer:as:dns}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"receives": {
|
"receives": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@novox/mesh-sdk": "^0.1.1"
|
"@novox/mesh-sdk": "^0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
@@ -10,18 +10,24 @@
|
|||||||
// **The access key and its secret are the mesh's, not the provisioner's (ADR 0048).** The mesh
|
// **The access key and its secret are the mesh's, not the provisioner's (ADR 0048).** The mesh
|
||||||
// derives the login (the access-key id) and hands it to both ends, and mints the secret key. minio
|
// derives the login (the access-key id) and hands it to both ends, and mints the secret key. minio
|
||||||
// creates the service account under exactly that access key with exactly that secret — a credential
|
// creates the service account under exactly that access key with exactly that secret — a credential
|
||||||
// the provisioner invented is one the consumer could never present. The bucket is derived from the
|
// the provisioner invented is one the consumer could never present.
|
||||||
// login, so teardown recomputes it with nothing to persist.
|
//
|
||||||
|
// **The bucket name is the mesh's too (ADR 0188).** It used to be computed here, from the login,
|
||||||
|
// and every consumer transcribed the same rule into its own definition by hand — two copies of
|
||||||
|
// one rule with nothing comparing them, and one of three was wrong for months. Now the rule is a
|
||||||
|
// line of this module's manifest (`serves.s3-bucket.bucket: ${consumer:as:dns}`), the mesh fills
|
||||||
|
// it per consumer, and the same filled value reaches this provisioner and the consumer's own
|
||||||
|
// configuration. There is no second computation to disagree with.
|
||||||
|
|
||||||
import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner";
|
import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner";
|
||||||
import { emit } from "@novox/mesh-sdk/events";
|
import { emit } from "@novox/mesh-sdk/events";
|
||||||
import { MinioClient, bucketFor } from "../client.js";
|
import { MinioClient } from "../client.js";
|
||||||
|
|
||||||
const minio = MinioClient.fromEnv();
|
const minio = MinioClient.fromEnv();
|
||||||
|
|
||||||
runProvisioner("s3-bucket", {
|
runProvisioner("s3-bucket", {
|
||||||
async create(p: Provision): Promise<void> {
|
async create(p: Provision): Promise<void> {
|
||||||
const bucket = bucketFor(p.as);
|
const bucket = bucketNamed(p.derived);
|
||||||
const accessKeyId = p.as;
|
const accessKeyId = p.as;
|
||||||
|
|
||||||
if (!(await minio.bucketExists(bucket))) await minio.createBucket(bucket);
|
if (!(await minio.bucketExists(bucket))) await minio.createBucket(bucket);
|
||||||
@@ -38,8 +44,8 @@ runProvisioner("s3-bucket", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(p: { as: string }): Promise<void> {
|
async remove(p: { as: string; derived: Readonly<Record<string, unknown>> }): Promise<void> {
|
||||||
const bucket = bucketFor(p.as);
|
const bucket = bucketNamed(p.derived);
|
||||||
|
|
||||||
// Revoking the key is what cuts the consumer's access. The bucket is emptied-then-dropped only if
|
// Revoking the key is what cuts the consumer's access. The bucket is emptied-then-dropped only if
|
||||||
// empty; a bucket that still holds objects is left for an operator rather than erroring on every
|
// empty; a bucket that still holds objects is left for an operator rather than erroring on every
|
||||||
@@ -57,10 +63,28 @@ runProvisioner("s3-bucket", {
|
|||||||
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
// Asked every minute by the harness: whether the backend still holds this consumer exactly as
|
||||||
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
// the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120).
|
||||||
async holds(p: Provision): Promise<boolean> {
|
async holds(p: Provision): Promise<boolean> {
|
||||||
return minio.canReachAs(bucketFor(p.as), p.as, p.password);
|
return minio.canReachAs(bucketNamed(p.derived), p.as, p.password);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** The bucket the mesh derived for this consumer.
|
||||||
|
*
|
||||||
|
* Absent means this module is running against a control plane that does not fill `${consumer:…}`
|
||||||
|
* yet, or a manifest whose `serves` block lost the line. Both are the same mistake from here —
|
||||||
|
* nobody said which bucket — and both are said rather than guessed: a provisioner that fell back
|
||||||
|
* to deriving one would restore the second rule and hide the fault behind a bucket that happens
|
||||||
|
* to be right. */
|
||||||
|
function bucketNamed(derived: Readonly<Record<string, unknown>>): string {
|
||||||
|
const bucket = derived.bucket;
|
||||||
|
if (typeof bucket !== "string" || bucket === "") {
|
||||||
|
throw new Error(
|
||||||
|
"the mesh did not say which bucket this consumer gets: minio's manifest must serve " +
|
||||||
|
"`bucket` under s3-bucket (novox/hq ADR 0188)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
/** Emit best-effort: a broker hiccup is logged and dropped, never allowed to throw back and fail a
|
/** Emit best-effort: a broker hiccup is logged and dropped, never allowed to throw back and fail a
|
||||||
* bucket that was made. */
|
* bucket that was made. */
|
||||||
async function announce(type: string, body: unknown): Promise<void> {
|
async function announce(type: string, body: unknown): Promise<void> {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/server.env",
|
"path": "${dir:state}/server.env",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=mesh-novox-ncloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=${bound:s3-bucket:bucket}\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "html",
|
"id": "html",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/server.env",
|
"path": "${dir:state}/server.env",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=mesh-novox-photos\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "net",
|
"id": "net",
|
||||||
|
|||||||
Reference in New Issue
Block a user