mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 19:57:37 +00:00
23bbee4f45
* feat(mesh): replace host-mode with shared sencho_mesh Docker network Phase D of the mesh redesign: drop the operator's `network_mode: host` requirement and the `host-gateway` extra_hosts pattern that did not work on cloud iptables-restrictive distros (OCI, etc.) or Docker Desktop. Each Sencho creates a shared `sencho_mesh` Docker bridge network on boot (default subnet 172.30.0.0/24, override via SENCHO_MESH_SUBNET), pins itself at `<network>+2`, and attaches every meshed user service to the same bridge. Compose overrides now emit IP-based `extra_hosts` plus a top-level `networks` block declaring `sencho_mesh` external. Override delivery: central renders for local stacks; for remote stacks it sends the fleet alias list to the remote's new `PUT /api/mesh/local- override/:stackName` endpoint, which renders against the remote's OWN local senchoIp and writes under its OWN DATA_DIR. Each node may use a different subnet without coordination beyond the env var. Opt-in / opt-out now trigger an automatic redeploy of the affected stack via the existing deploy code path (local: ComposeService; remote: HTTP POST through proxyFetch). The frontend opt-in sheet shows a confirmation modal (ConfirmModal) before the mutation. Failed redeploys emit both a mesh activity event and a durable audit-log row. Hardening: - Reserve port 1852 at opt-in (prevents user containers from racing the Sencho API listener). - ensureMeshNetwork refuses to continue if `sencho_mesh` exists with a mismatched subnet rather than silently routing to the wrong IP. - Idempotent network connect/disconnect helpers in DockerController. - optInStack rolls back the DB row if the just-inserted stack's override push fails (no half-states surviving across calls). - regenerateOverridesForNode runs in parallel and skips the just- pushed stack on opt-in. Operator template: drop `network_mode: host`, restore `ports: ["1852:1852"]`. Mesh now works identically on Linux LAN, OCI, and Docker Desktop without firewall changes. Docs: rewrite docs/features/sencho-mesh.mdx around the shared bridge network, document SENCHO_MESH_SUBNET, surface the host-network-service opt-in restriction, and cross-link with the Pilot Agent docs. BREAKING CHANGE: the operator's `docker-compose.yml` no longer uses `network_mode: host`. After upgrading, redeploy any meshed stacks once so they pick up the new IP-based override and join `sencho_mesh`. * fix(mesh): wrap stackName with path.basename in local-override fs ops CodeQL flagged js/path-injection on the new applyLocalOverride and removeLocalOverride methods because they are publicly reachable and its data-flow model does not recognize isValidStackName / isPathWithinBase as sanitizers. The validation IS sufficient (the allowlist regex blocks path separators, the path-prefix check blocks escape), but path.basename is a model CodeQL recognizes and is purely defensive: for any input that already passes isValidStackName, basename is the identity.
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import * as YAML from 'yaml';
|
|
import { buildAliasHosts, generateOverrideYaml, SENCHO_MESH_NETWORK } from '../services/MeshComposeOverride';
|
|
|
|
const SENCHO_IP = '172.30.0.2';
|
|
|
|
describe('generateOverrideYaml', () => {
|
|
it('emits services with extra_hosts pointing to the Sencho mesh IP', () => {
|
|
const yaml = generateOverrideYaml({
|
|
services: ['web', 'cache'],
|
|
aliases: [
|
|
{ host: 'db.api.opsix.sencho' },
|
|
{ host: 'etl.worker.opsix.sencho' },
|
|
],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
const parsed = YAML.parse(yaml) as Record<string, unknown>;
|
|
const services = parsed.services as Record<string, { extra_hosts: string[]; networks: string[] }>;
|
|
expect(Object.keys(services).sort()).toEqual(['cache', 'web']);
|
|
for (const svc of ['web', 'cache']) {
|
|
expect(services[svc].extra_hosts).toEqual([
|
|
`db.api.opsix.sencho:${SENCHO_IP}`,
|
|
`etl.worker.opsix.sencho:${SENCHO_IP}`,
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('attaches every service to the sencho_mesh network', () => {
|
|
const yaml = generateOverrideYaml({
|
|
services: ['web', 'cache', 'worker'],
|
|
aliases: [{ host: 'db.api.opsix.sencho' }],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
const parsed = YAML.parse(yaml) as Record<string, unknown>;
|
|
const services = parsed.services as Record<string, { networks: string[] }>;
|
|
for (const svc of ['web', 'cache', 'worker']) {
|
|
expect(services[svc].networks).toEqual([SENCHO_MESH_NETWORK]);
|
|
}
|
|
});
|
|
|
|
it('declares sencho_mesh as an external network at the top level', () => {
|
|
const yaml = generateOverrideYaml({
|
|
services: ['web'],
|
|
aliases: [],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
const parsed = YAML.parse(yaml) as Record<string, unknown>;
|
|
const networks = parsed.networks as Record<string, { external: boolean }>;
|
|
expect(networks[SENCHO_MESH_NETWORK]).toEqual({ external: true });
|
|
});
|
|
|
|
it('still attaches services to the network when no aliases exist yet', () => {
|
|
const yaml = generateOverrideYaml({
|
|
services: ['web'],
|
|
aliases: [],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
const parsed = YAML.parse(yaml) as Record<string, unknown>;
|
|
const services = parsed.services as Record<string, { extra_hosts?: string[]; networks: string[] }>;
|
|
expect(services.web.extra_hosts).toBeUndefined();
|
|
expect(services.web.networks).toEqual([SENCHO_MESH_NETWORK]);
|
|
});
|
|
|
|
it('produces stable output regardless of input ordering', () => {
|
|
const a = generateOverrideYaml({
|
|
services: ['web', 'cache'],
|
|
aliases: [
|
|
{ host: 'b.x.y.sencho' },
|
|
{ host: 'a.x.y.sencho' },
|
|
],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
const b = generateOverrideYaml({
|
|
services: ['cache', 'web'],
|
|
aliases: [
|
|
{ host: 'a.x.y.sencho' },
|
|
{ host: 'b.x.y.sencho' },
|
|
],
|
|
senchoIp: SENCHO_IP,
|
|
});
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it('uses the senchoIp argument verbatim in extra_hosts entries', () => {
|
|
const customIp = '10.42.7.99';
|
|
const yaml = generateOverrideYaml({
|
|
services: ['web'],
|
|
aliases: [{ host: 'svc.stack.node.sencho' }],
|
|
senchoIp: customIp,
|
|
});
|
|
expect(yaml).toContain(`svc.stack.node.sencho:${customIp}`);
|
|
});
|
|
});
|
|
|
|
describe('buildAliasHosts', () => {
|
|
it('maps services to alias hostnames', () => {
|
|
const out = buildAliasHosts({
|
|
nodeName: 'opsix',
|
|
stackName: 'api',
|
|
services: [{ service: 'db', ports: [5432] }, { service: 'cache', ports: [6379] }],
|
|
});
|
|
expect(out).toEqual(['db.api.opsix.sencho', 'cache.api.opsix.sencho']);
|
|
});
|
|
});
|