From a309deb4796daf3d159ab0b1ec73958419e4724e Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 23:25:45 +0200 Subject: [PATCH] Remove portainer: deprecated, and unassigned everywhere It held the docker socket behind a public name. Nothing depends on it; it is off both machines that ran it, with its data. --- modules/portainer/Dockerfile | 30 -------- modules/portainer/client.ts | 107 ----------------------------- modules/portainer/module.json | 114 ------------------------------- modules/portainer/package.json | 14 ---- modules/portainer/tools/index.ts | 50 -------------- modules/portainer/tsconfig.json | 12 ---- 6 files changed, 327 deletions(-) delete mode 100644 modules/portainer/Dockerfile delete mode 100644 modules/portainer/client.ts delete mode 100644 modules/portainer/module.json delete mode 100644 modules/portainer/package.json delete mode 100644 modules/portainer/tools/index.ts delete mode 100644 modules/portainer/tsconfig.json diff --git a/modules/portainer/Dockerfile b/modules/portainer/Dockerfile deleted file mode 100644 index 17820ac..0000000 --- a/modules/portainer/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# portainer's runtime: the tool runtime, carrying this module's compiled code. -# -# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in -# the base images, published like any other artifact — which is what makes this buildable by the -# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that -# happens to have the siblings. -# -# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the -# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`. -ARG BUILD_BASE -ARG RUNTIME_BASE - -FROM ${BUILD_BASE} AS build -# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own -# node_modules — the module is compiled against exactly the sdk it will run against. The compiler -# is invoked by its real path: node_modules/.bin entries are launcher symlinks the base image -# resolved away. -WORKDIR /app/modules/portainer -COPY . . -RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \ - --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist - -FROM ${RUNTIME_BASE} -COPY --from=build /app/modules/portainer/dist /app/modules/portainer/dist -# Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a -# provider's provisioner runs its reconcile loop in the same process, with the broker connected — -# the convention novox/hq issues 060/061 settled. A container that instead ran only its -# provisioner (`run`) served no tools and emitted no events; a container that named no command -# ran no provisioner at all. -ENV MESH_TOOL_MODULES=/app/modules/portainer/dist/tools/index.js diff --git a/modules/portainer/client.ts b/modules/portainer/client.ts deleted file mode 100644 index 2a92393..0000000 --- a/modules/portainer/client.ts +++ /dev/null @@ -1,107 +0,0 @@ -// The Portainer API client — portainer's own code, living in the module (novox/hq ADR 0039). -// portainer is tools-only: its "events" would really be the underlying containers' lifecycle, -// which the host owns and emits — so this module reads Portainer's own resources (endpoints, -// stacks, containers) and exposes them, and stops there. - -import { readFileSync } from "node:fs"; - -export interface PortainerEndpoint { - id: number; - name: string; - type: number; - url: string; - status: number; -} - -export interface PortainerStack { - id: number; - name: string; - type: number; - endpointId: number; - status: number; -} - -export interface PortainerContainer { - id: string; - names: string[]; - image: string; - state: string; - status: string; -} - -/** The settings-merged config the mesh delivers (novox/hq ADR 0046): { url, apiKey, token, password, user, ... }. */ -function meshConfig(file?: string): Record { - if (!file) return {}; - try { return JSON.parse(readFileSync(file, "utf8")) as Record; } - catch { return {}; } -} - -export class PortainerClient { - readonly baseUrl: string; - - constructor( - url: string, - private readonly token: string, - ) { - this.baseUrl = url.replace(/\/+$/, ""); - } - - /** - * Build from the module's resolved environment. The URL is MESH_PORTAINER_URL (or the local - * dashboard port) and the API token is MESH_PORTAINER_TOKEN — an access token minted in - * Portainer, sent as X-API-Key. Throws when no token is configured, so a misconfigured module - * exposes nothing rather than calling Portainer unauthenticated. - */ - static fromEnv(env: NodeJS.ProcessEnv = process.env): PortainerClient { - const cfg = meshConfig(env.MESH_PORTAINER_CONFIG_FILE); - const url = cfg.url ?? env.MESH_PORTAINER_URL ?? `https://127.0.0.1:${env.PORTAINER_PORT ?? "9443"}`; - const token = cfg.token ?? env.MESH_PORTAINER_TOKEN; - if (!token) throw new Error("no Portainer token — set MESH_PORTAINER_TOKEN"); - return new PortainerClient(url, token); - } - - private async get(path: string): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { headers: { "X-API-Key": this.token } }); - if (!res.ok) throw new Error(`Portainer ${path}: ${res.status} ${await res.text()}`); - return res.json() as Promise; - } - - /** The environments (endpoints) Portainer manages — each a Docker host or cluster it talks to. */ - async listEndpoints(): Promise { - const raw = await this.get("/api/endpoints"); - return (raw ?? []).map((e) => ({ - id: e.Id, - name: e.Name, - type: e.Type, - url: e.URL, - status: e.Status, - })); - } - - /** The stacks (compose/swarm deployments) Portainer knows about. */ - async listStacks(): Promise { - const raw = await this.get("/api/stacks"); - return (raw ?? []).map((s) => ({ - id: s.Id, - name: s.Name, - type: s.Type, - endpointId: s.EndpointId, - status: s.Status, - })); - } - - /** - * The containers on one endpoint, read through Portainer's Docker API proxy. Includes stopped - * containers, so the caller sees the whole picture rather than only what is running. - */ - async listContainers(endpointId: number): Promise { - const raw = await this.get(`/api/endpoints/${endpointId}/docker/containers/json?all=1`); - return (raw ?? []).map((c) => ({ - id: c.Id, - names: c.Names ?? [], - image: c.Image, - state: c.State, - status: c.Status, - })); - } -} diff --git a/modules/portainer/module.json b/modules/portainer/module.json deleted file mode 100644 index 0e94398..0000000 --- a/modules/portainer/module.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "module": "portainer", - "version": "1", - "slug": "portain", - "capabilities": [ - "container-runtime" - ], - "listens": [ - { - "name": "web", - "port": 9000, - "protocol": "tcp", - "from": "mesh", - "why": "the dashboard over http; its public name is a route grant and the proxy reaches it here" - }, - { - "name": "web-tls", - "port": 9443, - "protocol": "tcp", - "from": "mesh", - "why": "the same dashboard over its own tls; the runtime sidecar talks to it here" - } - ], - "resources": [ - { - "id": "mesh-state", - "type": "directory", - "mode": "0700", - "place": "mesh" - }, - { - "id": "data", - "type": "directory", - "mode": "0700" - }, - { - "id": "server", - "type": "container", - "name": "portainer", - "image": "portainer/portainer-ce@sha256:4d616db18cfeb5dd41a69c0958bc825c84483ea9cde1106eb82a5d26f3bd8b0e", - "ports": [ - "9000", - "9443" - ], - "volumes": [ - "${dir:data}:/data", - "/var/run/docker.sock:/var/run/docker.sock" - ] - }, - { - "id": "runtime-config", - "type": "file", - "path": "${dir:mesh-state}/config.json", - "mode": "0600", - "content": "{}\n", - "merge": "json" - }, - { - "id": "runtime", - "type": "container", - "name": "mesh-portainer", - "network": "host", - "volumes": [ - "${dir:mesh-state}/broker:/run/secrets/broker:ro", - "${dir:mesh-state}/config.json:/run/config/config.json:ro" - ], - "env": { - "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_PORTAINER_URL": "https://127.0.0.1:9443", - "MESH_PORTAINER_CONFIG_FILE": "/run/config/config.json" - }, - "restart-on": [ - "runtime-config" - ], - "artifact": "runtime" - } - ], - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, - "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], - "artifacts": [ - { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" - } - ] - }, - "requires": [ - "route" - ], - "contributes": { - "route": { - "label": "portainer", - "endpoint": "web" - } - }, - "binds": { - "route": "${dir:mesh-state}/route.json" - } -} diff --git a/modules/portainer/package.json b/modules/portainer/package.json deleted file mode 100644 index b9726e8..0000000 --- a/modules/portainer/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@novox/module-portainer", - "version": "0.1.0", - "description": "portainer — container management UI. Its API client and tools live here (novox/hq ADR 0039).", - "type": "module", - "private": true, - "dependencies": { - "@novox/mesh-sdk": "^0.1.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.6.0" - } -} diff --git a/modules/portainer/tools/index.ts b/modules/portainer/tools/index.ts deleted file mode 100644 index e750659..0000000 --- a/modules/portainer/tools/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -// portainer's tools — its own code (novox/hq ADR 0039), importing portainer's own client. They -// return structured data; the mesh serves them through the sdk's tool harness. portainer is -// tools-only (no events entrypoint): a container starting or stopping is the host's signal to emit, -// not Portainer's to re-announce. - -import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; -import { PortainerClient } from "../client.js"; - -export function getPortainerTools(portainer: PortainerClient): ToolDefinition[] { - return [ - { - name: "portainer_endpoints", - description: "List the environments (endpoints) Portainer manages — each a Docker host or cluster.", - input: {}, - run: async () => { - const endpoints = await portainer.listEndpoints(); - return { count: endpoints.length, endpoints }; - }, - }, - { - name: "portainer_stacks", - description: "List the stacks (compose/swarm deployments) Portainer knows about.", - input: {}, - run: async () => { - const stacks = await portainer.listStacks(); - return { count: stacks.length, stacks }; - }, - }, - { - name: "portainer_containers", - description: "List the containers on one Portainer endpoint, including stopped ones.", - input: { endpoint: { type: "number", description: "the endpoint id (see portainer_endpoints)" } }, - run: async (args) => { - const endpointId = Number(args.endpoint); - const containers = await portainer.listContainers(endpointId); - return { endpointId, count: containers.length, containers }; - }, - }, - ]; -} - -// The tools exist only when a token is configured; without one, portainer contributes none rather -// than failing the whole runtime. -registerModuleTools("portainer", (env) => { - try { - return getPortainerTools(PortainerClient.fromEnv(env)); - } catch { - return []; - } -}); diff --git a/modules/portainer/tsconfig.json b/modules/portainer/tsconfig.json deleted file mode 100644 index 426d382..0000000 --- a/modules/portainer/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["client.ts", "tools/index.ts"] -}