mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 16:52:41 +01:00
Allow AlmanacHost blueprints to build a meaningful CommandInterface
Summary: Ref T9253. Provide a meaningful command interface for Almanac hosts. Test Plan: Configued and leased a real host (`sbuild001.phacility.net`) and ran a command on it. ``` $ ./bin/drydock command --lease 90 -- ls / bin boot core dev etc home initrd.img lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var vmlinuz ``` Reviewers: chad, hach-que Reviewed By: chad, hach-que Maniphest Tasks: T9253 Differential Revision: https://secure.phabricator.com/D14126
This commit is contained in:
parent
9a270efe8a
commit
6a0eb9d84b
10 changed files with 169 additions and 113 deletions
|
@ -838,7 +838,6 @@ phutil_register_library_map(array(
|
||||||
'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
|
'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
|
||||||
'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
|
'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
|
||||||
'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
|
'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
|
||||||
'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php',
|
|
||||||
'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
|
'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
|
||||||
'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
|
'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
|
||||||
'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
|
'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
|
||||||
|
@ -846,6 +845,7 @@ phutil_register_library_map(array(
|
||||||
'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
|
'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
|
||||||
'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
|
'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
|
||||||
'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php',
|
'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php',
|
||||||
|
'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php',
|
||||||
'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
|
'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
|
||||||
'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php',
|
'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php',
|
||||||
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
|
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
|
||||||
|
@ -4555,7 +4555,6 @@ phutil_register_library_map(array(
|
||||||
'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||||
'DrydockLeaseStatus' => 'DrydockConstants',
|
'DrydockLeaseStatus' => 'DrydockConstants',
|
||||||
'DrydockLeaseViewController' => 'DrydockLeaseController',
|
'DrydockLeaseViewController' => 'DrydockLeaseController',
|
||||||
'DrydockLocalCommandInterface' => 'DrydockCommandInterface',
|
|
||||||
'DrydockLog' => array(
|
'DrydockLog' => array(
|
||||||
'DrydockDAO',
|
'DrydockDAO',
|
||||||
'PhabricatorPolicyInterface',
|
'PhabricatorPolicyInterface',
|
||||||
|
@ -4566,6 +4565,7 @@ phutil_register_library_map(array(
|
||||||
'DrydockLogQuery' => 'DrydockQuery',
|
'DrydockLogQuery' => 'DrydockQuery',
|
||||||
'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||||
'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow',
|
'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow',
|
||||||
|
'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow',
|
||||||
'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
|
'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
|
||||||
'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow',
|
'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow',
|
||||||
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||||
|
|
|
@ -119,11 +119,37 @@ final class DrydockAlmanacServiceHostBlueprintImplementation
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInterface(
|
public function getInterface(
|
||||||
|
DrydockBlueprint $blueprint,
|
||||||
DrydockResource $resource,
|
DrydockResource $resource,
|
||||||
DrydockLease $lease,
|
DrydockLease $lease,
|
||||||
$type) {
|
$type) {
|
||||||
// TODO: Actually do stuff here, this needs work and currently makes this
|
|
||||||
// entire exercise pointless.
|
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case DrydockCommandInterface::INTERFACE_TYPE:
|
||||||
|
$credential_phid = $blueprint->getFieldValue('credentialPHID');
|
||||||
|
$binding_phid = $resource->getAttribute('almanacBindingPHID');
|
||||||
|
|
||||||
|
$binding = id(new AlmanacBindingQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($binding_phid))
|
||||||
|
->executeOne();
|
||||||
|
if (!$binding) {
|
||||||
|
// TODO: This is probably a permanent failure, destroy this resource?
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unable to load binding "%s" to create command interface.',
|
||||||
|
$binding_phid));
|
||||||
|
}
|
||||||
|
|
||||||
|
$interface = $binding->getInterface();
|
||||||
|
|
||||||
|
return id(new DrydockSSHCommandInterface())
|
||||||
|
->setConfig('credentialPHID', $credential_phid)
|
||||||
|
->setConfig('host', $interface->getAddress())
|
||||||
|
->setConfig('port', $interface->getPort());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFieldSpecifications() {
|
public function getFieldSpecifications() {
|
||||||
|
|
|
@ -3,59 +3,21 @@
|
||||||
/**
|
/**
|
||||||
* @task lease Lease Acquisition
|
* @task lease Lease Acquisition
|
||||||
* @task resource Resource Allocation
|
* @task resource Resource Allocation
|
||||||
|
* @task interface Resource Interfaces
|
||||||
* @task log Logging
|
* @task log Logging
|
||||||
*/
|
*/
|
||||||
abstract class DrydockBlueprintImplementation extends Phobject {
|
abstract class DrydockBlueprintImplementation extends Phobject {
|
||||||
|
|
||||||
private $activeResource;
|
private $activeResource;
|
||||||
private $activeLease;
|
private $activeLease;
|
||||||
private $instance;
|
|
||||||
|
|
||||||
abstract public function getType();
|
abstract public function getType();
|
||||||
abstract public function getInterface(
|
|
||||||
DrydockResource $resource,
|
|
||||||
DrydockLease $lease,
|
|
||||||
$type);
|
|
||||||
|
|
||||||
abstract public function isEnabled();
|
abstract public function isEnabled();
|
||||||
|
|
||||||
abstract public function getBlueprintName();
|
abstract public function getBlueprintName();
|
||||||
abstract public function getDescription();
|
abstract public function getDescription();
|
||||||
|
|
||||||
public function getBlueprintClass() {
|
|
||||||
return get_class($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function loadLease($lease_id) {
|
|
||||||
// TODO: Get rid of this?
|
|
||||||
$query = id(new DrydockLeaseQuery())
|
|
||||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
||||||
->withIDs(array($lease_id))
|
|
||||||
->execute();
|
|
||||||
|
|
||||||
$lease = idx($query, $lease_id);
|
|
||||||
|
|
||||||
if (!$lease) {
|
|
||||||
throw new Exception(pht("No such lease '%d'!", $lease_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $lease;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getInstance() {
|
|
||||||
if (!$this->instance) {
|
|
||||||
throw new Exception(
|
|
||||||
pht('Attach the blueprint instance to the implementation.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function attachInstance(DrydockBlueprint $instance) {
|
|
||||||
$this->instance = $instance;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFieldSpecifications() {
|
public function getFieldSpecifications() {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
@ -105,6 +67,7 @@ abstract class DrydockBlueprintImplementation extends Phobject {
|
||||||
DrydockResource $resource,
|
DrydockResource $resource,
|
||||||
DrydockLease $lease);
|
DrydockLease $lease);
|
||||||
|
|
||||||
|
|
||||||
final public function releaseLease(
|
final public function releaseLease(
|
||||||
DrydockBlueprint $blueprint,
|
DrydockBlueprint $blueprint,
|
||||||
DrydockResource $resource,
|
DrydockResource $resource,
|
||||||
|
@ -236,6 +199,16 @@ abstract class DrydockBlueprintImplementation extends Phobject {
|
||||||
DrydockLease $lease);
|
DrydockLease $lease);
|
||||||
|
|
||||||
|
|
||||||
|
/* -( Resource Interfaces )------------------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
abstract public function getInterface(
|
||||||
|
DrydockBlueprint $blueprint,
|
||||||
|
DrydockResource $resource,
|
||||||
|
DrydockLease $lease,
|
||||||
|
$type);
|
||||||
|
|
||||||
|
|
||||||
/* -( Logging )------------------------------------------------------------ */
|
/* -( Logging )------------------------------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
@ -308,7 +281,7 @@ abstract class DrydockBlueprintImplementation extends Phobject {
|
||||||
$this->log(
|
$this->log(
|
||||||
pht(
|
pht(
|
||||||
"Blueprint '%s': Created New Template",
|
"Blueprint '%s': Created New Template",
|
||||||
$this->getBlueprintClass()));
|
get_class($this)));
|
||||||
|
|
||||||
return $resource;
|
return $resource;
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,18 +122,11 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInterface(
|
public function getInterface(
|
||||||
|
DrydockBlueprint $blueprint,
|
||||||
DrydockResource $resource,
|
DrydockResource $resource,
|
||||||
DrydockLease $lease,
|
DrydockLease $lease,
|
||||||
$type) {
|
$type) {
|
||||||
|
// TODO: This blueprint doesn't work at all.
|
||||||
switch ($type) {
|
|
||||||
case 'command':
|
|
||||||
return $this
|
|
||||||
->loadLease($resource->getAttribute('lease.host'))
|
|
||||||
->getInterface($type);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception(pht("No interface of type '%s'.", $type));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
abstract class DrydockInterface extends Phobject {
|
abstract class DrydockInterface extends Phobject {
|
||||||
|
|
||||||
private $config;
|
private $config = array();
|
||||||
|
|
||||||
abstract public function getInterfaceType();
|
abstract public function getInterfaceType();
|
||||||
|
|
||||||
final public function setConfiguration(array $config) {
|
final public function setConfig($key, $value) {
|
||||||
$this->config = $config;
|
$this->config[$key] = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
abstract class DrydockCommandInterface extends DrydockInterface {
|
abstract class DrydockCommandInterface extends DrydockInterface {
|
||||||
|
|
||||||
|
const INTERFACE_TYPE = 'command';
|
||||||
|
|
||||||
private $workingDirectory;
|
private $workingDirectory;
|
||||||
|
|
||||||
public function setWorkingDirectory($working_directory) {
|
public function setWorkingDirectory($working_directory) {
|
||||||
|
@ -14,7 +16,7 @@ abstract class DrydockCommandInterface extends DrydockInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function getInterfaceType() {
|
final public function getInterfaceType() {
|
||||||
return 'command';
|
return self::INTERFACE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function exec($command) {
|
final public function exec($command) {
|
||||||
|
@ -38,7 +40,7 @@ abstract class DrydockCommandInterface extends DrydockInterface {
|
||||||
protected function applyWorkingDirectoryToArgv(array $argv) {
|
protected function applyWorkingDirectoryToArgv(array $argv) {
|
||||||
if ($this->getWorkingDirectory() !== null) {
|
if ($this->getWorkingDirectory() !== null) {
|
||||||
$cmd = $argv[0];
|
$cmd = $argv[0];
|
||||||
$cmd = "(cd %s; {$cmd})";
|
$cmd = "(cd %s && {$cmd})";
|
||||||
$argv = array_merge(
|
$argv = array_merge(
|
||||||
array($cmd),
|
array($cmd),
|
||||||
array($this->getWorkingDirectory()),
|
array($this->getWorkingDirectory()),
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
final class DrydockLocalCommandInterface extends DrydockCommandInterface {
|
|
||||||
|
|
||||||
public function getExecFuture($command) {
|
|
||||||
$argv = func_get_args();
|
|
||||||
$argv = $this->applyWorkingDirectoryToArgv($argv);
|
|
||||||
|
|
||||||
return newv('ExecFuture', $argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,67 +2,57 @@
|
||||||
|
|
||||||
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
|
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
|
||||||
|
|
||||||
private $passphraseSSHKey;
|
private $credential;
|
||||||
private $connectTimeout;
|
private $connectTimeout;
|
||||||
|
|
||||||
private function openCredentialsIfNotOpen() {
|
private function loadCredential() {
|
||||||
if ($this->passphraseSSHKey !== null) {
|
if ($this->credential === null) {
|
||||||
return;
|
$credential_phid = $this->getConfig('credentialPHID');
|
||||||
}
|
|
||||||
|
|
||||||
$credential = id(new PassphraseCredentialQuery())
|
$this->credential = PassphraseSSHKey::loadFromPHID(
|
||||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
$credential_phid,
|
||||||
->withIDs(array($this->getConfig('credential')))
|
|
||||||
->needSecrets(true)
|
|
||||||
->executeOne();
|
|
||||||
|
|
||||||
if ($credential === null) {
|
|
||||||
throw new Exception(
|
|
||||||
pht(
|
|
||||||
'There is no credential with ID %d.',
|
|
||||||
$this->getConfig('credential')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($credential->getProvidesType() !==
|
|
||||||
PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE) {
|
|
||||||
throw new Exception(pht('Only private key credentials are supported.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID(
|
|
||||||
$credential->getPHID(),
|
|
||||||
PhabricatorUser::getOmnipotentUser());
|
PhabricatorUser::getOmnipotentUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->credential;
|
||||||
|
}
|
||||||
|
|
||||||
public function setConnectTimeout($timeout) {
|
public function setConnectTimeout($timeout) {
|
||||||
$this->connectTimeout = $timeout;
|
$this->connectTimeout = $timeout;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExecFuture($command) {
|
public function getExecFuture($command) {
|
||||||
$this->openCredentialsIfNotOpen();
|
$credential = $this->loadCredential();
|
||||||
|
|
||||||
$argv = func_get_args();
|
$argv = func_get_args();
|
||||||
$argv = $this->applyWorkingDirectoryToArgv($argv);
|
$argv = $this->applyWorkingDirectoryToArgv($argv);
|
||||||
$full_command = call_user_func_array('csprintf', $argv);
|
$full_command = call_user_func_array('csprintf', $argv);
|
||||||
|
|
||||||
$command_timeout = '';
|
$flags = array();
|
||||||
if ($this->connectTimeout !== null) {
|
$flags[] = '-o';
|
||||||
$command_timeout = csprintf(
|
$flags[] = 'LogLevel=quiet';
|
||||||
'-o %s',
|
|
||||||
'ConnectTimeout='.$this->connectTimeout);
|
$flags[] = '-o';
|
||||||
|
$flags[] = 'StrictHostKeyChecking=no';
|
||||||
|
|
||||||
|
$flags[] = '-o';
|
||||||
|
$flags[] = 'UserKnownHostsFile=/dev/null';
|
||||||
|
|
||||||
|
$flags[] = '-o';
|
||||||
|
$flags[] = 'BatchMode=yes';
|
||||||
|
|
||||||
|
if ($this->connectTimeout) {
|
||||||
|
$flags[] = '-o';
|
||||||
|
$flags[] = 'ConnectTimeout='.$this->connectTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ExecFuture(
|
return new ExecFuture(
|
||||||
'ssh '.
|
'ssh %Ls -l %P -p %s -i %P %s -- %s',
|
||||||
'-o LogLevel=quiet '.
|
$flags,
|
||||||
'-o StrictHostKeyChecking=no '.
|
$credential->getUsernameEnvelope(),
|
||||||
'-o UserKnownHostsFile=/dev/null '.
|
|
||||||
'-o BatchMode=yes '.
|
|
||||||
'%C -p %s -i %P %P@%s -- %s',
|
|
||||||
$command_timeout,
|
|
||||||
$this->getConfig('port'),
|
$this->getConfig('port'),
|
||||||
$this->passphraseSSHKey->getKeyfileEnvelope(),
|
$credential->getKeyfileEnvelope(),
|
||||||
$this->passphraseSSHKey->getUsernameEnvelope(),
|
|
||||||
$this->getConfig('host'),
|
$this->getConfig('host'),
|
||||||
$full_command);
|
$full_command);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DrydockManagementCommandWorkflow
|
||||||
|
extends DrydockManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('command')
|
||||||
|
->setSynopsis(pht('Run a command on a leased resource.'))
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'lease',
|
||||||
|
'param' => 'id',
|
||||||
|
'help' => pht('Lease ID.'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'argv',
|
||||||
|
'wildcard' => true,
|
||||||
|
'help' => pht('Command to execute.'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$lease_id = $args->getArg('lease');
|
||||||
|
if (!$lease_id) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
pht(
|
||||||
|
'Use %s to specify a lease.',
|
||||||
|
'--lease'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$argv = $args->getArg('argv');
|
||||||
|
if (!$argv) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
pht(
|
||||||
|
'Specify a command to run.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lease = id(new DrydockLeaseQuery())
|
||||||
|
->setViewer($this->getViewer())
|
||||||
|
->withIDs(array($lease_id))
|
||||||
|
->executeOne();
|
||||||
|
if (!$lease) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unable to load lease with ID "%s"!',
|
||||||
|
$lease_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check lease state, etc.
|
||||||
|
|
||||||
|
$interface = $lease->getInterface(DrydockCommandInterface::INTERFACE_TYPE);
|
||||||
|
|
||||||
|
list($stdout, $stderr) = call_user_func_array(
|
||||||
|
array($interface, 'execx'),
|
||||||
|
array('%Ls', $argv));
|
||||||
|
|
||||||
|
fprintf(STDOUT, $stdout);
|
||||||
|
fprintf(STDERR, $stderr);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -173,6 +173,24 @@ final class DrydockBlueprint extends DrydockDAO
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getInterface(
|
||||||
|
DrydockResource $resource,
|
||||||
|
DrydockLease $lease,
|
||||||
|
$type) {
|
||||||
|
|
||||||
|
$interface = $this->getImplementation()
|
||||||
|
->getInterface($this, $resource, $lease, $type);
|
||||||
|
|
||||||
|
if (!$interface) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unable to build resource interface of type "%s".',
|
||||||
|
$type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $interface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue