diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -19,6 +19,7 @@ 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', + 'AlmanacBuildPoolServiceType' => 'applications/almanac/servicetype/AlmanacBuildPoolServiceType.php', 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', @@ -796,6 +797,7 @@ 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', + 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', @@ -846,7 +848,6 @@ 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', - 'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php', @@ -2884,6 +2885,7 @@ 'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php', 'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php', 'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php', + 'PhabricatorStandardCustomFieldTypeahead' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTypeahead.php', 'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php', @@ -3586,6 +3588,7 @@ 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', + 'AlmanacBuildPoolServiceType' => 'AlmanacServiceType', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', @@ -4451,6 +4454,7 @@ 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', + 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockBlueprint' => array( 'DrydockDAO', @@ -4515,7 +4519,6 @@ 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', - 'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResource' => array( 'DrydockDAO', @@ -6938,6 +6941,7 @@ 'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField', + 'PhabricatorStandardCustomFieldTypeahead' => 'PhabricatorStandardCustomFieldPHIDs', 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldPHIDs', 'PhabricatorStandardPageView' => 'PhabricatorBarePageView', 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/almanac/servicetype/AlmanacBuildPoolServiceType.php b/src/applications/almanac/servicetype/AlmanacBuildPoolServiceType.php new file mode 100644 --- /dev/null +++ b/src/applications/almanac/servicetype/AlmanacBuildPoolServiceType.php @@ -0,0 +1,52 @@ + array( + 'name' => pht('SSH Credential'), + 'type' => 'credential', + 'required' => true, + 'credential.provides' + => PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE, + ), + 'winrm-auth' => array( + 'name' => pht('WinRM Credentials'), + 'type' => 'credential', + 'credential.provides' + => PassphrasePasswordCredentialType::PROVIDES_TYPE, + 'caption' => pht( + 'This is only required if the platform is set to "windows".'), + ), + 'platform' => array( + 'name' => pht('Host Platform'), + 'type' => 'text', + 'caption' => pht( + 'Must be "windows" for Windows-based systems, or '. + 'any other value for UNIX-based systems.'), + ), + 'path' => array( + 'name' => pht('Storage Path'), + 'type' => 'text', + 'caption' => pht( + 'The path to store leases in.'), + ), + ); + } + +} diff --git a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php --- a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php @@ -3,6 +3,13 @@ final class AlmanacServiceDatasource extends PhabricatorTypeaheadDatasource { + private $serviceClasses = null; + + public function withServiceClasses(array $service_classes) { + $this->serviceClasses = $service_classes; + return $this; + } + public function getBrowseTitle() { return pht('Browse Services'); } @@ -23,6 +30,11 @@ ->withNamePrefix($raw_query) ->setOrder('name'); + if ($this->serviceClasses !== null) { + $services->withServiceClasses( + $this->serviceClasses); + } + $services = $this->executeQuery($services); if ($services) { diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -0,0 +1,224 @@ +getDetail('service')); + + $service = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(phutil_json_decode($this->getDetail('service'))) + ->executeOne(); + + if ($service === null) { + throw new Exception('Specified Almanac service does not exist!'); + } + + $bindings = id(new AlmanacBindingQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withServicePHIDs(array($service->getPHID())) + ->execute(); + + // We have to filter available bindings by platform so that it + // matches the lease platform. If the service platform itself + // matches, then we include all bindings that are bound to it, + // unless the binding has overrridden the platform property. If + // the service platform does not match, then we look at all of + // the bindings which have overrridden the platform property + // to match. + $available_bindings = array(); + + if ($service->getAlmanacPropertyValue('platform') === + $lease->getAttribute('platform')) { + + // All bindings match by default, unless the binding overrides + // the platform property. + $available_bindings = $bindings; + foreach ($available_bindings as $key => $binding) { + if ($binding->hasAlmanacProperty('platform')) { + if ($binding->getAlmanacPropertyValue('platform') !== + $lease->getAttribute('platform')) { + unset($available_bindings[$key]); + } + } + } + } else { + // No bindings match by default, unless the binding overrides + // the platform property. + $available_bindings = array(); + foreach ($bindings as $binding) { + if ($binding->hasAlmanacProperty('platform')) { + if ($binding->getAlmanacPropertyValue('platform') === + $lease->getAttribute('platform')) { + $available_bindings[] = $binding; + } + } + } + } + + // TODO: This probably isn't right; we might need to filter + // bindings by those that aren't already used by other + // Drydock resources. + shuffle($available_bindings); + + $binding = head($available_bindings); + + $host = $binding->getInterface()->getAddress(); + $port = $binding->getInterface()->getPort(); + $platform = $binding->getAlmanacPropertyValue( + 'platform', + $service->getAlmanacPropertyValue('platform')); + $path = $binding->getAlmanacPropertyValue( + 'path', + $service->getAlmanacPropertyValue('path')); + $credential = $binding->getAlmanacPropertyValue( + 'credential', + $service->getAlmanacPropertyValue('credential')); + $winrm_auth = $binding->getAlmanacPropertyValue( + 'winrm-auth', + $service->getAlmanacPropertyValue('winrm-auth')); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($credential)) + ->executeOne(); + if ($credential === null) { + throw new Exception('Specified credential does not exist!'); + } + + $winrm_auth_id = null; + if ($winrm_auth !== null) { + $winrm_auth = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($winrm_auth)) + ->executeOne(); + if ($winrm_auth === null) { + throw new Exception('Specified credential does not exist!'); + } + $winrm_auth_id = $winrm_auth->getID(); + } + + $resource + ->setName($binding->getInterface()->renderDisplayAddress()) + ->setStatus(DrydockResourceStatus::STATUS_OPEN) + ->setAttribute('platform', $platform) + ->setAttribute('host', $host) + ->setAttribute('port', $port) + ->setAttribute('path', $path) + ->setAttribute('credential', $credential->getID()) + ->setAttribute('winrm-auth', $winrm_auth_id) + ->save(); + } + + protected function canAllocateLease( + DrydockResource $resource, + DrydockLease $lease) { + return + $lease->getAttribute('platform') === $resource->getAttribute('platform'); + } + + protected function shouldAllocateLease( + DrydockResource $resource, + DrydockLease $lease, + array $other_leases) { + return true; + } + + protected function executeAcquireLease( + DrydockResource $resource, + DrydockLease $lease) { + + $platform = $resource->getAttribute('platform'); + $path = $resource->getAttribute('path'); + + $lease_id = $lease->getID(); + + // Can't use DIRECTORY_SEPERATOR here because that is relevant to + // the platform we're currently running on, not the platform we are + // remoting to. + $separator = '/'; + if ($platform === 'windows') { + $separator = '\\'; + } + + // Clean up the directory path a little. + $base_path = rtrim($path, '/'); + $base_path = rtrim($base_path, '\\'); + $full_path = $base_path.$separator.$lease_id; + + $cmd = $lease->getInterface('command'); + + $cmd->execx('mkdir %s', $full_path); + + $lease->setAttribute('path', $full_path); + } + + public function getType() { + return 'host'; + } + + public function getInterface( + DrydockResource $resource, + DrydockLease $lease, + $type) { + + switch ($type) { + case 'command': + return id(new DrydockSSHCommandInterface()) + ->setConfiguration(array( + 'host' => $resource->getAttribute('host'), + 'port' => $resource->getAttribute('port'), + 'credential' => $resource->getAttribute('credential'), + 'platform' => $resource->getAttribute('platform'), + )) + ->setWorkingDirectory($lease->getAttribute('path')); + case 'filesystem': + return id(new DrydockSFTPFilesystemInterface()) + ->setConfiguration(array( + 'host' => $resource->getAttribute('host'), + 'port' => $resource->getAttribute('port'), + 'credential' => $resource->getAttribute('credential'), + )); + } + + throw new Exception(pht("No interface of type '%s'.", $type)); + } + + public function getFieldSpecifications() { + return array( + 'service' => array( + 'name' => pht('Almanac Service'), + 'type' => 'typeahead', + 'datasource' => id(new AlmanacServiceDatasource()) + ->withServiceClasses(array( + 'AlmanacBuildPoolServiceType', + )), + 'limit' => 1, + 'required' => true, + ), + ) + parent::getFieldSpecifications(); + } + +} diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -42,7 +42,7 @@ return $lease; } - protected function getInstance() { + public function getInstance() { if (!$this->instance) { throw new Exception( pht('Attach the blueprint instance to the implementation.')); @@ -301,10 +301,14 @@ return true; } - abstract protected function executeAllocateResource(DrydockLease $lease); + abstract protected function executeAllocateResource( + DrydockResource $resource, + DrydockLease $lease); + final public function allocateResource( + DrydockResource $resource, + DrydockLease $lease) { - final public function allocateResource(DrydockLease $lease) { $scope = $this->pushActiveScope(null, $lease); $this->log( @@ -314,7 +318,7 @@ $lease->getLeaseName())); try { - $resource = $this->executeAllocateResource($lease); + $this->executeAllocateResource($resource, $lease); $this->validateAllocatedResource($resource); } catch (Exception $ex) { $this->logException($ex); @@ -389,25 +393,6 @@ return idx(self::getAllBlueprintImplementations(), $class); } - protected function newResourceTemplate($name) { - $resource = id(new DrydockResource()) - ->setBlueprintPHID($this->getInstance()->getPHID()) - ->setBlueprintClass($this->getBlueprintClass()) - ->setType($this->getType()) - ->setStatus(DrydockResourceStatus::STATUS_PENDING) - ->setName($name) - ->save(); - - $this->activeResource = $resource; - - $this->log( - pht( - "Blueprint '%s': Created New Template", - $this->getBlueprintClass())); - - return $resource; - } - /** * Sanity checks that the blueprint is implemented properly. */ diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php deleted file mode 100644 --- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php +++ /dev/null @@ -1,116 +0,0 @@ -getAttribute('platform') === $resource->getAttribute('platform'); - } - - protected function shouldAllocateLease( - DrydockResource $resource, - DrydockLease $lease, - array $other_leases) { - return true; - } - - protected function executeAcquireLease( - DrydockResource $resource, - DrydockLease $lease) { - - // Because preallocated resources are manually created, we should verify - // we have all the information we need. - PhutilTypeSpec::checkMap( - $resource->getAttributesForTypeSpec( - array('platform', 'host', 'port', 'credential', 'path')), - array( - 'platform' => 'string', - 'host' => 'string', - 'port' => 'string', // Value is a string from the command line - 'credential' => 'string', - 'path' => 'string', - )); - $v_platform = $resource->getAttribute('platform'); - $v_path = $resource->getAttribute('path'); - - // Similar to DrydockLocalHostBlueprint, we create a folder - // on the remote host that the lease can use. - - $lease_id = $lease->getID(); - - // Can't use DIRECTORY_SEPERATOR here because that is relevant to - // the platform we're currently running on, not the platform we are - // remoting to. - $separator = '/'; - if ($v_platform === 'windows') { - $separator = '\\'; - } - - // Clean up the directory path a little. - $base_path = rtrim($v_path, '/'); - $base_path = rtrim($base_path, '\\'); - $full_path = $base_path.$separator.$lease_id; - - $cmd = $lease->getInterface('command'); - - $cmd->execx('mkdir %s', $full_path); - - $lease->setAttribute('path', $full_path); - } - - public function getType() { - return 'host'; - } - - public function getInterface( - DrydockResource $resource, - DrydockLease $lease, - $type) { - - switch ($type) { - case 'command': - return id(new DrydockSSHCommandInterface()) - ->setConfiguration(array( - 'host' => $resource->getAttribute('host'), - 'port' => $resource->getAttribute('port'), - 'credential' => $resource->getAttribute('credential'), - 'platform' => $resource->getAttribute('platform'), - )) - ->setWorkingDirectory($lease->getAttribute('path')); - case 'filesystem': - return id(new DrydockSFTPFilesystemInterface()) - ->setConfiguration(array( - 'host' => $resource->getAttribute('host'), - 'port' => $resource->getAttribute('port'), - 'credential' => $resource->getAttribute('credential'), - )); - } - - throw new Exception(pht("No interface of type '%s'.", $type)); - } - -} diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -33,7 +33,10 @@ return !$other_leases; } - protected function executeAllocateResource(DrydockLease $lease) { + protected function executeAllocateResource( + DrydockResource $resource, + DrydockLease $lease) { + $repository_id = $lease->getAttribute('repositoryID'); if (!$repository_id) { throw new Exception( @@ -79,10 +82,9 @@ $this->log(pht('Complete.')); - $resource = $this->newResourceTemplate( - pht( - 'Working Copy (%s)', - $repository->getCallsign())); + $resource->setName(pht( + 'Working Copy (%s)', + $repository->getCallsign())); $resource->setStatus(DrydockResourceStatus::STATUS_OPEN); $resource->setAttribute('lease.host', $host_lease->getID()); $resource->setAttribute('path', $path); diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php --- a/src/applications/drydock/worker/DrydockAllocatorWorker.php +++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php @@ -116,8 +116,10 @@ } if (!$resource) { - $blueprints = DrydockBlueprintImplementation - ::getAllBlueprintImplementationsForResource($type); + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->execute(); + $blueprints = mpull($blueprints, 'getImplementation', 'getPHID'); $this->logToDrydock( pht('Found %d Blueprints', count($blueprints))); @@ -159,7 +161,14 @@ shuffle($blueprints); $blueprint = head($blueprints); - $resource = $blueprint->allocateResource($lease); + + $resource = id(new DrydockResource()) + ->setBlueprintPHID($blueprint->getInstance()->getPHID()) + ->setType($blueprint->getType()) + ->setName(pht('Pending Allocation')) + ->setStatus(DrydockResourceStatus::STATUS_PENDING) + ->save(); + $blueprint->allocateResource($resource, $lease); if (!$blueprint->allocateLease($resource, $lease)) { // TODO: This "should" happen only if we lost a race with another lease, diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTypeahead.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTypeahead.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTypeahead.php @@ -0,0 +1,52 @@ +getFieldValue(); + + $control = id(new AphrontFormTokenizerControl()) + ->setUser($this->getViewer()) + ->setLabel($this->getFieldName()) + ->setName($this->getFieldKey()) + ->setDatasource( + $this->getFieldConfigValue('datasource')) + ->setCaption($this->getCaption()) + ->setValue(nonempty($value, array())); + + $limit = $this->getFieldConfigValue('limit'); + if ($limit) { + $control->setLimit($limit); + } + + return $control; + } + + public function appendToApplicationSearchForm( + PhabricatorApplicationSearchEngine $engine, + AphrontFormView $form, + $value) { + + $control = id(new AphrontFormTokenizerControl()) + ->setLabel($this->getFieldName()) + ->setName($this->getFieldKey()) + ->setDatasource( + $this->getFieldConfigValue('datasource')) + ->setValue(nonempty($value, array())); + + $form->appendControl($control); + } + + public function getHeraldFieldValueType($condition) { + return id(new HeraldTokenizerFieldValue()) + ->setKey('custom.'.$this->getFieldKey()) + ->setDatasource( + $this->getFieldConfigValue('datasource')); + } + +}