Files
Jonathan Miller 4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
style: fix all PHPCS PSR-12 violations across 74 files (7539 → 0 errors)
- Convert tabs to spaces (3,413 violations)
- Fix line endings, trailing whitespace, brace placement
- Break lines exceeding 150-char absolute limit
- Replace heredoc tab closers with spaces
- Fix empty elseif, forbidden function calls
- Update phpcs.xml: exclude rules inappropriate for CLI scripts
  (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder,
  empty catch blocks)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:07:51 -05:00

593 lines
17 KiB
PHP

<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.Plugins
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/Plugins/TerraformPlugin.php
* BRIEF: Enterprise plugin for Terraform projects
*/
declare(strict_types=1);
namespace MokoEnterprise\Plugins;
use MokoEnterprise\AbstractProjectPlugin;
/**
* Terraform Project Plugin
*
* Provides validation, metrics, and management capabilities for
* Terraform infrastructure-as-code projects.
*/
class TerraformPlugin extends AbstractProjectPlugin
{
/**
* {@inheritdoc}
*/
public function getProjectType(): string
{
return 'terraform';
}
/**
* {@inheritdoc}
*/
public function getPluginName(): string
{
return 'Terraform Enterprise Plugin';
}
/**
* {@inheritdoc}
*/
public function validateProject(array $config, string $projectPath): array
{
$errors = [];
$warnings = [];
// Check for .tf files
$tfFiles = $this->countFiles($projectPath, '*.tf');
if ($tfFiles === 0) {
$errors[] = 'No Terraform (.tf) files found';
}
// Check for main.tf
if (!$this->fileExists($projectPath, 'main.tf')) {
$warnings[] = 'No main.tf file found';
}
// Check for variables.tf
if (!$this->fileExists($projectPath, 'variables.tf')) {
$warnings[] = 'No variables.tf file found';
}
// Check for outputs.tf
if (!$this->fileExists($projectPath, 'outputs.tf')) {
$warnings[] = 'No outputs.tf file found';
}
// Check for terraform.tfvars in git
if (
$this->fileExists($projectPath, 'terraform.tfvars') &&
!$this->isInGitignore($projectPath, 'terraform.tfvars')
) {
$warnings[] = 'terraform.tfvars may contain secrets and should be in .gitignore';
}
// Check for .terraform directory in git
if (
$this->fileExists($projectPath, '.terraform') &&
!$this->isInGitignore($projectPath, '.terraform')
) {
$warnings[] = '.terraform directory should be in .gitignore';
}
// Check for backend configuration
if (!$this->hasBackendConfig($projectPath)) {
$warnings[] = 'No backend configuration found - state will be stored locally';
}
// Check for version constraints
if (!$this->hasVersionConstraints($projectPath)) {
$warnings[] = 'No Terraform version constraints defined';
}
// Check for proper formatting
$hasFormatIssues = $this->checkFormatting($projectPath);
if ($hasFormatIssues) {
$warnings[] = 'Some Terraform files may not be properly formatted (run terraform fmt)';
}
$this->log(
'Terraform project validation completed',
'info',
['errors' => count($errors), 'warnings' => count($warnings)]
);
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
];
}
/**
* {@inheritdoc}
*/
public function collectMetrics(string $projectPath, array $config): array
{
$metrics = [
'tf_files' => $this->countFiles($projectPath, '*.tf'),
'tfvars_files' => $this->countFiles($projectPath, '*.tfvars'),
'modules' => $this->countModules($projectPath),
'resources' => $this->countResources($projectPath),
'data_sources' => $this->countDataSources($projectPath),
'variables' => $this->countVariables($projectPath),
'outputs' => $this->countOutputs($projectPath),
'providers' => $this->detectProviders($projectPath),
'has_backend' => $this->hasBackendConfig($projectPath),
'has_lock_file' => $this->fileExists($projectPath, '.terraform.lock.hcl'),
'has_tests' => $this->hasTests($projectPath),
'terraform_version' => $this->detectTerraformVersion($projectPath),
];
// Count lines of code
$tfFiles = $this->findFiles($projectPath, '*.tf');
$totalLines = 0;
foreach ($tfFiles as $file) {
if (is_file($file)) {
$totalLines += count(file($file));
}
}
$metrics['total_lines'] = $totalLines;
// Record metrics
$this->recordMetric('terraform', 'tf_files', $metrics['tf_files']);
$this->recordMetric('terraform', 'resources', $metrics['resources']);
$this->recordMetric('terraform', 'total_lines', $totalLines);
$this->log('Collected Terraform metrics', 'info', $metrics);
return $metrics;
}
/**
* {@inheritdoc}
*/
public function healthCheck(string $projectPath, array $config): array
{
$issues = [];
$score = 100;
// Check for .tf files
if ($this->countFiles($projectPath, '*.tf') === 0) {
$issues[] = [
'severity' => 'critical',
'message' => 'No Terraform files found',
];
$score -= 30;
}
// Check for standard file structure
if (!$this->fileExists($projectPath, 'main.tf')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing main.tf',
];
$score -= 10;
}
if (!$this->fileExists($projectPath, 'variables.tf')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing variables.tf',
];
$score -= 5;
}
if (!$this->fileExists($projectPath, 'outputs.tf')) {
$issues[] = [
'severity' => 'info',
'message' => 'Missing outputs.tf',
];
$score -= 5;
}
// Check for backend configuration
if (!$this->hasBackendConfig($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No remote backend configured (state stored locally)',
];
$score -= 10;
}
// Check for lock file
if (!$this->fileExists($projectPath, '.terraform.lock.hcl')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing .terraform.lock.hcl (run terraform init)',
];
$score -= 10;
}
// Check for version constraints
if (!$this->hasVersionConstraints($projectPath)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No Terraform version constraints defined',
];
$score -= 5;
}
// Check for secrets in tfvars
if (
$this->fileExists($projectPath, 'terraform.tfvars') &&
!$this->isInGitignore($projectPath, 'terraform.tfvars')
) {
$issues[] = [
'severity' => 'warning',
'message' => 'terraform.tfvars not in .gitignore',
];
$score -= 10;
}
// Check .terraform directory
if (
$this->fileExists($projectPath, '.terraform') &&
!$this->isInGitignore($projectPath, '.terraform')
) {
$issues[] = [
'severity' => 'warning',
'message' => '.terraform directory not in .gitignore',
];
$score -= 5;
}
// Check formatting
if ($this->checkFormatting($projectPath)) {
$issues[] = [
'severity' => 'info',
'message' => 'Files not formatted (run terraform fmt)',
];
$score -= 5;
}
// Check for README
if (!$this->fileExists($projectPath, 'README.md')) {
$issues[] = [
'severity' => 'info',
'message' => 'Missing README.md',
];
$score -= 5;
}
$score = max(0, $score);
$this->log('Terraform health check completed', 'info', [
'score' => $score,
'issues_count' => count($issues),
]);
return [
'healthy' => $score >= 70,
'score' => $score,
'issues' => $issues,
];
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array
{
return [
'*.tf files',
'.terraform.lock.hcl',
];
}
/**
* {@inheritdoc}
*/
public function getRecommendedFiles(): array
{
return [
'main.tf',
'variables.tf',
'outputs.tf',
'versions.tf',
'terraform.tfvars.example',
'.gitignore',
'README.md',
'.terraform-version or .tool-versions',
'modules/ (for reusable modules)',
'examples/',
];
}
/**
* {@inheritdoc}
*/
public function getConfigSchema(): array
{
return [
'type' => 'object',
'properties' => [
'terraform_version' => [
'type' => 'string',
'description' => 'Required Terraform version',
],
'providers' => [
'type' => 'array',
'items' => ['type' => 'string'],
'description' => 'List of Terraform providers used',
],
'backend_type' => [
'type' => 'string',
'enum' => ['local', 's3', 'azurerm', 'gcs', 'remote', 'consul', 'etcd'],
'description' => 'Backend type for state storage',
],
'enable_validation' => [
'type' => 'boolean',
'description' => 'Enable terraform validate checks',
],
'enable_security_scan' => [
'type' => 'boolean',
'description' => 'Enable security scanning with tools like tfsec',
],
],
'required' => ['terraform_version'],
];
}
/**
* {@inheritdoc}
*/
public function getBestPractices(): array
{
return [
'Use remote backend for state storage (S3, Azure, GCS)',
'Define Terraform version constraints in versions.tf',
'Organize code into reusable modules',
'Use variables.tf for all input variables',
'Document outputs in outputs.tf',
'Never commit terraform.tfvars with sensitive data',
'Use .terraform-version or .tool-versions for version management',
'Run terraform fmt before committing',
'Use terraform validate to check syntax',
'Implement security scanning with tfsec or checkov',
'Use consistent naming conventions',
'Add descriptions to all variables and outputs',
'Pin provider versions in versions.tf',
'Use workspaces for environment separation',
'Document infrastructure in README with examples',
];
}
/**
* Check for backend configuration
*/
private function hasBackendConfig(string $projectPath): bool
{
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content && preg_match('/terraform\s*\{[^}]*backend\s+["\w]+\s*\{/', $content)) {
return true;
}
}
}
return false;
}
/**
* Check for version constraints
*/
private function hasVersionConstraints(string $projectPath): bool
{
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content && preg_match('/required_version\s*=/', $content)) {
return true;
}
}
}
return false;
}
/**
* Check formatting
*/
private function checkFormatting(string $projectPath): bool
{
// This is a simplified check - in production, would run terraform fmt -check
return false;
}
/**
* Count modules
*/
private function countModules(string $projectPath): int
{
$count = 0;
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/module\s+"[^"]+"\s*\{/', $content);
}
}
}
return $count;
}
/**
* Count resources
*/
private function countResources(string $projectPath): int
{
$count = 0;
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/resource\s+"[^"]+"\s+"[^"]+"\s*\{/', $content);
}
}
}
return $count;
}
/**
* Count data sources
*/
private function countDataSources(string $projectPath): int
{
$count = 0;
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/data\s+"[^"]+"\s+"[^"]+"\s*\{/', $content);
}
}
}
return $count;
}
/**
* Count variables
*/
private function countVariables(string $projectPath): int
{
$count = 0;
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/variable\s+"[^"]+"\s*\{/', $content);
}
}
}
return $count;
}
/**
* Count outputs
*/
private function countOutputs(string $projectPath): int
{
$count = 0;
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
$count += preg_match_all('/output\s+"[^"]+"\s*\{/', $content);
}
}
}
return $count;
}
/**
* Detect providers
*/
private function detectProviders(string $projectPath): array
{
$providers = [];
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content) {
if (preg_match_all('/provider\s+"([^"]+)"\s*\{/', $content, $matches)) {
$providers = array_merge($providers, $matches[1]);
}
}
}
}
return array_unique($providers);
}
/**
* Detect Terraform version
*/
private function detectTerraformVersion(string $projectPath): string
{
// Check .terraform-version
$versionFile = $this->readFile($projectPath, '.terraform-version');
if ($versionFile) {
return trim($versionFile);
}
// Check versions.tf
$tfFiles = $this->findFiles($projectPath, '*.tf');
foreach ($tfFiles as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content && preg_match('/required_version\s*=\s*"([^"]+)"/', $content, $matches)) {
return $matches[1];
}
}
}
return 'unknown';
}
/**
* Check for tests
*/
private function hasTests(string $projectPath): bool
{
return $this->fileExists($projectPath, 'test') ||
$this->fileExists($projectPath, 'tests') ||
$this->fileExists($projectPath, 'examples');
}
/**
* Check if in .gitignore
*/
private function isInGitignore(string $projectPath, string $path): bool
{
$gitignore = $this->readFile($projectPath, '.gitignore');
if (!$gitignore) {
return false;
}
return strpos($gitignore, $path) !== false;
}
}