✨ feat(ComputeEngineClient): Ajoute le nom de l'instance à l'entité Compute.
✨ feat(Website): Ajoute une relation Website-Serveur. ✨ feat(form/website): Ajoute le choix du serveur au formulaire de création de site. ✨ feat(infra/website): Crée un endpoint API pour le déploiement des sites. ✨ feat(twig): Ajoute des filtres twig pour slugify et récupérer la clé API principale. ✨ feat(compute): Ajoute une relation OneToMany vers Website. ♻️ refactor(ApiSubscriber): Gère les préfixes d'API privés séparément. ```
This commit is contained in:
38
migrations/Version20251112113223.php
Normal file
38
migrations/Version20251112113223.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251112113223 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE compute ADD name VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE website ADD server_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE website ADD CONSTRAINT FK_476F5DE71844E6B7 FOREIGN KEY (server_id) REFERENCES compute (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_476F5DE71844E6B7 ON website (server_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('ALTER TABLE website DROP CONSTRAINT FK_476F5DE71844E6B7');
|
||||
$this->addSql('DROP INDEX IDX_476F5DE71844E6B7');
|
||||
$this->addSql('ALTER TABLE website DROP server_id');
|
||||
$this->addSql('ALTER TABLE compute DROP name');
|
||||
}
|
||||
}
|
||||
20
src/Controller/Api/Private/Infra/WebsiteController.php
Normal file
20
src/Controller/Api/Private/Infra/WebsiteController.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api\Private\Infra;
|
||||
|
||||
use App\Repository\EsyWeb\WebsiteRepository;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class WebsiteController extends AbstractController
|
||||
{
|
||||
#[Route('/api/private/infra/websites/deploy', name: 'api_private_infra_website_deploy', methods: ['GET'])]
|
||||
public function websiteDeploy(WebsiteRepository $websiteRepository)
|
||||
{
|
||||
return new Response($this->renderView('api/private/infra/website/deploy.twig', [
|
||||
'websites' =>$websiteRepository->findBy(['state'=>'validate'], ['id' => 'ASC'])
|
||||
]),200,['Content-Type' => 'text/plain']);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use AllowDynamicProperties;
|
||||
use App\Entity\EsyWeb\Website;
|
||||
use App\Repository\ComputeRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[AllowDynamicProperties] #[ORM\Entity(repositoryClass: ComputeRepository::class)]
|
||||
@@ -29,6 +32,20 @@ class Compute
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $status = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Website>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Website::class, mappedBy: 'server')]
|
||||
private Collection $websites;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->websites = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -93,4 +110,46 @@ class Compute
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Website>
|
||||
*/
|
||||
public function getWebsites(): Collection
|
||||
{
|
||||
return $this->websites;
|
||||
}
|
||||
|
||||
public function addWebsite(Website $website): static
|
||||
{
|
||||
if (!$this->websites->contains($website)) {
|
||||
$this->websites->add($website);
|
||||
$website->setServer($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWebsite(Website $website): static
|
||||
{
|
||||
if ($this->websites->removeElement($website)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($website->getServer() === $this) {
|
||||
$website->setServer(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Entity\EsyWeb;
|
||||
|
||||
use App\Entity\Compute;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerDns;
|
||||
use App\Entity\Revendeur;
|
||||
@@ -52,6 +53,9 @@ class Website
|
||||
#[ORM\OneToMany(targetEntity: WebsiteKey::class, mappedBy: 'websitre')]
|
||||
private Collection $websiteKeys;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'websites')]
|
||||
private ?Compute $server = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->websiteDns = new ArrayCollection();
|
||||
@@ -212,4 +216,16 @@ class Website
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getServer(): ?Compute
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
public function setServer(?Compute $server): static
|
||||
{
|
||||
$this->server = $server;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
class ApiSubscriber
|
||||
{
|
||||
// Define the API prefix once for easier maintenance
|
||||
private const API_PRIVATE_PREFIX = '/api/private';
|
||||
private const API_PRIVATE_PREFIX_ESYWEB = '/api/private/esyweb';
|
||||
private const API_PRIVATE_PREFIX = '/api/private/infra';
|
||||
|
||||
/**
|
||||
* Handles exceptions specifically for the private API routes.
|
||||
@@ -25,23 +26,41 @@ class ApiSubscriber
|
||||
$request = $event->getRequest();
|
||||
|
||||
// 1. Quick exit if the route is not part of the private API
|
||||
if (!str_starts_with($request->getPathInfo(), self::API_PRIVATE_PREFIX)) {
|
||||
return;
|
||||
if (str_starts_with($request->getPathInfo(), self::API_PRIVATE_PREFIX_ESYWEB)) {
|
||||
|
||||
|
||||
$throwable = $event->getThrowable();
|
||||
|
||||
// 2. Handle only NotFoundHttpException
|
||||
if ($throwable instanceof NotFoundHttpException) {
|
||||
// Set error response
|
||||
$response = new JsonResponse([
|
||||
'state' => 'error',
|
||||
'message' => sprintf('Route `%s` not exist', $request->getPathInfo()),
|
||||
]);
|
||||
|
||||
$event->setResponse($response);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
if (str_starts_with($request->getPathInfo(), self::API_PRIVATE_PREFIX)) {
|
||||
|
||||
|
||||
$throwable = $event->getThrowable();
|
||||
|
||||
// 2. Handle only NotFoundHttpException
|
||||
if ($throwable instanceof NotFoundHttpException) {
|
||||
// Set error response
|
||||
$response = new JsonResponse([
|
||||
'state' => 'error',
|
||||
'message' => sprintf('Route `%s` not exist', $request->getPathInfo()),
|
||||
]);
|
||||
|
||||
$event->setResponse($response);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
$throwable = $event->getThrowable();
|
||||
|
||||
// 2. Handle only NotFoundHttpException
|
||||
if ($throwable instanceof NotFoundHttpException) {
|
||||
// Set error response
|
||||
$response = new JsonResponse([
|
||||
'state' => 'error',
|
||||
'message' => sprintf('Route `%s` not exist', $request->getPathInfo()),
|
||||
]);
|
||||
|
||||
$event->setResponse($response);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,31 +76,55 @@ class ApiSubscriber
|
||||
$request = $event->getRequest();
|
||||
$pathInfo = $request->getPathInfo();
|
||||
|
||||
if (!str_starts_with($pathInfo, self::API_PRIVATE_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
if (str_starts_with($pathInfo, self::API_PRIVATE_PREFIX_ESYWEB)) {
|
||||
|
||||
// 2. Define required headers and their respective error messages
|
||||
$requiredHeaders = [
|
||||
'EsyWebKey' => 'Missing Header `EsyWebKey`',
|
||||
'EsyWebDns' => 'Missing Header `EsyWebDns`',
|
||||
'EsyWebApiKey' => 'Missing Header `EsyWebApiKey`',
|
||||
];
|
||||
|
||||
// 3. Loop through required headers and check for existence (removes deep nesting)
|
||||
foreach ($requiredHeaders as $headerName => $errorMessage) {
|
||||
if (!$request->headers->has($headerName)) {
|
||||
$this->setAccessDeniedResponse($event, $errorMessage);
|
||||
// 2. Define required headers and their respective error messages
|
||||
$requiredHeaders = [
|
||||
'EsyWebKey' => 'Missing Header `EsyWebKey`',
|
||||
'EsyWebDns' => 'Missing Header `EsyWebDns`',
|
||||
'EsyWebApiKey' => 'Missing Header `EsyWebApiKey`',
|
||||
];
|
||||
|
||||
// 3. Loop through required headers and check for existence (removes deep nesting)
|
||||
foreach ($requiredHeaders as $headerName => $errorMessage) {
|
||||
if (!$request->headers->has($headerName)) {
|
||||
$this->setAccessDeniedResponse($event, $errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Validate the 'EsyWebKey' against the environment variable
|
||||
// Use environment variables via Symfony's container/services, but for a quick fix,
|
||||
// using $_ENV is kept from the original code (better to inject as a service parameter).
|
||||
if ($request->headers->get('EsyWebKey') !== ($_ENV['APP_SECRET'] ?? '')) {
|
||||
$this->setAccessDeniedResponse($event, 'Header `EsyWebKey` Is Invalid');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (str_starts_with($pathInfo, self::API_PRIVATE_PREFIX)) {
|
||||
|
||||
// 4. Validate the 'EsyWebKey' against the environment variable
|
||||
// Use environment variables via Symfony's container/services, but for a quick fix,
|
||||
// using $_ENV is kept from the original code (better to inject as a service parameter).
|
||||
if ($request->headers->get('EsyWebKey') !== ($_ENV['APP_SECRET'] ?? '')) {
|
||||
$this->setAccessDeniedResponse($event, 'Header `EsyWebKey` Is Invalid');
|
||||
return;
|
||||
|
||||
// 2. Define required headers and their respective error messages
|
||||
$requiredHeaders = [
|
||||
'EsyWebKey' => 'Missing Header `EsyWebKey`',
|
||||
];
|
||||
|
||||
// 3. Loop through required headers and check for existence (removes deep nesting)
|
||||
foreach ($requiredHeaders as $headerName => $errorMessage) {
|
||||
if (!$request->headers->has($headerName)) {
|
||||
$this->setAccessDeniedResponse($event, $errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Validate the 'EsyWebKey' against the environment variable
|
||||
// Use environment variables via Symfony's container/services, but for a quick fix,
|
||||
// using $_ENV is kept from the original code (better to inject as a service parameter).
|
||||
if ($request->headers->get('EsyWebKey') !== ($_ENV['APP_SECRET'] ?? '')) {
|
||||
$this->setAccessDeniedResponse($event, 'Header `EsyWebKey` Is Invalid');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Form\Artemis\EsyWeb;
|
||||
|
||||
use App\Entity\Compute;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\EsyWeb\Website;
|
||||
use App\Entity\Revendeur;
|
||||
@@ -33,6 +34,13 @@ class WebsiteType extends AbstractType
|
||||
'label' => 'Nom du site internet',
|
||||
'required' => true
|
||||
])
|
||||
->add('server',EntityType::class,[
|
||||
'label' => 'Serveur',
|
||||
'class' => Compute::class,
|
||||
'choice_label'=> function (Compute $compute) {
|
||||
return $compute->getName();
|
||||
}
|
||||
])
|
||||
->add('customer',EntityType::class,[
|
||||
'label' => 'Client',
|
||||
'attr' =>[
|
||||
|
||||
@@ -73,6 +73,7 @@ class ComputeEngineClient
|
||||
$compute->setInternalIp($network->getNetworkIP());
|
||||
$compute->setExternalIp($accessConfig->getNatIP());
|
||||
$compute->setStatus('down');
|
||||
$compute->setName($instance->getName());
|
||||
}
|
||||
|
||||
$this->entityManager->persist($compute);
|
||||
@@ -101,7 +102,6 @@ class ComputeEngineClient
|
||||
$instance = $this->client->get($request);
|
||||
|
||||
$compute->setStatus($instance->getStatus());
|
||||
$compute->name = $instance->getName();
|
||||
|
||||
return $compute;
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ class Client
|
||||
|
||||
public function servers()
|
||||
{
|
||||
return $this->cache->get('list_server_ovh',function () {
|
||||
return $this->cache->get('list_server_ovh', callback: function () {
|
||||
$list = [];
|
||||
$servers = $this->ovhClient->get('/dedicated/server');
|
||||
foreach ($servers as $server) {
|
||||
@@ -68,6 +68,7 @@ class Client
|
||||
$compute->setInternalIp($detail['ip']);
|
||||
$compute->setExternalIp($detail['ip']);
|
||||
$compute->setStatus('down');
|
||||
$compute->setName($server);
|
||||
}
|
||||
$this->em->persist($compute);
|
||||
$list[] = $compute;
|
||||
@@ -84,7 +85,6 @@ class Client
|
||||
$c = explode("|", $compute->getInstanceId());
|
||||
$detail = $this->ovhClient->get('/dedicated/server/' . $c[1]);
|
||||
$compute->setStatus($detail['state'] == "ok" ? "RUNNING" : "DOWN");
|
||||
$compute->name = $c[1];
|
||||
$compute->type = $c[0];
|
||||
return $compute;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,9 @@ use App\Entity\CustomerDevis;
|
||||
use App\Entity\CustomerDns;
|
||||
use App\Entity\CustomerOrder;
|
||||
use App\Entity\CustomerSplit;
|
||||
use App\Entity\EsyWeb\Website;
|
||||
use App\Entity\EsyWeb\WebsiteKey;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
@@ -22,9 +25,25 @@ class TwigOrderExtensions extends AbstractExtension
|
||||
new TwigFilter('countEmail',[$this,'countEmail']),
|
||||
new TwigFilter('skFormat',[$this,'skFormat']),
|
||||
new TwigFilter('noCompletedEch',[$this,'noCompletedEch']),
|
||||
new TwigFilter('slugify',[$this,'slugify']),
|
||||
new TwigFilter('mainKey',[$this,'mainKey']),
|
||||
];
|
||||
}
|
||||
|
||||
public function mainKey(Website $website): ?string
|
||||
{
|
||||
/** @var WebsiteKey $apiKey */
|
||||
$apiKey = $website->getWebsiteKeys()->filter(function (WebsiteKey $websiteKey) {
|
||||
return $websiteKey->getType() == "api_key";
|
||||
})->first();
|
||||
return $apiKey->getApiKey();
|
||||
}
|
||||
public function slugify(string $title): string
|
||||
{
|
||||
$s = new Slugify();
|
||||
return $s->slugify($title);
|
||||
}
|
||||
|
||||
public function noCompletedEch(CustomerSplit $customerSplit)
|
||||
{
|
||||
$nbEch = 0;
|
||||
|
||||
4
templates/api/private/infra/website/deploy.twig
Normal file
4
templates/api/private/infra/website/deploy.twig
Normal file
@@ -0,0 +1,4 @@
|
||||
[website_deploy]
|
||||
{% for website in websites %}
|
||||
{{ website.mainDns}} ansible_connection=ssh ansible_user=bot ansible_python_interpreter=/usr/bin/python3 path=/var/www/{{ website.title|slugify }} website_id={{ website.id }} api_key={{ website|mainKey }}
|
||||
{% endfor %}
|
||||
@@ -20,6 +20,9 @@
|
||||
<div class="flex-1">
|
||||
{{ form_row(form.revendeur) }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
{{ form_row(form.server) }}
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold px-4 py-2 rounded">Crée le site internet</button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user