116 lines
5.8 KiB
PHP
116 lines
5.8 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(
|
|
// Named after the vault entry it checks (ssh.private_key), not
|
|
// a shorthand: ReadinessPageTest's guard walks every
|
|
// SecretVault::REGISTRY key and demands a Check key that
|
|
// CONTAINS it (dots turned to underscores) — 'onboarding.
|
|
// ssh_key' does not contain 'ssh_private_key' and slipped
|
|
// through unnoticed until that guard existed (Task 12).
|
|
key: 'onboarding.ssh_private_key',
|
|
group: self::GROUP,
|
|
severity: Check::SEVERITY_BLOCKING,
|
|
label: __('readiness.onboarding.ssh_key'),
|
|
breaks: __('readiness.onboarding.ssh_key_breaks'),
|
|
// The SSH identity card lives on the 'platform' tab
|
|
// (integrations.blade.php: "own machines"), beside the
|
|
// public-key setting it pairs with — not 'integrations',
|
|
// which is not a member of Integrations::TABS at all and
|
|
// sent an operator clicking this exact blocking check to the
|
|
// wrong tab (Fix-Runde, Befund 1).
|
|
tab: 'platform',
|
|
// 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'),
|
|
// SECRETS_KEY has no field anywhere on Integrations' 'services'
|
|
// or 'platform' tabs — the only place an operator can actually
|
|
// set it is the raw .env editor on the 'env' tab.
|
|
tab: 'env',
|
|
// 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'),
|
|
// Same reasoning as onboarding.secrets_key just above:
|
|
// VPN_CONFIG_KEY has no console field either, only the raw
|
|
// .env editor.
|
|
tab: 'env',
|
|
// 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'),
|
|
// Two of the three values this check reads (wgHubPubkey,
|
|
// wgEndpoint) are 'platform'-tab fields; only the third
|
|
// (subnet) is .env-only and cannot be changed from the
|
|
// console at all (see the docblock on satisfied: below). The
|
|
// tab an operator can actually act on is 'platform'.
|
|
tab: 'platform',
|
|
// 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(),
|
|
),
|
|
];
|
|
}
|
|
}
|