Skip to content

Commit

Permalink
Added possibility of configuring multiple AWS SSM adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrkreft committed Jun 27, 2020
1 parent a09be8b commit b11a432
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 168 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2020-06-27
### Added
- Enabled possibility to have multiple AWS SSM adapters.

## [0.3.1] - 2020-06-22
### Fixed
- Fixed misinterpreting `null` as a lack of default entry value.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ bin/console pk:config:validate dev
### Adapters
To be able to use a different configuration sources adapters are needed.
By default, package provides:
* aws_ssm (`PK\Config\StorageAdapter\AwsSsm`) - for AWS Simple Systems Manager parameters
* aws_ssm (multiple)(`PK\Config\StorageAdapter\{AwsSsm, AwsSsmByPath}`) - for AWS Simple Systems Manager parameters
* local_env (`PK\Config\StorageAdapter\LocalEnv`) - for local environment variables

and each of those is available to be instantiated via component configuration.
Expand All @@ -70,6 +70,8 @@ If needed a new adapter can be easily created. Just remember to interface it wit

:information_source: Order of the adapters in each environment is also a priority. If the first adapter provides value, the following will be ignored.

:information_source: If adapter has multiple option assigned it can be configured with multiple different instances. If so each can be referenced in env.adapters like {adapter}.{name} (i.e. aws_ssm.default)

### Testing
```bash
composer test
Expand Down
80 changes: 51 additions & 29 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,45 +50,67 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->append($this->entries())
->arrayNode('adapters')
->children()
->arrayNode('aws_ssm')
->info('Integrates AWS Simple Systems Manager.')
->canBeEnabled()
->children()
->arrayNode('client')
->isRequired()
->children()
->arrayNode('credentials')
->isRequired()
->children()
->scalarNode('key')->isRequired()->end()
->scalarNode('secret')->isRequired()->end()
->end()
->end()
->end();

$this->addAdaptersSection($treeBuilder->getRootNode());

return $treeBuilder;
}

private function addAdaptersSection(ArrayNodeDefinition $rootNode): void
{
$rootNode
->children()
->arrayNode('adapters')
->children()
->arrayNode('aws_ssm')
->info('Integrates AWS Simple Systems Manager.')
->useAttributeAsKey('name')
->beforeNormalization()
/** @param mixed $v */
->ifTrue(function ($v) {
return isset($v['client']) && !isset($v['client']['client']);
})
->then(function (array $v): array {
return ['default' => $v];
})
->end()
->arrayPrototype()

->children()
->arrayNode('client')
->isRequired()
->children()
->arrayNode('credentials')
->isRequired()
->children()
->scalarNode('key')->isRequired()->end()
->scalarNode('secret')->isRequired()->end()
->end()
->scalarNode('version')->defaultValue('latest')->end()
->scalarNode('region')->isRequired()->end()
->end()
->scalarNode('version')->defaultValue('latest')->end()
->scalarNode('region')->isRequired()->end()
->end()
->scalarNode('path')
->info(
'If provided parameters will be fetched by path.
`{env}` substring will be replaced on fetching with given environment.'
)
->defaultValue(null)
->end()
->end()
->scalarNode('path')
->info(
'If provided parameters will be fetched by path.
`{env}` substring will be replaced on fetching with given environment.'
)
->defaultNull()
->end()
->end()
->arrayNode('local_env')
->info('Integrates local environment variables.')
->canBeEnabled()

->end()
->end()
->arrayNode('local_env')
->info('Integrates local environment variables.')
->canBeEnabled()
->end()
->end()
->end()
->end();

return $treeBuilder;
}

private function entries(bool $inEnvironment = false): ArrayNodeDefinition
Expand Down
74 changes: 61 additions & 13 deletions src/DependencyInjection/PKConfigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,71 @@ private function resolveAdapterReference(array $adaptersMap, string $adapterId):
*/
private function processAdapters(array $config, ContainerBuilder $container, YamlFileLoader $loader): array
{
$adaptersMap = $this->processAwsSsm($config['aws_ssm'], $container, $loader);
$adaptersMap = array_merge($adaptersMap, $this->processLocalEnv($config['local_env'], $container, $loader));

return $adaptersMap;
}

/**
* @param mixed[] $config
*
* @return string[]
*/
private function processAwsSsm(array $config, ContainerBuilder $container, YamlFileLoader $loader): array
{
if (empty($config)) {
return [];
}
if (!class_exists(SsmClient::class)) {
throw new LogicException(
'Cannot enable aws_ssm without AWS SDK. Try running "composer require aws/aws-sdk-php".'
);
}

$adaptersMap = [];
if ($this->isConfigEnabled($container, $config['aws_ssm'])) {
if (!class_exists(SsmClient::class)) {
throw new LogicException(
'Cannot enable aws_ssm without AWS SDK. Try running "composer require aws/aws-sdk-php".'
);
$loader->load('adapters/aws_ssm.yaml');

foreach ($config as $name => $adapter) {
$serviceId = "pk.config.adapter.ssm_client.$name";
$container->setDefinition(
$serviceId,
new Definition(
$adapter['path'] ?
'%pk.config.adapter.ssm_by_path_client.class%' :
'%pk.config.adapter.ssm_client.class%',
array_filter([
'$ssmClient' => new Definition(
'%pk.config.aws.ssm_client.class%',
['$args' => $adapter['client']]
),
'$path' => $adapter['path'],
])
)
);
if ('default' === $name) {
$adaptersMap['aws_ssm'] = $serviceId;
}
$container->setParameter('pk.config.aws.ssm_client.args', $config['aws_ssm']['client']);
$container->setParameter('pk.config.adapter.ssm_client.path', $config['aws_ssm']['path']);
$loader->load('adapters/aws_ssm.yaml');
$adaptersMap['aws_ssm'] = 'pk.config.adapter.ssm_client';
}
if ($this->isConfigEnabled($container, $config['local_env'])) {
$loader->load('adapters/local_env.yaml');
$adaptersMap['local_env'] = 'pk.config.adapter.local_env';
$adaptersMap["aws_ssm.$name"] = $serviceId;
}

return $adaptersMap;
}

/**
* @param mixed[] $config
*
* @return string[]
*/
private function processLocalEnv(array $config, ContainerBuilder $container, YamlFileLoader $loader): array
{
if (!$this->isConfigEnabled($container, $config)) {
return [];
}
$loader->load('adapters/local_env.yaml');

return [
'local_env' => 'pk.config.adapter.local_env',
];
}
}
15 changes: 1 addition & 14 deletions src/Resources/config/adapters/aws_ssm.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
parameters:
pk.config.adapter.ssm_client.class: PK\Config\StorageAdapter\AwsSsm
pk.config.adapter.ssm_by_path_client.class: PK\Config\StorageAdapter\AwsSsmByPath
pk.config.aws.ssm_client.class: Aws\Ssm\SsmClient

services:
PK\Config\StorageAdapter\AwsSsm: '@pk.config.adapter.ssm_client'

pk.config.adapter.ssm_client:
class: '%pk.config.adapter.ssm_client.class%'
arguments:
$path: '%pk.config.adapter.ssm_client.path%'
$ssmClient: '@pk.config.aws.ssm_client'

pk.config.aws.ssm_client:
class: '%pk.config.aws.ssm_client.class%'
arguments:
$args: '%pk.config.aws.ssm_client.args%'
31 changes: 5 additions & 26 deletions src/StorageAdapter/AwsSsm.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,34 @@ class AwsSsm implements StorageAdapterInterface
*/
private $ssmClient;

/**
* @var string|null
*/
private $path;

public function __construct(SsmClient $ssmClient, ?string $path)
public function __construct(SsmClient $ssmClient)
{
$this->ssmClient = $ssmClient;
$this->path = $path;
}

/**
* {@inheritdoc}
*/
public function fetch(string $environment): array
{
$path = str_replace('{env}', $environment, $this->path);

$entries = [];
$parameters = $nextToken = null;
while (!$parameters || $nextToken) {
$parameters = $this->doFetch($environment, $nextToken);
$parameters = $this->doFetch($nextToken);

foreach ($parameters['Parameters'] as $parameter) {
$name = str_replace($path, '', $parameter['Name']);
$entries[] = new Entry($name, $parameter['Value']);
$entries[] = new Entry($parameter['Name'], $parameter['Value']);
}
$nextToken = $parameters['NextToken'] ?? null;
}

return $entries;
}

private function doFetch(string $environment, ?string $nextToken): Result
private function doFetch(?string $nextToken): Result
{
if (!$this->path) {
return $this->ssmClient->getParameters(
array_filter([
'WithDecryption' => true,
'NextToken' => $nextToken,
])
);
}

$path = str_replace('{env}', $environment, $this->path);

return $this->ssmClient->getParametersByPath(
return $this->ssmClient->getParameters(
array_filter([
'Path' => $path,
'WithDecryption' => true,
'NextToken' => $nextToken,
])
Expand Down
66 changes: 66 additions & 0 deletions src/StorageAdapter/AwsSsmByPath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace PK\Config\StorageAdapter;

use Aws\Result;
use Aws\Ssm\SsmClient;
use PK\Config\Entry;
use PK\Config\StorageAdapterInterface;

class AwsSsmByPath implements StorageAdapterInterface
{
private const ENV_PLACEHOLDER = '{env}';

/**
* @var SsmClient
*/
private $ssmClient;

/**
* @var string
*/
private $path;

public function __construct(SsmClient $ssmClient, string $path)
{
$this->ssmClient = $ssmClient;
$this->path = $path;
}

/**
* {@inheritdoc}
*/
public function fetch(string $environment): array
{
$path = str_replace(self::ENV_PLACEHOLDER, $environment, $this->path);

$entries = [];
$parameters = $nextToken = null;
while (!$parameters || $nextToken) {
$parameters = $this->doFetch($nextToken, $path);

foreach ($parameters['Parameters'] as $parameter) {
$entries[] = new Entry(
str_replace($path, '', $parameter['Name']),
$parameter['Value']
);
}
$nextToken = $parameters['NextToken'] ?? null;
}

return $entries;
}

private function doFetch(?string $nextToken, string $path): Result
{
return $this->ssmClient->getParametersByPath(
array_filter([
'Path' => $path,
'WithDecryption' => true,
'NextToken' => $nextToken,
])
);
}
}
17 changes: 9 additions & 8 deletions tests/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,17 @@ public function testShouldCreateConfigurationPerEnv(): void
'enabled' => true,
],
'aws_ssm' => [
'client' => [
'credentials' => [
'key' => 'key',
'secret' => 'secret',
'default' => [
'client' => [
'credentials' => [
'key' => 'key',
'secret' => 'secret',
],
'version' => 'latest',
'region' => 'EU',
],
'version' => 'latest',
'region' => 'EU',
'path' => '/path',
],
'path' => '/path',
'enabled' => true,
],
],
],
Expand Down
Loading

0 comments on commit b11a432

Please sign in to comment.