Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 6 additions & 48 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,13 @@
# Changelog

## 0.0.13 — 2026-06-22
## 0.0.14 — 2026-06-23

### Added

- OpenAPI `receptacle-v1` : propriétés recommandées sous `metadata` pour `CREATE_INSTANCE` billing (`subscriptionId`, `formulaId`, `formulaName`).
- Fixture `orchestration-command-create-enriched.json` : exemple metadata billing AM.

### Changed
### Breaking

- Documentation uniquement ; le DTO `OrchestrationCommand` accepte déjà `metadata` libre (`additionalProperties`).

## 0.0.12 — 2026-06-22
- Configuration : un seul paramètre `application_token` / env `AM_DRIVER_APPLICATION_TOKEN` (les 4 clés legacy restent acceptées en alias de migration).
- HTTP : uniquement `X-AM-Application-Token` (entrée et sortie vers AM).
- Retrait de `instanceIntegrationToken` dans `OrchestrationCommand`.

### Added

- **`create_instance_execution`** (`sync` | `deferred`, défaut `sync`) : en mode `deferred`, `CREATE_INSTANCE` répond HTTP 200 immédiatement ; l’hôte dispatche l’exécution via `DeferredCreateInstanceDispatcherInterface` ; le callback AM part après `executeCreateInstance()`.
- **`DeferredCreateInstanceDispatcherInterface`**, **`DeferredCreateInstanceWorker`**, commande console **`am-driver:execute-deferred-create-instance`**.
- **`OrchestrationCommandLifecycleStoreInterface`** : suivi des commandes en cours (`idempotency-in-progress/`) pour éviter un double provisionnement pendant le traitement async.

### Changed

- **`OrchestrationCommandProcessor::executeCreateInstance()`** : méthode publique pour l’exécution différée (handler + idempotence + callback).

## 0.0.10 — 2026-06-11

### Added

- **`OrchestrationCommand`** : champs enrichis optionnels pour `CREATE_INSTANCE` uniquement :
- `name` (getter `name()`)
- `credentials.login` (getter `credentialsLogin()`)
- `metadata` (getter `metadata()`, défaut `[]`)
- Validation : `name`, `credentials`, `metadata` interdits hors `CREATE_INSTANCE` ; clé legacy `shortname` rejetée.

## 0.0.9 — 2026-06-11

### Breaking changes

- **Correlation key** : `tenantId` remplacé par `instanceId` partout dans le bundle (DTO, snapshots, état opérationnel, consommation, espace disque local).
- **`OrchestrationCommand`** : `tenantId` supprimé du corps ; `correlationId` devient optionnel. Clés requises : `operation`, `appId`, `instanceId`, `idempotencyKey`, `occurredAt`.
- **`ConsumptionWebhookEvent`** : champ JSON `tenantId` → `instanceId`.
- **`ManagedInstanceResourceSnapshot`** : champ JSON `tenantId` → `instanceId` ; getter `instanceId()`.
- **Stores / managers** : paramètres et méthodes `findByTenantId()` → `findByInstanceId()` ; `load($instanceId)`, `save($instanceId, …)`, etc.
- **`InstanceOperationalStateValidator`** : exige `instance.instanceId` ; garde-fou `expected_tenant_id` supprimé (utiliser `expected_instance_id`).
- **CLI** : `--tenant-id` → `--instance-id` (`consumption:push`, `orchestration:simulate`).

### Migration

- Renommer les fichiers locaux `snapshots/{tenantId}.json` → `snapshots/{instanceId}.json` (et équivalents operational-state / receipts).
- Mettre à jour les payloads AM et les appels `pushResourceConsumption($instanceId, …)`.

## 0.0.8

Versions antérieures : voir tags Git.
- `ApplicationTokenAuthenticator` partagé (contrôleurs Symfony + `ReceptacleHttpKernel`).
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "application-manager-tools/am-driver",
"description": "Symfony bundle and framework-agnostic core to integrate managed applications with Application Manager (orchestration, consumption, operational state).",
"type": "symfony-bundle",
"version": "0.0.13",
"version": "0.0.14",
"license": "MIT",
"keywords": ["application-manager", "orchestration", "symfony-bundle"],
"require": {
Expand Down
3 changes: 1 addition & 2 deletions src/Bridge/Console/Command/CallbackSendCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected function configure(): void
->setName('callback:send')
->setDescription('Send an orchestration callback to Application Manager')
->addOption('am-url', null, InputOption::VALUE_REQUIRED, 'AM base URL')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-Orchestration-Callback-Token')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-AM-Application-Token')
->addOption('idempotency-key', null, InputOption::VALUE_REQUIRED, 'idempotencyKey')
->addOption('status', null, InputOption::VALUE_REQUIRED, 'SUCCEEDED|FAILED|RETRYABLE_FAILURE', 'SUCCEEDED')
->addOption('message', null, InputOption::VALUE_OPTIONAL, 'Optional message')
Expand All @@ -43,7 +43,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$client = new AmApiClient(HttpClient::create(), new AmApiClientConfig(
(string) $input->getOption('am-url'),
'unused-consumption-token',
(string) $input->getOption('token'),
));

Expand Down
3 changes: 1 addition & 2 deletions src/Bridge/Console/Command/ConsumptionPushCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected function configure(): void
->addOption('source', null, InputOption::VALUE_REQUIRED, 'source', 'am-driver-cli')
->addOption('occurred-at', null, InputOption::VALUE_REQUIRED, 'ISO8601 occurredAt')
->addOption('am-url', null, InputOption::VALUE_REQUIRED, 'AM base URL')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-Consumption-Webhook-Token');
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-AM-Application-Token');
}

protected function execute(InputInterface $input, OutputInterface $output): int
Expand All @@ -55,7 +55,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$client = new AmApiClient(HttpClient::create(), new AmApiClientConfig(
(string) $input->getOption('am-url'),
(string) $input->getOption('token'),
'unused-callback-token',
));

$event = new ConsumptionWebhookEvent(
Expand Down
5 changes: 3 additions & 2 deletions src/Bridge/Console/Command/OrchestrationSimulateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ApplicationManagerTools\AmDriver\Bridge\Console\Command;

use ApplicationManagerTools\AmDriver\Core\Http\ApplicationTokenAuthenticator;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
Expand All @@ -25,7 +26,7 @@ protected function configure(): void
->addArgument('operation', InputArgument::OPTIONAL, 'create|stop|start', 'create')
->addOption('base-url', null, InputOption::VALUE_REQUIRED, 'Receptacle base URL', 'http://127.0.0.1:8099')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'Command path', '/internal/am/orchestration/commands')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-Orchestration-Command-Token', 'dev-command-token')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-AM-Application-Token', 'dev-application-token')
->addOption('instance-id', null, InputOption::VALUE_REQUIRED, 'instanceId', 'am_ins_10000000-0000-4000-8000-000000000001')
->addOption('app-id', null, InputOption::VALUE_REQUIRED, 'appId', 'am_app_10000000-0000-4000-8000-000000000001')
->addOption('target-id', null, InputOption::VALUE_REQUIRED, 'targetId', 'local-receptacle');
Expand Down Expand Up @@ -63,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$response = $client->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
'X-Orchestration-Command-Token' => (string) $input->getOption('token'),
ApplicationTokenAuthenticator::HEADER_NAME => (string) $input->getOption('token'),
],
'json' => $payload,
]);
Expand Down
6 changes: 2 additions & 4 deletions src/Bridge/Console/Command/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ protected function configure(): void
->setDescription('Start the managed-app receptacle HTTP server (built-in PHP server)')
->addOption('port', null, InputOption::VALUE_REQUIRED, 'Listen port', '8099')
->addOption('host', null, InputOption::VALUE_REQUIRED, 'Listen host', '127.0.0.1')
->addOption('token-command', null, InputOption::VALUE_REQUIRED, 'X-Orchestration-Command-Token', 'dev-command-token')
->addOption('token-state', null, InputOption::VALUE_REQUIRED, 'X-Instance-Operational-State-Token', 'dev-state-token')
->addOption('application-token', null, InputOption::VALUE_REQUIRED, 'X-AM-Application-Token', 'dev-application-token')
->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Persistence directory', sys_get_temp_dir().'/am-driver-receptacle')
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Consumption source identifier', 'am-driver-receptacle');
}
Expand All @@ -47,8 +46,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'source' => $source,
'orchestration_path' => $orchestrationPath,
'operational_state_path' => $statePath,
'token_command' => (string) $input->getOption('token-command'),
'token_state' => (string) $input->getOption('token-state'),
'application_token' => (string) $input->getOption('application-token'),
]);

$router = __DIR__.'/../../../Core/Cli/ReceptacleServerRouter.php';
Expand Down
5 changes: 3 additions & 2 deletions src/Bridge/Console/Command/StatePushSampleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ApplicationManagerTools\AmDriver\Bridge\Console\Command;

use ApplicationManagerTools\AmDriver\Core\Http\ApplicationTokenAuthenticator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -20,7 +21,7 @@ protected function configure(): void
->setDescription('POST a sample instance-operational-state.v1 document to a receptacle')
->addOption('base-url', null, InputOption::VALUE_REQUIRED, 'Receptacle base URL', 'http://127.0.0.1:8099')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'Operational state path', '/internal/am/instance-operational-state')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-Instance-Operational-State-Token', 'dev-state-token')
->addOption('token', null, InputOption::VALUE_REQUIRED, 'X-AM-Application-Token', 'dev-application-token')
->addOption('fixture', null, InputOption::VALUE_REQUIRED, 'Fixture file path');
}

Expand All @@ -47,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$response = HttpClient::create()->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json',
'X-Instance-Operational-State-Token' => (string) $input->getOption('token'),
ApplicationTokenAuthenticator::HEADER_NAME => (string) $input->getOption('token'),
],
'body' => $body,
]);
Expand Down
28 changes: 8 additions & 20 deletions src/Bridge/Symfony/Controller/OperationalStateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ApplicationManagerTools\AmDriver\Bridge\Symfony\Controller;

use ApplicationManagerTools\AmDriver\Core\Exception\ValidationException;
use ApplicationManagerTools\AmDriver\Core\Http\ApplicationTokenAuthenticator;
use ApplicationManagerTools\AmDriver\Core\OperationalState\OperationalStateProcessor;
use ApplicationManagerTools\AmDriver\Core\Validation\JsonPayloadValidator;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -19,24 +20,23 @@ final class OperationalStateController
/** @var OperationalStateProcessor */
private $processor;

/** @var string */
private $expectedToken;
/** @var ApplicationTokenAuthenticator */
private $authenticator;

public function __construct(OperationalStateProcessor $processor, string $operationalStateToken)
public function __construct(OperationalStateProcessor $processor, string $applicationToken)
{
$this->processor = $processor;
$this->expectedToken = $operationalStateToken;
$this->authenticator = new ApplicationTokenAuthenticator($applicationToken);
}

public function __invoke(Request $request): JsonResponse
{
if (!$this->tokenMatches($request)) {
return new JsonResponse(['error' => 'Invalid operational state token'], Response::HTTP_UNAUTHORIZED);
if (!$this->authenticator->matchesRequest($request)) {
return new JsonResponse(['error' => 'Invalid application token'], Response::HTTP_UNAUTHORIZED);
}

try {
$document = JsonPayloadValidator::parseJsonObject((string) $request->getContent());
$this->processor->process($document);
$this->processor->process(JsonPayloadValidator::parseJsonObject((string) $request->getContent()));

return new JsonResponse(['accepted' => true], Response::HTTP_OK);
} catch (ValidationException $e) {
Expand All @@ -45,16 +45,4 @@ public function __invoke(Request $request): JsonResponse
return new JsonResponse(['error' => 'Transient error'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}

private function tokenMatches(Request $request): bool
{
foreach (['X-AM-Application-Token', 'X-Instance-Operational-State-Token'] as $header) {
$token = trim((string) $request->headers->get($header, ''));
if ('' !== $token && hash_equals($this->expectedToken, $token)) {
return true;
}
}

return false;
}
}
25 changes: 7 additions & 18 deletions src/Bridge/Symfony/Controller/OrchestrationCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use ApplicationManagerTools\AmDriver\Core\Dto\OrchestrationCommand;
use ApplicationManagerTools\AmDriver\Core\Exception\ValidationException;
use ApplicationManagerTools\AmDriver\Core\Http\ApplicationTokenAuthenticator;
use ApplicationManagerTools\AmDriver\Core\Orchestration\OrchestrationCommandProcessor;
use ApplicationManagerTools\AmDriver\Core\Validation\JsonPayloadValidator;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -19,19 +20,19 @@ final class OrchestrationCommandController
/** @var OrchestrationCommandProcessor */
private $processor;

/** @var string */
private $expectedToken;
/** @var ApplicationTokenAuthenticator */
private $authenticator;

public function __construct(OrchestrationCommandProcessor $processor, string $orchestrationCommandToken)
public function __construct(OrchestrationCommandProcessor $processor, string $applicationToken)
{
$this->processor = $processor;
$this->expectedToken = $orchestrationCommandToken;
$this->authenticator = new ApplicationTokenAuthenticator($applicationToken);
}

public function __invoke(Request $request): JsonResponse
{
if (!$this->tokenMatches($request)) {
return new JsonResponse(['error' => 'Invalid orchestration command token'], Response::HTTP_UNAUTHORIZED);
if (!$this->authenticator->matchesRequest($request)) {
return new JsonResponse(['error' => 'Invalid application token'], Response::HTTP_UNAUTHORIZED);
}

try {
Expand All @@ -47,16 +48,4 @@ public function __invoke(Request $request): JsonResponse
return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
}
}

private function tokenMatches(Request $request): bool
{
foreach (['X-AM-Application-Token', 'X-Orchestration-Command-Token'] as $header) {
$token = trim((string) $request->headers->get($header, ''));
if ('' !== $token && hash_equals($this->expectedToken, $token)) {
return true;
}
}

return false;
}
}
9 changes: 5 additions & 4 deletions src/Bridge/Symfony/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('am_base_url')->defaultValue('%env(AM_DRIVER_AM_BASE_URL)%')->end()
->scalarNode('source')->defaultValue('%env(AM_DRIVER_SOURCE)%')->end()
->scalarNode('data_dir')->defaultValue('%kernel.project_dir%/var/am-driver')->end()
->scalarNode('consumption_webhook_token')->defaultValue('%env(AM_DRIVER_CONSUMPTION_WEBHOOK_TOKEN)%')->end()
->scalarNode('orchestration_callback_token')->defaultValue('%env(AM_DRIVER_ORCHESTRATION_CALLBACK_TOKEN)%')->end()
->scalarNode('orchestration_command_token')->defaultValue('%env(AM_DRIVER_ORCHESTRATION_COMMAND_TOKEN)%')->end()
->scalarNode('operational_state_token')->defaultValue('%env(AM_DRIVER_OPERATIONAL_STATE_TOKEN)%')->end()
->scalarNode('application_token')->defaultValue('%env(AM_DRIVER_APPLICATION_TOKEN)%')->end()
->scalarNode('consumption_webhook_token')->defaultNull()->info('Deprecated: use application_token.')->end()
->scalarNode('orchestration_callback_token')->defaultNull()->info('Deprecated: use application_token.')->end()
->scalarNode('orchestration_command_token')->defaultNull()->info('Deprecated: use application_token.')->end()
->scalarNode('operational_state_token')->defaultNull()->info('Deprecated: use application_token.')->end()
->floatNode('http_timeout')->defaultValue(10.0)->end()
->integerNode('consumption_max_retries')->defaultValue(3)->end()
->integerNode('consumption_retry_delay_ms')->defaultValue(500)->end()
Expand Down
28 changes: 28 additions & 0 deletions src/Bridge/Symfony/DependencyInjection/ReceiverRoutePaths.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,35 @@ public static function finalize(array $config): array
$config['route_prefix'] = $prefix;
$config['orchestration_commands_path'] ??= self::orchestrationCommandsPath($prefix);
$config['operational_state_path'] ??= self::operationalStatePath($prefix);
$config['application_token'] = self::resolveApplicationToken($config);

return $config;
}

/**
* @param array<string, mixed> $config
*/
private static function resolveApplicationToken(array $config): string
{
$token = trim((string) ($config['application_token'] ?? ''));
if ('' !== $token && !str_starts_with($token, '%env(')) {
return $token;
}

foreach (
[
'orchestration_command_token',
'consumption_webhook_token',
'orchestration_callback_token',
'operational_state_token',
] as $legacyKey
) {
$legacy = trim((string) ($config[$legacyKey] ?? ''));
if ('' !== $legacy) {
return $legacy;
}
}

return '' !== $token ? $token : '%env(AM_DRIVER_APPLICATION_TOKEN)%';
}
}
3 changes: 1 addition & 2 deletions src/Bridge/Symfony/Factory/AmApiClientConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ public static function create(array $config): AmApiClientConfig
{
return new AmApiClientConfig(
(string) $config['am_base_url'],
(string) $config['consumption_webhook_token'],
(string) $config['orchestration_callback_token'],
(string) $config['application_token'],
(float) $config['http_timeout'],
(int) $config['consumption_max_retries'],
(int) $config['consumption_retry_delay_ms'],
Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Symfony/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ services:

ApplicationManagerTools\AmDriver\Bridge\Symfony\Controller\OrchestrationCommandController:
arguments:
$orchestrationCommandToken: '%am_driver.config.orchestration_command_token%'
$applicationToken: '%am_driver.config.application_token%'
tags:
- controller.service_arguments
public: true

ApplicationManagerTools\AmDriver\Bridge\Symfony\Controller\OperationalStateController:
arguments:
$operationalStateToken: '%am_driver.config.operational_state_token%'
$applicationToken: '%am_driver.config.application_token%'
tags:
- controller.service_arguments
public: true
Expand Down
6 changes: 2 additions & 4 deletions src/Core/Cli/ReceptacleBootstrapFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ final class ReceptacleBootstrapFactory
* source: string,
* orchestration_path: string,
* operational_state_path: string,
* token_command: string,
* token_state: string
* application_token: string
* } $config
*/
public static function createKernel(array $config): ReceptacleHttpKernel
Expand All @@ -53,8 +52,7 @@ public static function createKernel(array $config): ReceptacleHttpKernel
),
$config['orchestration_path'],
$config['operational_state_path'],
$config['token_command'],
$config['token_state'],
$config['application_token'],
);
}

Expand Down
Loading
Loading