#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_provision.php * VERSION: 01.00.00 * BRIEF: Provision a new client environment end-to-end */ declare(strict_types=1); final class ClientProvision { private string $giteaUrl = 'https://git.mokoconsulting.tech'; private string $giteaToken = ''; private string $grafanaUrl = ''; private string $grafanaToken = ''; private string $configFile = ''; private string $step = ''; private bool $dryRun = false; /** @var array */ private array $config = []; private string $org = ''; private string $repoName = ''; public function run(): int { $this->parseArgs(); if ($this->configFile === '') { $this->log('ERROR: --config is required.'); $this->printUsage(); return 1; } if (!file_exists($this->configFile)) { $this->log("ERROR: Not found: {$this->configFile}"); return 1; } $json = file_get_contents($this->configFile); $this->config = json_decode($json, true); if (!is_array($this->config)) { $this->log('ERROR: Invalid JSON in config file.'); return 1; } $this->giteaToken = $this->config['gitea_token'] ?? getenv('GA_TOKEN') ?: ''; $this->grafanaUrl = $this->config['grafana_url'] ?? getenv('GRAFANA_URL') ?: ''; $this->grafanaToken = $this->config['grafana_token'] ?? getenv('GRAFANA_TOKEN') ?: ''; $this->giteaUrl = $this->config['gitea_url'] ?? $this->giteaUrl; if ($this->giteaToken === '') { $this->log('ERROR: gitea_token or GA_TOKEN required.'); return 1; } $this->org = $this->config['org'] ?? ''; $clientName = $this->config['name'] ?? ''; if ($this->org === '' || $clientName === '') { $this->log('ERROR: "org" and "name" required in config.'); return 1; } $this->repoName = 'client-waas-' . $clientName; $this->log("=== Client Provisioning: {$clientName} ==="); $this->log(" Org: {$this->org}"); $this->log(" Repo: {$this->repoName}"); if ($this->dryRun) { $this->log(' Mode: DRY RUN'); } $this->log(''); $steps = [ 'repo' => 'createRepo', 'variables' => 'setVariables', 'secrets' => 'setSecrets', 'monitoring' => 'setupMonitoring', 'summary' => 'printSummary', ]; $exitCode = 0; foreach ($steps as $name => $method) { if ($this->step !== '' && $this->step !== $name) { continue; } $result = $this->$method(); if ($result !== 0) { $exitCode = 1; } } return $exitCode; } private function createRepo(): int { $this->log('[1/5] Creating repository...'); $check = $this->giteaApi( 'GET', "/api/v1/repos/{$this->org}/{$this->repoName}" ); if ($check['code'] === 200) { $this->log(" SKIP: repo already exists"); return 0; } if ($this->dryRun) { $this->log( " WOULD CREATE: {$this->org}/{$this->repoName}" ); return 0; } $payload = json_encode([ 'owner' => $this->org, 'name' => $this->repoName, 'description' => ($this->config['name'] ?? '') . ' WaaS site', 'private' => true, 'git_content' => true, 'topics' => true, 'labels' => true, ]); $resp = $this->giteaApi( 'POST', '/api/v1/repos/MokoConsulting/' . 'Template-Client-WaaS/generate', $payload ); if ($resp['code'] < 200 || $resp['code'] >= 300) { $this->log(" ERROR: HTTP {$resp['code']}"); return 1; } $this->log(' OK: Repo created'); $this->giteaApi( 'POST', "/api/v1/repos/{$this->org}/{$this->repoName}/branches", json_encode([ 'new_branch_name' => 'dev', 'old_branch_name' => 'main', ]) ); $this->log(' OK: dev branch created'); return 0; } private function setVariables(): int { $this->log('[2/5] Setting repo variables...'); $vars = $this->config['variables'] ?? []; if (empty($vars)) { $this->log(' SKIP: No variables in config'); return 0; } $errors = 0; $api = "/api/v1/repos/{$this->org}/{$this->repoName}" . "/actions/variables"; foreach ($vars as $name => $value) { if ($this->dryRun) { $display = strlen($value) > 40 ? substr($value, 0, 37) . '...' : $value; $this->log(" WOULD SET: {$name} = {$display}"); continue; } $ok = $this->setOrCreateVariable($api, $name, $value); if ($ok) { $this->log(" OK: {$name}"); } else { $this->log(" ERROR: {$name}"); $errors++; } } return $errors > 0 ? 1 : 0; } private function setSecrets(): int { $this->log('[3/5] Setting repo secrets...'); $secrets = $this->config['secrets'] ?? []; if (empty($secrets)) { $this->log(' SKIP: No secrets in config'); return 0; } $errors = 0; $api = "/api/v1/repos/{$this->org}/{$this->repoName}" . "/actions/secrets"; foreach ($secrets as $name => $value) { if (str_starts_with($value, '@')) { $keyPath = substr($value, 1); if (!file_exists($keyPath)) { $this->log(" ERROR: {$name} file not found: {$keyPath}"); $errors++; continue; } $value = file_get_contents($keyPath); } if ($this->dryRun) { $this->log(" WOULD SET: {$name} (len: " . strlen($value) . ")"); continue; } $resp = $this->giteaApi( 'PUT', "{$api}/{$name}", json_encode(['data' => $value]) ); if ($resp['code'] >= 200 && $resp['code'] < 300) { $this->log(" OK: {$name}"); } else { $this->log(" ERROR: {$name} (HTTP {$resp['code']})"); $errors++; } } return $errors > 0 ? 1 : 0; } private function setupMonitoring(): int { $this->log('[4/5] Setting up monitoring...'); $mon = $this->config['monitoring'] ?? []; if (empty($mon)) { $this->log(' SKIP: No monitoring config'); return 0; } $dashFile = $mon['grafana_dashboard'] ?? ''; if ( $dashFile !== '' && $this->grafanaUrl !== '' && $this->grafanaToken !== '' ) { $this->pushGrafanaDashboard( $dashFile, $mon['grafana_folder'] ?? 'Clients' ); } $urls = $mon['urls'] ?? []; $domains = $mon['domains'] ?? []; $api = "/api/v1/repos/{$this->org}/{$this->repoName}" . "/actions/variables"; if (!empty($urls)) { $urlStr = implode("\n", $urls); if ($this->dryRun) { $this->log(" WOULD SET: MONITORED_URLS"); } else { $this->setOrCreateVariable($api, 'MONITORED_URLS', $urlStr); $this->log(' OK: MONITORED_URLS'); } } if (!empty($domains)) { $domainStr = implode("\n", $domains); if ($this->dryRun) { $this->log(" WOULD SET: MONITORED_DOMAINS"); } else { $this->setOrCreateVariable($api, 'MONITORED_DOMAINS', $domainStr); $this->log(' OK: MONITORED_DOMAINS'); } } return 0; } private function pushGrafanaDashboard(string $file, string $folder): void { if (!file_exists($file)) { $this->log(" WARN: Dashboard not found: {$file}"); return; } if ($this->dryRun) { $this->log(" WOULD PUSH: dashboard to \"{$folder}\""); return; } $dashboard = json_decode(file_get_contents($file), true); if (!is_array($dashboard)) { $this->log(' ERROR: Invalid dashboard JSON'); return; } $folderId = $this->resolveGrafanaFolder($folder); $dashboard['id'] = null; $resp = $this->grafanaApi( 'POST', '/api/dashboards/db', json_encode([ 'dashboard' => $dashboard, 'folderId' => $folderId, 'overwrite' => true, ]) ); if ($resp['code'] === 200) { $data = json_decode($resp['body'], true); $this->log(" OK: Dashboard (uid: " . ($data['uid'] ?? '?') . ")"); } else { $this->log(" ERROR: Dashboard push (HTTP {$resp['code']})"); } } private function resolveGrafanaFolder(string $title): int { $resp = $this->grafanaApi('GET', '/api/folders'); if ($resp['code'] !== 200) { return 0; } $folders = json_decode($resp['body'], true); if (!is_array($folders)) { return 0; } foreach ($folders as $f) { if (strcasecmp($f['title'] ?? '', $title) === 0) { return (int) ($f['id'] ?? 0); } } return 0; } private function printSummary(): int { $vars = $this->config['variables'] ?? []; $secrets = $this->config['secrets'] ?? []; $clientName = $this->config['name'] ?? ''; $this->log(''); $this->log('[5/5] Provisioning summary'); $this->log(str_repeat('=', 60)); $this->log(" Repo: {$this->giteaUrl}/{$this->org}/{$this->repoName}"); $this->log(' Variables: ' . count($vars) . ' set'); $this->log(' Secrets: ' . count($secrets) . ' set'); $this->log(''); $this->log('Next steps:'); $this->log(' 1. Clone and customize the Joomla template'); $this->log(' 2. Push to dev to trigger dev deployment'); $this->log(' 3. Merge dev -> main for production release'); $this->log(str_repeat('=', 60)); return 0; } private function setOrCreateVariable( string $api, string $name, string $value ): bool { $resp = $this->giteaApi( 'PUT', "{$api}/{$name}", json_encode(['value' => $value]) ); if ($resp['code'] === 404) { $resp = $this->giteaApi( 'POST', $api, json_encode(['name' => $name, 'value' => $value]) ); } return $resp['code'] >= 200 && $resp['code'] < 300; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case '--config': $this->configFile = $args[++$i] ?? ''; break; case '--step': $this->step = $args[++$i] ?? ''; break; case '--dry-run': $this->dryRun = true; break; case '--help': case '-h': $this->printUsage(); exit(0); default: $this->log("WARNING: Unknown arg: {$args[$i]}"); break; } } } private function printUsage(): void { $this->log('Usage: client_provision.php --config [options]'); $this->log(''); $this->log('Provision a new client environment end-to-end.'); $this->log(''); $this->log('Options:'); $this->log(' --config Client config JSON'); $this->log(' --step Run one step: repo, variables, secrets, monitoring, summary'); $this->log(' --dry-run Preview without changes'); $this->log(' --help, -h Show this help'); $this->log(''); $this->log('Environment variables:'); $this->log(' GA_TOKEN Gitea API token'); $this->log(' GRAFANA_URL Grafana instance URL'); $this->log(' GRAFANA_TOKEN Grafana API token'); } private function giteaApi( string $method, string $endpoint, ?string $body = null ): array { return $this->httpRequest( $this->giteaUrl . $endpoint, $method, "token {$this->giteaToken}", $body ); } private function grafanaApi( string $method, string $endpoint, ?string $body = null ): array { return $this->httpRequest( $this->grafanaUrl . $endpoint, $method, "Bearer {$this->grafanaToken}", $body ); } private function httpRequest( string $url, string $method, string $auth, ?string $body = null ): array { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Accept: application/json', "Authorization: {$auth}", ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $responseBody = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); return ['code' => 0, 'body' => "cURL error: {$error}"]; } curl_close($ch); return ['code' => $httpCode, 'body' => $responseBody]; } private function log(string $message): void { fwrite(STDERR, $message . PHP_EOL); } } $app = new ClientProvision(); exit($app->run());