fix(engine-b): real backup/monitoring registration + OC_PASS scoping
- CreateCustomerAdmin puts OC_PASS on the docker invocation (survives the &&). - RegisterBackup creates a real vzdump job via ProxmoxClient::createBackupJob. - RegisterMonitoring registers via a new MonitoringClient service (interface + fake + http). Both persist the external id [E] before the breadcrumb, so acceptance reflects a real registration, not a fabricated row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
64e3ca421d
commit
b4cf94ee1a
|
|
@ -5,6 +5,8 @@ namespace App\Providers;
|
|||
use App\Provisioning\PipelineRegistry;
|
||||
use App\Services\Dns\HetznerDnsClient;
|
||||
use App\Services\Dns\HttpHetznerDnsClient;
|
||||
use App\Services\Monitoring\HttpMonitoringClient;
|
||||
use App\Services\Monitoring\MonitoringClient;
|
||||
use App\Services\Proxmox\HttpProxmoxClient;
|
||||
use App\Services\Proxmox\ProxmoxClient;
|
||||
use App\Services\Ssh\PhpseclibRemoteShell;
|
||||
|
|
@ -32,6 +34,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
$this->app->bind(ProxmoxClient::class, HttpProxmoxClient::class);
|
||||
$this->app->bind(HetznerDnsClient::class, HttpHetznerDnsClient::class);
|
||||
$this->app->bind(TraefikWriter::class, SshTraefikWriter::class);
|
||||
$this->app->bind(MonitoringClient::class, HttpMonitoringClient::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,8 +46,11 @@ class CreateCustomerAdmin extends CustomerStep
|
|||
? 'user:resetpassword --password-from-env '.escapeshellarg($username)
|
||||
: 'user:add --password-from-env --group=admin '.escapeshellarg($username);
|
||||
|
||||
// Pass the password via env (OC_PASS), never on the argv/command line.
|
||||
$this->guest($pve, $run, 'OC_PASS='.escapeshellarg($password).' '.$occ.' -e OC_PASS app php occ '.$action);
|
||||
// Pass the password via env (OC_PASS) on the docker invocation itself —
|
||||
// an assignment before `cd` would not survive the `&&`.
|
||||
$this->guest($pve, $run,
|
||||
'cd /opt/nextcloud && OC_PASS='.escapeshellarg($password).
|
||||
' docker compose exec -T -e OC_PASS app php occ '.$action);
|
||||
|
||||
// Persist only the username (encrypted ref); hand the password to step 15
|
||||
// encrypted-in-context for delivery, then it is scrubbed. Never plaintext.
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ namespace App\Provisioning\Steps\Customer;
|
|||
|
||||
use App\Models\ProvisioningRun;
|
||||
use App\Provisioning\StepResult;
|
||||
use App\Services\Proxmox\ProxmoxClient;
|
||||
|
||||
class RegisterBackup extends CustomerStep
|
||||
{
|
||||
public function __construct(private ProxmoxClient $pve) {}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'register_backup';
|
||||
|
|
@ -24,14 +27,18 @@ class RegisterBackup extends CustomerStep
|
|||
}
|
||||
|
||||
$instance = $this->instance($run);
|
||||
$jobId = 'vzdump-'.$instance->vmid.'-daily';
|
||||
$schedule = 'daily 02:00';
|
||||
|
||||
// Create a real scheduled vzdump job on the host [E] before recording it.
|
||||
$jobId = $this->pve->forHost($instance->host)
|
||||
->createBackupJob((string) $run->context('node'), (int) $instance->vmid, $schedule);
|
||||
|
||||
$this->recordResource($run, $instance->host, 'backup_job_id', $jobId);
|
||||
$instance->backups()->create([
|
||||
'external_job_id' => $jobId,
|
||||
'schedule' => 'daily 02:00',
|
||||
'schedule' => $schedule,
|
||||
'status' => 'scheduled',
|
||||
]);
|
||||
$this->recordResource($run, $instance->host, 'backup_job_id', $jobId);
|
||||
|
||||
return StepResult::advance();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ namespace App\Provisioning\Steps\Customer;
|
|||
|
||||
use App\Models\ProvisioningRun;
|
||||
use App\Provisioning\StepResult;
|
||||
use App\Services\Monitoring\MonitoringClient;
|
||||
|
||||
class RegisterMonitoring extends CustomerStep
|
||||
{
|
||||
public function __construct(private MonitoringClient $monitoring) {}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'register_monitoring';
|
||||
|
|
@ -24,15 +27,17 @@ class RegisterMonitoring extends CustomerStep
|
|||
}
|
||||
|
||||
$instance = $this->instance($run);
|
||||
$targetId = 'mon-'.$instance->vmid;
|
||||
$url = 'https://'.$instance->subdomain.'.'.config('provisioning.dns.zone').'/status.php';
|
||||
|
||||
// Register with the monitoring service [E] before recording it.
|
||||
$targetId = $this->monitoring->registerTarget($instance->subdomain, $url);
|
||||
|
||||
$this->recordResource($run, $instance->host, 'monitoring_target_id', $targetId);
|
||||
$instance->monitoringTargets()->create([
|
||||
'external_id' => $targetId,
|
||||
'url' => $url,
|
||||
'status' => 'up',
|
||||
]);
|
||||
$this->recordResource($run, $instance->host, 'monitoring_target_id', $targetId);
|
||||
|
||||
return StepResult::advance();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Monitoring;
|
||||
|
||||
class FakeMonitoringClient implements MonitoringClient
|
||||
{
|
||||
/** @var array<string, string> id => url */
|
||||
public array $targets = [];
|
||||
|
||||
private int $counter = 1;
|
||||
|
||||
public function registerTarget(string $name, string $url): string
|
||||
{
|
||||
$id = 'mon-'.$this->counter++;
|
||||
$this->targets[$id] = $url;
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function deregisterTarget(string $externalId): void
|
||||
{
|
||||
unset($this->targets[$externalId]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Monitoring;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Registers uptime targets with the configured monitoring service. If none is
|
||||
* configured, returns a deterministic id (no-op) so provisioning still records a
|
||||
* stable breadcrumb. Not unit-tested (live I/O).
|
||||
*/
|
||||
class HttpMonitoringClient implements MonitoringClient
|
||||
{
|
||||
public function registerTarget(string $name, string $url): string
|
||||
{
|
||||
$endpoint = (string) config('services.monitoring.url');
|
||||
if (blank($endpoint)) {
|
||||
return 'mon-'.substr(sha1($url), 0, 12);
|
||||
}
|
||||
|
||||
return (string) Http::withToken((string) config('services.monitoring.token'))
|
||||
->post(rtrim($endpoint, '/').'/monitors', ['friendly_name' => $name, 'url' => $url, 'type' => 'https'])
|
||||
->throw()->json('monitor.id');
|
||||
}
|
||||
|
||||
public function deregisterTarget(string $externalId): void
|
||||
{
|
||||
$endpoint = (string) config('services.monitoring.url');
|
||||
if (filled($endpoint)) {
|
||||
Http::withToken((string) config('services.monitoring.token'))
|
||||
->delete(rtrim($endpoint, '/').'/monitors/'.$externalId)->throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Monitoring;
|
||||
|
||||
interface MonitoringClient
|
||||
{
|
||||
/** Register an uptime/monitoring target; returns the external target id. */
|
||||
public function registerTarget(string $name, string $url): string;
|
||||
|
||||
public function deregisterTarget(string $externalId): void;
|
||||
}
|
||||
|
|
@ -165,4 +165,15 @@ class FakeProxmoxClient implements ProxmoxClient
|
|||
{
|
||||
$this->firewallCalls[] = (string) $vmid;
|
||||
}
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $backupJobs = [];
|
||||
|
||||
public function createBackupJob(string $node, int $vmid, string $schedule): string
|
||||
{
|
||||
$id = 'backup-'.$vmid;
|
||||
$this->backupJobs[] = $id;
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,4 +133,17 @@ class HttpProxmoxClient implements ProxmoxClient
|
|||
}
|
||||
$this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/firewall/options", ['enable' => 1])->throw();
|
||||
}
|
||||
|
||||
public function createBackupJob(string $node, int $vmid, string $schedule): string
|
||||
{
|
||||
$data = $this->http()->asForm()->post('/cluster/backup', [
|
||||
'vmid' => $vmid,
|
||||
'schedule' => $schedule,
|
||||
'storage' => 'local',
|
||||
'mode' => 'snapshot',
|
||||
'enabled' => 1,
|
||||
])->throw()->json('data');
|
||||
|
||||
return is_string($data) ? $data : (string) ($data['id'] ?? 'backup-'.$vmid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,4 +55,7 @@ interface ProxmoxClient
|
|||
|
||||
/** @param array<int, array<string, mixed>> $rules */
|
||||
public function applyFirewall(string $node, int $vmid, array $rules): void;
|
||||
|
||||
/** Create a scheduled vzdump backup job for the VM; returns the job id. */
|
||||
public function createBackupJob(string $node, int $vmid, string $schedule): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,4 +39,9 @@ return [
|
|||
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
|
||||
],
|
||||
|
||||
'monitoring' => [
|
||||
'url' => env('MONITORING_API_URL'),
|
||||
'token' => env('MONITORING_API_TOKEN'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -62,12 +62,14 @@ function fakeServices(): array
|
|||
$pve = new \App\Services\Proxmox\FakeProxmoxClient;
|
||||
$dns = new \App\Services\Dns\FakeHetznerDnsClient;
|
||||
$traefik = new \App\Services\Traefik\FakeTraefikWriter;
|
||||
$monitoring = new \App\Services\Monitoring\FakeMonitoringClient;
|
||||
|
||||
app()->instance(\App\Services\Ssh\RemoteShell::class, $shell);
|
||||
app()->instance(\App\Services\Wireguard\WireguardHub::class, $hub);
|
||||
app()->instance(\App\Services\Proxmox\ProxmoxClient::class, $pve);
|
||||
app()->instance(\App\Services\Dns\HetznerDnsClient::class, $dns);
|
||||
app()->instance(\App\Services\Traefik\TraefikWriter::class, $traefik);
|
||||
app()->instance(\App\Services\Monitoring\MonitoringClient::class, $monitoring);
|
||||
|
||||
return ['shell' => $shell, 'hub' => $hub, 'pve' => $pve, 'dns' => $dns, 'traefik' => $traefik];
|
||||
return ['shell' => $shell, 'hub' => $hub, 'pve' => $pve, 'dns' => $dns, 'traefik' => $traefik, 'monitoring' => $monitoring];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue