diff --git a/azure-pipelines.tags.yml b/azure-pipelines.tags.yml
index 192cbd8ee..b5cbc838e 100644
--- a/azure-pipelines.tags.yml
+++ b/azure-pipelines.tags.yml
@@ -22,6 +22,10 @@ resources:
type: github
endpoint: keboola
name: keboola/configuration-variables-resolver
+ - repository: encryption-api-php-client
+ type: github
+ endpoint: keboola
+ name: keboola/encryption-api-php-client
- repository: input-mapping
type: github
endpoint: keboola
@@ -58,6 +62,10 @@ resources:
type: github
endpoint: keboola
name: keboola/settle
+ - repository: slicer
+ type: github
+ endpoint: keboola
+ name: keboola/slicer
- repository: staging-provider
type: github
endpoint: keboola
@@ -66,10 +74,6 @@ resources:
type: github
endpoint: keboola
name: keboola/vault-api-php-client
- - repository: slicer
- type: github
- endpoint: keboola
- name: keboola/slicer
variables:
DOCKER_BUILDKIT: 1
@@ -100,6 +104,14 @@ jobs:
libraryPath: libs/configuration-variables-resolver
tagPrefix: configuration-variables-resolver/
+ - template: azure-pipelines/jobs/split-library.yml
+ parameters:
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/encryption-api-client/')
+ sourceRepo: platform-libraries
+ targetRepo: encryption-api-php-client
+ libraryPath: libs/encryption-api-client
+ tagPrefix: encryption-api-client/
+
- template: azure-pipelines/jobs/split-library.yml
parameters:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/input-mapping/')
@@ -172,6 +184,14 @@ jobs:
libraryPath: libs/settle
tagPrefix: settle/
+ - template: azure-pipelines/jobs/split-library.yml
+ parameters:
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/slicer/')
+ sourceRepo: platform-libraries
+ targetRepo: slicer
+ libraryPath: libs/slicer
+ tagPrefix: slicer/
+
- template: azure-pipelines/jobs/split-library.yml
parameters:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/staging-provider/')
@@ -180,7 +200,6 @@ jobs:
libraryPath: libs/staging-provider
tagPrefix: staging-provider/
-
- template: azure-pipelines/jobs/split-library.yml
parameters:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/vault-api-client/')
@@ -188,11 +207,3 @@ jobs:
targetRepo: vault-api-php-client
libraryPath: libs/vault-api-client
tagPrefix: vault-api-client/
-
- - template: azure-pipelines/jobs/split-library.yml
- parameters:
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/slicer/')
- sourceRepo: platform-libraries
- targetRepo: slicer
- libraryPath: libs/slicer
- tagPrefix: slicer/
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 804997871..44801bb84 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -22,6 +22,10 @@ resources:
type: github
endpoint: keboola
name: keboola/configuration-variables-resolver
+ - repository: encryption-api-php-client
+ type: github
+ endpoint: keboola
+ name: keboola/encryption-api-php-client
- repository: input-mapping
type: github
endpoint: keboola
@@ -58,6 +62,10 @@ resources:
type: github
endpoint: keboola
name: keboola/settle
+ - repository: slicer
+ type: github
+ endpoint: keboola
+ name: keboola/slicer
- repository: staging-provider
type: github
endpoint: keboola
@@ -66,10 +74,6 @@ resources:
type: github
endpoint: keboola
name: keboola/vault-api-php-client
- - repository: slicer
- type: github
- endpoint: keboola
- name: keboola/slicer
variables:
DOCKER_BUILDKIT: 1
@@ -88,6 +92,7 @@ stages:
apiBundle:libs/api-bundle \
azureApiClient:libs/azure-api-client \
configurationVariablesResolver:libs/configuration-variables-resolver \
+ encryptionApiClient:libs/encryption-api-client \
inputMapping:libs/input-mapping \
k8sClient:libs/k8s-client \
loggingBundle:libs/logging-bundle \
@@ -97,9 +102,9 @@ stages:
sandboxesServiceApiClient:libs/sandboxes-service-api-client \
serviceClient:libs/service-client \
settle:libs/settle \
+ slicer:libs/slicer \
stagingProvider:libs/staging-provider \
- vaultApiClient:libs/vault-api-client \
- slicer:libs/slicer
+ vaultApiClient:libs/vault-api-client
displayName: 'Find changes'
name: findChanges
@@ -140,6 +145,14 @@ stages:
jobs:
- template: libs/configuration-variables-resolver/azure-pipelines.tests.yml
+ - stage: tests_encryptionApiClient
+ displayName: Tests - Encryption API Client
+ lockBehavior: sequential
+ dependsOn: build
+ condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_encryptionApiClient'])
+ jobs:
+ - template: libs/encryption-api-client/azure-pipelines.tests.yml
+
- stage: tests_inputMapping
displayName: Tests - Input Mapping
lockBehavior: sequential
@@ -212,6 +225,14 @@ stages:
jobs:
- template: libs/settle/azure-pipelines.tests.yml
+ - stage: tests_slicer
+ displayName: Tests - Slicer
+ lockBehavior: sequential
+ dependsOn: build
+ condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_slicer'])
+ jobs:
+ - template: libs/slicer/azure-pipelines.tests.yml
+
- stage: tests_stagingProvider
displayName: Tests - Staging Provider
lockBehavior: sequential
@@ -228,14 +249,6 @@ stages:
jobs:
- template: libs/vault-api-client/azure-pipelines.tests.yml
- - stage: tests_slicer
- displayName: Tests - Slicer
- lockBehavior: sequential
- dependsOn: build
- condition: and(succeeded(), dependencies.build.outputs['checkChanges.findChanges.changedProjects_slicer'])
- jobs:
- - template: libs/slicer/azure-pipelines.tests.yml
-
- stage: testsResults
displayName: Wait for tests results
dependsOn:
@@ -243,6 +256,7 @@ stages:
- tests_apiBundle
- tests_azureApiClient
- tests_configurationVariablesResolver
+ - tests_encryptionApiClient
- tests_inputMapping
- tests_k8sClient
- tests_loggingBundle
@@ -252,14 +266,15 @@ stages:
- tests_sandboxesServiceApiClient
- tests_serviceClient
- tests_settle
+ - tests_slicer
- tests_stagingProvider
- tests_vaultApiClient
- - tests_slicer
condition: |
and(
in(dependencies.tests_apiBundle.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_azureApiClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_configurationVariablesResolver.result, 'Succeeded', 'Skipped'),
+ in(dependencies.tests_encryptionApiClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_inputMapping.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_k8sClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_loggingBundle.result, 'Succeeded', 'Skipped'),
@@ -269,9 +284,9 @@ stages:
in(dependencies.tests_sandboxesServiceApiClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_serviceClient.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_settle.result, 'Succeeded', 'Skipped'),
+ in(dependencies.tests_slicer.result, 'Succeeded', 'Skipped'),
in(dependencies.tests_stagingProvider.result, 'Succeeded', 'Skipped'),
- in(dependencies.tests_vaultApiClient.result, 'Succeeded', 'Skipped'),
- in(dependencies.tests_slicer.result, 'Succeeded', 'Skipped')
+ in(dependencies.tests_vaultApiClient.result, 'Succeeded', 'Skipped')
)
jobs:
- job:
@@ -307,6 +322,14 @@ stages:
libraryPath: libs/configuration-variables-resolver
tagPrefix: configuration-variables-resolver/
+ - template: azure-pipelines/jobs/split-library.yml
+ parameters:
+ condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_encryptionApiClient'])
+ sourceRepo: platform-libraries
+ targetRepo: encryption-api-php-client
+ libraryPath: libs/encryption-api-client
+ tagPrefix: encryption-api-client/
+
- template: azure-pipelines/jobs/split-library.yml
parameters:
condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_inputMapping'])
@@ -379,6 +402,14 @@ stages:
libraryPath: libs/settle
tagPrefix: settle/
+ - template: azure-pipelines/jobs/split-library.yml
+ parameters:
+ condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_slicer'])
+ sourceRepo: platform-libraries
+ targetRepo: slicer
+ libraryPath: libs/slicer
+ tagPrefix: slicer/
+
- template: azure-pipelines/jobs/split-library.yml
parameters:
condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_stagingProvider'])
@@ -394,11 +425,3 @@ stages:
targetRepo: vault-api-php-client
libraryPath: libs/vault-api-client
tagPrefix: vault-api-client/
-
- - template: azure-pipelines/jobs/split-library.yml
- parameters:
- condition: and(succeeded(), stageDependencies.build.checkChanges.outputs['findChanges.changedProjects_slicer'])
- sourceRepo: platform-libraries
- targetRepo: slicer
- libraryPath: libs/slicer
- tagPrefix: slicer/
diff --git a/docker-compose.yml b/docker-compose.yml
index 73fb66e69..33f788d86 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -60,6 +60,11 @@ services:
- STORAGE_API_TOKEN_MASTER
- STORAGE_API_URL
+ dev-encryption-api-client:
+ <<: *dev
+ image: keboola/encryption-api-client
+ working_dir: /code/libs/encryption-api-client
+
dev-input-mapping:
<<: *dev
image: keboola/input-mapping
@@ -77,6 +82,10 @@ services:
image: keboola/k8s-client
working_dir: /code/libs/k8s-client
+ dev-logging-bundle:
+ <<: *dev
+ working_dir: /code/libs/logging-bundle
+
dev-messenger-bundle:
<<: *dev82
working_dir: /code/libs/messenger-bundle
@@ -117,6 +126,11 @@ services:
image: keboola/settle
working_dir: /code/libs/settle
+ dev-slicer:
+ <<: *dev
+ image: keboola/slicer
+ working_dir: /code/libs/slicer
+
dev-staging-provider:
<<: *dev
image: keboola/staging-provider
@@ -132,12 +146,3 @@ services:
<<: *dev82
image: keboola/vault-api-client
working_dir: /code/libs/vault-api-client
-
- dev-slicer:
- <<: *dev
- image: keboola/slicer
- working_dir: /code/libs/slicer
-
- dev-logging-bundle:
- <<: *dev
- working_dir: /code/libs/logging-bundle
diff --git a/libs/encryption-api-client/.gitignore b/libs/encryption-api-client/.gitignore
new file mode 100644
index 000000000..48b8bf907
--- /dev/null
+++ b/libs/encryption-api-client/.gitignore
@@ -0,0 +1 @@
+vendor/
diff --git a/libs/encryption-api-client/LICENSE b/libs/encryption-api-client/LICENSE
new file mode 100644
index 000000000..d16cdbea9
--- /dev/null
+++ b/libs/encryption-api-client/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Keboola :(){:|:&};: s.r.o.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/encryption-api-client/README.md b/libs/encryption-api-client/README.md
new file mode 100644
index 000000000..771b3f3ef
--- /dev/null
+++ b/libs/encryption-api-client/README.md
@@ -0,0 +1,28 @@
+# Encryption API Client
+
+## Installation
+```bash
+composer require keboola/encryption-api-php-client
+```
+
+## Usage
+
+```php
+use Keboola\EncryptionApiClient\Migrations;
+
+$migrations = new Migrations(getenv('STORAGE_API_TOKEN'));
+
+$resultMessage = $migrations->migrateConfiguration(
+ sourceStorageApiToken: '...',
+ destinationStack: 'connection.europe-west3.gcp.keboola.com',
+ destinationStorageApiToken: '...',
+ componentId: 'keboola.data-apps',
+ configId: '123456',
+ branchId: '102',
+ dryRun: true,
+);
+```
+
+## License
+
+MIT licensed, see [LICENSE](./LICENSE) file.
diff --git a/libs/encryption-api-client/azure-pipelines.tests.yml b/libs/encryption-api-client/azure-pipelines.tests.yml
new file mode 100644
index 000000000..18cd72ca9
--- /dev/null
+++ b/libs/encryption-api-client/azure-pipelines.tests.yml
@@ -0,0 +1,21 @@
+jobs:
+ - template: ../../azure-pipelines/jobs/run-tests.yml
+ parameters:
+ displayName: Tests (PHP 7.4)
+ serviceName: dev74
+ testCommand: |
+ bash -c '\
+ echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/xdebug.ini
+ cd libs/encryption-api-client && \
+ composer update --prefer-lowest && \
+ composer ci'
+
+ - template: ../../azure-pipelines/jobs/run-tests.yml
+ parameters:
+ displayName: Tests (PHP 8.2)
+ serviceName: dev82
+ testCommand: |
+ bash -c '\
+ cd libs/encryption-api-client && \
+ composer update && \
+ composer ci'
diff --git a/libs/encryption-api-client/composer.json b/libs/encryption-api-client/composer.json
new file mode 100644
index 000000000..03584d082
--- /dev/null
+++ b/libs/encryption-api-client/composer.json
@@ -0,0 +1,61 @@
+{
+ "name": "keboola/encryption-api-php-client",
+ "type": "library",
+ "license": "MIT",
+ "description": "Keboola Encryption API Client",
+ "authors": [
+ {
+ "name": "Keboola",
+ "email": "devel@keboola.com"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Keboola\\EncryptionApiClient\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Keboola\\EncryptionApiClient\\Tests\\": "tests/"
+ }
+ },
+ "require": {
+ "php": "^7.4 || ^8.2",
+ "ext-json": "*",
+ "ext-curl": "*",
+ "guzzlehttp/guzzle": "^6.5 || ^7.8"
+ },
+ "config": {
+ "lock": false,
+ "sort-packages": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true,
+ "infection/extension-installer": true
+ }
+ },
+ "require-dev": {
+ "infection/infection": "0.26.6 || ^0.27.9",
+ "keboola/coding-standard": "^15.0",
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5",
+ "sempro/phpunit-pretty-print": "^1.4"
+ },
+ "scripts": {
+ "ci": [
+ "@composer validate --no-check-publish --no-check-all",
+ "@phpcs",
+ "@phpstan",
+ "@tests",
+ "@infection"
+ ],
+ "phpcs": "phpcs -n --ignore=vendor,cache,Kernel.php --extensions=php .",
+ "phpcbf": "phpcbf --extensions=php src tests",
+ "phpstan": "phpstan analyse --no-progress -c phpstan.neon",
+ "tests": [
+ "@putenv XDEBUG_MODE=coverage",
+ "phpunit --coverage-clover /tmp/build-logs/clover.xml --coverage-xml=/tmp/build-logs/coverage-xml --log-junit=/tmp/build-logs/phpunit.junit.xml"
+ ],
+ "infection": "infection --log-verbosity=all --threads=20 --min-covered-msi=75 --coverage=/tmp/build-logs --only-covered"
+ }
+}
diff --git a/libs/encryption-api-client/infection.json.dist b/libs/encryption-api-client/infection.json.dist
new file mode 100644
index 000000000..022744e3f
--- /dev/null
+++ b/libs/encryption-api-client/infection.json.dist
@@ -0,0 +1,14 @@
+{
+ "timeout": 60,
+ "source": {
+ "directories": [
+ "src\/"
+ ]
+ },
+ "logs": {
+ "text": "/tmp/infection.log"
+ },
+ "mutators": {
+ "@default": true
+ }
+}
diff --git a/libs/encryption-api-client/phpcs.xml b/libs/encryption-api-client/phpcs.xml
new file mode 100644
index 000000000..47081669c
--- /dev/null
+++ b/libs/encryption-api-client/phpcs.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+ */src/Kernel.php
+
diff --git a/libs/encryption-api-client/phpstan.neon b/libs/encryption-api-client/phpstan.neon
new file mode 100644
index 000000000..c8d6d1662
--- /dev/null
+++ b/libs/encryption-api-client/phpstan.neon
@@ -0,0 +1,14 @@
+parameters:
+ checkMissingIterableValueType: false
+ level: max
+ paths:
+ - src
+ - tests
+ ignoreErrors:
+ -
+ message: '#Static method GuzzleHttp\\Middleware::httpErrors\(\) invoked with 1 parameter, 0 required\.#'
+ path: %currentWorkingDirectory%/src/Common.php
+ reportUnmatched: false # error occurs only at PHP 7.4/Guzzle 6.5
+
+includes:
+ - vendor/phpstan/phpstan-phpunit/extension.neon
diff --git a/libs/encryption-api-client/phpunit.xml.dist b/libs/encryption-api-client/phpunit.xml.dist
new file mode 100644
index 000000000..bb3064b30
--- /dev/null
+++ b/libs/encryption-api-client/phpunit.xml.dist
@@ -0,0 +1,26 @@
+
+
+
+
+ tests/
+
+
+
+
+ src/
+ tests/
+
+
+
diff --git a/libs/encryption-api-client/src/Common.php b/libs/encryption-api-client/src/Common.php
new file mode 100644
index 000000000..e6c03e352
--- /dev/null
+++ b/libs/encryption-api-client/src/Common.php
@@ -0,0 +1,163 @@
+ 'application/json'];
+ private const MAX_HTTP_ERROR_MESSAGE_LENGTH = 1024^2;
+
+ /**
+ * @throws ClientException
+ */
+ public function __construct(array $headers, array $config)
+ {
+ // Initialize handlers (start with those supplied in constructor)
+ $handlerStack = $config['handler'] ?? HandlerStack::create();
+
+ $handlerStack->push(
+ Middleware::retry(
+ self::createDefaultDecider($config['backoffMaxTries'] ?? self::DEFAULT_BACKOFF_RETRIES),
+ ),
+ );
+
+ if (class_exists('GuzzleHttp\BodySummarizer')) {
+ $handlerStack->remove('http_errors');
+ $handlerStack->unshift(
+ Middleware::httpErrors(new BodySummarizer(self::MAX_HTTP_ERROR_MESSAGE_LENGTH)),
+ 'http_errors',
+ );
+ }
+
+ if (!isset($config['url'])) {
+ throw new InvalidArgumentException('url must be set');
+ }
+
+ $this->client = new Client(
+ [
+ 'base_uri' => $config['url'],
+ 'headers' => array_merge(
+ $headers,
+ self::DEFAULT_HEADERS,
+ ),
+ 'handler' => $handlerStack,
+ ],
+ );
+ }
+
+ protected function apiGet(string $url): array
+ {
+ $request = new Request('GET', $url);
+ return $this->sendRequest($request);
+ }
+
+ protected function apiDelete(string $url): void
+ {
+ $request = new Request('DELETE', $url);
+ $this->sendRequest($request);
+ }
+
+ protected function apiPost(string $url, array $body): array
+ {
+ $request = new Request(
+ 'POST',
+ $url,
+ [
+ 'Content-Type' => 'application/json',
+ ],
+ json_encode($body, JSON_THROW_ON_ERROR),
+ );
+ return $this->sendRequest($request);
+ }
+
+ protected function apiPatch(string $url, array $body): array
+ {
+ $request = new Request(
+ 'PATCH',
+ $url,
+ [
+ 'Content-Type' => 'application/json',
+ ],
+ json_encode($body, JSON_THROW_ON_ERROR),
+ );
+ return $this->sendRequest($request);
+ }
+
+ protected function sendRequest(Request $request): array
+ {
+ try {
+ $response = $this->client->send($request);
+ $body = $response->getBody()->getContents();
+ if ($body === '') {
+ return [];
+ }
+ return (array) json_decode($body, true, 512, JSON_THROW_ON_ERROR);
+ } catch (RequestException $e) {
+ $message = $e->getMessage();
+ if ($e->getResponse()) {
+ try {
+ $response = (array) json_decode(
+ $e->getResponse()->getBody()->getContents(),
+ true,
+ 512,
+ JSON_THROW_ON_ERROR,
+ );
+ if (!empty($response['message'])) {
+ $message = $response['message'];
+ }
+ } catch (Throwable $e) {
+ }
+ }
+ throw new ClientException('Encryption API error: ' . $message, $e->getCode(), $e);
+ } catch (GuzzleException $e) {
+ throw new ClientException('Encryption API error: ' . $e->getMessage(), $e->getCode(), $e);
+ } catch (JsonException $e) {
+ throw new ClientException('Unable to parse response body into JSON: ' . $e->getMessage());
+ }
+ }
+
+ private static function createDefaultDecider(int $maxRetries): Closure
+ {
+ return function (
+ int $retries,
+ RequestInterface $request,
+ ?ResponseInterface $response = null,
+ ?Throwable $error = null
+ ) use ($maxRetries) {
+ if ($retries >= $maxRetries) {
+ return false;
+ } elseif ($response && $response->getStatusCode() >= 500) {
+ return true;
+ } elseif ($error && $error->getCode() >= 500) {
+ return true;
+ } elseif ($error &&
+ (is_a($error, RequestException::class) || is_a($error, ConnectException::class)) &&
+ in_array($error->getHandlerContext()['errno'] ?? 0, [CURLE_RECV_ERROR, CURLE_SEND_ERROR], true)
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+ }
+}
diff --git a/libs/encryption-api-client/src/Encryption.php b/libs/encryption-api-client/src/Encryption.php
new file mode 100644
index 000000000..b7573c887
--- /dev/null
+++ b/libs/encryption-api-client/src/Encryption.php
@@ -0,0 +1,32 @@
+ $sapiToken], $config);
+ }
+
+ public function encryptPlainTextForConfiguration(
+ string $value,
+ string $projectId,
+ string $componentId,
+ string $configId
+ ): string {
+ $url = 'encrypt?';
+ $url .= http_build_query([
+ 'projectId' => $projectId,
+ 'componentId' => $componentId,
+ 'configId' => $configId,
+ ]);
+
+ // because at the moment Common client is made for json requests/responses,
+ // we need to wrap the value in an array
+ $result = $this->apiPost($url, ['#value' => $value]);
+ return $result['#value'];
+ }
+}
diff --git a/libs/encryption-api-client/src/Exception/ClientException.php b/libs/encryption-api-client/src/Exception/ClientException.php
new file mode 100644
index 000000000..b48bea289
--- /dev/null
+++ b/libs/encryption-api-client/src/Exception/ClientException.php
@@ -0,0 +1,11 @@
+ $manageApiToken], $config);
+ }
+
+ public function migrateConfiguration(
+ string $sourceStorageApiToken,
+ string $destinationStack,
+ string $destinationStorageApiToken,
+ string $componentId,
+ string $configId,
+ string $branchId,
+ bool $dryRun = false
+ ): array {
+ $queryParams = [];
+ if ($dryRun) {
+ $queryParams['dry-run'] = 'true';
+ }
+
+ $url = 'migrate-configuration';
+ if (count($queryParams) > 0) {
+ $url .= '?' . http_build_query($queryParams);
+ }
+
+ return $this->apiPost($url, [
+ 'sourceStorageApiToken' => $sourceStorageApiToken,
+ 'destinationStack' => $destinationStack,
+ 'destinationStorageApiToken' => $destinationStorageApiToken,
+ 'componentId' => $componentId,
+ 'configId' => $configId,
+ 'branchId' => $branchId,
+ ]);
+ }
+}
diff --git a/libs/encryption-api-client/tests/EncryptionTest.php b/libs/encryption-api-client/tests/EncryptionTest.php
new file mode 100644
index 000000000..fc6dbe30d
--- /dev/null
+++ b/libs/encryption-api-client/tests/EncryptionTest.php
@@ -0,0 +1,58 @@
+ 'application/json'],
+ '{"#value":"KBC::ConfigSecure::abcdefghijkl"}',
+ ),
+ ],
+ );
+
+ $stack = HandlerStack::create($mock);
+
+ $requestsMade = [];
+ $history = Middleware::history($requestsMade);
+ $stack->push($history);
+
+ $encryptionClient = new Encryption(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+ $result = $encryptionClient->encryptPlainTextForConfiguration(
+ 'plainValue',
+ 'project-id',
+ 'keboola.component-id',
+ 'config-id',
+ );
+ self::assertSame('KBC::ConfigSecure::abcdefghijkl', $result);
+
+ self::assertCount(1, $requestsMade);
+ $request = $requestsMade[0]['request'];
+ self::assertInstanceOf(RequestInterface::class, $request);
+ self::assertSame('POST', $request->getMethod());
+ self::assertSame(
+ 'https://encryption.keboola.com/encrypt?projectId=project-id'
+ . '&componentId=keboola.component-id&configId=config-id',
+ (string) $request->getUri(),
+ );
+ self::assertSame('{"#value":"plainValue"}', $request->getBody()->getContents());
+ }
+}
diff --git a/libs/encryption-api-client/tests/MigrationsTest.php b/libs/encryption-api-client/tests/MigrationsTest.php
new file mode 100644
index 000000000..5f3255ccb
--- /dev/null
+++ b/libs/encryption-api-client/tests/MigrationsTest.php
@@ -0,0 +1,395 @@
+ 'application/json'],
+ '{"message":"Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'."'
+ .',"data":{"destinationStack":"some-stack","componentId":"keboola.some-component",'
+ .'"configId":"1234","branchId":"102"}}',
+ ),
+ ],
+ );
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+ $result = $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ self::assertIsArray($result);
+ self::assertSame(
+ [
+ 'message' => 'Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'.',
+ 'data' => [
+ 'destinationStack' => 'some-stack',
+ 'componentId' => 'keboola.some-component',
+ 'configId' => '1234',
+ 'branchId' => '102',
+ ],
+ ],
+ $result,
+ );
+
+ /** @var Request $request */
+ $request = $container[0]['request'];
+ self::assertSame('POST', $request->getMethod());
+ self::assertSame('application/json', $request->getHeader('Accept')[0]);
+ self::assertSame('application/json', $request->getHeader('Content-Type')[0]);
+ self::assertSame('some-token', $request->getHeader('X-KBC-ManageApiToken')[0]);
+ self::assertSame(
+ 'https://encryption.keboola.com/migrate-configuration',
+ (string) $request->getUri(),
+ );
+ }
+
+ public function testDryRun(): void
+ {
+ $mock = new MockHandler([
+ new Response(),
+ new Response(),
+ ]);
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ false
+ );
+
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ true
+ );
+
+ /** @var Request $request */
+
+ $request = array_shift($container)['request'];
+ self::assertSame(
+ 'https://encryption.keboola.com/migrate-configuration',
+ (string) $request->getUri(),
+ );
+
+ $request = array_shift($container)['request'];
+ self::assertSame(
+ 'https://encryption.keboola.com/migrate-configuration?dry-run=true',
+ (string) $request->getUri(),
+ );
+ }
+
+ public function testRetryCurlExceptionFail(): void
+ {
+ $mock = new MockHandler(
+ [
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ ],
+ function (ResponseInterface $a) {
+ // abusing the mockhandler here: override the mock response and throw a Request exception
+ throw new RequestException(
+ 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104',
+ new Request('GET', 'https://example.com'),
+ null,
+ null,
+ [
+ 'errno' => 56,
+ 'error' => 'OpenSSL SSL_read: Connection reset by peer, errno 104',
+ ],
+ );
+ },
+ );
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com', 'backoffMaxTries' => 2],
+ );
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage('Encryption API error: Encryption API error: cURL error 56:');
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ }
+
+ public function testRetryCurlException(): void
+ {
+ $mock = new MockHandler(
+ [
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ new Response(
+ 200,
+ ['Content-Type' => 'application/json'],
+ '{"message":"Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'."'
+ .',"data":{"destinationStack":"some-stack","componentId":"sandboxes.data-apps","configId":"1234"'
+ . ',"branchId":"102"}}',
+ ),
+ ],
+ function (ResponseInterface $a) {
+ if ($a->getStatusCode() === 500) {
+ // abusing the mockhandler here: override the mock response and throw a Request exception
+ throw new RequestException(
+ 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104',
+ new Request('GET', 'https://example.com'),
+ null,
+ null,
+ [
+ 'errno' => 56,
+ 'error' => 'OpenSSL SSL_read: Connection reset by peer, errno 104',
+ ],
+ );
+ }
+ },
+ );
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+ $result = $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ self::assertIsArray($result);
+ self::assertSame(
+ [
+ 'message' => 'Configuration with ID \'1234\' successfully migrated to stack \'some-stack\'.',
+ 'data' => [
+ 'destinationStack' => 'some-stack',
+ 'componentId' => 'sandboxes.data-apps',
+ 'configId' => '1234',
+ 'branchId' => '102',
+ ],
+ ],
+ $result,
+ );
+ }
+
+ public function testRetryCurlExceptionWithoutContext(): void
+ {
+ $mock = new MockHandler(
+ [
+ new Response(500, ['Content-Type' => 'application/json'], 'not used'),
+ ],
+ function (ResponseInterface $a) {
+ // abusing the mockhandler here: override the mock response and throw a Request exception
+ throw new RequestException(
+ 'Encryption API error: cURL error 56: OpenSSL SSL_read: Connection reset by peer, errno 104',
+ new Request('GET', 'https://example.com'),
+ null,
+ null,
+ [],
+ );
+ },
+ );
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage('Encryption API error: Encryption API error: cURL error 56:');
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ }
+
+ public function testInvalidJsonResponse(): void
+ {
+ $mock = new MockHandler(
+ [
+ new Response(
+ 200,
+ ['Content-Type' => 'application/json'],
+ '{"message":"Configuration with ID \'1234\' successf#$@&*',
+ ),
+ ],
+ );
+
+ // Add the history middleware to the handler stack.
+ $container = [];
+ $history = Middleware::history($container);
+ $stack = HandlerStack::create($mock);
+ $stack->push($history);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage(
+ 'Unable to parse response body into JSON: Control character error, possibly incorrectly encoded'
+ );
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ }
+
+ public function testConnectionException(): void
+ {
+ $mock = new MockHandler(
+ [
+ new ConnectException('connection error', new Request('GET', 'test')),
+ ],
+ );
+
+ $stack = HandlerStack::create($mock);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage(
+ 'Encryption API error: connection error'
+ );
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ }
+
+ public function testRequestException(): void
+ {
+ $mock = new MockHandler(
+ [
+ new RequestException(
+ 'simple error message',
+ new Request('GET', 'test')
+ ),
+ new RequestException(
+ 'error message to be overridden with response body message',
+ new Request('GET', 'test'),
+ new Response(400, [], '{"message":"bad bad request"}')
+ ),
+ ],
+ );
+
+ $stack = HandlerStack::create($mock);
+
+ $migrations = new Migrations(
+ 'some-token',
+ ['handler' => $stack, 'url' => 'https://encryption.keboola.com'],
+ );
+
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage(
+ 'Encryption API error: simple error message'
+ );
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+
+ $this->expectException(ClientException::class);
+ $this->expectExceptionMessage(
+ 'Encryption API error: bad bad request'
+ );
+ $migrations->migrateConfiguration(
+ 'some-token',
+ 'some-stack',
+ 'some-token',
+ 'keboola.some-component',
+ '1234',
+ '102',
+ );
+ }
+}
diff --git a/libs/encryption-api-client/tests/bootstrap.php b/libs/encryption-api-client/tests/bootstrap.php
new file mode 100644
index 000000000..06fdcb453
--- /dev/null
+++ b/libs/encryption-api-client/tests/bootstrap.php
@@ -0,0 +1,5 @@
+