Skip to content

Commit 2ed3400

Browse files
jderussechalasr
andauthored
Add SSM EnvVarLoader in SymfonyBundle (#490)
* Add SSM EnvVarLoader in SyfonyBundle * Apply suggestions from code review Co-Authored-By: Robin Chalas <chalasr@users.noreply.github.com> * Add minimum stability in Integration too * Fix CS Co-authored-by: Robin Chalas <chalasr@users.noreply.github.com>
1 parent 478f362 commit 2ed3400

File tree

8 files changed

+160
-5
lines changed

8 files changed

+160
-5
lines changed

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
"symfony/http-kernel": "^4.4 || ^5.0"
1818
},
1919
"require-dev": {
20-
"async-aws/s3": "^0.3",
21-
"async-aws/ses": "^0.3",
22-
"async-aws/sqs": "^0.3",
20+
"async-aws/s3": "^0.3 || ^0.4",
21+
"async-aws/ses": "^0.3 || ^0.4",
22+
"async-aws/sqs": "^0.3 || ^0.4",
23+
"async-aws/ssm": "^0.1",
2324
"matthiasnoback/symfony-config-test": "^4.1",
2425
"nyholm/symfony-bundle-test": "^1.6.1"
2526
},

src/DependencyInjection/AsyncAwsExtension.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace AsyncAws\Symfony\Bundle\DependencyInjection;
66

7+
use AsyncAws\Symfony\Bundle\Secrets\SsmVault;
78
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
89
use Symfony\Component\DependencyInjection\ContainerBuilder;
910
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -20,6 +21,7 @@ public function load(array $configs, ContainerBuilder $container)
2021

2122
$usedServices = $this->registerConfiguredServices($container, $config);
2223
$usedServices = $this->registerInstalledServices($container, $config, $usedServices);
24+
$this->registerEnvLoader($container, $config);
2325
$this->autowireServices($container, $usedServices);
2426
}
2527

@@ -33,7 +35,7 @@ private function registerConfiguredServices(ContainerBuilder $container, array $
3335
foreach ($config['clients'] as $name => $data) {
3436
$client = $availableServices[$data['type']]['class'];
3537
if (!class_exists($client)) {
36-
throw new InvalidConfigurationException(sprintf('You have configured "async_aws.%s" but the "%s" package is not installed. Try running "composer require %s"', $name, $name, $availableServices[$name]['package']));
38+
throw new InvalidConfigurationException(sprintf('You have configured "async_aws.%s" but the "%s" package is not installed. Try running "composer require %s"', $name, $data['type'], $availableServices[$data['type']]['package']));
3739
}
3840

3941
$serviceConfig = array_merge($defaultConfig, $data);
@@ -96,6 +98,46 @@ private function addServiceDefinition(ContainerBuilder $container, string $name,
9698
$container->setDefinition(sprintf('async_aws.client.%s', $name), $definition);
9799
}
98100

101+
private function registerEnvLoader(ContainerBuilder $container, array $config): void
102+
{
103+
if (!$config['secrets']['enabled']) {
104+
return;
105+
}
106+
107+
$availableServices = AwsPackagesProvider::getAllServices();
108+
if (!class_exists($className = $availableServices['ssm']['class'])) {
109+
throw new InvalidConfigurationException(sprintf('You have enabled "async_aws.secrets" but the "%s" package is not installed. Try running "composer require %s"', 'ssm', $availableServices['ssm']['package']));
110+
}
111+
112+
if (null !== $client = $config['secrets']['client']) {
113+
if (!isset($config['clients'][$client])) {
114+
throw new InvalidConfigurationException(sprintf('The client "%s" configured in "async_aws.secrets" does not exists. Available clients are "%s"', $client, implode(', ', \array_keys($config['clients']))));
115+
}
116+
if ('ssm' !== $config['clients'][$client]['type']) {
117+
throw new InvalidConfigurationException(sprintf('The client "%s" configured in "async_aws.secrets" is not a SSM client.', $client));
118+
}
119+
} else {
120+
if (!isset($config['clients']['ssm'])) {
121+
$client = 'ssm';
122+
} else {
123+
$client = 'secrets';
124+
$i = 1;
125+
while (isset($config['clients'][$client])) {
126+
$client = 'secrets_' . $i;
127+
}
128+
}
129+
$this->addServiceDefinition($container, $client, $config, $className);
130+
}
131+
132+
$container->register(SsmVault::class)
133+
->setAutoconfigured(true)
134+
->setArguments([
135+
new Reference('async_aws.client.' . $client),
136+
$config['secrets']['path'],
137+
$config['secrets']['recursive'],
138+
]);
139+
}
140+
99141
private function autowireServices(ContainerBuilder $container, array $usedServices): void
100142
{
101143
$awsServices = AwsPackagesProvider::getAllServices();

src/DependencyInjection/Configuration.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,23 @@ public function getConfigTreeBuilder()
3232
->children()
3333
->booleanNode('register_service')->info('If set to false, no service will be created for this AWS type.')->defaultTrue()->end()
3434
->arrayNode('config')->info('Configuration specific to this service.')->normalizeKeys(false)->prototype('variable')->end()->end()
35-
->enumNode('type')->info('A valid AWS type. The service name will be used as default. ')->values(AwsPackagesProvider::getServiceNames())->end()
35+
->enumNode('type')->info('A valid AWS type. The service name will be used as default.')->values(AwsPackagesProvider::getServiceNames())->end()
3636
->scalarNode('credential_provider')->info('A service name for AsyncAws\Core\Credentials\CredentialProvider.')->end()
3737
->scalarNode('http_client')->info('A service name for Symfony\Contracts\HttpClient\HttpClientInterface.')->end()
3838
->scalarNode('logger')->info('A service name for Psr\Log\LoggerInterface.')->end()
3939
->end()
4040
->end()
4141
->end()
42+
43+
->arrayNode('secrets')
44+
->info('The SSM EnvLoader configuration.')
45+
->canBeEnabled()
46+
->children()
47+
->scalarNode('path')->info('Path to the parameters.')->defaultNull()->end()
48+
->booleanNode('recursive')->info('Retrieve all parameters within a hierarchy.')->defaultValue(true)->end()
49+
->scalarNode('client')->info('Name of the SSM client. When null, use the default SSM configuration.')->defaultNull()->end()
50+
->end()
51+
->end()
4252
->end();
4353

4454
return $treeBuilder;

src/Secrets/SsmVault.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace AsyncAws\Symfony\Bundle\Secrets;
4+
5+
use AsyncAws\Ssm\SsmClient;
6+
use AsyncAws\Ssm\ValueObject\Parameter;
7+
use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
8+
9+
class SsmVault implements EnvVarLoaderInterface
10+
{
11+
private $client;
12+
13+
private $path;
14+
15+
private $recursive;
16+
17+
public function __construct(SsmClient $client, ?string $path, bool $recursive)
18+
{
19+
$this->client = $client;
20+
$this->path = $path ?? '/';
21+
$this->recursive = $recursive;
22+
}
23+
24+
public function loadEnvVars(): array
25+
{
26+
$parameters = $this->client->getParametersByPath([
27+
'Path' => $this->path,
28+
'Recursive' => $this->recursive,
29+
'WithDecryption' => true,
30+
]);
31+
32+
$secrets = [];
33+
$prefixLen = \strlen($this->path);
34+
/** @var Parameter $parameter */
35+
foreach ($parameters as $parameter) {
36+
if ((null === $name = $parameter->getName()) || (null === $value = $parameter->getValue())) {
37+
continue;
38+
}
39+
$name = \strtoupper(\strtr(ltrim(substr($name, $prefixLen), '/'), '/', '_'));
40+
$secrets[$name] = $value;
41+
}
42+
43+
return $secrets;
44+
}
45+
}

tests/Functional/BundleInitializationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use AsyncAws\Ses\SesClient;
99
use AsyncAws\Sns\SnsClient;
1010
use AsyncAws\Sqs\SqsClient;
11+
use AsyncAws\Ssm\SsmClient;
1112
use AsyncAws\Symfony\Bundle\AsyncAwsBundle;
13+
use AsyncAws\Symfony\Bundle\Secrets\SsmVault;
1214
use Nyholm\BundleTest\BaseBundleTestCase;
1315
use Nyholm\BundleTest\CompilerPass\PublicServicePass;
1416
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
@@ -33,6 +35,7 @@ public function testInitBundle()
3335
$this->assertServiceExists('async_aws.client.sqs', SqsClient::class);
3436
$this->assertServiceExists('async_aws.client.ses', SesClient::class);
3537
$this->assertServiceExists('async_aws.client.foobar', SqsClient::class);
38+
$this->assertServiceExists('async_aws.client.secret', SsmClient::class);
3639

3740
// Test autowired clients
3841
$this->assertServiceExists(S3Client::class, S3Client::class);
@@ -42,6 +45,9 @@ public function testInitBundle()
4245
// Test autowire by name
4346
$this->assertServiceExists(SqsClient::class . ' $foobar', SqsClient::class);
4447

48+
// Test secret
49+
$this->assertServiceExists(SsmVault::class, SsmVault::class);
50+
4551
$container = $this->getContainer();
4652
self::assertFalse($container->has(SqsClient::class . ' $notFound'));
4753
self::assertFalse($container->has(S3Client::class . ' $foobar'));

tests/Functional/Resources/config/default.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ async_aws:
88
ses: ~
99
foobar:
1010
type: sqs
11+
secret:
12+
type: ssm
13+
secrets:
14+
path: /application1
15+
recursive: true
16+
client: secret

tests/Unit/DependencyInjection/ConfigurationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ public function testDefaultValues(): void
2828
'credential_provider' => null,
2929
'config' => [],
3030
'clients' => [],
31+
'secrets' => [
32+
'enabled' => false,
33+
'path' => null,
34+
'recursive' => true,
35+
'client' => null,
36+
],
3137
]);
3238
}
3339

tests/Unit/Secrets/SsmVaultTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace AsyncAws\Symfony\Bundle\Tests\Unit\Secrets;
4+
5+
use AsyncAws\Core\Test\ResultMockFactory;
6+
use AsyncAws\Ssm\Result\GetParametersByPathResult;
7+
use AsyncAws\Ssm\SsmClient;
8+
use AsyncAws\Ssm\ValueObject\Parameter;
9+
use AsyncAws\Symfony\Bundle\Secrets\SsmVault;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class SsmVaultTest extends TestCase
13+
{
14+
/**
15+
* @dataProvider provideParameters
16+
*/
17+
public function testLoadEnvVars($path, $parameterName, $expected)
18+
{
19+
$client = $this->createMock(SsmClient::class);
20+
$ssmVault = new SsmVault($client, $path, true);
21+
22+
$client->expects(self::once())
23+
->method('getParametersByPath')
24+
->willReturn(ResultMockFactory::create(GetParametersByPathResult::class, ['Parameters' => [new Parameter(['Name' => $parameterName, 'Value' => 'value'])]]));
25+
26+
$actual = $ssmVault->loadEnvVars();
27+
28+
self::assertEquals($expected, $actual);
29+
}
30+
31+
public function provideParameters(): iterable
32+
{
33+
yield 'simple' => [null, '/FOO', ['FOO' => 'value']];
34+
yield 'case insensitive' => [null, '/fOo', ['FOO' => 'value']];
35+
yield 'remove prefix' => ['/my_app', '/my_app/foo', ['FOO' => 'value']];
36+
yield 'remove trailing' => ['/my_app/', '/my_app/foo', ['FOO' => 'value']];
37+
yield 'recursive' => [null, '/foo/bar', ['FOO_BAR' => 'value']];
38+
}
39+
}

0 commit comments

Comments
 (0)