diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..b8fab63 --- /dev/null +++ b/.env.dist @@ -0,0 +1,15 @@ +# Environment variables can be set from a .env file placed in the location you +# run client scripts from. One can also be placed in the ACSF gfs mount for +# each environment. +# +# The AH_* variables should be removed when running on Acquia environments. +# +# The username of the user the API key belongs to. +ACSF_API_USERNAME=api.user +# The API key to access the API with. +# https://docs.acquia.com/site-factory/extend/api/#obtaining-your-api-key +ACSF_API_KEY=abc123 +# The ACSF site group. +AH_SITE_GROUP=example +# The ACSF environment. +AH_SITE_ENVIRONMENT=01dev diff --git a/.gitignore b/.gitignore index 428def3..e3748c4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea/* /vendor/ .phpcs-cache +.phpunit.result.cache # macOS @@ -9,6 +10,7 @@ .DS_Store .AppleDouble .LSOverride +.env # Icon must end with two \r Icon diff --git a/README.md b/README.md index 10cba5d..e4e7941 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Example scripts are available in the [examples](examples/) folder. require 'vendor/autoload.php'; - use swichers\Acsf\Client\ServiceLoader; + use swichers\Acsf\Client\ClientFactory; $base_config = [ 'username' => 'example.user', @@ -45,8 +45,7 @@ Example scripts are available in the [examples](examples/) folder. 'environment' => 'live', ]; - $client = ServiceLoader::buildFromConfig(['acsf.client.connection' => $base_config]) - ->get('acsf.client'); + $client = ClientFactory::createFromArray($base_config); // Check the service status. print_r($client->getAction('Status')->ping()); diff --git a/composer.json b/composer.json index 70f4565..adb2b14 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "symfony/dependency-injection": "^4.3", "symfony/finder": "^4.3", "symfony/http-client": "^4.3", - "symfony/yaml": "^4.3" + "symfony/yaml": "^4.3", + "symfony/dotenv": "^4.4" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || ^0.6.0", diff --git a/composer.lock b/composer.lock index a7ee3d5..9f8286c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1ead70dc5f6a76254f4ef4287cfe9ccc", + "content-hash": "557f445ee2417d42e25978baa45becf6", "packages": [ { "name": "doctrine/annotations", @@ -369,6 +369,77 @@ "homepage": "https://symfony.com", "time": "2020-01-31T09:49:27+00:00" }, + { + "name": "symfony/dotenv", + "version": "v4.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "f17675595fd7527c57d11bd3d733eb5d41600128" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/f17675595fd7527c57d11bd3d733eb5d41600128", + "reference": "f17675595fd7527c57d11bd3d733eb5d41600128", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "symfony/process": "^3.4.2|^4.0|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-02T16:08:58+00:00" + }, { "name": "symfony/filesystem", "version": "v4.4.4", @@ -2453,5 +2524,6 @@ "platform-dev": [], "platform-overrides": { "php": "7.2" - } + }, + "plugin-api-version": "1.1.0" } diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index 4f4773f..0000000 --- a/examples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.php diff --git a/examples/README.md b/examples/README.md index 20b85e9..bdc90dc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,11 +1,11 @@ # Library Usage Examples This folder contains some examples of how to use the library to script ACSF -deployments and associated tasks. To use these examples rename the -`example.config.php` file to `config.php` and add your ACSF credentials. - +deployments and associated tasks. To use these examples copy the `.env.dist` +file to `.env` in this folder. Then edit and configure each value in that file. + ```sh -cp example.config.php config.php +cp ../.env.dist .env ``` ## Files diff --git a/examples/backport.php b/examples/backport.php index de9205e..136c0a9 100644 --- a/examples/backport.php +++ b/examples/backport.php @@ -11,10 +11,9 @@ declare(strict_types = 1); use swichers\Acsf\Client\Endpoints\Entity\EntityInterface; -use swichers\Acsf\Client\ServiceLoader; +use swichers\Acsf\Client\ClientFactory; -require 'config.php'; -require '../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // The environment to backport to. define('TARGET_ENV', $argv[1] ?? ''); @@ -36,15 +35,7 @@ $start_time = new DateTime(); -$base_config = [ - 'username' => API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => ['environment' => SOURCE_ENV] + $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment(SOURCE_ENV); $sites = $client->getAction('Sites')->listAll(); $site_ids = array_column($sites['sites'], 'id'); @@ -80,7 +71,7 @@ function (EntityInterface $task, $task_status) { ); // Change to the target environment. -$client->setConfig(['environment' => TARGET_ENV] + $base_config); +$client->setEnvironment(TARGET_ENV); $refs = $client->getAction('Vcs')->list(['stack_id' => STACK_ID]); if (!in_array(DEPLOY_REF, $refs['available'])) { diff --git a/examples/backup.php b/examples/backup.php index 0903b09..75a9f6b 100644 --- a/examples/backup.php +++ b/examples/backup.php @@ -10,11 +10,10 @@ declare(strict_types = 1); +use swichers\Acsf\Client\ClientFactory; use swichers\Acsf\Client\Endpoints\Entity\EntityInterface; -use swichers\Acsf\Client\ServiceLoader; -require 'config.php'; -require '../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // The environment to back up. define('TARGET_ENV', $argv[1] ?? ''); @@ -30,15 +29,7 @@ $start_time = new DateTime(); -$base_config = [ - 'username' => API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => ['environment' => TARGET_ENV] + $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment(TARGET_ENV); $client->getAction('Sites')->backupAll( ['components' => ['database']], diff --git a/examples/cc.php b/examples/cc.php index 854068e..86f37b4 100644 --- a/examples/cc.php +++ b/examples/cc.php @@ -9,10 +9,9 @@ declare(strict_types = 1); -use swichers\Acsf\Client\ServiceLoader; +use swichers\Acsf\Client\ClientFactory; -require 'config.php'; -require '../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // The environment to redeploy code on. define('TARGET_ENV', $argv[1] ?? ''); @@ -30,15 +29,7 @@ $start_time = new DateTime(); -$base_config = [ - 'username' => API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => ['environment' => TARGET_ENV] + $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment(TARGET_ENV); printf("Clearing site caches.\n"); $client->getAction('Sites')->clearCaches(); diff --git a/examples/deploy-uat.php b/examples/deploy-uat.php index 1b677a7..f114996 100644 --- a/examples/deploy-uat.php +++ b/examples/deploy-uat.php @@ -12,12 +12,11 @@ declare(strict_types = 1); +use swichers\Acsf\Client\ClientFactory; use swichers\Acsf\Client\ClientInterface; use swichers\Acsf\Client\Endpoints\Entity\EntityInterface; -use swichers\Acsf\Client\ServiceLoader; -require 'config.php'; -require '../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // The code reference to deploy to uat. define('DEPLOY_REF', $argv[1] ?? ''); @@ -38,15 +37,7 @@ $start_time = new DateTime(); -$base_config = [ - 'username' => API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => ['environment' => 'live'] + $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment('live'); $task_id = start_production_backport($client, TARGET_ENV); if (FALSE === $task_id) { @@ -66,7 +57,7 @@ function (EntityInterface $task, array $task_status) { ); // Swap to the destination environment. -$client->setConfig(['environment' => TARGET_ENV] + $base_config); +$client->setEnvironment(TARGET_ENV); $refs = $client->getAction('Vcs')->list(['stack_id' => STACK_ID]); if (!in_array(DEPLOY_REF, $refs['available'])) { diff --git a/examples/deploy.php b/examples/deploy.php index 371afa6..197312c 100644 --- a/examples/deploy.php +++ b/examples/deploy.php @@ -10,11 +10,10 @@ declare(strict_types = 1); +use swichers\Acsf\Client\ClientFactory; use swichers\Acsf\Client\Endpoints\Entity\EntityInterface; -use swichers\Acsf\Client\ServiceLoader; -require 'config.php'; -require '../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; // The environment to deploy to. define('TARGET_ENV', $argv[1] ?? ''); @@ -34,16 +33,7 @@ $start_time = new DateTime(); -$base_config = [ - 'username' => API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, - 'environment' => TARGET_ENV, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment(TARGET_ENV); $refs = $client->getAction('Vcs')->list(['stack_id' => STACK_ID]); if (!in_array(DEPLOY_REF, $refs['available'])) { diff --git a/examples/example.config.php b/examples/example.config.php deleted file mode 100644 index 3dd452e..0000000 --- a/examples/example.config.php +++ /dev/null @@ -1,15 +0,0 @@ - API_USERNAME, - 'api_key' => API_KEY, - 'site_group' => ACSF_SITE_GROUP, -]; - -$client = ServiceLoader::buildFromConfig( - ['acsf.client.connection' => ['environment' => TARGET_ENV] + $base_config] -)->get('acsf.client'); +$client = ClientFactory::createFromEnvironment(TARGET_ENV); $refs = $client->getAction('Vcs')->list(['stack_id' => STACK_ID]); printf("Redeploying code: %s\n", $refs['current']); diff --git a/examples/simple.php b/examples/simple.php index d4fa043..04f781c 100644 --- a/examples/simple.php +++ b/examples/simple.php @@ -9,21 +9,11 @@ declare(strict_types = 1); -require 'vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; -use swichers\Acsf\Client\ServiceLoader; +use swichers\Acsf\Client\ClientFactory; -$base_config = [ - 'username' => 'example.user', - 'api_key' => 'example.key', - 'site_group' => 'example.group', - 'environment' => 'live', -]; - -// Utilize the Symfony service container for ease of client creation. -$client = - ServiceLoader::buildFromConfig(['acsf.client.connection' => $base_config]) - ->get('acsf.client'); +$client = ClientFactory::create('example.user', 'example.key', 'example.group', 'live'); // Grab all available sites. $site_ids = array_column($client->getAction('Sites')->listAll()['sites'], 'id'); @@ -35,7 +25,7 @@ $client->getEntity('Task', intval($task_info['task_id']))->wait(); // Change the connection to the target environment. -$client->setConfig(['environment' => 'uat'] + $base_config); +$client->setEnvironment('uat'); // Deploy a new tag to the target environment. $task_info = $client->getAction('Update')->updateCode('tags/1.5.0-build'); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ef954f0..a1ad6eb 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,6 +18,7 @@ + diff --git a/src/Client.php b/src/Client.php index 7b69704..d7446ed 100644 --- a/src/Client.php +++ b/src/Client.php @@ -150,10 +150,34 @@ public function setConfig(array $config): array { $this->validateConfig($config); $old_config = $this->config ?? []; $this->config = $config; + $this->config['site_group'] = strtolower($this->config['site_group'] ?? ''); + $this->setEnvironment($config['environment'] ?? ''); return $old_config; } + /** + * {@inheritdoc} + */ + public function setEnvironment(string $environment): string { + $current_environment = $this->getEnvironment(); + + // AH_SITE_ENVIRONMENT can have the stack ID in it, so let's blindly strip + // starting numbers from our given environment. This should be safe because + // valid environment names cannot start with numbers anyway. + $environment = (string) preg_replace('/^\d/m', '', $environment); + $this->config['environment'] = strtolower($environment); + + return $current_environment; + } + + /** + * {@inheritdoc} + */ + public function getEnvironment(): string { + return $this->config['environment']; + } + /** * {@inheritdoc} */ diff --git a/src/ClientFactory.php b/src/ClientFactory.php new file mode 100644 index 0000000..58daf61 --- /dev/null +++ b/src/ClientFactory.php @@ -0,0 +1,227 @@ + $config]); + } + + /** + * Create a Client by using environment variables. + * + * Looks for the following environment variables and uses them to create a + * client: + * + * ACSF_API_USERNAME + * ACSF_API_KEY + * AH_SITE_GROUP + * AH_SITE_ENVIRONMENT + * + * The passed in environment value will be preferred over ACSF_ENVIRONMENT. + * + * @param string|null $environment + * The ACSF environment to target. Defaults to dev. + * @param bool|true $loadEnvFile + * If true, attempt to load a .env file and use its values. + * + * @return \swichers\Acsf\Client\ClientInterface + * An initialized client. + * + * @see \swichers\Acsf\Client\ClientFactory::getEnvPaths() + */ + public static function createFromEnvironment(string $environment = NULL, bool $loadEnvFile = TRUE): ClientInterface { + + if ($loadEnvFile) { + try { + (new Dotenv())->load(...self::getEnvPaths()); + } + catch (PathException $x) { + // We don't necessarily care if this fails. It just means we rely on the + // actual system environment variables. + } + } + + // Prefer what was passed in, fall back to the environment defined target, + // and then finally fall back to dev. + $environment = + ($environment ?? (string) getenv('AH_SITE_ENVIRONMENT')) ?? 'dev'; + + return self::createFromArray( + [ + 'username' => getenv('ACSF_API_USERNAME'), + 'api_key' => getenv('ACSF_API_KEY'), + 'site_group' => getenv('AH_SITE_GROUP'), + 'environment' => $environment, + ] + ); + } + + /** + * Create a client using values directly. + * + * @param string $username + * The username to use. + * @param string $key + * The API key to use. + * @param string $siteGroup + * The ACSF site group. + * @param string $environment + * The ACSF environment. i.e. 01dev. + * + * @return \swichers\Acsf\Client\ClientInterface + * An initialized client. + */ + public static function create(string $username, string $key, string $siteGroup, string $environment = 'dev'): ClientInterface { + + return self::createFromArray( + [ + 'username' => $username, + 'api_key' => $key, + 'site_group' => $siteGroup, + 'environment' => $environment ?: 'dev', + ] + ); + } + + /** + * Get a container with ACSF client services. + * + * @param string|null $servicePath + * The path to scan for the services file. + * @param string $serviceFile + * The name of the services file. + * @param array|null $config + * A key value array of configuration to set on the container. The client + * uses acsf.client.connection for its configuration. + * + * @return \Symfony\Component\DependencyInjection\ContainerBuilder + * A container with the discovered services. + * + * @BUG Can't get unique clients via the service container. + * Do we want this behavior? It's clunky to get a workable client using the + * container, but easy to change the config after we have it. + * https://symfony.com/doc/current/service_container/shared.html + */ + public static function getServices(string $servicePath = NULL, string $serviceFile = 'services.yml', array $config = []): ContainerBuilder { + + static $containerBuilder; + + // Default to the client library configuration. + if (empty($servicePath)) { + $servicePath = __DIR__ . '/../'; + } + + $servicePath = realpath($servicePath); + + if (empty($containerBuilder[$servicePath][$serviceFile])) { + // This is required for the automatic loading of Annotation enabled + // classes. + AnnotationRegistry::registerLoader('class_exists'); + + $containerBuilder[$servicePath][$serviceFile] = new ContainerBuilder(); + + $loader = new YamlFileLoader( + $containerBuilder[$servicePath][$serviceFile], + new FileLocator($servicePath) + ); + $loader->load($serviceFile); + + } + + if (!empty($config)) { + foreach ($config as $key => $value) { + $containerBuilder[$servicePath][$serviceFile]->setParameter( + $key, + $value + ); + } + } + + return $containerBuilder[$servicePath][$serviceFile]; + } + + /** + * Create a client through a service container. + * + * @param array $config + * A key value array of client configuration. + * @param string|null $servicePath + * The path to scan for the services file. + * @param string $serviceFile + * The name of the services file. + * + * @return \swichers\Acsf\Client\ClientInterface + * An initialized client. + * + * @see \swichers\Acsf\Client\ClientFactory::createFromArray() + * @see \swichers\Acsf\Client\ClientFactory::getServices() + */ + public static function createFromConfig(array $config, string $servicePath = NULL, string $serviceFile = 'services.yml'): ClientInterface { + + $container = self::getServices( + $servicePath, + $serviceFile, + $config + ); + + return $container->get('acsf.client'); + } + + /** + * Get the list of paths to look for .env files in. + * + * @return string[]|array + * A list of paths to check for .env files. + */ + protected static function getEnvPaths(): array { + + $paths = [ + getcwd(), + ]; + + // Add Acquia environment paths. + if (!empty(getenv('AH_SITE_NAME'))) { + $paths[] = vsprintf( + '/mnt/gfs/%s.%s', + [ + getenv('AH_SITE_GROUP'), + getenv('AH_SITE_ENVIRONMENT'), + ] + ); + } + + array_walk( + $paths, + static function (&$item) { + + $item .= '/.env'; + } + ); + + return $paths; + } + +} diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 73ac714..a520622 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -137,4 +137,23 @@ public function setConfig(array $config): array; */ public function getConfig(): array; + /** + * Set the active environment. + * + * @param string $environment + * The environment to change to. + * + * @return string + * The previous environment. + */ + public function setEnvironment(string $environment): string; + + /** + * Get the active environment. + * + * @return string + * The active environment. + */ + public function getEnvironment(): string; + } diff --git a/src/ServiceLoader.php b/src/ServiceLoader.php index 3d5bea6..065b802 100644 --- a/src/ServiceLoader.php +++ b/src/ServiceLoader.php @@ -2,13 +2,14 @@ namespace swichers\Acsf\Client; -use Doctrine\Common\Annotations\AnnotationRegistry; -use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; /** * Symfony helper class to load ACSF Client services into a container. + * + * @deprecated This class has been superseded by ClientFactory. + * + * @see \swichers\Acsf\Client\ClientFactory */ class ServiceLoader { @@ -27,34 +28,14 @@ class ServiceLoader { * Do we want this behavior? It's clunky to get a workable client using the * container, but easy to change the config after we have it. * https://symfony.com/doc/current/service_container/shared.html + * + * @deprecated Replaced by the getServices method in ClientFactory. + * + * @see \swichers\Acsf\Client\ClientFactory::getServices() */ public static function build(string $servicePath = NULL, string $serviceFile = 'services.yml'): ContainerBuilder { - static $containerBuilder; - - // Default to the client library configuration. - if (empty($servicePath)) { - $servicePath = __DIR__ . '/../'; - } - - $servicePath = realpath($servicePath); - - if (empty($containerBuilder[$servicePath][$serviceFile])) { - // This is required for the automatic loading of Annotation enabled - // classes. - AnnotationRegistry::registerLoader('class_exists'); - - $containerBuilder[$servicePath][$serviceFile] = new ContainerBuilder(); - - $loader = new YamlFileLoader( - $containerBuilder[$servicePath][$serviceFile], - new FileLocator($servicePath) - ); - $loader->load($serviceFile); - - } - - return $containerBuilder[$servicePath][$serviceFile]; + return ClientFactory::getServices($servicePath, $serviceFile); } /** @@ -70,16 +51,13 @@ public static function build(string $servicePath = NULL, string $serviceFile = ' * @return \Symfony\Component\DependencyInjection\ContainerBuilder * A container with the discovered services. * - * @see \swichers\Acsf\Client\ServiceLoader::build() + * @deprecated Replaced by createFromConfig in ClientFactory + * + * @see \swichers\Acsf\Client\ClientFactory::createFromConfig() */ public static function buildFromConfig(array $config, string $servicePath = NULL, string $serviceFile = 'services.yml'): ContainerBuilder { - $container = self::build($servicePath, $serviceFile); - - foreach ($config as $key => $value) { - $container->setParameter($key, $value); - } - return $container; + return ClientFactory::getServices($servicePath, $serviceFile, $config); } }