#!/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_inventory.php * VERSION: 09.22.00 * BRIEF: Discover and list all client-waas repos with their server configuration status */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class ClientInventoryCli extends CliFramework { private string $giteaUrl = 'https://git.mokoconsulting.tech'; private string $token = ''; private bool $jsonOutput = false; protected function configure(): void { $this->setDescription('Discover and list all client-waas repos with their server configuration status'); $this->addArgument('--gitea-url', 'Gitea URL (default: https://git.mokoconsulting.tech)', 'https://git.mokoconsulting.tech'); $this->addArgument('--token', 'Gitea API token', ''); $this->addArgument('--json', 'Output results as JSON', false); } protected function run(): int { $this->giteaUrl = rtrim($this->getArgument('--gitea-url'), '/'); $this->token = $this->getArgument('--token'); $this->jsonOutput = (bool) $this->getArgument('--json'); if ($this->token === '') { $this->log('ERROR', '--token is required.'); return 1; } $this->log('INFO', "Scanning Gitea instance: {$this->giteaUrl}"); // Step 1: List all orgs $orgs = $this->fetchOrgs(); if ($orgs === null) { $this->log('ERROR', 'Failed to fetch organizations.'); return 1; } $this->log('INFO', 'Found ' . count($orgs) . ' organization(s).'); // Step 2 & 3: For each org, find client-waas repos $inventory = []; foreach ($orgs as $org) { $orgName = $org['username'] ?? $org['name'] ?? ''; if ($orgName === '') { continue; } $repos = $this->fetchOrgRepos($orgName); if ($repos === null) { $this->log('WARNING', "Could not fetch repos for org: {$orgName}"); continue; } foreach ($repos as $repo) { $repoName = $repo['name'] ?? ''; if (strpos($repoName, 'client-waas') === false) { continue; } $hasDevConfig = $this->checkVariables($orgName, $repoName, ['DEV_SYNC_HOST', 'DEV_SYNC_PATH']); $hasLiveConfig = $this->checkVariables($orgName, $repoName, ['LIVE_SSH_HOST', 'LIVE_SYNC_PATH']); $lastPush = $repo['updated_at'] ?? 'unknown'; if ($lastPush !== 'unknown') { $lastPush = substr($lastPush, 0, 19); } $status = 'OK'; if (!$hasDevConfig && !$hasLiveConfig) { $status = 'UNCONFIGURED'; } elseif (!$hasDevConfig) { $status = 'NO DEV'; } elseif (!$hasLiveConfig) { $status = 'NO LIVE'; } $inventory[] = [ 'org' => $orgName, 'repo' => $repoName, 'has_dev_config' => $hasDevConfig, 'has_live_config' => $hasLiveConfig, 'last_push' => $lastPush, 'status' => $status, ]; } } // Output results if ($this->jsonOutput) { fwrite(STDOUT, json_encode($inventory, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); return 0; } if (count($inventory) === 0) { $this->log('INFO', 'No client-waas repos found.'); return 0; } // Print table $this->log('INFO', ''); $this->log('INFO', sprintf( '%-20s | %-35s | %-10s | %-11s | %-19s | %s', 'Org', 'Repo', 'Dev Config', 'Live Config', 'Last Push', 'Status' )); $this->log('INFO', str_repeat('-', 120)); foreach ($inventory as $entry) { $this->log('INFO', sprintf( '%-20s | %-35s | %-10s | %-11s | %-19s | %s', $entry['org'], $entry['repo'], $entry['has_dev_config'] ? 'Yes' : 'No', $entry['has_live_config'] ? 'Yes' : 'No', $entry['last_push'], $entry['status'] )); } $this->log('INFO', ''); $this->log('INFO', 'Total: ' . count($inventory) . ' client-waas repo(s).'); return 0; } private function fetchOrgs(): ?array { // Try admin endpoint first, fall back to user-visible orgs $response = $this->apiRequest('GET', '/api/v1/admin/orgs?limit=50'); if ($response['code'] >= 200 && $response['code'] < 300) { $data = json_decode($response['body'], true); if (is_array($data)) { return $data; } } $this->log('INFO', 'Admin orgs endpoint unavailable, falling back to user orgs...'); $response = $this->apiRequest('GET', '/api/v1/user/orgs?limit=50'); if ($response['code'] >= 200 && $response['code'] < 300) { $data = json_decode($response['body'], true); if (is_array($data)) { return $data; } } return null; } private function fetchOrgRepos(string $org): ?array { $page = 1; $allRepos = []; while (true) { $response = $this->apiRequest('GET', "/api/v1/orgs/{$org}/repos?limit=50&page={$page}"); if ($response['code'] < 200 || $response['code'] >= 300) { return $page === 1 ? null : $allRepos; } $data = json_decode($response['body'], true); if (!is_array($data) || count($data) === 0) { break; } $allRepos = array_merge($allRepos, $data); $page++; } return $allRepos; } private function checkVariables(string $org, string $repo, array $requiredVars): bool { $response = $this->apiRequest('GET', "/api/v1/repos/{$org}/{$repo}/actions/variables"); if ($response['code'] < 200 || $response['code'] >= 300) { return false; } $data = json_decode($response['body'], true); if (!is_array($data)) { return false; } $existingVars = []; foreach ($data as $variable) { if (isset($variable['name'])) { $existingVars[] = $variable['name']; } } foreach ($requiredVars as $var) { if (!in_array($var, $existingVars, true)) { return false; } } return true; } private function apiRequest(string $method, string $endpoint, ?string $body = null): array { $url = $this->giteaUrl . $endpoint; $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: token {$this->token}", ]); 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 ClientInventoryCli(); exit($app->execute());