#!/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: 09.22.00 * BRIEF: Provision a new client environment end-to-end */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class ClientProvisionCli extends CliFramework { private string $giteaUrl = 'https://git.mokoconsulting.tech'; private string $giteaToken = ''; private string $grafanaUrl = ''; private string $grafanaToken = ''; private string $configFile = ''; private string $step = ''; /** @var array */ private array $config = []; private string $org = ''; private string $repoName = ''; protected function configure(): void { $this->setDescription('Provision a new client environment end-to-end'); $this->addArgument('--config', 'Client config JSON', ''); $this->addArgument('--step', 'Run one step: repo, variables, secrets, monitoring, summary', ''); } protected function run(): int { $this->configFile = $this->getArgument('--config'); $this->step = $this->getArgument('--step'); if ($this->configFile === '') { $this->log('ERROR', '--config is required.'); 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('MOKOGITEA_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 MOKOGITEA_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('INFO', "=== Client Provisioning: {$clientName} ==="); $this->log('INFO', " Org: {$this->org}"); $this->log('INFO', " Repo: {$this->repoName}"); if ($this->dryRun) { $this->log('INFO', ' Mode: DRY RUN'); } echo "\n"; $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('INFO', '[1/5] Creating repository...'); $check = $this->giteaApi( 'GET', "/api/v1/repos/{$this->org}/{$this->repoName}" ); if ($check['code'] === 200) { $this->log('INFO', ' SKIP: repo already exists'); return 0; } if ($this->dryRun) { $this->log('INFO', " 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('INFO', ' 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('INFO', ' OK: dev branch created'); return 0; } private function setVariables(): int { $this->log('INFO', '[2/5] Setting repo variables...'); $vars = $this->config['variables'] ?? []; if (empty($vars)) { $this->log('INFO', ' 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('INFO', " WOULD SET: {$name} = {$display}"); continue; } $ok = $this->setOrCreateVariable($api, $name, $value); if ($ok) { $this->log('INFO', " OK: {$name}"); } else { $this->log('ERROR', " {$name}"); $errors++; } } return $errors > 0 ? 1 : 0; } private function setSecrets(): int { $this->log('INFO', '[3/5] Setting repo secrets...'); $secrets = $this->config['secrets'] ?? []; if (empty($secrets)) { $this->log('INFO', ' 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('INFO', " 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('INFO', " OK: {$name}"); } else { $this->log('ERROR', " {$name} (HTTP {$resp['code']})"); $errors++; } } return $errors > 0 ? 1 : 0; } private function setupMonitoring(): int { $this->log('INFO', '[4/5] Setting up monitoring...'); $mon = $this->config['monitoring'] ?? []; if (empty($mon)) { $this->log('INFO', ' 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('INFO', ' WOULD SET: MONITORED_URLS'); } else { $this->setOrCreateVariable($api, 'MONITORED_URLS', $urlStr); $this->log('INFO', ' OK: MONITORED_URLS'); } } if (!empty($domains)) { $domainStr = implode("\n", $domains); if ($this->dryRun) { $this->log('INFO', ' WOULD SET: MONITORED_DOMAINS'); } else { $this->setOrCreateVariable($api, 'MONITORED_DOMAINS', $domainStr); $this->log('INFO', ' OK: MONITORED_DOMAINS'); } } return 0; } private function pushGrafanaDashboard(string $file, string $folder): void { if (!file_exists($file)) { $this->warning("Dashboard not found: {$file}"); return; } if ($this->dryRun) { $this->log('INFO', " 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('INFO', " 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'] ?? []; echo "\n"; $this->log('INFO', '[5/5] Provisioning summary'); echo str_repeat('=', 60) . "\n"; echo " Repo: {$this->giteaUrl}/{$this->org}/{$this->repoName}\n"; echo ' Variables: ' . count($vars) . " set\n"; echo ' Secrets: ' . count($secrets) . " set\n"; echo "\n"; echo "Next steps:\n"; echo " 1. Clone and customize the Joomla template\n"; echo " 2. Push to dev to trigger dev deployment\n"; echo " 3. Merge dev -> main for production release\n"; echo str_repeat('=', 60) . "\n"; 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 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]; } } $app = new ClientProvisionCli(); exit($app->execute());