config_preview_deploy-1.0.0-alpha3/src/Controller/ProductionController.php
src/Controller/ProductionController.php
<?php
declare(strict_types=1);
namespace Drupal\config_preview_deploy\Controller;
use Drupal\config_preview_deploy\ProductionConfigDeployer;
use Drupal\config_preview_deploy\ConfigVerifier;
use Drupal\config_preview_deploy\HashVerification;
use Drupal\config_preview_deploy\ConfigExporter;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\Config\ConfigManagerInterface;
/**
* Production endpoints controller.
*/
class ProductionController extends ControllerBase {
/**
* The config verifier service.
*/
protected ConfigVerifier $configVerifier;
/**
* The production config deployer service.
*/
protected ProductionConfigDeployer $productionConfigDeployer;
/**
* The file system service.
*/
protected FileSystemInterface $fileSystem;
/**
* The hash verification service.
*
* @var \Drupal\config_preview_deploy\HashVerification
*/
protected HashVerification $hashVerification;
/**
* The config manager service.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected ConfigManagerInterface $configManager;
/**
* The config exporter.
*
* @var \Drupal\config_preview_deploy\ConfigExporter
*/
protected ConfigExporter $configExport;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->configVerifier = $container->get('config_preview_deploy.config_verifier');
$instance->productionConfigDeployer = $container->get('config_preview_deploy.production_config_deployer');
$instance->fileSystem = $container->get('file_system');
$instance->hashVerification = $container->get('config_preview_deploy.hash_verification');
$instance->configManager = $container->get('config.manager');
$instance->configExport = $container->get('config_preview_deploy.config_export');
return $instance;
}
/**
* Deploys configuration diff endpoint.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* JSON response with deployment result.
*/
public function deploy(Request $request): JsonResponse {
try {
// Parse JSON request body.
$content = $request->getContent();
$data = json_decode($content, TRUE);
if (!is_array($data)) {
return new JsonResponse([
'error' => $this->t('Invalid request format')->render(),
], 400);
}
// Validate required fields.
$requiredFields = ['diff', 'environment', 'auth_hash', 'timestamp'];
foreach ($requiredFields as $field) {
if (!isset($data[$field])) {
return new JsonResponse([
'error' => $this->t('Missing required field: @field', ['@field' => $field])->render(),
], 400);
}
}
$diff = $data['diff'];
$environment = $data['environment'];
$authHash = $data['auth_hash'];
$timestamp = (int) $data['timestamp'];
// Validate authentication including timestamp.
if (!$this->hashVerification->verifyHash($authHash, $timestamp)) {
$this->getLogger('config_preview_deploy')->warning('Invalid authentication in deployment request from @env', [
'@env' => $environment,
]);
return new JsonResponse([
'error' => $this->t('Invalid authentication')->render(),
'success' => FALSE,
], 403);
}
try {
// Deploy the configuration diff.
// The deployDiff method now handles validation internally
// using ConfigDiff.
$result = $this->productionConfigDeployer->deployDiff(
$diff,
$environment
);
// Update last change timestamp on successful deployment.
$this->configVerifier->updateLastChange($environment);
$this->getLogger('config_preview_deploy')->notice('Configuration deployed from @env with checkpoint @checkpoint on @prod_env', [
'@env' => $environment,
'@checkpoint' => $result['checkpoint_id'],
'@prod_env' => $this->configVerifier->getEnvironmentName(),
]);
return new JsonResponse($result);
}
catch (\RuntimeException $e) {
// Deployment validation errors return 400.
return new JsonResponse([
'error' => $e->getMessage(),
'success' => FALSE,
], 400);
}
catch (\Exception $e) {
$this->getLogger('config_preview_deploy')->error('Deployment endpoint error: @message', [
'@message' => $e->getMessage(),
]);
return new JsonResponse([
'error' => $this->t('Internal server error')->render(),
'success' => FALSE,
], 500);
}
}
catch (\Exception $e) {
// This outer catch should never be reached now.
return new JsonResponse([
'error' => $this->t('Unexpected error')->render(),
'success' => FALSE,
], 500);
}
}
/**
* Exports the active configuration as a tarball.
*
* This endpoint provides the same format as Drupal core's config export.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\Response
* Response with configuration tarball.
*/
public function exportConfig(Request $request): Response {
try {
// Verify hash authentication from request headers.
$authHash = $request->headers->get('X-Config-Deploy-Hash');
$timestamp = (int) $request->headers->get('X-Config-Deploy-Timestamp');
if (!$authHash || !$timestamp) {
return new JsonResponse(['error' => 'Missing authentication headers'], 401);
}
// Validate the hash.
if (!$this->hashVerification->verifyHash($authHash, $timestamp)) {
return new JsonResponse(['error' => 'Invalid authentication hash'], 403);
}
// Export configuration using the dedicated service.
$tarballContent = $this->configExport->exportConfigTarball();
// Return the tarball as a response.
$response = new Response($tarballContent);
$response->headers->set('Content-Type', 'application/gzip');
$response->headers->set('Content-Disposition', 'attachment; filename="config.tar.gz"');
$response->headers->set('Content-Length', (string) strlen($tarballContent));
$this->getLogger('config_preview_deploy')->notice('Configuration exported for environment via API');
return $response;
}
catch (\Exception $e) {
$this->getLogger('config_preview_deploy')->error('Config export error: @message', [
'@message' => $e->getMessage(),
]);
return new JsonResponse([
'error' => $this->t('Export failed: @error', ['@error' => $e->getMessage()])->render(),
], 500);
}
}
/**
* Returns production configuration status with token authentication.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object with timestamp and token parameters.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* JSON response with production configuration status.
*/
public function getStatus(Request $request): JsonResponse {
try {
$timestamp = $request->query->get('timestamp');
$token = $request->query->get('token');
// Check if required parameters are present.
if ($timestamp === NULL || $token === NULL) {
return new JsonResponse(['error' => 'Missing required parameters'], 403);
}
$timestamp = (int) $timestamp;
// Validate token using production host + timestamp.
if (!$this->hashVerification->verifyHash($token, $timestamp)) {
return new JsonResponse(['error' => 'Invalid verification hash'], 403);
}
// Return production configuration status.
$lastChange = $this->state()->get('config_preview_deploy.last_change', 0);
$lastDeployedFrom = $this->state()->get('config_preview_deploy.last_deployed_from', '');
$responseData = [
'last_change' => $lastChange,
'last_deployed_from' => $lastDeployedFrom,
'environment' => $this->configVerifier->getEnvironmentName(),
'timestamp' => time(),
];
return new JsonResponse($responseData);
}
catch (\Exception $e) {
$this->getLogger('config_preview_deploy')->error('Status endpoint error: @message', [
'@message' => $e->getMessage(),
]);
return new JsonResponse([
'error' => $this->t('Internal server error')->render(),
], 500);
}
}
}
