diff --git a/.gitattributes b/.gitattributes index b844132..cae1733 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,7 @@ /.gitignore export-ignore /.gitattributes export-ignore /.env.dist +/deployer.phar export-ignore /docker-compose.yml export-ignore /infection.json.dist export-ignore /phive.xml export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 098df9a..1d1a937 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,4 +19,4 @@ jobs: ci: uses: codenamephp/workflows.php/.github/workflows/ci.yml@1 with: - php-versions: '["8.1","8.2]' + php-versions: '["8.1","8.2"]' diff --git a/.idea/copyright/Apache2.xml b/.idea/copyright/Apache2.xml new file mode 100644 index 0000000..ddf769e --- /dev/null +++ b/.idea/copyright/Apache2.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..53a32f1 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/deployer.secrets.iml b/.idea/deployer.secrets.iml index 5ae24ee..befade1 100644 --- a/.idea/deployer.secrets.iml +++ b/.idea/deployer.secrets.iml @@ -5,6 +5,20 @@ + + + + + + + + + + + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..a161c77 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,130 @@ + + + + \ No newline at end of file diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml new file mode 100644 index 0000000..0df0049 --- /dev/null +++ b/.idea/php-test-framework.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index e52129d..24f3fba 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -13,6 +13,20 @@ + + + + + + + + + + + + + + @@ -29,6 +43,11 @@ + + + + + diff --git a/.idea/runConfigurations/All.xml b/.idea/runConfigurations/All.xml new file mode 100644 index 0000000..52dec9d --- /dev/null +++ b/.idea/runConfigurations/All.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json index 878f761..5fd3aac 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,10 @@ } ], "require": { - "php": "^8.1" + "php": "^8.1", + "codenamephp/deployer.base": "^3.0", + "codenamephp/platform.secretsmanager.base": "^1.0.1", + "deployer/deployer": "^7.2" }, "autoload": { "psr-4": { @@ -34,12 +37,11 @@ "psalm": "tools/psalm --threads=10 --long-progress", "composer-unused": "tools/composer-unused --no-progress --no-interaction", "composer-require-checker": "tools/composer-require-checker --no-interaction", - "infection": "XDEBUG_MODE=coverage tools/infection --min-msi=95 --min-covered-msi=95 --threads=4 --no-progress --show-mutations", + "infection": "XDEBUG_MODE=coverage tools/infection --min-msi=100 --min-covered-msi=100 --threads=4 --no-progress --show-mutations", "ci-all": [ "@phpunit", "@psalm", "@composer-unused", - "@composer-require-checker", "@infection" ] }, @@ -50,5 +52,8 @@ "composer-require-checker": "Checks for missing required composer packages", "infection": "Creates mutation tests to discover missing test coverage", "ci-all": "Runs all ci tools in sequence" + }, + "require-dev": { + "mockery/mockery": "^1.5" } } diff --git a/deployer.phar b/deployer.phar new file mode 120000 index 0000000..20277de --- /dev/null +++ b/deployer.phar @@ -0,0 +1 @@ +vendor/deployer/deployer/dep \ No newline at end of file diff --git a/infection.json5.dist b/infection.json5.dist index c3bbefb..95c3626 100644 --- a/infection.json5.dist +++ b/infection.json5.dist @@ -14,5 +14,6 @@ }, "mutators": { "@default": true - } + }, + "bootstrap":"./test/bootstrap.php" } \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index 488dc80..c90ae21 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,7 @@ cacheDirectory=".cache/psalm" findUnusedBaselineEntry="true" findUnusedCode="true" + autoloader="./test/bootstrap.php" > diff --git a/src/Settings/SettingsInterface.php b/src/Settings/SettingsInterface.php new file mode 100644 index 0000000..6423f88 --- /dev/null +++ b/src/Settings/SettingsInterface.php @@ -0,0 +1,72 @@ +. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace de\codenamephp\deployer\secrets\Settings; + +use de\codenamephp\platform\secretsManager\base\Secret\SecretInterface; +use Deployer\Host\Host; + +/** + * Interface to set settings either globally or on hosts + * + * @psalm-api + */ +interface SettingsInterface { + + /** + * Resolves the given secret and sets it either on all hosts or if no hosts are given as a global setting + * + * Implementations should make use of the \de\codenamephp\platform\secretsManager\base\Client\ClientInterface to resolve the secret + * + * @param string $settingsKey The key to set the payload of the secret to + * @param SecretInterface $secret The secret to get the payload for + * @param Host ...$hosts Hosts to set the secret on. If no hosts are given the secret will be set globally + * @return void + */ + public function set(string $settingsKey, SecretInterface $secret, Host ...$hosts) : void; + + /** + * An array of settings keys and secrets to set. The array key is used as settings key. + * + * Resolves the given secrets and sets them either on all hosts or if no hosts are given as global settings + * + * Implementations should make use of the \de\codenamephp\platform\secretsManager\base\Client\ClientInterface to resolve the secrets + * + * @param array $secretsToSet The key/secrets mapping + * @param Host ...$hosts Hosts to set the secrets on. If no hosts are given the secrets will be set globally + * @return void + */ + public function setMultiple(array $secretsToSet, Host ...$hosts) : void; + + /** + * Fetches the payload content of the given secret. Implementations should use the \de\codenamephp\platform\secretsManager\base\Client\ClientInterface to + * resolve the secret + * + * @param SecretInterface $secret The secret to fetch the payload for + * @return string The payload content + */ + public function fetch(SecretInterface $secret) : string; + + /** + * Fetches the payload content of the given secrets. Implementations should use the \de\codenamephp\platform\secretsManager\base\Client\ClientInterface to + * resolve the secrets. Implementations MUST preserve the order and array keys of the given array as they may be used to identify the secrets + * + * @param array $secretsToResolve The secrets to fetch the payload for + * @return array The payload content of the secrets with the keys preserved + */ + public function fetchMultiple(array $secretsToResolve) : array; +} \ No newline at end of file diff --git a/src/Settings/WithClient.php b/src/Settings/WithClient.php new file mode 100644 index 0000000..4176de3 --- /dev/null +++ b/src/Settings/WithClient.php @@ -0,0 +1,50 @@ +. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace de\codenamephp\deployer\secrets\Settings; + +use de\codenamephp\deployer\base\functions\All; +use de\codenamephp\deployer\base\functions\iSet; +use de\codenamephp\platform\secretsManager\base\Client\ClientInterface; +use de\codenamephp\platform\secretsManager\base\Secret\SecretInterface; +use Deployer\Host\Host; + +/** + * Settings implementation that uses a client to fetch the payload of a secret and then sets it on the hosts or as global settings + * + * @psalm-api + */ +final class WithClient implements SettingsInterface { + + public function __construct(public readonly ClientInterface $client, public readonly iSet $deployerFunctions = new All()) {} + + public function fetch(SecretInterface $secret) : string { + return $this->client->fetchPayload($secret)->getContent(); + } + public function fetchMultiple(array $secretsToResolve) : array { + return array_map(fn(SecretInterface $secret) : string => $this->fetch($secret), $secretsToResolve); + + } + public function set(string $settingsKey, SecretInterface $secret, Host ...$hosts) : void { + $payload = $this->fetch($secret); + $hosts ? array_map(static fn(Host $host) => $host->set($settingsKey, $payload), $hosts) : $this->deployerFunctions->set($settingsKey, $payload); + } + + public function setMultiple(array $secretsToSet, Host ...$hosts) : void { + foreach($secretsToSet as $settingsKey => $secret) $this->set($settingsKey, $secret, ...$hosts); + } +} \ No newline at end of file diff --git a/test/Settings/WithClientTest.php b/test/Settings/WithClientTest.php new file mode 100644 index 0000000..73df326 --- /dev/null +++ b/test/Settings/WithClientTest.php @@ -0,0 +1,180 @@ +. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace de\codenamephp\deployer\secrets\test\Settings; + +use de\codenamephp\deployer\base\functions\iSet; +use de\codenamephp\deployer\secrets\Settings\WithClient; +use de\codenamephp\platform\secretsManager\base\Client\ClientInterface; +use de\codenamephp\platform\secretsManager\base\Secret\Payload\PayloadInterface; +use de\codenamephp\platform\secretsManager\base\Secret\SecretInterface; +use Deployer\Host\Host; +use Mockery; +use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; +use PHPUnit\Framework\TestCase; + +final class WithClientTest extends TestCase { + + use MockeryPHPUnitIntegration; + + public function testSetMultiple() : void { + $payload1 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret1']); + $payload2 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret2']); + $payload3 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret3']); + + $secret1 = $this->createMock(SecretInterface::class); + $secret2 = $this->createMock(SecretInterface::class); + $secret3 = $this->createMock(SecretInterface::class); + + $client = Mockery::mock(ClientInterface::class); + $client->allows('fetchPayload')->once()->ordered()->with($secret1)->andReturn($payload1); + $client->allows('fetchPayload')->once()->ordered()->with($secret2)->andReturn($payload2); + $client->allows('fetchPayload')->once()->ordered()->with($secret3)->andReturn($payload3); + + $deployerFunctions = Mockery::mock(iSet::class); + $deployerFunctions->allows('set')->once()->ordered()->with('key1', 'secret1'); + $deployerFunctions->allows('set')->once()->ordered()->with('key2', 'secret2'); + $deployerFunctions->allows('set')->once()->ordered()->with('key3', 'secret3'); + + $sut = new WithClient($client, $deployerFunctions); + + $sut->setMultiple(['key1' => $secret1, 'key2' => $secret2, 'key3' => $secret3]); + } + + public function testSetMultiple_withHosts() : void { + $host1 = Mockery::mock(Host::class); + $host1->allows('set')->once()->ordered()->with('key1', 'secret1'); + $host1->allows('set')->once()->ordered()->with('key2', 'secret2'); + $host1->allows('set')->once()->ordered()->with('key3', 'secret3'); + $host2 = Mockery::mock(Host::class); + $host2->allows('set')->once()->ordered()->with('key1', 'secret1'); + $host2->allows('set')->once()->ordered()->with('key2', 'secret2'); + $host2->allows('set')->once()->ordered()->with('key3', 'secret3'); + $host3 = Mockery::mock(Host::class); + $host3->allows('set')->once()->ordered()->with('key1', 'secret1'); + $host3->allows('set')->once()->ordered()->with('key2', 'secret2'); + $host3->allows('set')->once()->ordered()->with('key3', 'secret3'); + + $payload1 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret1']); + $payload2 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret2']); + $payload3 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret3']); + + $secret1 = $this->createMock(SecretInterface::class); + $secret2 = $this->createMock(SecretInterface::class); + $secret3 = $this->createMock(SecretInterface::class); + + $client = Mockery::mock(ClientInterface::class); + $client->allows('fetchPayload')->once()->ordered()->with($secret1)->andReturn($payload1); + $client->allows('fetchPayload')->once()->ordered()->with($secret2)->andReturn($payload2); + $client->allows('fetchPayload')->once()->ordered()->with($secret3)->andReturn($payload3); + + $deployerFunctions = $this->createMock(iSet::class); + $deployerFunctions->expects(self::never())->method('set'); + + $sut = new WithClient($client, $deployerFunctions); + + $sut->setMultiple(['key1' => $secret1, 'key2' => $secret2, 'key3' => $secret3], $host1, $host2, $host3); + } + + public function testSet() : void { + $payload = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret']); + $secret = $this->createMock(SecretInterface::class); + + $client = $this->createMock(ClientInterface::class); + $client->expects(self::once())->method('fetchPayload')->with($secret)->willReturn($payload); + + $deployerFunctions = $this->createMock(iSet::class); + $deployerFunctions->expects(self::once())->method('set')->with('some.key', 'secret'); + + $sut = new WithClient($client, $deployerFunctions); + + $sut->set('some.key', $secret); + } + + public function testSet_withHosts() : void { + $host1 = $this->createMock(Host::class); + $host1->expects(self::once())->method('set')->with('some.key', 'secret'); + $host2 = $this->createMock(Host::class); + $host2->expects(self::once())->method('set')->with('some.key', 'secret'); + $host3 = $this->createMock(Host::class); + $host3->expects(self::once())->method('set')->with('some.key', 'secret'); + + $payload = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret']); + $secret = $this->createMock(SecretInterface::class); + + $client = $this->createMock(ClientInterface::class); + $client->expects(self::once())->method('fetchPayload')->with($secret)->willReturn($payload); + + $deployerFunctions = $this->createMock(iSet::class); + $deployerFunctions->expects(self::never())->method('set'); + + $sut = new WithClient($client, $deployerFunctions); + + $sut->set('some.key', $secret, $host1, $host2, $host3); + } + + public function testFetchMultiple() : void { + $payload1 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret1']); + $payload2 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret2']); + $payload3 = $this->createConfiguredMock(PayloadInterface::class, ['getContent' => 'secret3']); + + $secret1 = $this->createMock(SecretInterface::class); + $secret2 = $this->createMock(SecretInterface::class); + $secret3 = $this->createMock(SecretInterface::class); + + $client = Mockery::mock(ClientInterface::class); + $client->allows('fetchPayload')->once()->ordered()->with($secret1)->andReturn($payload1); + $client->allows('fetchPayload')->once()->ordered()->with($secret2)->andReturn($payload2); + $client->allows('fetchPayload')->once()->ordered()->with($secret3)->andReturn($payload3); + + $sut = new WithClient($client); + + self::assertSame(['key1' => 'secret1', 'key2' => 'secret2', 'key3' => 'secret3'], $sut->fetchMultiple(['key1' => $secret1, 'key2' => $secret2, 'key3' => $secret3])); + } + + public function test__construct() : void { + $client = $this->createMock(ClientInterface::class); + $deployerFunctions = $this->createMock(iSet::class); + + $sut = new WithClient($client, $deployerFunctions); + + self::assertSame($client, $sut->client); + self::assertSame($deployerFunctions, $sut->deployerFunctions); + } + + public function test__construct_withMinimalParameters() : void { + $client = $this->createMock(ClientInterface::class); + + $sut = new WithClient($client); + + self::assertSame($client, $sut->client); + self::assertInstanceOf(iSet::class, $sut->deployerFunctions); + } + + public function testFetch() : void { + $secret = $this->createMock(SecretInterface::class); + $payload = $this->createMock(PayloadInterface::class); + $payload->expects(self::once())->method('getContent')->willReturn('some secret'); + + $client = $this->createMock(ClientInterface::class); + $client->expects(self::once())->method('fetchPayload')->with($secret)->willReturn($payload); + + $sut = new WithClient($client); + + self::assertSame('some secret', $sut->fetch($secret)); + } +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..dab1602 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,21 @@ +. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +require_once __DIR__ . '/../vendor/autoload.php'; + +Phar::loadPhar(__DIR__ . '/../deployer.phar'); +require_once 'phar://deployer.phar/vendor/autoload.php'; \ No newline at end of file diff --git a/test/phpunit.dist.xml b/test/phpunit.dist.xml index cd63c0a..c0dfd46 100644 --- a/test/phpunit.dist.xml +++ b/test/phpunit.dist.xml @@ -1,24 +1,23 @@ -