Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions server/src/Console/Commands/InstallerJobStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Fleetbase\RegistryBridge\Console\Commands;

use Fleetbase\RegistryBridge\Support\Installer\InstallerJobStore;
use Illuminate\Console\Command;

/**
* Status of a queued installer job, for `flb` to follow the jobs registry:install, registry:update and
* registry:uninstall print.
*/
class InstallerJobStatus extends Command
{
/**
* Log lines shown in the human summary.
*/
public const LOG_TAIL = 10;

protected $signature = 'registry:job
{id : Installer job id}
{--json : Print the job record as JSON}';

protected $description = 'Show the status of an installer job.';

public function handle(InstallerJobStore $jobs): int
{
$id = (string) (is_string($this->argument('id')) ? $this->argument('id') : '');
$job = $id !== '' ? $jobs->find($id) : null;

if ($job === null) {
$message = sprintf('Installer job %s was not found.', $id);
if ($this->option('json')) {
$this->line((string) json_encode(['error' => $message], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$this->error($message);
}

return self::FAILURE;
}

if ($this->option('json')) {
$this->line((string) json_encode($job, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$this->describe($job);
}

return self::SUCCESS;
}

/**
* @param array<string, mixed> $job
*/
protected function describe(array $job): void
{
$extension = is_array($job['extension'] ?? null) ? $job['extension'] : [];
$status = static::text($job['status'] ?? null, 'unknown');

$this->line(sprintf('Job %s: %s of %s', static::text($job['id'] ?? null), static::text($job['action'] ?? null, 'install'), static::text($extension['name'] ?? $extension['id'] ?? null, 'extension')));
$this->line(sprintf('Status: %s', $status));
if ($status === 'failed') {
$this->line(sprintf('Stage: %s (failed)', static::text($job['failed_stage'] ?? $job['stage'] ?? null, 'unknown')));
} elseif (static::text($job['stage'] ?? null) !== '') {
$this->line(sprintf('Stage: %s', static::text($job['stage'])));
}
$this->line(sprintf('Progress: %d%%', is_numeric($job['progress'] ?? null) ? (int) $job['progress'] : 0));
if ($status === 'queued' && is_int($job['position'] ?? null)) {
$this->line(sprintf('Position: %d job(s) ahead', $job['position']));
}
if (static::text($job['message'] ?? null) !== '') {
$this->line(sprintf('Message: %s', static::text($job['message'])));
}

$log = is_array($job['log'] ?? null) ? array_slice($job['log'], -self::LOG_TAIL) : [];
if ($log !== []) {
$this->line('Log:');
foreach ($log as $line) {
$this->line(' ' . static::text($line));
}
}
}

protected static function text(mixed $value, string $default = ''): string
{
return is_scalar($value) && (string) $value !== '' ? (string) $value : $default;
}
}
115 changes: 115 additions & 0 deletions server/src/Console/Commands/RegisterInstance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Fleetbase\RegistryBridge\Console\Commands;

use Fleetbase\RegistryBridge\Exceptions\RegistryException;
use Fleetbase\RegistryBridge\Support\InstanceRegistration;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\StreamableInputInterface;

/**
* Register this instance with the Registry Service from a server shell (`flb instance register`).
*
* Pass the registration code on stdin with `--code-stdin` to keep it out of the process list. The instance
* token is stored encrypted in settings and never printed.
*
* Running it on an instance that is already registered rotates that instance on the service (matched by its current
* token or its host): same id, installs and licence seats, new token. Web and queue workers pick the new
* registration up on their next request or job, without `octane:reload`.
*/
class RegisterInstance extends Command
{
protected $signature = 'registry:register
{--code= : Registration code from your registry organization}
{--code-stdin : Read the registration code from stdin}
{--service-url= : Registry Service URL (defaults to the configured one)}
{--name= : Instance name (defaults to the app name)}
{--host= : Instance URL (defaults to the app URL)}
{--console-version= : Console version (defaults to the console package.json)}
{--json : Print the stored registration as JSON}';

protected $description = 'Register this instance with the Registry Service using a registration code.';

/**
* The registration is resolved from the container so tests can replace its settings writer.
*/
public function handle(InstanceRegistration $registration): int
{
$code = $this->code();
if ($code === null) {
return $this->refuse('Pass the registration code with exactly one of --code or --code-stdin.');
}

try {
$summary = $registration::register(
$code,
$this->stringOption('service-url'),
$this->stringOption('name'),
$this->stringOption('host'),
$this->stringOption('console-version')
);
} catch (RegistryException $e) {
return $this->refuse($e->getMessage());
}

if ($this->option('json')) {
$this->line((string) json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$organization = is_array($summary['organization'] ?? null) ? $summary['organization'] : [];
$rotated = ($summary['rotated'] ?? false) === true;
$this->info(sprintf('%s %s (%s) with %s.', $rotated ? 'Re-registered' : 'Registered', static::text($summary['name'] ?? null, 'this instance'), static::text($summary['id'] ?? null, 'unknown id'), static::text($organization['name'] ?? $organization['slug'] ?? null, 'the registry organization')));
if ($rotated) {
$this->line('Same instance on the service: its installs and licence seats are kept, and the previous token no longer works.');
}
$this->line(sprintf('Registry Service: %s', static::text($summary['service_url'] ?? null)));
$this->line(sprintf('Instance token: %s (stored encrypted)', static::text($summary['token_hint'] ?? null, 'stored')));
}

return self::SUCCESS;
}

/**
* The code from --code or stdin, trimmed; null unless exactly one of them was given.
*/
protected function code(): ?string
{
$option = $this->stringOption('code');
$stdin = (bool) $this->option('code-stdin');
if (($option !== null) === $stdin) {
return null;
}

if ($option !== null) {
return $option;
}

$stream = $this->input instanceof StreamableInputInterface ? $this->input->getStream() : null;
$stream ??= fopen('php://stdin', 'r');
$code = is_resource($stream) ? stream_get_contents($stream) : false;

return is_string($code) ? trim($code) : '';
}

protected function stringOption(string $name): ?string
{
$value = $this->option($name);

return is_string($value) && trim($value) !== '' ? trim($value) : null;
}

protected static function text(mixed $value, string $default = ''): string
{
return is_scalar($value) && (string) $value !== '' ? (string) $value : $default;
}

protected function refuse(string $message): int
{
if ($this->option('json')) {
$this->line((string) json_encode(['error' => $message], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
} else {
$this->error($message);
}

return self::FAILURE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -516,38 +516,20 @@ public function testConfig(AdminRequest $request)
*/
public function registerInstance(AdminRequest $request)
{
$code = trim((string) $request->input('code', ''));
if ($code === '') {
return response()->error('Enter the registration code from your registry organization.', 422);
}

$config = RegistryConfig::current();
$serviceUrl = trim((string) $request->input('service_url', ''));
$serviceUrl = $serviceUrl !== '' ? RegistryConfig::normalizeUrl($serviceUrl) : $config->serviceUrl();
if (!$serviceUrl) {
return response()->error('Set the Registry Service URL before registering this instance.', 422);
}

$payload = InstanceRegistration::payload(
$code,
(string) ($request->input('name') ?: config('app.name', 'Fleetbase')),
$request->input('host') ?: config('app.url'),
$config->instanceKind(),
$request->input('console_version') ?: InstanceRegistration::detectConsoleVersion(config('fleetbase.console.path')),
InstanceRegistration::detectApiVersion(),
InstanceRegistration::previousToken($config, $serviceUrl)
);

try {
$registration = (new RegistryClient($serviceUrl, null, RegistryConfig::V2, $config))->registerInstance($payload);
InstanceRegistration::register(
(string) $request->input('code', ''),
(string) $request->input('service_url', ''),
(string) $request->input('name', ''),
(string) $request->input('host', ''),
(string) $request->input('console_version', '')
);
} catch (RegistryServiceException $e) {
return response()->error($e->getMessage(), $e->status() >= 400 && $e->status() < 500 ? 422 : 502);
} catch (RegistryException $e) {
return response()->error($e->getMessage(), 503);
}

InstanceRegistration::store($registration, $serviceUrl);

return response()->json($this->configPayload(RegistryConfig::current()));
}

Expand Down
2 changes: 2 additions & 0 deletions server/src/Providers/RegistryBridgeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class RegistryBridgeServiceProvider extends CoreServiceProvider
\Fleetbase\RegistryBridge\Console\Commands\InstallExtension::class,
\Fleetbase\RegistryBridge\Console\Commands\UninstallExtension::class,
\Fleetbase\RegistryBridge\Console\Commands\UpdateExtension::class,
\Fleetbase\RegistryBridge\Console\Commands\InstallerJobStatus::class,
\Fleetbase\RegistryBridge\Console\Commands\RegisterInstance::class,
];

/**
Expand Down
63 changes: 60 additions & 3 deletions server/src/Support/InstanceRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Composer\InstalledVersions;
use Fleetbase\Models\Setting;
use Fleetbase\RegistryBridge\Exceptions\RegistryServiceException;
use Fleetbase\RegistryBridge\Exceptions\RegistryUnavailableException;
use Illuminate\Contracts\Encryption\StringEncrypter;
use Illuminate\Support\Arr;

Expand All @@ -25,6 +27,46 @@ class InstanceRegistration

public const SETTING_INSTANCE = 'registry-bridge.registry.instance';

/**
* Exchange a registration code for an instance token and store it (the admin endpoint and `registry:register`).
*
* The service URL defaults to the configured one; name, host and console version default to the app name,
* the app URL and the console's package.json. An instance already registered with that service sends its
* current token, so the service rotates it instead of creating a second instance.
*
* @return array<string, mixed> the stored instance summary (never the token)
*
* @throws RegistryServiceException with status 422 when the code or service URL is missing, or the service's status when it refuses the code
* @throws RegistryUnavailableException when the service cannot be reached
*/
public static function register(string $code, ?string $serviceUrl = null, ?string $name = null, ?string $host = null, ?string $consoleVersion = null): array
{
$code = trim($code);
if ($code === '') {
throw new RegistryServiceException('Enter the registration code from your registry organization.', 422);
}

$config = RegistryConfig::current();
$serviceUrl = $serviceUrl !== null && trim($serviceUrl) !== '' ? RegistryConfig::normalizeUrl(trim($serviceUrl)) : $config->serviceUrl();
if (!$serviceUrl) {
throw new RegistryServiceException('Set the Registry Service URL before registering this instance.', 422);
}

$payload = static::payload(
$code,
$name ?: (static::configString('app.name') ?? 'Fleetbase'),
$host ?: static::configString('app.url'),
$config->instanceKind(),
$consoleVersion ?: static::detectConsoleVersion(static::configString('fleetbase.console.path')),
static::detectApiVersion(),
static::previousToken($config, $serviceUrl)
);

$registration = (new RegistryClient($serviceUrl, null, RegistryConfig::V2, $config))->registerInstance($payload);

return static::store($registration, $serviceUrl);
}

/**
* Body for `POST /v1/instances/register`.
*
Expand Down Expand Up @@ -135,9 +177,9 @@ public static function store(array $registration, string $serviceUrl): array
{
$summary = static::instanceSummary($registration['instance'], $serviceUrl, $registration['token'], date(DATE_ATOM), ($registration['rotated'] ?? false) === true);

Setting::configure(static::SETTING_SERVICE_URL, $serviceUrl);
Setting::configure(static::SETTING_INSTANCE_TOKEN, static::encryptToken($registration['token'], app('encrypter')));
Setting::configure(static::SETTING_INSTANCE, $summary);
static::configure(static::SETTING_SERVICE_URL, $serviceUrl);
static::configure(static::SETTING_INSTANCE_TOKEN, static::encryptToken($registration['token'], app('encrypter')));
static::configure(static::SETTING_INSTANCE, $summary);

config([
'registry-bridge.registry.service_url' => $serviceUrl,
Expand All @@ -151,6 +193,21 @@ public static function store(array $registration, string $serviceUrl): array
return $summary;
}

/**
* Write one setting (overridden in tests, which have no database).
*/
protected static function configure(string $key, mixed $value): void
{
Setting::configure($key, $value);
}

protected static function configString(string $key): ?string
{
$value = config($key);

return is_string($value) && $value !== '' ? $value : null;
}

/**
* Forget the instance token and registration details (the service URL is kept).
*/
Expand Down
Loading