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 @@ -945,13 +945,16 @@ 'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', + 'DrydockConduitAPIMethod' => 'applications/drydock/conduit/DrydockConduitAPIMethod.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', 'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php', + 'DrydockCreateLeaseConduitAPIMethod' => 'applications/drydock/conduit/DrydockCreateLeaseConduitAPIMethod.php', 'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php', 'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php', 'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php', + 'DrydockDestroyLeaseConduitAPIMethod' => 'applications/drydock/conduit/DrydockDestroyLeaseConduitAPIMethod.php', 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php', @@ -1008,6 +1011,7 @@ 'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php', 'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php', 'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php', + 'DrydockRequestAuthorizationConduitAPIMethod' => 'applications/drydock/conduit/DrydockRequestAuthorizationConduitAPIMethod.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php', 'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php', @@ -5489,13 +5493,16 @@ 'DrydockCommandError' => 'Phobject', 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockCommandQuery' => 'DrydockQuery', + 'DrydockConduitAPIMethod' => 'ConduitAPIMethod', 'DrydockConsoleController' => 'DrydockController', 'DrydockConstants' => 'Phobject', 'DrydockController' => 'PhabricatorController', 'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability', + 'DrydockCreateLeaseConduitAPIMethod' => 'DrydockConduitAPIMethod', 'DrydockDAO' => 'PhabricatorLiskDAO', 'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability', 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', + 'DrydockDestroyLeaseConduitAPIMethod' => 'DrydockConduitAPIMethod', 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockInterface' => 'Phobject', 'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType', @@ -5561,6 +5568,7 @@ 'DrydockRepositoryOperationType' => 'Phobject', 'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker', 'DrydockRepositoryOperationViewController' => 'DrydockRepositoryOperationController', + 'DrydockRequestAuthorizationConduitAPIMethod' => 'DrydockConduitAPIMethod', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', 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 @@ -15,6 +15,27 @@ abstract public function getBlueprintName(); abstract public function getDescription(); + public function getSummary() { + return $this->getDescription(); + } + + public function getLeaseAttributesSpecification() { + return null; + } + + public function getLeaseAttributesDescriptions() { + throw new PhutilMethodNotImplementedException(); + } + + public function validateLeaseAttributes(array $lease_attributes) { + $attribute_spec = $this->getLeaseAttributesSpecification(); + PhutilTypeSpec::checkMap($lease_attributes, $attribute_spec); + } + + public function getLeaseAttributesDataExample() { + return null; + } + public function getBlueprintIcon() { return 'fa-map-o'; } 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 @@ -23,6 +23,37 @@ return pht('Allows Drydock to check out working copies of repositories.'); } + public function getLeaseAttributesSpecification() { + return array( + 'repositories.map' => 'map>', + 'repositories.strict' => 'optional bool', + ); + } + + public function getLeaseAttributesDescriptions() { + return array( + 'repositories.map' => pht( + 'Maps clone folder names to information about what repositories they '. + 'should contain and what state those repositories should be in.'), + 'repositories.strict' => pht( + 'Set to true in order to force the working copy to contain only '. + 'the repositories specified.'), + ); + } + + public function getLeaseAttributesDataExample() { + return array( + 'repositories.map' => array( + 'janitorial-services' => array( + 'phid' => 'PHID-REPO-gb3x27pjw4wffij3od44', + 'default' => true, + 'commit' => '3a66dac400a50632f442aed6cb8cf65c80e5ae70', + ), + ), + 'repositories.strict' => true, + ); + } + public function canAnyBlueprintEverAllocateResourceForLease( DrydockLease $lease) { return true; diff --git a/src/applications/drydock/conduit/DrydockConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockConduitAPIMethod.php @@ -0,0 +1,18 @@ + $implementation) { + $type = $implementation->getType(); + $name = $implementation->getBlueprintName(); + $desc = $implementation->getSummary(); + $out[] = "| `{$class}` | `{$type}` | **{$name}** | {$desc} |"; + } + + $out[] = null; + $out[] = pht( + 'Some blueprint implementations also define attributes which should '. + 'be provided when leasing resources. You should provide these '. + 'attributes using the `leaseAttributes` parameter when calling '. + 'this method. The attributes you provide depend on the implementation '. + 'class for the blueprint you are using.'); + + $head_key = pht('Key'); + + foreach ($implementations as $class => $implementation) { + $type = $implementation->getType(); + $name = $implementation->getBlueprintName(); + + $out[] = "== {$name} =="; + $out[] = null; + $out[] = $implementation->getDescription(); + $out[] = null; + $out[] = pht( + 'Blueprints with implementations of class `%s` will lease '. + 'resources of the `%s` type.', + $class, + $type); + $out[] = null; + + $spec = $implementation->getLeaseAttributesSpecification(); + if (!$spec) { + $out[] = pht( + '//(This implementation does not specify any lease attributes)//'); + $out[] = null; + continue; + } + + $desc = $implementation->getLeaseAttributesDescriptions(); + $out[] = "| {$head_key} | {$head_type} | {$head_desc} |"; + $out[] = '|-------------|--------------|--------------|'; + foreach ($spec as $key => $key_type) { + $key_desc = idx($desc, $key); + $out[] = "| `{$key}` | //{$key_type}// | {$key_desc} |"; + } + + $example = $implementation->getLeaseAttributesDataExample(); + if ($example !== null) { + $json = new PhutilJSON(); + $rendered = $json->encodeFormatted($example); + + $out[] = pht('For example:'); + $out[] = '```lang=json'; + $out[] = $rendered; + $out[] = '```'; + } + } + + return implode("\n", $out); + } + + protected function defineParamTypes() { + return array( + 'blueprintPHID' => 'required phid', + 'leaseAttributes' => 'optional map', + ); + } + + protected function defineErrorTypes() { + return array( + 'ERR_NO_BLUEPRINT' => pht( + 'No active blueprint exists with the specified PHID.'), + 'ERR_NOT_AUTHORIZED' => pht( + 'You are not authorized to use this blueprint.'), + ); + } + + protected function defineReturnType() { + return 'wild'; + } + + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + $blueprint_phid = $request->getValue('blueprintPHID'); + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withPHIDs(array($blueprint_phid)) + ->executeOne(); + if (!$blueprint) { + throw new ConduitException('ERR_NO_BLUEPRINT'); + } + + $authorization = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint_phid)) + ->withObjectPHIDs(array($viewer->getPHID())) + ->withBlueprintStates(array( + DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED, + )) + ->withObjectStates(array(DrydockAuthorization::OBJECTAUTH_ACTIVE)) + ->executeOne(); + if (!$authorization) { + throw new ConduitException('ERR_NOT_AUTHORIZED'); + } + + $implementation = $blueprint->getImplementation(); + $lease = id(new DrydockLease()) + ->setOwnerPHID($viewer->getPHID()) + ->setResourceType($implementation->getType()) + ->setAuthorizingPHID($viewer->getPHID()) + ->setAllowedBlueprintPHIDs(array($blueprint_phid)); + + $lease_attributes = $request->getValue('leaseAttributes', array()); + $implementation->validateLeaseAttributes($lease_attributes); + foreach ($lease_attributes as $key => $value) { + $lease->setAttribute($key, $value); + } + + $lease->queueForActivation(); + + $search = id(new ConduitCall('drydock.lease.search', array( + 'constraints' => array( + 'phids' => array($lease->getPHID()), + ), + ))) + ->setUser($viewer) + ->execute(); + return idxv($search, array('data', 0)); + } + +} diff --git a/src/applications/drydock/conduit/DrydockDestroyLeaseConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockDestroyLeaseConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockDestroyLeaseConduitAPIMethod.php @@ -0,0 +1,63 @@ + 'required phid', + ); + } + + protected function defineErrorTypes() { + return array( + 'ERR_NO_LEASE' => pht('No active lease exists with this ID.'), + 'ERR_NOT_AUTHORIZED' => pht( + 'You are not authorized to destroy this lease.'), + ); + } + + protected function defineReturnType() { + return 'map'; + } + + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($request->getValue('leasePHID'))) + ->executeOne(); + if (!$lease) { + throw new ConduitException('ERR_NO_LEASE'); + } + + if ($lease->getOwnerPHID() !== $viewer->getPHID()) { + throw new ConduitException('ERR_NOT_AUTHORIZED'); + } + + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + + $search = id(new ConduitCall('drydock.lease.search', array( + 'constraints' => array( + 'phids' => array($lease->getPHID()), + ), + ))) + ->setUser($viewer) + ->execute(); + return idxv($search, array('data', 0)); + } + +} diff --git a/src/applications/drydock/conduit/DrydockRequestAuthorizationConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockRequestAuthorizationConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockRequestAuthorizationConduitAPIMethod.php @@ -0,0 +1,73 @@ + 'required phid', + ); + } + + protected function defineErrorTypes() { + return array( + 'ERR_NO_BLUEPRINT' => pht( + 'No active blueprint exists with the specified PHID.'), + ); + } + + protected function defineReturnType() { + return 'map'; + } + + protected function execute(ConduitAPIRequest $request) { + $viewer = $request->getUser(); + $blueprint_phid = $request->getValue('blueprintPHID'); + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withPHIDs(array($blueprint_phid)) + ->executeOne(); + if (!$blueprint) { + throw new ConduitException('ERR_NO_BLUEPRINT'); + } + + $authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($viewer->getPHID())) + ->withBlueprintStates(array( + DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED, + )) + ->withObjectStates(array(DrydockAuthorization::OBJECTAUTH_ACTIVE)) + ->execute(); + $authorizations = mpull($authorizations, null, 'getBlueprintPHID'); + $old = array_keys($authorizations); + if (!in_array($blueprint_phid, $old)) { + $new = array_mergev(array($old, array($blueprint_phid))); + DrydockAuthorization::applyAuthorizationChanges( + $viewer, + $viewer->getPHID(), + $old, + $new); + } + + $search = id(new ConduitCall('drydock.authorization.search', array( + 'constraints' => array( + 'objectPHIDs' => array($viewer->getPHID()), + 'blueprintPHIDs' => array($blueprint_phid), + ), + ))) + ->setUser($viewer) + ->execute(); + return idxv($search, array('data', 0)); + } + +}