CluPilotCloud/app/Support/Readiness/OnboardingChecks.php

93 lines
4.2 KiB
PHP

<?php
namespace App\Support\Readiness;
use App\Models\Datacenter;
use App\Services\Secrets\SecretVault;
use App\Services\Wireguard\ConfigVault;
use App\Support\ProvisioningSettings;
/**
* What has to be in place before a single host can be onboarded at all: the
* SSH identity CluPilot deploys to every host, the two encryption keys the
* vault and the VPN config store need to work at all, the WireGuard hub a new
* host joins, and somewhere to onboard it into.
*/
final class OnboardingChecks
{
public const GROUP = 'onboarding';
/** @return array<int, Check> */
public static function all(): array
{
return [
new Check(
key: 'onboarding.ssh_key',
group: self::GROUP,
severity: Check::SEVERITY_BLOCKING,
label: __('readiness.onboarding.ssh_key'),
breaks: __('readiness.onboarding.ssh_key_breaks'),
tab: 'integrations',
// EstablishSshTrust only deploys the PUBLIC half. Every step
// after it (HostStep's own shell helper), and later every
// Traefik write on a host already onboarded (SshTraefikWriter),
// connects with THIS private key.
satisfied: filled(app(SecretVault::class)->get('ssh.private_key')),
),
new Check(
key: 'onboarding.secrets_key',
group: self::GROUP,
severity: Check::SEVERITY_BLOCKING,
label: __('readiness.onboarding.secrets_key'),
breaks: __('readiness.onboarding.secrets_key_breaks'),
tab: 'integrations',
// isUsable() rather than a bare filled() on the config value —
// SecretCipher's own docblock warns that a second, shorter
// rule that only checks emptiness lets a malformed-but-non-
// empty key sail through, exactly the gap that once let two
// other call sites reach an uncaught RuntimeException.
satisfied: app(SecretVault::class)->isUsable(),
),
new Check(
key: 'onboarding.vpn_config_key',
group: self::GROUP,
severity: Check::SEVERITY_BLOCKING,
label: __('readiness.onboarding.vpn_config_key'),
breaks: __('readiness.onboarding.vpn_config_key_breaks'),
tab: 'integrations',
// Same reuse-not-reimplement reasoning as isUsable() above:
// ConfigVault::available() is the one place that already knows
// what counts as a usable key.
satisfied: ConfigVault::available(),
),
new Check(
key: 'onboarding.wg_hub',
group: self::GROUP,
severity: Check::SEVERITY_BLOCKING,
label: __('readiness.onboarding.wg_hub'),
breaks: __('readiness.onboarding.wg_hub_breaks'),
tab: 'integrations',
// Two of three is no half a tunnel, it is none: ConfigureWireguard
// writes the peer config from all three at once. Subnet is read
// straight from config(), never through Settings/ProvisioningSettings
// — that class's own docblock explains why: the compose file
// (vpn-dns, vpn-gateway) reads the same CLUPILOT_WG_SUBNET at
// container boot, so a console-only override would let the
// console show one value while the running containers use another.
satisfied: filled(ProvisioningSettings::wgHubPublicKey())
&& filled(ProvisioningSettings::wgEndpoint())
&& filled((string) config('provisioning.wireguard.subnet')),
),
new Check(
key: 'onboarding.datacenter',
group: self::GROUP,
severity: Check::SEVERITY_BLOCKING,
label: __('readiness.onboarding.datacenter'),
breaks: __('readiness.onboarding.datacenter_breaks'),
tab: 'datacenters',
satisfied: Datacenter::query()->exists(),
),
];
}
}