Add Edge Broker service and rename 'rows' column to 'terminal_rows' in Edge Gateway schema

This commit is contained in:
Jeppe Bundgaard
2026-04-22 19:31:21 +02:00
parent 4c32eae49a
commit e09e23025d
7 changed files with 88 additions and 6 deletions
+31
View File
@@ -80,6 +80,16 @@ services:
retries: 10 retries: 10
start_period: 20s start_period: 20s
edge-broker:
build:
context: .
dockerfile: services/edge-broker/Dockerfile
container_name: edge-broker
environment:
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
ports:
- "4300:4300"
caddy: caddy:
image: caddy:2.7.6-alpine image: caddy:2.7.6-alpine
container_name: caddy container_name: caddy
@@ -196,11 +206,14 @@ services:
container_name: php1 container_name: php1
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "true" AUTO_COMPOSER_INSTALL: "true"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -213,11 +226,14 @@ services:
container_name: php2 container_name: php2
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -230,11 +246,14 @@ services:
container_name: php3 container_name: php3
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -247,11 +266,14 @@ services:
container_name: php4 container_name: php4
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -264,11 +286,14 @@ services:
container_name: php5 container_name: php5
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -281,11 +306,14 @@ services:
container_name: php-staging container_name: php-staging
depends_on: depends_on:
- redis-staging - redis-staging
- edge-broker
command: ["php-fpm"] command: ["php-fpm"]
env_file: env_file:
- .env.staging - .env.staging
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -298,11 +326,14 @@ services:
container_name: php-cron container_name: php-cron
depends_on: depends_on:
- redis - redis
- edge-broker
command: ["sh", "-c", "while true; do php index.php run cron; sleep 60; done"] command: ["sh", "-c", "while true; do php index.php run cron; sleep 60; done"]
env_file: env_file:
- .env - .env
environment: environment:
AUTO_COMPOSER_INSTALL: "false" AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes: volumes:
- ./services/nginx/app:/var/www/html - ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
+20
View File
@@ -20,6 +20,17 @@ const traefikSource = [
readRequiredSource("services", "traefik", "traefik.prod.yml"), readRequiredSource("services", "traefik", "traefik.prod.yml"),
].join("\n"); ].join("\n");
function readComposeServiceBlock(composeSource, serviceName) {
const escapedServiceName = serviceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const servicePattern = new RegExp(
`^\\s{2}${escapedServiceName}:\\n([\\s\\S]*?)(?=^\\s{2}[A-Za-z0-9_-]+:|^volumes:|^networks:|\\Z)`,
"m"
);
const match = composeSource.match(servicePattern);
assert.ok(match, `Expected docker compose service block for ${serviceName}`);
return match[0];
}
test("traefik does not expose a dedicated public edge broker port", () => { test("traefik does not expose a dedicated public edge broker port", () => {
assert.doesNotMatch(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/); assert.doesNotMatch(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/);
}); });
@@ -40,3 +51,12 @@ test("php services receive broker websocket environment defaults", () => {
assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev\}/); assert.match(composeSource, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev\}/);
} }
}); });
test("base docker compose wires the broker into each php worker", () => {
for (const serviceName of ["php1", "php2", "php3", "php4", "php5", "php-staging", "php-cron"]) {
const serviceBlock = readComposeServiceBlock(baseComposeSource, serviceName);
assert.match(serviceBlock, /\n\s+depends_on:\s*\n[\s\S]*?\n\s+- edge-broker/);
assert.match(serviceBlock, /EDGE_BROKER_URL:\s*\$\{EDGE_BROKER_URL:-http:\/\/edge-broker:4300\}/);
assert.match(serviceBlock, /EDGE_BROKER_SHARED_SECRET:\s*\$\{EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev\}/);
}
});
File diff suppressed because one or more lines are too long
@@ -1690,7 +1690,7 @@ BASH;
'shell_command' => null, 'shell_command' => null,
'shell_args_json' => [], 'shell_args_json' => [],
'cols' => $cols, 'cols' => $cols,
'rows' => $rows, 'terminal_rows' => $rows,
'transcript' => null, 'transcript' => null,
'metadata_json' => [ 'metadata_json' => [
'root_dir' => self::DEFAULT_INSTALL_DIR, 'root_dir' => self::DEFAULT_INSTALL_DIR,
@@ -211,7 +211,7 @@ class edge_gateway_schema_bootstrap
shell_command VARCHAR(255) NULL, shell_command VARCHAR(255) NULL,
shell_args_json JSON NULL, shell_args_json JSON NULL,
cols INT NULL, cols INT NULL,
rows INT NULL, terminal_rows INT NULL,
transcript LONGTEXT NULL, transcript LONGTEXT NULL,
metadata_json JSON NULL, metadata_json JSON NULL,
expires_at DATETIME NULL, expires_at DATETIME NULL,
@@ -278,8 +278,9 @@ class edge_gateway_schema_bootstrap
self::ensureColumn('edge_gateway_shell_sessions', 'shell_command', 'VARCHAR(255) NULL AFTER cwd'); self::ensureColumn('edge_gateway_shell_sessions', 'shell_command', 'VARCHAR(255) NULL AFTER cwd');
self::ensureColumn('edge_gateway_shell_sessions', 'shell_args_json', 'JSON NULL AFTER shell_command'); self::ensureColumn('edge_gateway_shell_sessions', 'shell_args_json', 'JSON NULL AFTER shell_command');
self::ensureColumn('edge_gateway_shell_sessions', 'cols', 'INT NULL AFTER shell_args_json'); self::ensureColumn('edge_gateway_shell_sessions', 'cols', 'INT NULL AFTER shell_args_json');
self::ensureColumn('edge_gateway_shell_sessions', 'rows', 'INT NULL AFTER cols'); self::renameColumnIfPresent('edge_gateway_shell_sessions', 'rows', 'terminal_rows', 'INT NULL', 'cols');
self::ensureColumn('edge_gateway_shell_sessions', 'transcript', 'LONGTEXT NULL AFTER rows'); self::ensureColumn('edge_gateway_shell_sessions', 'terminal_rows', 'INT NULL AFTER cols');
self::ensureColumn('edge_gateway_shell_sessions', 'transcript', 'LONGTEXT NULL AFTER terminal_rows');
self::ensureColumn('edge_gateway_shell_sessions', 'metadata_json', 'JSON NULL AFTER transcript'); self::ensureColumn('edge_gateway_shell_sessions', 'metadata_json', 'JSON NULL AFTER transcript');
self::ensureColumn('edge_gateway_shell_sessions', 'expires_at', 'DATETIME NULL AFTER metadata_json'); self::ensureColumn('edge_gateway_shell_sessions', 'expires_at', 'DATETIME NULL AFTER metadata_json');
self::ensureColumn('edge_gateway_shell_sessions', 'approved_at', 'DATETIME NULL AFTER expires_at'); self::ensureColumn('edge_gateway_shell_sessions', 'approved_at', 'DATETIME NULL AFTER expires_at');
@@ -311,6 +312,34 @@ class edge_gateway_schema_bootstrap
); );
} }
private static function renameColumnIfPresent(
string $table,
string $from,
string $to,
string $definition,
?string $afterColumn = null
): void {
global $db;
if (!self::tableHasColumn($table, $from) || self::tableHasColumn($table, $to)) {
return;
}
if (!preg_match('/^[A-Za-z0-9_]+$/', $table)
|| !preg_match('/^[A-Za-z0-9_]+$/', $from)
|| !preg_match('/^[A-Za-z0-9_]+$/', $to)
|| ($afterColumn !== null && !preg_match('/^[A-Za-z0-9_]+$/', $afterColumn))) {
throw new \RuntimeException('Invalid schema bootstrap identifier');
}
$positionClause = $afterColumn === null ? '' : " AFTER `$afterColumn`";
$db->query(
"ALTER TABLE `$table`
CHANGE COLUMN `$from` `$to` $definition$positionClause"
);
}
private static function tableHasColumn(string $table, string $column): bool private static function tableHasColumn(string $table, string $column): bool
{ {
global $db; global $db;
@@ -52,7 +52,7 @@ class edge_gateway_shell_sessions_o extends db
$this->shell_command = new object_property($this->table, $this->id, 'shell_command', 'string', false); $this->shell_command = new object_property($this->table, $this->id, 'shell_command', 'string', false);
$this->shell_args_json = new object_property($this->table, $this->id, 'shell_args_json', 'json', false); $this->shell_args_json = new object_property($this->table, $this->id, 'shell_args_json', 'json', false);
$this->cols = new object_property($this->table, $this->id, 'cols', 'int', false); $this->cols = new object_property($this->table, $this->id, 'cols', 'int', false);
$this->rows = new object_property($this->table, $this->id, 'rows', 'int', false); $this->rows = new object_property($this->table, $this->id, 'terminal_rows', 'int', false);
$this->transcript = new object_property($this->table, $this->id, 'transcript', 'text', false); $this->transcript = new object_property($this->table, $this->id, 'transcript', 'text', false);
$this->metadata_json = new object_property($this->table, $this->id, 'metadata_json', 'json', false); $this->metadata_json = new object_property($this->table, $this->id, 'metadata_json', 'json', false);
$this->expires_at = new object_property($this->table, $this->id, 'expires_at', 'string', false); $this->expires_at = new object_property($this->table, $this->id, 'expires_at', 'string', false);
@@ -45,4 +45,6 @@ it('stores operation metadata and event timelines for management workflows', fun
expect($bootstrapContent)->toContain('summary_json JSON NULL'); expect($bootstrapContent)->toContain('summary_json JSON NULL');
expect($bootstrapContent)->toContain('context_json JSON NULL'); expect($bootstrapContent)->toContain('context_json JSON NULL');
expect($bootstrapContent)->toContain('session_token_hash CHAR(64) NOT NULL'); expect($bootstrapContent)->toContain('session_token_hash CHAR(64) NOT NULL');
expect($bootstrapContent)->toContain('terminal_rows INT NULL');
expect($bootstrapContent)->toContain("renameColumnIfPresent('edge_gateway_shell_sessions', 'rows', 'terminal_rows', 'INT NULL', 'cols')");
}); });