-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
392 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
<?php | ||
|
||
namespace Drutiny\Acquia; | ||
|
||
use Drutiny\Target\Exception\InvalidTargetException; | ||
use Drutiny\Target\TargetSourceInterface; | ||
use Drutiny\Target\TargetInterface; | ||
use Drutiny\Acquia\Api\AcquiaCli; | ||
use Drutiny\Attribute\AsTarget; | ||
use Drutiny\Attribute\UseService; | ||
use Drutiny\Target\DrushTarget; | ||
use Drutiny\Target\Exception\TargetServiceUnavailable; | ||
use Drutiny\Target\Service\Drush; | ||
use Drutiny\Target\Service\ServiceInterface; | ||
use Drutiny\Target\Transport\SshTransport; | ||
use Drutiny\Target\Transport\TransportInterface; | ||
|
||
/** | ||
* Acquia Target | ||
*/ | ||
#[UseService(AcquiaCli::class, 'setAcquiaCli')] | ||
#[AsTarget(name: 'acli')] | ||
class AcliTarget extends DrushTarget implements TargetSourceInterface | ||
{ | ||
protected AcquiaCli $api; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getId():string | ||
{ | ||
return $this['alias']; | ||
} | ||
|
||
/** | ||
* Setter callback for Acquia CLI service. | ||
*/ | ||
public function setAcquiaCli(AcquiaCli $api) { | ||
$this->api = $api; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
protected function configureService(ServiceInterface $service): void | ||
{ | ||
// If there is no Drupal site, then there is no point in running a drush service. | ||
if ($service instanceof Drush && $this['acquia.cloud.environment.vcs[path]'] == 'tags/WELCOME') { | ||
throw new TargetServiceUnavailable("Acquia CLI target environment has the WELCOME tag deployed. There is no Drupal here."); | ||
} | ||
parent::configureService($service); | ||
} | ||
|
||
/** | ||
* Parse target data. | ||
*/ | ||
public function parse(string $alias, ?string $uri = null): TargetInterface | ||
{ | ||
if (str_starts_with($alias, '@')) { | ||
$alias = substr($alias, 1); | ||
} | ||
if (!str_contains($alias, '.')) { | ||
throw new InvalidTargetException("Incorrect format. Acquia CLI target must use alias syntax: appname.env"); | ||
} | ||
$this['alias'] = $alias; | ||
list($app, $env) = explode('.', $alias); | ||
|
||
$realm = 'prod'; | ||
if (str_contains($app, ':')) { | ||
list($realm, $app) = explode(':', $app); | ||
} | ||
|
||
$this['alias'] = $alias; | ||
|
||
$this->logger->info('Loading environment from API...'); | ||
$application = $this->api->findApplication($realm, $app); | ||
$environment = $this->api->findEnvironment($application['uuid'], $env); | ||
|
||
$this->api->mapToTarget($application, $this, 'acquia.cloud.application'); | ||
$this->api->mapToTarget($environment, $this, 'acquia.cloud.environment'); | ||
|
||
list($machine_name, ) = explode('.', $environment['default_domain']); | ||
$this['acquia.cloud.machine_name'] = $machine_name; | ||
|
||
$this->setUri($uri ?: $environment['active_domain']); | ||
|
||
if ($environment['type'] == 'drupal') { | ||
list($user, $host) = explode('@', $environment['ssh_url'], 2); | ||
// At the remote, this is what the Drush alias should be. This becomes | ||
// an environment variable (e.g. $DRUSH_ALIAS). | ||
$this['drush.alias'] = "@$user"; | ||
// Tell DrushService where the drupal site is. | ||
$this['drush.root'] = "/var/www/html/$user/docroot"; | ||
|
||
// We need to load the Transport early to load drush attributes correctly. | ||
$this->transport = $this->loadTransport(); | ||
$this->rebuildEnvVars(); | ||
|
||
if ($this['acquia.cloud.environment.vcs[path]'] == 'tags/WELCOME') { | ||
$this->setUri($uri ?: $environment['default_domain']); | ||
$this->logger->critical("Acquia target environment has the WELCOME tag deployed. There is no Drupal here."); | ||
return $this; | ||
} | ||
|
||
$this->buildAttributes(); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getAvailableTargets():array | ||
{ | ||
$targets = []; | ||
$applications = $this->api->runApiCommand('api:applications:list'); | ||
foreach ($applications as $app) { | ||
$this->logger->notice("Building environment targets for {$app['name']}."); | ||
|
||
$environments = $this->api->runApiCommand('api:applications:environment-list', [$app['uuid']]); | ||
foreach ($environments as $env) { | ||
$targets[] = [ | ||
'id' => $app['hosting']['id'] . '.' . $env['name'], | ||
'uri' => $env['active_domain'], | ||
'name' => sprintf('%s: %s', $env['label'], $app['name']), | ||
]; | ||
} | ||
} | ||
return $targets; | ||
} | ||
|
||
/** | ||
* Setup SSH based remote exection service. | ||
*/ | ||
protected function addRemoteTransport($user, $host):TransportInterface | ||
{ | ||
// Don't need to setup transport if its already handled be an extending class. | ||
if ($this->transport instanceof SshTransport) { | ||
return $this->transport; | ||
} | ||
$transport = new SshTransport($this->localCommand); | ||
$transport->setConfig('User', $user); | ||
$transport->setConfig('Host', $host); | ||
return $transport; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function loadTransport(): TransportInterface | ||
{ | ||
if ($this['acquia.cloud.environment']['type'] == 'drupal') { | ||
list($user, $host) = explode('@', $this['acquia.cloud.environment']['ssh_url'], 2); | ||
return $this->addRemoteTransport($user, $host); | ||
} | ||
return parent::loadTransport(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
<?php | ||
|
||
namespace Drutiny\Acquia\Api; | ||
|
||
use Drutiny\Entity\Exception\DataNotFoundException; | ||
use Drutiny\Helper\TextCleaner; | ||
use Drutiny\Target\TargetInterface; | ||
use InvalidArgumentException; | ||
use Psr\Cache\CacheItemInterface; | ||
use Symfony\Component\Process\Process; | ||
use Symfony\Contracts\Cache\CacheInterface; | ||
|
||
class AcquiaCli implements CloudApiInterface { | ||
|
||
const DEFAULT_LOCATIONS = [ | ||
'vendor/bin', | ||
'/usr/local/bin', | ||
'$HOME/.composer/vendor/bin', | ||
'/usr/bin', | ||
'/bin', | ||
]; | ||
|
||
protected string $bin; | ||
|
||
public function __construct(protected CacheInterface $cache) { | ||
// Look for a local version of ACLI. | ||
$bins = array_filter(self::DEFAULT_LOCATIONS, function ($dir) { | ||
return file_exists($dir . '/acli'); | ||
}); | ||
if (count($bins) > 1) { | ||
$bins = array_filter($bins, fn ($bin) => is_executable("$bin/acli")); | ||
} | ||
if (!empty($bins)) { | ||
$this->bin = reset($bins) . "/acli"; | ||
} | ||
} | ||
|
||
public function isAvailable(): bool { | ||
return isset($this->bin) && !empty($this->bin); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function findApplication($realm, $site):array | ||
{ | ||
$apps = $this->cache->get('acquia.cloud.applications', function (CacheItemInterface $item) { | ||
$item->expiresAfter(86400); | ||
$output = $this->prepareCommand('api:applications:list')->mustRun()->getOutput(); | ||
return json_decode($output, TRUE); | ||
}); | ||
|
||
foreach ($apps as $app) { | ||
if (empty($app['hosting'])) { | ||
continue; | ||
} | ||
list($stage, $sitegroup) = explode(':', $app['hosting']['id'], 2); | ||
|
||
if ($realm == $stage && $site == $sitegroup) { | ||
return $app; | ||
} | ||
} | ||
throw new DataNotFoundException("Cannot find Acquia application matching target criteria: $realm:$site."); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function findEnvironment($uuid, $env):array | ||
{ | ||
foreach ($this->getEnvironments($uuid) as $environment) { | ||
if ($environment['name'] == $env) { | ||
return $environment; | ||
} | ||
} | ||
throw new DataNotFoundException("Cannot find Acquia application environment: $env."); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getEnvironments(string $uuid): array { | ||
return $this->cache->get('acquia.cloud.'.$uuid.'.environments', function (CacheItemInterface $item) use ($uuid) { | ||
$item->expiresAfter(86400); | ||
return $this->runApiCommand('api:applications:environment-list', [$uuid]); | ||
}); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getEnvironment($uuid):array | ||
{ | ||
return $this->cache->get('acquia.cloud.environment.'.$uuid, function (CacheItemInterface $item) use ($uuid) { | ||
$item->expiresAfter(86400); | ||
return $this->runApiCommand('api:environments:find', [$uuid]); | ||
}); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getApplication($uuid):array | ||
{ | ||
return $this->cache->get('acquia.cloud.application.'.$uuid, function (CacheItemInterface $item) use ($uuid) { | ||
$item->expiresAfter(86400); | ||
return $this->runApiCommand('api:applications:find', [$uuid]); | ||
}); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function mapToTarget(array $data, TargetInterface $target, $namespace):void | ||
{ | ||
foreach ($data as $key => $value) { | ||
if ('_' == substr($key, 0, 1)) { | ||
continue; | ||
} | ||
$target[$namespace.'.'.$key] = $value; | ||
} | ||
} | ||
|
||
/** | ||
* Run an Acquia CLI API command and return the json decoded response. | ||
*/ | ||
public function runApiCommand(string $command, array $args = [], array $options = []): array { | ||
if (!str_starts_with($command, 'api')) { | ||
throw new InvalidArgumentException("Command is not allowed. Must use and api: prefixed command."); | ||
} | ||
if (!$this->isAvailable()) { | ||
throw new AcquiaCliNotAvailableException(); | ||
} | ||
$cid = md5(json_encode([$command, $args, $options])); | ||
$process = $this->prepareCommand($command, $args, $options); | ||
return $this->cache->get($cid, function (CacheItemInterface $item) use ($process) { | ||
$item->expiresAfter(3600); | ||
$output = $process->mustRun()->getOutput(); | ||
return TextCleaner::decodeDirtyJson($output); | ||
}); | ||
} | ||
|
||
/** | ||
* Prepare a command to run with Acquia CLI. | ||
*/ | ||
protected function prepareCommand(string $command, array $args = [], array $options = []):Process { | ||
$cmd = $this->bin . ' ' . $command . ' ' . implode(' ', $args); | ||
|
||
$opts = []; | ||
foreach ($options as $name => $value) { | ||
$opt_type = (strlen($name) > 1) ? '--' : '-'; | ||
// Flag opts. | ||
if (is_null($value) || is_bool($value)) { | ||
$opts[] = $opt_type . $name; | ||
continue; | ||
} | ||
if ($opt_type == '--') { | ||
$opts[] = $opt_type . '=' . $value; | ||
} | ||
else { | ||
$opts[] = $opt_type . ' ' . $value; | ||
} | ||
} | ||
if (!empty($opts)) { | ||
$cmd .= ' ' . implode(' ', $opts); | ||
} | ||
|
||
return Process::fromShellCommandline($cmd); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace Drutiny\Acquia\Api; | ||
|
||
class AcquiaCliNotAvailableException extends \Exception { | ||
public function __construct() | ||
{ | ||
parent::__construct("Acquia CLI could not be found. Please ensure Acquia CLI is installed locally on your system."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.