Files
ladill-hosting/app/Services/Infrastructure/ContaboInfrastructureCloudInitBuilder.php
T
isaaccladandCursor e251a4cf60
Deploy Ladill Hosting / deploy (push) Failing after 17s
Initial Ladill Hosting app with Gitea deploy pipeline.
Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 16:24:20 +00:00

177 lines
7.5 KiB
PHP

<?php
namespace App\Services\Infrastructure;
use App\Models\ContaboInfrastructureProvision;
use App\Models\EmailServer;
use App\Services\Hosting\Providers\ContaboProvider;
use Illuminate\Support\Facades\File;
class ContaboInfrastructureCloudInitBuilder
{
public function __construct(
private ContaboProvider $contaboProvider,
) {}
public function build(ContaboInfrastructureProvision $provision): string
{
return match ($provision->purpose) {
ContaboInfrastructureProvision::PURPOSE_HOSTING_NODE => $this->buildHostingNode($provision),
ContaboInfrastructureProvision::PURPOSE_EMAIL_PRIMARY => $this->buildMailPrimary($provision),
ContaboInfrastructureProvision::PURPOSE_EMAIL_EXTENSION => $this->buildMailExtension($provision),
default => throw new \InvalidArgumentException("Unknown provision purpose: {$provision->purpose}"),
};
}
private function buildHostingNode(ContaboInfrastructureProvision $provision): string
{
$helperScript = File::get(base_path('deploy/bin/ladill-hosting-admin'));
$sudoers = "www-data ALL=(root) NOPASSWD: /usr/local/bin/ladill-hosting-admin *\n";
$completionUrl = $provision->completionUrl();
return $this->contaboProvider->generateCloudInit([
'packages' => ['curl', 'wget', 'git', 'unzip', 'nginx', 'php-fpm', 'mysql-client', 'quota'],
'ssh_keys' => array_filter([$provision->ssh_public_key]),
'write_files' => [
[
'path' => '/opt/ladill/ladill-hosting-admin',
'permissions' => '0755',
'owner' => 'root:root',
'content' => $helperScript,
],
[
'path' => '/opt/ladill/ladill-hosting.sudoers',
'permissions' => '0440',
'owner' => 'root:root',
'content' => $sudoers,
],
],
'run_commands' => [
'install -Dm755 /opt/ladill/ladill-hosting-admin /usr/local/bin/ladill-hosting-admin',
'install -Dm440 /opt/ladill/ladill-hosting.sudoers /etc/sudoers.d/ladill-hosting',
'visudo -cf /etc/sudoers.d/ladill-hosting',
$this->completionCurlCommand($completionUrl, ['bootstrap' => 'hosting']),
],
]);
}
private function buildMailPrimary(ContaboInfrastructureProvision $provision): string
{
$payload = (array) ($provision->payload ?? []);
$mailHost = (string) ($payload['mail_host'] ?? config('mailinfra.mail_host', 'mail.ladill.com'));
$dbName = config('infrastructure.mail_stack.db_database', 'mail');
$dbUser = config('infrastructure.mail_stack.db_username', 'mailuser');
$dbPassword = (string) ($provision->generated_mail_db_password ?? '');
$vmailRoot = config('infrastructure.mail_stack.vmail_root', '/var/vmail');
$vmailUid = (string) config('infrastructure.mail_stack.vmail_uid', 5000);
$vmailGid = (string) config('infrastructure.mail_stack.vmail_gid', 5000);
$exportClients = (string) ($payload['export_client_ips'] ?? '');
$bootstrapScript = str_replace(
[
'__MAIL_HOST__', '__DB_PASSWORD__', '__DB_NAME__', '__DB_USER__', '__COMPLETION_URL__',
'__VMAIL_ROOT__', '__VMAIL_UID__', '__VMAIL_GID__', '__EXPORT_CLIENTS__',
],
[
$mailHost, $dbPassword, $dbName, $dbUser, $provision->completionUrl(),
$vmailRoot, $vmailUid, $vmailGid, $exportClients,
],
File::get(resource_path('infrastructure/scripts/mail-primary-bootstrap.sh'))
);
$writeFiles = [
$this->scriptFile('/opt/ladill/nfs-primary-setup.sh', 'infrastructure/scripts/nfs-primary-setup.sh'),
$this->scriptFile('/opt/ladill/vmail-usage-scan.sh', 'infrastructure/scripts/vmail-usage-scan.sh'),
[
'path' => '/opt/ladill/mail-primary-bootstrap.sh',
'permissions' => '0755',
'owner' => 'root:root',
'encoding' => 'b64',
'content' => base64_encode($bootstrapScript),
],
];
return $this->contaboProvider->generateCloudInit([
'packages' => ['curl', 'wget', 'jq', 'ca-certificates', 'sudo'],
'ssh_keys' => array_filter([$provision->ssh_public_key]),
'write_files' => $writeFiles,
'run_commands' => [
'bash /opt/ladill/mail-primary-bootstrap.sh',
],
]);
}
private function buildMailExtension(ContaboInfrastructureProvision $provision): string
{
$payload = (array) ($provision->payload ?? []);
$mailHost = (string) ($payload['mail_host'] ?? config('mailinfra.mail_host', 'mail.ladill.com'));
$primaryIp = (string) ($payload['primary_ip'] ?? '');
$vmailRoot = config('infrastructure.mail_stack.vmail_root', '/var/vmail');
$mountOpts = config('infrastructure.mail_stack.nfs_mount_options', 'nfsvers=4.1,rw,hard,timeo=600,retrans=2,_netdev');
if ($primaryIp === '' && ! empty($payload['primary_email_server_id'])) {
$primary = EmailServer::find($payload['primary_email_server_id']);
$primaryIp = $primary?->ip_address ?? '';
$vmailRoot = $primary?->pool_vmail_root ?: $vmailRoot;
}
$bootstrapScript = str_replace(
[
'__MAIL_HOST__', '__PRIMARY_IP__', '__COMPLETION_URL__',
'__VMAIL_ROOT__', '__MOUNT_OPTS__',
],
[
$mailHost, $primaryIp, $provision->completionUrl(),
$vmailRoot, $mountOpts,
],
File::get(resource_path('infrastructure/scripts/mail-extension-bootstrap.sh'))
);
$writeFiles = [
$this->scriptFile('/opt/ladill/nfs-extension-setup.sh', 'infrastructure/scripts/nfs-extension-setup.sh'),
$this->scriptFile('/opt/ladill/vmail-usage-scan.sh', 'infrastructure/scripts/vmail-usage-scan.sh'),
[
'path' => '/opt/ladill/mail-extension-bootstrap.sh',
'permissions' => '0755',
'owner' => 'root:root',
'encoding' => 'b64',
'content' => base64_encode($bootstrapScript),
],
];
return $this->contaboProvider->generateCloudInit([
'packages' => ['curl', 'wget', 'jq', 'ca-certificates', 'sudo'],
'ssh_keys' => array_filter([$provision->ssh_public_key]),
'write_files' => $writeFiles,
'run_commands' => [
'bash /opt/ladill/mail-extension-bootstrap.sh',
],
]);
}
/**
* @return array<string, mixed>
*/
private function scriptFile(string $remotePath, string $localRelativePath): array
{
return [
'path' => $remotePath,
'permissions' => '0755',
'owner' => 'root:root',
'encoding' => 'b64',
'content' => base64_encode(File::get(resource_path($localRelativePath))),
];
}
/**
* @param array<string, mixed> $extra
*/
private function completionCurlCommand(string $url, array $extra = []): string
{
$payload = json_encode(array_merge(['status' => 'ok'], $extra), JSON_UNESCAPED_SLASHES);
return "curl -fsS -X POST ".escapeshellarg($url)." -H 'Content-Type: application/json' -d ".escapeshellarg($payload)." || true";
}
}