diff --git a/src/BrefCloudClient.php b/src/BrefCloudClient.php index 7fab979..6873cea 100644 --- a/src/BrefCloudClient.php +++ b/src/BrefCloudClient.php @@ -299,9 +299,17 @@ public function findEnvironment(string $teamSlug, string $appName, string $envir return $result; } - public function removeEnvironment(int $environmentId): void + /** + * @return string|null A message from Bref Cloud to show once the environment is removed. + */ + public function removeEnvironment(int $environmentId, bool $deleteSecrets = false): ?string { - $this->client->request('DELETE', '/api/v1/environments/' . $environmentId); + /** @var array{message?: string|null} $result */ + $result = $this->client->request('DELETE', '/api/v1/environments/' . $environmentId, [ + 'json' => ['delete_secrets' => $deleteSecrets], + ])->toArray(); + + return $result['message'] ?? null; } /** diff --git a/src/Commands/Remove.php b/src/Commands/Remove.php index 2daa9cc..5a819a8 100644 --- a/src/Commands/Remove.php +++ b/src/Commands/Remove.php @@ -6,17 +6,25 @@ use Bref\Cli\Cli\IO; use Bref\Cli\Cli\Styles; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; use function Amp\delay; class Remove extends ApplicationCommand { + public function __construct( + private readonly ?BrefCloudClient $brefCloud = null, + ) { + parent::__construct(); + } + protected function configure(): void { $this ->setName('remove') - ->setDescription('Remove an environment of an application'); + ->setDescription('Remove an environment of an application') + ->addOption('delete-secrets', null, InputOption::VALUE_NONE, 'Also delete the secrets of the environment (kept by default, so that deploying it again reuses them)'); parent::configure(); } @@ -35,12 +43,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int IO::spin('removing'); - $brefCloud = new BrefCloudClient; + $brefCloud = $this->brefCloud ?? new BrefCloudClient; $environment = $brefCloud->findEnvironment($team, $appName, $environmentName); $environmentId = $environment['id']; IO::verbose('Triggering asynchronous removal of environment ' . $environmentId); - $brefCloud->removeEnvironment($environmentId); + $message = $brefCloud->removeEnvironment($environmentId, (bool) $input->getOption('delete-secrets')); while (true) { try { @@ -56,6 +64,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int IO::spinSuccess('removed'); + if ($message) { + IO::writeln(['', $message]); + } + return 0; } } diff --git a/tests/Commands/CommandTestCase.php b/tests/Commands/CommandTestCase.php index d4819a0..e4ac6a9 100644 --- a/tests/Commands/CommandTestCase.php +++ b/tests/Commands/CommandTestCase.php @@ -3,6 +3,7 @@ namespace Bref\Cli\Test\Commands; use Bref\Cli\BrefCloudClient; +use Closure; use Laravel\AgentDetector\AgentDetector; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\MockHttpClient; @@ -39,7 +40,8 @@ protected function runByAnAgent(): void } /** - * @param array $routes Responses indexed by path, a MockResponse or data to return as JSON. + * @param array $routes Responses indexed by path, a MockResponse or data to return as JSON, + * or a closure that returns one of those for the HTTP method. */ protected function brefCloud(array $routes): BrefCloudClient { @@ -54,7 +56,9 @@ protected function brefCloud(array $routes): BrefCloudClient $this->fail("Unexpected request: $method $url"); } - return $routes[$path] instanceof MockResponse ? $routes[$path] : $this->json($routes[$path]); + $response = $routes[$path] instanceof Closure ? $routes[$path]($method) : $routes[$path]; + + return $response instanceof MockResponse ? $response : $this->json($response); }, 'https://bref.cloud'); return new BrefCloudClient(client: $client); diff --git a/tests/Commands/RemoveTest.php b/tests/Commands/RemoveTest.php new file mode 100644 index 0000000..5ac4377 --- /dev/null +++ b/tests/Commands/RemoveTest.php @@ -0,0 +1,63 @@ +remove(['message' => 'Secrets of this environment are kept in SSM Parameter Store under /shop/prod/.']); + + $this->assertSame(0, $status, $display); + $this->assertStringContainsString('Secrets of this environment are kept in SSM Parameter Store under /shop/prod/.', $display); + } + + public function test_secrets_are_kept_unless_asked(): void + { + $this->remove([]); + $this->assertSame(['delete_secrets' => false], $this->requestBodies['/api/v1/environments/12']); + + $this->remove([], ['--delete-secrets' => true]); + $this->assertSame(['delete_secrets' => true], $this->requestBodies['/api/v1/environments/12']); + } + + public function test_bref_cloud_may_return_no_message(): void + { + [$status, $display] = $this->remove([]); + + $this->assertSame(0, $status, $display); + $this->assertStringContainsString('removed', $display); + } + + /** + * @param array $deletion What Bref Cloud returns when asked to remove the environment + * @param array $options Options of the command + * @return array{int, string} The exit code and the output + */ + private function remove(array $deletion, array $options = []): array + { + $command = new Remove($this->brefCloud([ + '/api/v1/environments/find' => $this->environment(), + '/api/v1/environments/12' => fn(string $method) => $method === 'DELETE' + ? $this->json($deletion, 202) + : $this->json(['message' => 'Not found'], 404), + ])); + $configFile = sys_get_temp_dir() . '/bref-cli-test-' . bin2hex(random_bytes(4)) . '.yml'; + file_put_contents($configFile, "service: shop\nbref:\n team: acme\n"); + $input = new ArrayInput(['--env' => 'prod', '--config' => $configFile, ...$options]); + $input->setInteractive(false); + $output = new BufferedOutput; + IO::init($input, $output); + + $status = $command->run($input, $output); + IO::stop(); + unlink($configFile); + + return [$status, $output->fetch()]; + } +}