93 lines
4.2 KiB
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(),
|
|
),
|
|
];
|
|
}
|
|
}
|