Refactor subuser permissions and enhance artifact management

This commit is contained in:
Jeppe Bundgaard
2026-07-01 13:18:27 +02:00
parent 11d39af934
commit 866a5be126
19 changed files with 756 additions and 167 deletions
@@ -6,6 +6,7 @@ use Exception;
class edge_gateway_agent_artifact_locator
{
private const EDGE_AGENT_BUILD_ARTIFACT_DIRECTORY = 'build/install';
private const ROUTER_ARTIFACT_DIRECTORY = 'resources/edge-gateway-agent';
private const DEFAULT_MOUNTED_ARTIFACT_DIRECTORY = '/services/edge-agent/php-agent';
private const DEFAULT_BAKED_ARTIFACT_DIRECTORY = '/opt/truckwash-edge-agent-artifacts';
@@ -28,6 +29,10 @@ class edge_gateway_agent_artifact_locator
$candidateDirectories[] = self::normalizePath($configuredDirectory);
}
foreach (self::edgeAgentBuildDirectories($basePath) as $directory) {
$candidateDirectories[] = $directory;
}
$candidateDirectories[] = self::routerArtifactDirectory($basePath);
$mountedArtifactDirectory = $mountedArtifactDirectory ?? self::mountedArtifactDirectory();
@@ -40,9 +45,9 @@ class edge_gateway_agent_artifact_locator
$candidateDirectories[] = self::normalizePath($bakedArtifactDirectory);
}
$candidateDirectories[] = self::normalizePath(dirname($basePath, 3) . DIRECTORY_SEPARATOR . 'services' . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent');
$candidateDirectories[] = self::normalizePath(dirname($basePath, 2) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent');
$candidateDirectories[] = self::normalizePath(dirname($basePath) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent');
foreach (self::legacyPhpAgentDirectories($basePath) as $directory) {
$candidateDirectories[] = $directory;
}
$paths = [];
foreach (array_values(array_unique($candidateDirectories)) as $directory) {
@@ -102,6 +107,42 @@ class edge_gateway_agent_artifact_locator
return self::normalizePath($basePath . DIRECTORY_SEPARATOR . self::ROUTER_ARTIFACT_DIRECTORY);
}
/**
* @return array<int,string>
*/
private static function edgeAgentBuildDirectories(string $basePath): array
{
$relative = str_replace('/', DIRECTORY_SEPARATOR, self::EDGE_AGENT_BUILD_ARTIFACT_DIRECTORY);
$directories = [
dirname($basePath, 4) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . $relative,
dirname($basePath, 3) . DIRECTORY_SEPARATOR . 'services' . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . $relative,
dirname($basePath, 2) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . $relative,
dirname($basePath) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . $relative,
];
if (DIRECTORY_SEPARATOR === '/') {
array_unshift($directories, '/edge-agent/' . self::EDGE_AGENT_BUILD_ARTIFACT_DIRECTORY);
$directories[] = '/services/edge-agent/' . self::EDGE_AGENT_BUILD_ARTIFACT_DIRECTORY;
}
return array_values(array_unique(array_map(
static fn(string $directory): string => self::normalizePath($directory),
$directories
)));
}
/**
* @return array<int,string>
*/
private static function legacyPhpAgentDirectories(string $basePath): array
{
return [
self::normalizePath(dirname($basePath, 3) . DIRECTORY_SEPARATOR . 'services' . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent'),
self::normalizePath(dirname($basePath, 2) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent'),
self::normalizePath(dirname($basePath) . DIRECTORY_SEPARATOR . 'edge-agent' . DIRECTORY_SEPARATOR . 'php-agent'),
];
}
private static function mountedArtifactDirectory(): ?string
{
if (DIRECTORY_SEPARATOR !== '/') {
@@ -7,6 +7,7 @@ use Exception;
class edge_gateway_install_service
{
private const ARTIFACTS = [
'manifest.json' => 'application/json; charset=utf-8',
'agent.php' => 'application/x-httpd-php; charset=utf-8',
'lan-worker.php' => 'application/x-httpd-php; charset=utf-8',
'auto-updater.php' => 'application/x-httpd-php; charset=utf-8',
@@ -42,6 +43,10 @@ class edge_gateway_install_service
*/
public function readArtifact(string $fileName): string
{
if ($fileName === 'manifest.json') {
return $this->buildManifest();
}
$path = $this->artifactPath($fileName);
$contents = file_get_contents($path);
if ($contents === false) {
@@ -66,13 +71,48 @@ class edge_gateway_install_service
*/
public function artifactPath(string $fileName): string
{
if (!array_key_exists($fileName, self::ARTIFACTS)) {
if (!array_key_exists($fileName, self::ARTIFACTS) || $fileName === 'manifest.json') {
throw new Exception('Unknown edge agent artifact');
}
return edge_gateway_agent_artifact_locator::resolve($fileName);
}
/**
* @throws Exception
*/
private function buildManifest(): string
{
$artifacts = [];
foreach (self::ARTIFACTS as $fileName => $contentType) {
if ($fileName === 'manifest.json') {
continue;
}
$path = $this->artifactPath($fileName);
$sha256 = hash_file('sha256', $path);
$bytes = filesize($path);
if ($sha256 === false || $bytes === false) {
throw new Exception('Unable to inspect edge agent artifact: ' . $fileName);
}
$artifacts[] = [
'name' => $fileName,
'sha256' => $sha256,
'bytes' => $bytes,
'content_type' => $contentType,
];
}
return json_encode([
'schema_version' => 1,
'package' => 'truckwash-edge-agent',
'version' => edge_gateway_manager::DEFAULT_INSTALL_VERSION,
'generated_at' => gmdate('c'),
'artifacts' => $artifacts,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
private function manager(): edge_gateway_manager
{
return $this->manager ?? new edge_gateway_manager();
@@ -33,6 +33,7 @@ class edge_gateway_manager
public const STATUS_DEGRADED = 'DEGRADED';
public const STATUS_OFFLINE = 'OFFLINE';
public const DEFAULT_RELEASE_CHANNEL = 'stable';
public const DEFAULT_INSTALL_VERSION = 'compose-php-agent-v3';
public const DEFAULT_AGENT_SERVICE_NAME = 'truckwash-edge-agent.service';
public const DEFAULT_STACK_SERVICE_NAME = 'truckwash-edge-gateway-stack.service';
public const DEFAULT_COMPOSE_STACK_FILE = 'docker-compose.gateway.yml';
@@ -2309,6 +2310,7 @@ class edge_gateway_manager
'minioBaseImage' => self::DEFAULT_MINIO_BASE_IMAGE,
'heartbeatIntervalSeconds' => 15,
'operationPollTimeoutSeconds' => self::COMMAND_POLL_TIMEOUT_SECONDS,
'installedVersion' => self::DEFAULT_INSTALL_VERSION,
], JSON_UNESCAPED_SLASHES);
$script = <<<'BASH'
@@ -2543,6 +2545,42 @@ fetch_http() {
[ "$cleanup_body" -eq 1 ] && rm -f "$body_path"
rm -f "$headers_path"
}
verify_manifest_artifact() {
local manifest_path="$1"
local artifact_name="$2"
local artifact_path="$3"
log_info "Verifying ${artifact_name} checksum"
php -r '
$manifestPath = $argv[1];
$artifactName = $argv[2];
$artifactPath = $argv[3];
$manifest = json_decode((string)file_get_contents($manifestPath), true);
if (!is_array($manifest)) {
fwrite(STDERR, "Invalid artifact manifest: " . $manifestPath . PHP_EOL);
exit(1);
}
$expected = null;
foreach ((array)($manifest["artifacts"] ?? []) as $artifact) {
if (is_array($artifact) && ($artifact["name"] ?? null) === $artifactName) {
$expected = (string)($artifact["sha256"] ?? "");
break;
}
}
if ($expected === null || $expected === "") {
fwrite(STDERR, "Artifact missing from manifest: " . $artifactName . PHP_EOL);
exit(1);
}
if (!is_file($artifactPath)) {
fwrite(STDERR, "Downloaded artifact is missing: " . $artifactPath . PHP_EOL);
exit(1);
}
$actual = hash_file("sha256", $artifactPath);
if ($actual === false || !hash_equals($expected, $actual)) {
fwrite(STDERR, "Artifact checksum mismatch for " . $artifactName . PHP_EOL);
exit(1);
}
' "$manifest_path" "$artifact_name" "$artifact_path"
}
cleanup_existing_installation() {
if [ "$INSTALL_DIR" != "/opt/truckwash-edge-agent" ]; then
echo "Refusing to remove unexpected install directory: $INSTALL_DIR" >&2
@@ -2707,6 +2745,7 @@ run_step "Updating package lists" apt-get update
run_step "Installing base packages" apt-get install -y curl ca-certificates docker.io php-cli php-curl php-mbstring php-sqlite3
run_step "Installing Docker Compose runtime" install_compose_runtime
begin_install_phase "DOWNLOAD_ARTIFACTS" "Downloading edge gateway artifacts"
fetch_http "Download artifact manifest" "__MANIFEST_URL__" "$INSTALL_DIR/manifest.json"
fetch_http "Download PHP edge agent" "__AGENT_URL__" "$INSTALL_DIR/agent.php"
fetch_http "Download LAN worker" "__WORKER_URL__" "$INSTALL_DIR/lan-worker.php"
fetch_http "Download auto-updater" "__AUTO_UPDATER_URL__" "$INSTALL_DIR/auto-updater.php"
@@ -2717,6 +2756,17 @@ fetch_http "Download auto-updater Dockerfile" "__AUTO_UPDATER_DOCKERFILE_URL__"
fetch_http "Download gateway launcher" "__LAUNCHER_URL__" "$INSTALL_DIR/gateway-launcher.sh"
fetch_http "Download compose stack service unit" "__STACK_SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-gateway-stack.service"
fetch_http "Download compatibility service unit" "__LEGACY_SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-agent.service"
begin_install_phase "VERIFY_ARTIFACTS" "Verifying edge gateway artifacts"
run_step "Verifying PHP edge agent" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "agent.php" "$INSTALL_DIR/agent.php"
run_step "Verifying LAN worker" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "lan-worker.php" "$INSTALL_DIR/lan-worker.php"
run_step "Verifying auto-updater" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "auto-updater.php" "$INSTALL_DIR/auto-updater.php"
run_step "Verifying compose stack" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "docker-compose.gateway.yml" "$INSTALL_DIR/docker-compose.gateway.yml"
run_step "Verifying edge-agent Dockerfile" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "Dockerfile.edge-agent" "$INSTALL_DIR/Dockerfile.edge-agent"
run_step "Verifying lan-worker Dockerfile" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "Dockerfile.lan-worker" "$INSTALL_DIR/Dockerfile.lan-worker"
run_step "Verifying auto-updater Dockerfile" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "Dockerfile.auto-updater" "$INSTALL_DIR/Dockerfile.auto-updater"
run_step "Verifying gateway launcher" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "gateway-launcher.sh" "$INSTALL_DIR/gateway-launcher.sh"
run_step "Verifying compose stack service unit" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "truckwash-edge-gateway-stack.service" "$INSTALL_DIR/truckwash-edge-gateway-stack.service"
run_step "Verifying compatibility service unit" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "truckwash-edge-agent.service" "$INSTALL_DIR/truckwash-edge-agent.service"
begin_install_phase "WRITE_CONFIG" "Writing gateway configuration"
cat > "$CONFIG_TEMPLATE_PATH" <<'EOF_JSON'
__CONFIG_JSON__
@@ -2745,6 +2795,7 @@ BASH;
'__INSTALL_TOKEN__' => $plainToken,
'__VERIFY_URL__' => $this->buildInstallTokenVerifyUrl($plainToken),
'__STATUS_URL__' => rtrim($this->getApiBaseUrl(), '/') . '/edge-agent/install-token/status',
'__MANIFEST_URL__' => $this->buildAgentArtifactUrl('manifest.json'),
'__AGENT_URL__' => $this->buildAgentArtifactUrl('agent.php'),
'__WORKER_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_LAN_WORKER_ARTIFACT),
'__AUTO_UPDATER_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_AUTO_UPDATER_ARTIFACT),
@@ -81,6 +81,7 @@ class edgeGatewaysRoute
$this->get('/edge-agent/install-token/verify', fn() => $this->handleInstallTokenVerify());
$this->post('/edge-agent/install-token/status', fn() => $this->handleAgentInstallTokenStatus());
$this->get('/edge-agent/install.sh', fn() => $this->renderInstallScript());
$this->get('/edge-agent/artifacts/manifest.json', fn() => $this->renderArtifact('manifest.json'));
$this->get('/edge-agent/artifacts/agent.php', fn() => $this->renderArtifact('agent.php'));
$this->get('/edge-agent/artifacts/lan-worker.php', fn() => $this->renderArtifact('lan-worker.php'));
$this->get('/edge-agent/artifacts/auto-updater.php', fn() => $this->renderArtifact('auto-updater.php'));